diff --git a/src/KeyringClient.ts b/src/KeyringClient.ts index 8cd417f89..0f4f1923f 100644 --- a/src/KeyringClient.ts +++ b/src/KeyringClient.ts @@ -25,7 +25,7 @@ import { } from './internal/api'; import { KeyringRpcMethod } from './internal/rpc'; import type { JsonRpcRequest } from './JsonRpcRequest'; -import { strictMask } from './utils'; +import { strictMask } from './superstruct'; export type Sender = { send(request: JsonRpcRequest): Promise; diff --git a/src/eth/erc4337/types.ts b/src/eth/erc4337/types.ts index b1376ac67..de5a27dc1 100644 --- a/src/eth/erc4337/types.ts +++ b/src/eth/erc4337/types.ts @@ -1,6 +1,7 @@ import { type Infer } from 'superstruct'; -import { UrlStruct, exactOptional, object } from '../../superstruct'; +import { exactOptional, object } from '../../superstruct'; +import { UrlStruct } from '../../utils'; import { EthAddressStruct, EthBytesStruct, EthUint256Struct } from '../types'; /** diff --git a/src/eth/types.test.ts b/src/eth/types.test.ts index 7a6c74846..5afc80128 100644 --- a/src/eth/types.test.ts +++ b/src/eth/types.test.ts @@ -1,4 +1,4 @@ -import { UrlStruct } from '../superstruct'; +import { UrlStruct } from '../utils'; describe('types', () => { it('is a valid BundlerUrl', () => { diff --git a/src/superstruct.ts b/src/superstruct.ts index 3e0e7a085..150320836 100644 --- a/src/superstruct.ts +++ b/src/superstruct.ts @@ -1,5 +1,5 @@ import type { Infer, Context } from 'superstruct'; -import { Struct, define, object as stObject } from 'superstruct'; +import { Struct, assert, define, object as stObject } from 'superstruct'; import type { ObjectSchema, OmitBy, @@ -127,19 +127,21 @@ export function definePattern( } /** - * Validates if a given value is a valid URL. + * Assert that a value is valid according to a struct. * - * @param value - The value to be validated. - * @returns A boolean indicating if the value is a valid URL. + * It is similar to superstruct's mask function, but it does not ignore extra + * properties. + * + * @param value - Value to check. + * @param struct - Struct to validate the value against. + * @param message - Error message to throw if the value is not valid. + * @returns The value if it is valid. */ -export const UrlStruct = define('Url', (value: unknown) => { - let url; - - try { - url = new URL(value as string); - } catch (_) { - return false; - } - - return url.protocol === 'http:' || url.protocol === 'https:'; -}); +export function strictMask( + value: unknown, + struct: Struct, + message?: string, +): Type { + assert(value, struct, message); + return value; +} diff --git a/src/utils/caip.test.ts b/src/utils/caip.test.ts new file mode 100644 index 000000000..1cb757399 --- /dev/null +++ b/src/utils/caip.test.ts @@ -0,0 +1,86 @@ +import { isCaipAssetId, isCaipAssetType } from './caip'; + +describe('isCaipAssetType', () => { + // Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases + it.each([ + 'eip155:1/slip44:60', + 'bip122:000000000019d6689c085ae165831e93/slip44:0', + 'cosmos:cosmoshub-3/slip44:118', + 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', + 'cosmos:Binance-Chain-Tigris/slip44:714', + 'cosmos:iov-mainnet/slip44:234', + 'lip9:9ee11e9df416b18b/slip44:134', + 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f', + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d', + ])('returns true for a valid asset type %s', (id) => { + expect(isCaipAssetType(id)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + '', + '!@#$%^&*()', + 'foo', + 'eip155', + 'eip155:', + 'eip155:1', + 'eip155:1:', + 'eip155:1:0x0000000000000000000000000000000000000000:2', + 'bip122', + 'bip122:', + 'bip122:000000000019d6689c085ae165831e93', + 'bip122:000000000019d6689c085ae165831e93/', + 'bip122:000000000019d6689c085ae165831e93/tooooooolong', + 'bip122:000000000019d6689c085ae165831e93/tooooooolong:asset', + 'eip155:1/erc721', + 'eip155:1/erc721:', + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/', + ])('returns false for an invalid asset type %s', (id) => { + expect(isCaipAssetType(id)).toBe(false); + }); +}); + +describe('isCaipAssetId', () => { + // Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases + it.each([ + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', + 'hedera:mainnet/nft:0.0.55492/12', + ])('returns true for a valid asset id %s', (id) => { + expect(isCaipAssetId(id)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + '', + '!@#$%^&*()', + 'foo', + 'eip155', + 'eip155:', + 'eip155:1', + 'eip155:1:', + 'eip155:1:0x0000000000000000000000000000000000000000:2', + 'bip122', + 'bip122:', + 'bip122:000000000019d6689c085ae165831e93', + 'bip122:000000000019d6689c085ae165831e93/', + 'bip122:000000000019d6689c085ae165831e93/tooooooolong', + 'bip122:000000000019d6689c085ae165831e93/tooooooolong:asset', + 'eip155:1/erc721', + 'eip155:1/erc721:', + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/', + ])('returns false for an invalid asset id %s', (id) => { + expect(isCaipAssetType(id)).toBe(false); + }); +}); diff --git a/src/utils/caip.ts b/src/utils/caip.ts new file mode 100644 index 000000000..b526c0be8 --- /dev/null +++ b/src/utils/caip.ts @@ -0,0 +1,59 @@ +import { is, type Infer } from 'superstruct'; + +import { definePattern } from '../superstruct'; + +const CAIP_ASSET_TYPE_REGEX = + /^(?(?[-a-z0-9]{3,8}):(?[-_a-zA-Z0-9]{1,32}))\/(?[-a-z0-9]{3,8}):(?[-.%a-zA-Z0-9]{1,128})$/u; + +const CAIP_ASSET_ID_REGEX = + /^(?(?[-a-z0-9]{3,8}):(?[-_a-zA-Z0-9]{1,32}))\/(?[-a-z0-9]{3,8}):(?[-.%a-zA-Z0-9]{1,128})\/(?[-.%a-zA-Z0-9]{1,78})$/u; + +/** + * A CAIP-19 asset type identifier, i.e., a human-readable type of asset identifier. + */ +export const CaipAssetTypeStruct = definePattern( + 'CaipAssetType', + CAIP_ASSET_TYPE_REGEX, +); +export type CaipAssetType = Infer; + +/** + * A CAIP-19 asset ID identifier, i.e., a human-readable type of asset ID. + */ +export const CaipAssetIdStruct = definePattern( + 'CaipAssetId', + CAIP_ASSET_ID_REGEX, +); +export type CaipAssetId = Infer; + +/** + * Check if the given value is a {@link CaipAssetType}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link CaipAssetType}. + * @example + * ```ts + * isCaipAssetType('eip155:1/slip44:60'); // true + * isCaipAssetType('cosmos:cosmoshub-3/slip44:118'); // true + * isCaipAssetType('hedera:mainnet/nft:0.0.55492/12'); // false + * ``` + */ +export function isCaipAssetType(value: unknown): value is CaipAssetType { + return is(value, CaipAssetTypeStruct); +} + +/** + * Check if the given value is a {@link CaipAssetId}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link CaipAssetId}. + * @example + * ```ts + * isCaipAssetType('eip155:1/slip44:60'); // false + * isCaipAssetType('cosmos:cosmoshub-3/slip44:118'); // false + * isCaipAssetType('hedera:mainnet/nft:0.0.55492/12'); // true + * ``` + */ +export function isCaipAssetId(value: unknown): value is CaipAssetId { + return is(value, CaipAssetIdStruct); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 000000000..c1a9cc9a1 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,4 @@ +export * from './caip'; +export * from './typing'; +export * from './url'; +export * from './uuid'; diff --git a/src/utils.test-d.ts b/src/utils/typing.test-d.ts similarity index 79% rename from src/utils.test-d.ts rename to src/utils/typing.test-d.ts index 894d8f322..8e88cf9ed 100644 --- a/src/utils.test-d.ts +++ b/src/utils/typing.test-d.ts @@ -1,5 +1,5 @@ -import type { Extends } from './utils'; -import { expectTrue } from './utils'; +import type { Extends } from './typing'; +import { expectTrue } from './typing'; expectTrue(); diff --git a/src/utils.test.ts b/src/utils/typing.test.ts similarity index 80% rename from src/utils.test.ts rename to src/utils/typing.test.ts index b54237d1b..f1b283db3 100644 --- a/src/utils.test.ts +++ b/src/utils/typing.test.ts @@ -1,4 +1,4 @@ -import { expectTrue } from './utils'; +import { expectTrue } from './typing'; describe('expectTrue', () => { it('does nothing since expectTrue is an empty function', () => { diff --git a/src/utils.ts b/src/utils/typing.ts similarity index 59% rename from src/utils.ts rename to src/utils/typing.ts index e4617b88c..f9c88e135 100644 --- a/src/utils.ts +++ b/src/utils/typing.ts @@ -1,16 +1,3 @@ -import { assert } from 'superstruct'; -import type { Struct } from 'superstruct'; - -import { definePattern } from './superstruct'; - -/** - * UUIDv4 struct. - */ -export const UuidStruct = definePattern( - 'UuidV4', - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu, -); - /** * Omit keys from a union type. * @@ -23,26 +10,6 @@ export type OmitUnion = Type extends any ? Omit : never; -/** - * Assert that a value is valid according to a struct. - * - * It is similar to superstruct's mask function, but it does not ignore extra - * properties. - * - * @param value - Value to check. - * @param struct - Struct to validate the value against. - * @param message - Error message to throw if the value is not valid. - * @returns The value if it is valid. - */ -export function strictMask( - value: unknown, - struct: Struct, - message?: string, -): Type { - assert(value, struct, message); - return value; -} - /** * Type that resolves to `true` if `Child` extends `Base`, otherwise `false`. * diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 000000000..3037dea7d --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,19 @@ +import { define } from 'superstruct'; + +/** + * Validates if a given value is a valid URL. + * + * @param value - The value to be validated. + * @returns A boolean indicating if the value is a valid URL. + */ +export const UrlStruct = define('Url', (value: unknown) => { + let url; + + try { + url = new URL(value as string); + } catch (_) { + return false; + } + + return url.protocol === 'http:' || url.protocol === 'https:'; +}); diff --git a/src/utils/uuid.ts b/src/utils/uuid.ts new file mode 100644 index 000000000..954cf9e2e --- /dev/null +++ b/src/utils/uuid.ts @@ -0,0 +1,9 @@ +import { definePattern } from '../superstruct'; + +/** + * UUIDv4 struct. + */ +export const UuidStruct = definePattern( + 'UuidV4', + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu, +);