From 48267f8e18de9a5480d473c0aa99624bc0468410 Mon Sep 17 00:00:00 2001 From: Eric Wolff Date: Fri, 13 Dec 2024 09:36:10 -0700 Subject: [PATCH] feat: add additional CAIP-19 types and parsing functions to align with proposal (#227) Added full support for [CAIP-19 Asset IDs](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md) including the necessary regex, structs, and parsing functions. A few notes: - While the regex of a chain namespace is technically the same as an asset namespace, the official proposal makes a clear distinction between the two, and so it's within the spirit of the spec to break these out into separate properties. The same cannot be said for a chain reference vs asset reference: these require different regex for validation. - Previously, `CaipAssetId` required a tokenId (`/${string}`) to be present. This has been changed to allow for tokenId to be optional (as dictated by the official proposal) in both the type and its corresponding regex string - For completeness, `toCaipAccountId` and `parseCaipAssetId` functions were added --- src/__fixtures__/caip-types.ts | 29 ++ src/caip-types.test-d.ts | 38 +++ src/caip-types.test.ts | 515 +++++++++++++++++++++++++++++++-- src/caip-types.ts | 280 +++++++++++++++++- src/index.test.ts | 14 + src/node.test.ts | 14 + 6 files changed, 864 insertions(+), 26 deletions(-) diff --git a/src/__fixtures__/caip-types.ts b/src/__fixtures__/caip-types.ts index b0af50db0..a03c5d462 100644 --- a/src/__fixtures__/caip-types.ts +++ b/src/__fixtures__/caip-types.ts @@ -33,3 +33,32 @@ 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_TYPE_FIXTURES = [ + 'eip155:1/slip44:60', + 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f', + 'bip122:000000000019d6689c085ae165831e93/slip44:0', + 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', + 'cosmos:cosmoshub-3/slip44:118', + 'cosmos:Binance-Chain-Tigris/slip44:714', + 'lip9:9ee11e9df416b18b/slip44:134', + '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_TYPE_FIXTURES.map((value) => value.split('/')[1]?.split(':')[0]), + ), +); + +export const CAIP_ASSET_REFERENCE_FIXTURES = Array.from( + new Set( + 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 5b94402ac..0afcfc029 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,30 @@ 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/tokenId', +); +expectAssignable('namespace:reference/:assetReference/'); +expectAssignable(':reference/assetNamespace:/'); +expectAssignable( + `${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}/${embeddedString}`, +); + // Not valid caip strings: expectAssignable('namespace:😀'); @@ -50,3 +78,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..2b7e89507 100644 --- a/src/caip-types.test.ts +++ b/src/caip-types.test.ts @@ -1,24 +1,39 @@ 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, + parseCaipAssetType, parseCaipChainId, + toCaipAccountId, + toCaipAssetId, + toCaipAssetType, toCaipChainId, - KnownCaipNamespace, - CAIP_NAMESPACE_REGEX, - CAIP_REFERENCE_REGEX, } from './caip-types'; describe('isCaipChainId', () => { @@ -151,21 +166,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 +245,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 +412,136 @@ describe('parseCaipAccountId', () => { }); }); +describe('parseCaipAssetType', () => { + it('parses valid asset types', () => { + expect(parseCaipAssetType('eip155:1/slip44:60')).toMatchInlineSnapshot(` + { + "assetNamespace": "slip44", + "assetReference": "60", + "chain": { + "namespace": "eip155", + "reference": "1", + }, + "chainId": "eip155:1", + } + `); + + expect( + parseCaipAssetType('bip122:000000000019d6689c085ae165831e93/slip44:0'), + ).toMatchInlineSnapshot(` + { + "assetNamespace": "slip44", + "assetReference": "0", + "chain": { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + }, + "chainId": "bip122:000000000019d6689c085ae165831e93", + } + `); + + expect(parseCaipAssetType('cosmos:cosmoshub-3/slip44:118')) + .toMatchInlineSnapshot(` + { + "assetNamespace": "slip44", + "assetReference": "118", + "chain": { + "namespace": "cosmos", + "reference": "cosmoshub-3", + }, + "chainId": "cosmos:cosmoshub-3", + } + `); + + expect( + parseCaipAssetType( + '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(() => parseCaipAssetType(input as any)).toThrow( + 'Invalid CAIP asset type.', + ); + }); +}); + +describe('parseCaipAssetId', () => { + it('parses valid asset ids', () => { + expect( + parseCaipAssetId( + '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": "0.0.55492", + "chain": { + "namespace": "hedera", + "reference": "mainnet", + }, + "chainId": "hedera:mainnet", + "tokenId": "12", + } + `); + }); + + 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 +591,292 @@ 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('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( + 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'; + 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 = 'erc721'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + const tokenId = '123'; + expect(() => + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), + ).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 = 'erc721'; + const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f'; + const tokenId = '123'; + expect(() => + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), + ).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'; + const tokenId = '123'; + expect(() => + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), + ).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 = 'erc721'; + const tokenId = '123'; + expect(() => + toCaipAssetId( + namespace, + reference, + assetNamespace, + assetReference, + tokenId, + ), + ).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..5ce5742f0 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,6 +13,12 @@ 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; @@ -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. */ @@ -139,6 +169,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 +272,70 @@ 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. + * + * 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.'); + } + + return { + assetNamespace: match.groups.assetNamespace as CaipAssetNamespace, + assetReference: match.groups.assetReference as CaipAssetReference, + tokenId: match.groups.tokenId as CaipTokenId, + 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 +369,153 @@ 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 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}. + * + * 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, +): CaipAssetId { + 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()}`, + ); + } + + if (!isCaipTokenId(tokenId)) { + throw new Error( + `Invalid "tokenId", must match: ${CAIP_TOKEN_ID_REGEX.toString()}`, + ); + } + + return `${namespace}:${reference}/${assetNamespace}:${assetReference}/${tokenId}`; +} diff --git a/src/index.test.ts b/src/index.test.ts index 9e36ce705..3ccf23082 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,17 @@ describe('index', () => { "numberToHex", "object", "parseCaipAccountId", + "parseCaipAssetId", + "parseCaipAssetType", "parseCaipChainId", "remove0x", "satisfiesVersionRange", "signedBigIntToBytes", "stringToBytes", "timeSince", + "toCaipAccountId", + "toCaipAssetId", + "toCaipAssetType", "toCaipChainId", "valueToBytes", "wrapError", diff --git a/src/node.test.ts b/src/node.test.ts index 5f7d5ed4d..d28d2f3bd 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,8 @@ describe('node', () => { "numberToHex", "object", "parseCaipAccountId", + "parseCaipAssetId", + "parseCaipAssetType", "parseCaipChainId", "readFile", "readJsonFile", @@ -143,6 +154,9 @@ describe('node', () => { "signedBigIntToBytes", "stringToBytes", "timeSince", + "toCaipAccountId", + "toCaipAssetId", + "toCaipAssetType", "toCaipChainId", "valueToBytes", "wrapError",