From 8b22bb429705764c26633477a1c840d84c5c1b15 Mon Sep 17 00:00:00 2001 From: Eric Wolff Date: Thu, 12 Dec 2024 14:39:01 -0700 Subject: [PATCH 1/2] feat: add additional CAIP-19 types and parsing functions to align with proposal --- src/__fixtures__/caip-types.ts | 34 +++ src/caip-types.test-d.ts | 44 ++++ src/caip-types.test.ts | 387 ++++++++++++++++++++++++++++++--- src/caip-types.ts | 210 +++++++++++++++++- src/index.test.ts | 12 + src/node.test.ts | 12 + 6 files changed, 671 insertions(+), 28 deletions(-) diff --git a/src/__fixtures__/caip-types.ts b/src/__fixtures__/caip-types.ts index b0af50db0..3e5eabbe8 100644 --- a/src/__fixtures__/caip-types.ts +++ b/src/__fixtures__/caip-types.ts @@ -33,3 +33,37 @@ export const CAIP_ACCOUNT_ID_FIXTURES = [ export const CAIP_ACCOUNT_ADDRESS_FIXTURES = Array.from( new Set(CAIP_ACCOUNT_ID_FIXTURES.map((value) => value.split(':')[2])), ); + +export const CAIP_ASSET_ID_FIXTURES = [ + 'eip155:1/slip44:60', + 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f', + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', + 'bip122:000000000019d6689c085ae165831e93/slip44:0', + 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', + 'cosmos:cosmoshub-3/slip44:118', + 'cosmos:Binance-Chain-Tigris/slip44:714', + 'lip9:9ee11e9df416b18b/slip44:134', + 'hedera:mainnet/nft:0.0.55492/12', + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w', + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', +] as const; + +export const CAIP_ASSET_NAMESPACE_FIXTURES = Array.from( + new Set( + CAIP_ASSET_ID_FIXTURES.map((value) => value.split('/')[1]?.split(':')[0]), + ), +); + +export const CAIP_ASSET_REFERENCE_FIXTURES = Array.from( + new Set( + CAIP_ASSET_ID_FIXTURES.map((value) => value.split('/')[1]?.split(':')[1]), + ), +); + +export const CAIP_ASSET_TYPE_FIXTURES = Array.from( + new Set( + CAIP_ASSET_ID_FIXTURES.map((value) => + value.split('/').slice(0, 2).join('/'), + ), + ), +); diff --git a/src/caip-types.test-d.ts b/src/caip-types.test-d.ts index 5b94402ac..5418479a6 100644 --- a/src/caip-types.test-d.ts +++ b/src/caip-types.test-d.ts @@ -3,6 +3,10 @@ import { expectAssignable, expectNotAssignable } from 'tsd'; import type { CaipAccountAddress, CaipAccountId, + CaipAssetId, + CaipAssetNamespace, + CaipAssetReference, + CaipAssetType, CaipChainId, CaipNamespace, CaipReference, @@ -33,6 +37,36 @@ expectAssignable( expectAssignable('string'); expectAssignable(`${embeddedString}`); +expectAssignable('string'); +expectAssignable(`${embeddedString}`); + +expectAssignable('string'); +expectAssignable(`${embeddedString}`); + +expectAssignable( + 'namespace:reference/assetNamespace:assetReference', +); +expectAssignable('namespace:reference/:'); +expectAssignable(':reference/assetNamespace:'); +expectAssignable( + `${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}`, +); + +expectAssignable( + 'namespace:reference/assetNamespace:assetReference', +); +expectAssignable( + 'namespace:reference/assetNamespace:assetReference/tokenId', +); +expectAssignable('namespace:reference/:assetReference/'); +expectAssignable(':reference/assetNamespace:'); +expectAssignable( + `${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}`, +); +expectAssignable( + `${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}/${embeddedString}`, +); + // Not valid caip strings: expectAssignable('namespace:😀'); @@ -50,3 +84,13 @@ expectNotAssignable(0); expectNotAssignable('🙃'); expectNotAssignable(0); + +expectNotAssignable(0); + +expectNotAssignable(0); + +expectNotAssignable(0); +expectNotAssignable('🙃'); + +expectNotAssignable(0); +expectNotAssignable('🙃'); diff --git a/src/caip-types.test.ts b/src/caip-types.test.ts index ed9e90932..7a7294fb1 100644 --- a/src/caip-types.test.ts +++ b/src/caip-types.test.ts @@ -1,24 +1,37 @@ import { CAIP_ACCOUNT_ADDRESS_FIXTURES, CAIP_ACCOUNT_ID_FIXTURES, + CAIP_ASSET_ID_FIXTURES, + CAIP_ASSET_NAMESPACE_FIXTURES, + CAIP_ASSET_REFERENCE_FIXTURES, + CAIP_ASSET_TYPE_FIXTURES, CAIP_CHAIN_ID_FIXTURES, CAIP_NAMESPACE_FIXTURES, CAIP_REFERENCE_FIXTURES, } from './__fixtures__'; import { + CAIP_ACCOUNT_ADDRESS_REGEX, + CAIP_ASSET_NAMESPACE_REGEX, + CAIP_ASSET_REFERENCE_REGEX, + CAIP_NAMESPACE_REGEX, + CAIP_REFERENCE_REGEX, + CAIP_TOKEN_ID_REGEX, isCaipAccountAddress, isCaipAccountId, + isCaipAssetId, + isCaipAssetNamespace, + isCaipAssetReference, + isCaipAssetType, isCaipChainId, isCaipNamespace, isCaipReference, - isCaipAssetType, - isCaipAssetId, + KnownCaipNamespace, parseCaipAccountId, + parseCaipAssetId, parseCaipChainId, + toCaipAccountId, + toCaipAssetId, toCaipChainId, - KnownCaipNamespace, - CAIP_NAMESPACE_REGEX, - CAIP_REFERENCE_REGEX, } from './caip-types'; describe('isCaipChainId', () => { @@ -151,21 +164,53 @@ describe('isCaipAccountAddress', () => { }); }); -describe('isCaipAssetType', () => { - // Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases +describe('isCaipAssetNamespace', () => { + it.each([...CAIP_ASSET_NAMESPACE_FIXTURES])( + 'returns true for a valid asset namespace %s', + (assetNamespace) => { + expect(isCaipAssetNamespace(assetNamespace)).toBe(true); + }, + ); + + it.each([true, false, null, undefined, 1, {}, [], 'abC', '12', '123456789'])( + 'returns false for an invalid asset namespace %s', + (assetNamespace) => { + expect(isCaipAssetNamespace(assetNamespace)).toBe(false); + }, + ); +}); + +describe('isCaipAssetReference', () => { + it.each([...CAIP_ASSET_REFERENCE_FIXTURES])( + 'returns true for a valid asset reference %s', + (assetReference) => { + expect(isCaipAssetReference(assetReference)).toBe(true); + }, + ); + 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); + true, + false, + null, + undefined, + 1, + {}, + [], + '', + '!@#$%^&*()', + Array(129).fill('0').join(''), + ])('returns false for an invalid asset reference %s', (assetReference) => { + expect(isCaipAssetReference(assetReference)).toBe(false); }); +}); + +describe('isCaipAssetType', () => { + it.each([...CAIP_ASSET_TYPE_FIXTURES])( + 'returns true for a valid asset type %s', + (assetType) => { + expect(isCaipAssetType(assetType)).toBe(true); + }, + ); it.each([ true, @@ -198,13 +243,12 @@ describe('isCaipAssetType', () => { }); 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([...CAIP_ASSET_ID_FIXTURES])( + 'returns true for a valid asset id %s', + (id) => { + expect(isCaipAssetId(id)).toBe(true); + }, + ); it.each([ true, @@ -366,6 +410,113 @@ describe('parseCaipAccountId', () => { }); }); +describe('parseCaipAssetId', () => { + it('parses valid asset ids', () => { + expect(parseCaipAssetId('eip155:1/slip44:60')).toMatchInlineSnapshot(` + { + "assetNamespace": "slip44", + "assetReference": "60", + "chain": { + "namespace": "eip155", + "reference": "1", + }, + "chainId": "eip155:1", + } + `); + + expect( + parseCaipAssetId( + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', + ), + ).toMatchInlineSnapshot(` + { + "assetNamespace": "erc721", + "assetReference": "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", + "chain": { + "namespace": "eip155", + "reference": "1", + }, + "chainId": "eip155:1", + "tokenId": "771769", + } + `); + + expect(parseCaipAssetId('bip122:000000000019d6689c085ae165831e93/slip44:0')) + .toMatchInlineSnapshot(` + { + "assetNamespace": "slip44", + "assetReference": "0", + "chain": { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + }, + "chainId": "bip122:000000000019d6689c085ae165831e93", + } + `); + + expect(parseCaipAssetId('cosmos:cosmoshub-3/slip44:118')) + .toMatchInlineSnapshot(` + { + "assetNamespace": "slip44", + "assetReference": "118", + "chain": { + "namespace": "cosmos", + "reference": "cosmoshub-3", + }, + "chainId": "cosmos:cosmoshub-3", + } + `); + + expect(parseCaipAssetId('hedera:mainnet/nft:0.0.55492/12')) + .toMatchInlineSnapshot(` + { + "assetNamespace": "nft", + "assetReference": "0.0.55492", + "chain": { + "namespace": "hedera", + "reference": "mainnet", + }, + "chainId": "hedera:mainnet", + "tokenId": "12", + } + `); + + expect( + parseCaipAssetId( + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w', + ), + ).toMatchInlineSnapshot(` + { + "assetNamespace": "nft", + "assetReference": "Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w", + "chain": { + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + }, + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + } + `); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + 'foo', + 'foobarbazquz:1', + 'foo:', + 'foo:foobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquz', + 'eip155:1', + 'eip155:1:', + ])('throws for invalid input %s', (input) => { + expect(() => parseCaipAssetId(input as any)).toThrow( + 'Invalid CAIP asset ID.', + ); + }); +}); + describe('toCaipChainId', () => { // This function relies on @metamask/utils CAIP helpers. Those are being // tested with a variety of inputs. @@ -415,3 +566,189 @@ describe('toCaipChainId', () => { ); }); }); + +describe('toCaipAccountId', () => { + it('returns a valid CAIP-10 account ID when given a valid namespace, reference, and accountAddress', () => { + const namespace = 'eip'; + const reference = '1'; + const accountAddress = '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb'; + expect(toCaipAccountId(namespace, reference, accountAddress)).toBe( + `${namespace}:${reference}:${accountAddress}`, + ); + }); + + it.each([ + // Too short, must have 3 chars at least + '', + 'xs', + // Not matching + '!@#$%^&*()', + // Too long + 'namespacetoolong', + ])('throws for invalid namespaces: %s', (namespace) => { + const reference = '1'; + const accountAddress = '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb'; + expect(() => toCaipAccountId(namespace, reference, accountAddress)).toThrow( + `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*()', + // Too long + '012345678901234567890123456789012', // 33 chars + ])('throws for invalid reference: %s', (reference) => { + const namespace = 'eip'; + const accountAddress = '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb'; + expect(() => toCaipAccountId(namespace, reference, accountAddress)).toThrow( + `Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*()', + // Too long + Array(129).fill('0').join(''), + ])('throws for invalid accountAddress: %s', (accountAddress) => { + const namespace = 'eip'; + const reference = '1'; + expect(() => toCaipAccountId(namespace, reference, accountAddress)).toThrow( + `Invalid "accountAddress", must match: ${CAIP_ACCOUNT_ADDRESS_REGEX.toString()}`, + ); + }); +}); + +describe('toCaipAssetId', () => { + it('returns a valid CAIP-19 asset ID when given a valid namespace, reference, assetNamespace, and assetReference', () => { + const namespace = 'eip'; + const reference = '1'; + const assetNamespace = 'erc20'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + expect( + toCaipAssetId(namespace, reference, assetNamespace, assetReference), + ).toBe(`${namespace}:${reference}/${assetNamespace}:${assetReference}`); + }); + + it('returns a valid CAIP-19 asset ID when given a valid namespace, reference, assetNamespace, assetReference, and tokenId', () => { + const namespace = 'eip'; + const reference = '1'; + const assetNamespace = 'erc721'; + const assetReference = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d'; + const tokenId = '771769'; + expect( + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), + ).toBe( + `${namespace}:${reference}/${assetNamespace}:${assetReference}/${tokenId}`, + ); + }); + + it.each([ + // Too short, must have 3 chars at least + '', + 'xs', + // Not matching + '!@#$%^&*()', + // Too long + 'namespacetoolong', + ])('throws for invalid namespaces: %s', (namespace) => { + const reference = '1'; + const assetNamespace = 'erc20'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + expect(() => + toCaipAssetId(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*()', + // Too long + '012345678901234567890123456789012', // 33 chars + ])('throws for invalid reference: %s', (reference) => { + const namespace = 'eip'; + const assetNamespace = 'erc20'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + expect(() => + toCaipAssetId(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*', + // Too long + '012345789', + ])('throws for invalid assetNamespace: %s', (assetNamespace) => { + const namespace = 'eip'; + const reference = '1'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + expect(() => + toCaipAssetId(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "assetNamespace", must match: ${CAIP_ASSET_NAMESPACE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*()', + // Too long + Array(129).fill('0').join(''), + ])('throws for invalid assetReference: %s', (assetReference) => { + const namespace = 'eip'; + const reference = '1'; + const assetNamespace = 'erc20'; + expect(() => + toCaipAssetId(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "assetReference", must match: ${CAIP_ASSET_REFERENCE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*()', + // Too long + Array(79).fill('0').join(''), + ])('throws for invalid tokenId: %s', (tokenId) => { + const namespace = 'eip'; + const reference = '1'; + const assetNamespace = 'erc721'; + const assetReference = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d'; + expect(() => + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), + ).toThrow( + `Invalid "tokenId", must match: ${CAIP_TOKEN_ID_REGEX.toString()}`, + ); + }); +}); diff --git a/src/caip-types.ts b/src/caip-types.ts index c6a40de5d..91e0bf739 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -1,5 +1,5 @@ -import { is, pattern, string } from '@metamask/superstruct'; import type { Infer, Struct } from '@metamask/superstruct'; +import { is, pattern, string } from '@metamask/superstruct'; export const CAIP_CHAIN_ID_REGEX = /^(?[-a-z0-9]{3,8}):(?[-_a-zA-Z0-9]{1,32})$/u; @@ -13,11 +13,17 @@ export const CAIP_ACCOUNT_ID_REGEX = export const CAIP_ACCOUNT_ADDRESS_REGEX = /^[-.%a-zA-Z0-9]{1,128}$/u; +export const CAIP_ASSET_NAMESPACE_REGEX = /^[-a-z0-9]{3,8}$/u; + +export const CAIP_ASSET_REFERENCE_REGEX = /^[-.%a-zA-Z0-9]{1,128}$/u; + +export const CAIP_TOKEN_ID_REGEX = /^[-.%a-zA-Z0-9]{1,78}$/u; + export 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; export 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-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-2 chain ID, i.e., a human-readable namespace and reference. @@ -58,6 +64,30 @@ export const CaipAccountAddressStruct = pattern( ); export type CaipAccountAddress = Infer; +/** + * A CAIP-19 asset namespace, i.e., a namespace domain of an asset. + */ +export const CaipAssetNamespaceStruct = pattern( + string(), + CAIP_ASSET_NAMESPACE_REGEX, +); +export type CaipAssetNamespace = Infer; + +/** + * A CAIP-19 asset reference, i.e., an identifier for an asset within a given namespace. + */ +export const CaipAssetReferenceStruct = pattern( + string(), + CAIP_ASSET_REFERENCE_REGEX, +); +export type CaipAssetReference = Infer; + +/** + * A CAIP-19 asset token ID, i.e., a unique identifier for an addressable asset of a given type + */ +export const CaipTokenIdStruct = pattern(string(), CAIP_TOKEN_ID_REGEX); +export type CaipTokenId = Infer; + /** * A CAIP-19 asset type identifier, i.e., a human-readable type of asset identifier. */ @@ -74,7 +104,9 @@ export const CaipAssetIdStruct = pattern( string(), CAIP_ASSET_ID_REGEX, ) as Struct; -export type CaipAssetId = `${string}:${string}/${string}:${string}/${string}`; +export type CaipAssetId = + | `${string}:${string}/${string}:${string}` + | `${string}:${string}/${string}:${string}/${string}`; /** Known CAIP namespaces. */ export enum KnownCaipNamespace { @@ -139,6 +171,40 @@ export function isCaipAccountAddress( return is(value, CaipAccountAddressStruct); } +/** + * Check if the given value is a {@link CaipAssetNamespace}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link CaipAssetNamespace}. + */ +export function isCaipAssetNamespace( + value: unknown, +): value is CaipAssetNamespace { + return is(value, CaipAssetNamespaceStruct); +} + +/** + * Check if the given value is a {@link CaipAssetReference}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link CaipAssetReference}. + */ +export function isCaipAssetReference( + value: unknown, +): value is CaipAssetReference { + return is(value, CaipAssetReferenceStruct); +} + +/** + * Check if the given value is a {@link CaipTokenId}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link CaipTokenId}. + */ +export function isCaipTokenId(value: unknown): value is CaipTokenId { + return is(value, CaipTokenIdStruct); +} + /** * Check if the given value is a {@link CaipAssetType}. * @@ -208,6 +274,41 @@ export function parseCaipAccountId(caipAccountId: CaipAccountId): { }; } +/** + * Parse a CAIP-19 asset ID to an object containing the chain ID, parsed chain ID, + * asset namespace, asset reference, and token ID. + * + * This validates the CAIP-19 asset ID before parsing it. + * + * @param caipAssetId - The CAIP-19 asset ID to validate and parse. + * @returns The parsed CAIP-19 asset ID. + */ +export function parseCaipAssetId(caipAssetId: CaipAssetId): { + assetNamespace: CaipAssetNamespace; + assetReference: CaipAssetReference; + tokenId?: CaipTokenId; + chainId: CaipChainId; + chain: { namespace: CaipNamespace; reference: CaipReference }; +} { + const match = CAIP_ASSET_ID_REGEX.exec(caipAssetId); + if (!match?.groups) { + throw new Error('Invalid CAIP asset ID.'); + } + + const tokenId = match.groups.tokenId as CaipTokenId; + + return { + assetNamespace: match.groups.assetNamespace as CaipAssetNamespace, + assetReference: match.groups.assetReference as CaipAssetReference, + ...(tokenId ? { tokenId } : {}), + chainId: match.groups.chainId as CaipChainId, + chain: { + namespace: match.groups.namespace as CaipNamespace, + reference: match.groups.reference as CaipReference, + }, + }; +} + /** * Chain ID as defined per the CAIP-2 * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md}. @@ -241,3 +342,106 @@ export function toCaipChainId( return `${namespace}:${reference}`; } + +/** + * Account ID as defined per the CAIP-10 + * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md}. + * + * It defines a way to uniquely identify any blockchain account in a human-readable + * way. + * + * @param namespace - The standard (ecosystem) of similar blockchains. + * @param reference - Identity of a blockchain within a given namespace. + * @param accountAddress - The address of the blockchain account. + * @throws {@link Error} + * This exception is thrown if the inputs do not comply with the CAIP-10 + * syntax specification + * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md#syntax}. + * @returns A CAIP account ID. + */ +export function toCaipAccountId( + namespace: CaipNamespace, + reference: CaipReference, + accountAddress: CaipAccountAddress, +): CaipAccountId { + if (!isCaipNamespace(namespace)) { + throw new Error( + `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, + ); + } + + if (!isCaipReference(reference)) { + throw new Error( + `Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`, + ); + } + + if (!isCaipAccountAddress(accountAddress)) { + throw new Error( + `Invalid "accountAddress", must match: ${CAIP_ACCOUNT_ADDRESS_REGEX.toString()}`, + ); + } + + return `${namespace}:${reference}:${accountAddress}`; +} + +/** + * Asset ID as defined per the CAIP-19 + * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md}. + * + * It defines a way to uniquely identify any blockchain asset in a human-readable + * way. + * + * @param namespace - The standard (ecosystem) of similar blockchains. + * @param reference - Identity of a blockchain within a given namespace. + * @param assetNamespace - The namespace domain of an asset. + * @param assetReference - The identity of an asset within a given namespace. + * @param tokenId - The unique identifier for an addressable asset of a given type. + * @throws {@link Error} + * This exception is thrown if the inputs do not comply with the CAIP-19 + * syntax specification + * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax}. + * @returns A CAIP asset ID. + */ +export function toCaipAssetId( + namespace: CaipNamespace, + reference: CaipReference, + assetNamespace: CaipAssetNamespace, + assetReference: CaipAssetReference, + tokenId?: CaipTokenId, +): CaipAccountId { + if (!isCaipNamespace(namespace)) { + throw new Error( + `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, + ); + } + + if (!isCaipReference(reference)) { + throw new Error( + `Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`, + ); + } + + if (!isCaipAssetNamespace(assetNamespace)) { + throw new Error( + `Invalid "assetNamespace", must match: ${CAIP_ASSET_NAMESPACE_REGEX.toString()}`, + ); + } + + if (!isCaipAssetReference(assetReference)) { + throw new Error( + `Invalid "assetReference", must match: ${CAIP_ASSET_REFERENCE_REGEX.toString()}`, + ); + } + + // do not throw if tokenId is falsy unless it is an empty string + if ((tokenId && !isCaipTokenId(tokenId)) || tokenId === '') { + throw new Error( + `Invalid "tokenId", must match: ${CAIP_TOKEN_ID_REGEX.toString()}`, + ); + } + + return `${namespace}:${reference}/${assetNamespace}:${assetReference}${ + tokenId ? `/${tokenId}` : '' + }`; +} diff --git a/src/index.test.ts b/src/index.test.ts index 9e36ce705..08810bea9 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -8,17 +8,23 @@ describe('index', () => { "CAIP_ACCOUNT_ADDRESS_REGEX", "CAIP_ACCOUNT_ID_REGEX", "CAIP_ASSET_ID_REGEX", + "CAIP_ASSET_NAMESPACE_REGEX", + "CAIP_ASSET_REFERENCE_REGEX", "CAIP_ASSET_TYPE_REGEX", "CAIP_CHAIN_ID_REGEX", "CAIP_NAMESPACE_REGEX", "CAIP_REFERENCE_REGEX", + "CAIP_TOKEN_ID_REGEX", "CaipAccountAddressStruct", "CaipAccountIdStruct", "CaipAssetIdStruct", + "CaipAssetNamespaceStruct", + "CaipAssetReferenceStruct", "CaipAssetTypeStruct", "CaipChainIdStruct", "CaipNamespaceStruct", "CaipReferenceStruct", + "CaipTokenIdStruct", "ChecksumStruct", "Duration", "ESCAPE_CHARACTERS_REGEXP", @@ -100,10 +106,13 @@ describe('index', () => { "isCaipAccountAddress", "isCaipAccountId", "isCaipAssetId", + "isCaipAssetNamespace", + "isCaipAssetReference", "isCaipAssetType", "isCaipChainId", "isCaipNamespace", "isCaipReference", + "isCaipTokenId", "isErrorWithCode", "isErrorWithMessage", "isErrorWithStack", @@ -130,12 +139,15 @@ describe('index', () => { "numberToHex", "object", "parseCaipAccountId", + "parseCaipAssetId", "parseCaipChainId", "remove0x", "satisfiesVersionRange", "signedBigIntToBytes", "stringToBytes", "timeSince", + "toCaipAccountId", + "toCaipAssetId", "toCaipChainId", "valueToBytes", "wrapError", diff --git a/src/node.test.ts b/src/node.test.ts index 5f7d5ed4d..111e75574 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -8,17 +8,23 @@ describe('node', () => { "CAIP_ACCOUNT_ADDRESS_REGEX", "CAIP_ACCOUNT_ID_REGEX", "CAIP_ASSET_ID_REGEX", + "CAIP_ASSET_NAMESPACE_REGEX", + "CAIP_ASSET_REFERENCE_REGEX", "CAIP_ASSET_TYPE_REGEX", "CAIP_CHAIN_ID_REGEX", "CAIP_NAMESPACE_REGEX", "CAIP_REFERENCE_REGEX", + "CAIP_TOKEN_ID_REGEX", "CaipAccountAddressStruct", "CaipAccountIdStruct", "CaipAssetIdStruct", + "CaipAssetNamespaceStruct", + "CaipAssetReferenceStruct", "CaipAssetTypeStruct", "CaipChainIdStruct", "CaipNamespaceStruct", "CaipReferenceStruct", + "CaipTokenIdStruct", "ChecksumStruct", "Duration", "ESCAPE_CHARACTERS_REGEXP", @@ -105,10 +111,13 @@ describe('node', () => { "isCaipAccountAddress", "isCaipAccountId", "isCaipAssetId", + "isCaipAssetNamespace", + "isCaipAssetReference", "isCaipAssetType", "isCaipChainId", "isCaipNamespace", "isCaipReference", + "isCaipTokenId", "isErrorWithCode", "isErrorWithMessage", "isErrorWithStack", @@ -135,6 +144,7 @@ describe('node', () => { "numberToHex", "object", "parseCaipAccountId", + "parseCaipAssetId", "parseCaipChainId", "readFile", "readJsonFile", @@ -143,6 +153,8 @@ describe('node', () => { "signedBigIntToBytes", "stringToBytes", "timeSince", + "toCaipAccountId", + "toCaipAssetId", "toCaipChainId", "valueToBytes", "wrapError", From 225dff4eea7343b221ad45bd825225a8444f0cf8 Mon Sep 17 00:00:00 2001 From: Eric Wolff Date: Fri, 13 Dec 2024 08:52:31 -0700 Subject: [PATCH 2/2] chore: update caip assetType and assetId to reflect proper spec implementation --- src/__fixtures__/caip-types.ts | 21 ++-- src/caip-types.test-d.ts | 8 +- src/caip-types.test.ts | 214 ++++++++++++++++++++++++++------- src/caip-types.ts | 104 +++++++++++++--- src/index.test.ts | 2 + src/node.test.ts | 2 + 6 files changed, 273 insertions(+), 78 deletions(-) diff --git a/src/__fixtures__/caip-types.ts b/src/__fixtures__/caip-types.ts index 3e5eabbe8..a03c5d462 100644 --- a/src/__fixtures__/caip-types.ts +++ b/src/__fixtures__/caip-types.ts @@ -34,36 +34,31 @@ export const CAIP_ACCOUNT_ADDRESS_FIXTURES = Array.from( new Set(CAIP_ACCOUNT_ID_FIXTURES.map((value) => value.split(':')[2])), ); -export const CAIP_ASSET_ID_FIXTURES = [ +export const CAIP_ASSET_TYPE_FIXTURES = [ 'eip155:1/slip44:60', 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f', - 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', 'bip122:000000000019d6689c085ae165831e93/slip44:0', 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', 'cosmos:cosmoshub-3/slip44:118', 'cosmos:Binance-Chain-Tigris/slip44:714', 'lip9:9ee11e9df416b18b/slip44:134', - 'hedera:mainnet/nft:0.0.55492/12', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', ] as const; +export const CAIP_ASSET_ID_FIXTURES = [ + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', + 'hedera:mainnet/nft:0.0.55492/12', +] as const; + export const CAIP_ASSET_NAMESPACE_FIXTURES = Array.from( new Set( - CAIP_ASSET_ID_FIXTURES.map((value) => value.split('/')[1]?.split(':')[0]), + CAIP_ASSET_TYPE_FIXTURES.map((value) => value.split('/')[1]?.split(':')[0]), ), ); export const CAIP_ASSET_REFERENCE_FIXTURES = Array.from( new Set( - CAIP_ASSET_ID_FIXTURES.map((value) => value.split('/')[1]?.split(':')[1]), - ), -); - -export const CAIP_ASSET_TYPE_FIXTURES = Array.from( - new Set( - CAIP_ASSET_ID_FIXTURES.map((value) => - value.split('/').slice(0, 2).join('/'), - ), + CAIP_ASSET_TYPE_FIXTURES.map((value) => value.split('/')[1]?.split(':')[1]), ), ); diff --git a/src/caip-types.test-d.ts b/src/caip-types.test-d.ts index 5418479a6..0afcfc029 100644 --- a/src/caip-types.test-d.ts +++ b/src/caip-types.test-d.ts @@ -52,17 +52,11 @@ expectAssignable( `${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}`, ); -expectAssignable( - 'namespace:reference/assetNamespace:assetReference', -); expectAssignable( 'namespace:reference/assetNamespace:assetReference/tokenId', ); expectAssignable('namespace:reference/:assetReference/'); -expectAssignable(':reference/assetNamespace:'); -expectAssignable( - `${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}`, -); +expectAssignable(':reference/assetNamespace:/'); expectAssignable( `${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}/${embeddedString}`, ); diff --git a/src/caip-types.test.ts b/src/caip-types.test.ts index 7a7294fb1..2b7e89507 100644 --- a/src/caip-types.test.ts +++ b/src/caip-types.test.ts @@ -28,9 +28,11 @@ import { KnownCaipNamespace, parseCaipAccountId, parseCaipAssetId, + parseCaipAssetType, parseCaipChainId, toCaipAccountId, toCaipAssetId, + toCaipAssetType, toCaipChainId, } from './caip-types'; @@ -410,9 +412,9 @@ describe('parseCaipAccountId', () => { }); }); -describe('parseCaipAssetId', () => { - it('parses valid asset ids', () => { - expect(parseCaipAssetId('eip155:1/slip44:60')).toMatchInlineSnapshot(` +describe('parseCaipAssetType', () => { + it('parses valid asset types', () => { + expect(parseCaipAssetType('eip155:1/slip44:60')).toMatchInlineSnapshot(` { "assetNamespace": "slip44", "assetReference": "60", @@ -425,24 +427,8 @@ describe('parseCaipAssetId', () => { `); expect( - parseCaipAssetId( - 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', - ), + parseCaipAssetType('bip122:000000000019d6689c085ae165831e93/slip44:0'), ).toMatchInlineSnapshot(` - { - "assetNamespace": "erc721", - "assetReference": "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", - "chain": { - "namespace": "eip155", - "reference": "1", - }, - "chainId": "eip155:1", - "tokenId": "771769", - } - `); - - expect(parseCaipAssetId('bip122:000000000019d6689c085ae165831e93/slip44:0')) - .toMatchInlineSnapshot(` { "assetNamespace": "slip44", "assetReference": "0", @@ -454,7 +440,7 @@ describe('parseCaipAssetId', () => { } `); - expect(parseCaipAssetId('cosmos:cosmoshub-3/slip44:118')) + expect(parseCaipAssetType('cosmos:cosmoshub-3/slip44:118')) .toMatchInlineSnapshot(` { "assetNamespace": "slip44", @@ -467,33 +453,72 @@ describe('parseCaipAssetId', () => { } `); - expect(parseCaipAssetId('hedera:mainnet/nft:0.0.55492/12')) - .toMatchInlineSnapshot(` + expect( + parseCaipAssetType( + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w', + ), + ).toMatchInlineSnapshot(` { "assetNamespace": "nft", - "assetReference": "0.0.55492", + "assetReference": "Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w", "chain": { - "namespace": "hedera", - "reference": "mainnet", + "namespace": "solana", + "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", }, - "chainId": "hedera:mainnet", - "tokenId": "12", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", } `); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + 'foo', + 'foobarbazquz:1', + 'foo:', + 'foo:foobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquz', + 'eip155:1', + 'eip155:1:', + ])('throws for invalid input %s', (input) => { + expect(() => parseCaipAssetType(input as any)).toThrow( + 'Invalid CAIP asset type.', + ); + }); +}); +describe('parseCaipAssetId', () => { + it('parses valid asset ids', () => { expect( parseCaipAssetId( - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w', + 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', ), ).toMatchInlineSnapshot(` + { + "assetNamespace": "erc721", + "assetReference": "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", + "chain": { + "namespace": "eip155", + "reference": "1", + }, + "chainId": "eip155:1", + "tokenId": "771769", + } + `); + + expect(parseCaipAssetId('hedera:mainnet/nft:0.0.55492/12')) + .toMatchInlineSnapshot(` { "assetNamespace": "nft", - "assetReference": "Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w", + "assetReference": "0.0.55492", "chain": { - "namespace": "solana", - "reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "namespace": "hedera", + "reference": "mainnet", }, - "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "chainId": "hedera:mainnet", + "tokenId": "12", } `); }); @@ -624,17 +649,92 @@ describe('toCaipAccountId', () => { }); }); -describe('toCaipAssetId', () => { - it('returns a valid CAIP-19 asset ID when given a valid namespace, reference, assetNamespace, and assetReference', () => { +describe('toCaipAssetType', () => { + it('returns a valid CAIP-19 asset type when given a valid namespace, reference, assetNamespace, and assetReference', () => { const namespace = 'eip'; const reference = '1'; const assetNamespace = 'erc20'; const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; expect( - toCaipAssetId(namespace, reference, assetNamespace, assetReference), + toCaipAssetType(namespace, reference, assetNamespace, assetReference), ).toBe(`${namespace}:${reference}/${assetNamespace}:${assetReference}`); }); + it.each([ + // Too short, must have 3 chars at least + '', + 'xs', + // Not matching + '!@#$%^&*()', + // Too long + 'namespacetoolong', + ])('throws for invalid namespaces: %s', (namespace) => { + const reference = '1'; + const assetNamespace = 'erc20'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + expect(() => + toCaipAssetType(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*()', + // Too long + '012345678901234567890123456789012', // 33 chars + ])('throws for invalid reference: %s', (reference) => { + const namespace = 'eip'; + const assetNamespace = 'erc20'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + expect(() => + toCaipAssetType(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*', + // Too long + '012345789', + ])('throws for invalid assetNamespace: %s', (assetNamespace) => { + const namespace = 'eip'; + const reference = '1'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + expect(() => + toCaipAssetType(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "assetNamespace", must match: ${CAIP_ASSET_NAMESPACE_REGEX.toString()}`, + ); + }); + + it.each([ + // Too short, must have 1 char at least + '', + // Not matching + '!@#$%^&*()', + // Too long + Array(129).fill('0').join(''), + ])('throws for invalid assetReference: %s', (assetReference) => { + const namespace = 'eip'; + const reference = '1'; + const assetNamespace = 'erc20'; + expect(() => + toCaipAssetType(namespace, reference, assetNamespace, assetReference), + ).toThrow( + `Invalid "assetReference", must match: ${CAIP_ASSET_REFERENCE_REGEX.toString()}`, + ); + }); +}); + +describe('toCaipAssetId', () => { it('returns a valid CAIP-19 asset ID when given a valid namespace, reference, assetNamespace, assetReference, and tokenId', () => { const namespace = 'eip'; const reference = '1'; @@ -664,10 +764,17 @@ describe('toCaipAssetId', () => { 'namespacetoolong', ])('throws for invalid namespaces: %s', (namespace) => { const reference = '1'; - const assetNamespace = 'erc20'; + const assetNamespace = 'erc721'; const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + const tokenId = '123'; expect(() => - toCaipAssetId(namespace, reference, assetNamespace, assetReference), + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), ).toThrow( `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, ); @@ -682,10 +789,17 @@ describe('toCaipAssetId', () => { '012345678901234567890123456789012', // 33 chars ])('throws for invalid reference: %s', (reference) => { const namespace = 'eip'; - const assetNamespace = 'erc20'; + const assetNamespace = 'erc721'; const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + const tokenId = '123'; expect(() => - toCaipAssetId(namespace, reference, assetNamespace, assetReference), + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), ).toThrow( `Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`, ); @@ -702,8 +816,15 @@ describe('toCaipAssetId', () => { const namespace = 'eip'; const reference = '1'; const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + const tokenId = '123'; expect(() => - toCaipAssetId(namespace, reference, assetNamespace, assetReference), + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), ).toThrow( `Invalid "assetNamespace", must match: ${CAIP_ASSET_NAMESPACE_REGEX.toString()}`, ); @@ -719,9 +840,16 @@ describe('toCaipAssetId', () => { ])('throws for invalid assetReference: %s', (assetReference) => { const namespace = 'eip'; const reference = '1'; - const assetNamespace = 'erc20'; + const assetNamespace = 'erc721'; + const tokenId = '123'; expect(() => - toCaipAssetId(namespace, reference, assetNamespace, assetReference), + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), ).toThrow( `Invalid "assetReference", must match: ${CAIP_ASSET_REFERENCE_REGEX.toString()}`, ); diff --git a/src/caip-types.ts b/src/caip-types.ts index 91e0bf739..5ce5742f0 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -23,7 +23,7 @@ export 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; export 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-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-2 chain ID, i.e., a human-readable namespace and reference. @@ -104,9 +104,7 @@ export const CaipAssetIdStruct = pattern( string(), CAIP_ASSET_ID_REGEX, ) as Struct; -export type CaipAssetId = - | `${string}:${string}/${string}:${string}` - | `${string}:${string}/${string}:${string}/${string}`; +export type CaipAssetId = `${string}:${string}/${string}:${string}/${string}`; /** Known CAIP namespaces. */ export enum KnownCaipNamespace { @@ -274,6 +272,37 @@ export function parseCaipAccountId(caipAccountId: CaipAccountId): { }; } +/** + * Parse a CAIP-19 asset type to an object containing the chain ID, parsed chain ID, + * asset namespace, and asset reference + * + * This validates the CAIP-19 asset type before parsing it. + * + * @param caipAssetType - The CAIP-19 asset type to validate and parse. + * @returns The parsed CAIP-19 asset type. + */ +export function parseCaipAssetType(caipAssetType: CaipAssetType): { + assetNamespace: CaipAssetNamespace; + assetReference: CaipAssetReference; + chainId: CaipChainId; + chain: { namespace: CaipNamespace; reference: CaipReference }; +} { + const match = CAIP_ASSET_TYPE_REGEX.exec(caipAssetType); + if (!match?.groups) { + throw new Error('Invalid CAIP asset type.'); + } + + return { + assetNamespace: match.groups.assetNamespace as CaipAssetNamespace, + assetReference: match.groups.assetReference as CaipAssetReference, + chainId: match.groups.chainId as CaipChainId, + chain: { + namespace: match.groups.namespace as CaipNamespace, + reference: match.groups.reference as CaipReference, + }, + }; +} + /** * Parse a CAIP-19 asset ID to an object containing the chain ID, parsed chain ID, * asset namespace, asset reference, and token ID. @@ -286,7 +315,7 @@ export function parseCaipAccountId(caipAccountId: CaipAccountId): { export function parseCaipAssetId(caipAssetId: CaipAssetId): { assetNamespace: CaipAssetNamespace; assetReference: CaipAssetReference; - tokenId?: CaipTokenId; + tokenId: CaipTokenId; chainId: CaipChainId; chain: { namespace: CaipNamespace; reference: CaipReference }; } { @@ -295,12 +324,10 @@ export function parseCaipAssetId(caipAssetId: CaipAssetId): { throw new Error('Invalid CAIP asset ID.'); } - const tokenId = match.groups.tokenId as CaipTokenId; - return { assetNamespace: match.groups.assetNamespace as CaipAssetNamespace, assetReference: match.groups.assetReference as CaipAssetReference, - ...(tokenId ? { tokenId } : {}), + tokenId: match.groups.tokenId as CaipTokenId, chainId: match.groups.chainId as CaipChainId, chain: { namespace: match.groups.namespace as CaipNamespace, @@ -385,6 +412,56 @@ export function toCaipAccountId( return `${namespace}:${reference}:${accountAddress}`; } +/** + * Asset Type as defined per the CAIP-19 + * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md}. + * + * It defines a way to uniquely identify any blockchain asset in a human-readable + * way. + * + * @param namespace - The standard (ecosystem) of similar blockchains. + * @param reference - Identity of a blockchain within a given namespace. + * @param assetNamespace - The namespace domain of an asset. + * @param assetReference - The identity of an asset within a given namespace. + * @throws {@link Error} + * This exception is thrown if the inputs do not comply with the CAIP-19 + * syntax specification + * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax}. + * @returns A CAIP asset type. + */ +export function toCaipAssetType( + namespace: CaipNamespace, + reference: CaipReference, + assetNamespace: CaipAssetNamespace, + assetReference: CaipAssetReference, +): CaipAssetType { + if (!isCaipNamespace(namespace)) { + throw new Error( + `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, + ); + } + + if (!isCaipReference(reference)) { + throw new Error( + `Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`, + ); + } + + if (!isCaipAssetNamespace(assetNamespace)) { + throw new Error( + `Invalid "assetNamespace", must match: ${CAIP_ASSET_NAMESPACE_REGEX.toString()}`, + ); + } + + if (!isCaipAssetReference(assetReference)) { + throw new Error( + `Invalid "assetReference", must match: ${CAIP_ASSET_REFERENCE_REGEX.toString()}`, + ); + } + + return `${namespace}:${reference}/${assetNamespace}:${assetReference}`; +} + /** * Asset ID as defined per the CAIP-19 * {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md}. @@ -408,8 +485,8 @@ export function toCaipAssetId( reference: CaipReference, assetNamespace: CaipAssetNamespace, assetReference: CaipAssetReference, - tokenId?: CaipTokenId, -): CaipAccountId { + tokenId: CaipTokenId, +): CaipAssetId { if (!isCaipNamespace(namespace)) { throw new Error( `Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`, @@ -434,14 +511,11 @@ export function toCaipAssetId( ); } - // do not throw if tokenId is falsy unless it is an empty string - if ((tokenId && !isCaipTokenId(tokenId)) || tokenId === '') { + if (!isCaipTokenId(tokenId)) { throw new Error( `Invalid "tokenId", must match: ${CAIP_TOKEN_ID_REGEX.toString()}`, ); } - return `${namespace}:${reference}/${assetNamespace}:${assetReference}${ - tokenId ? `/${tokenId}` : '' - }`; + return `${namespace}:${reference}/${assetNamespace}:${assetReference}/${tokenId}`; } diff --git a/src/index.test.ts b/src/index.test.ts index 08810bea9..3ccf23082 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -140,6 +140,7 @@ describe('index', () => { "object", "parseCaipAccountId", "parseCaipAssetId", + "parseCaipAssetType", "parseCaipChainId", "remove0x", "satisfiesVersionRange", @@ -148,6 +149,7 @@ describe('index', () => { "timeSince", "toCaipAccountId", "toCaipAssetId", + "toCaipAssetType", "toCaipChainId", "valueToBytes", "wrapError", diff --git a/src/node.test.ts b/src/node.test.ts index 111e75574..d28d2f3bd 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -145,6 +145,7 @@ describe('node', () => { "object", "parseCaipAccountId", "parseCaipAssetId", + "parseCaipAssetType", "parseCaipChainId", "readFile", "readJsonFile", @@ -155,6 +156,7 @@ describe('node', () => { "timeSince", "toCaipAccountId", "toCaipAssetId", + "toCaipAssetType", "toCaipChainId", "valueToBytes", "wrapError",