Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 78ea315

Browse files
committedDec 12, 2024··
feat: add additional CAIP-19 types and parsing functions to align with proposal
1 parent a5886d3 commit 78ea315

File tree

6 files changed

+671
-28
lines changed

6 files changed

+671
-28
lines changed
 

‎src/__fixtures__/caip-types.ts

+34
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,37 @@ export const CAIP_ACCOUNT_ID_FIXTURES = [
3333
export const CAIP_ACCOUNT_ADDRESS_FIXTURES = Array.from(
3434
new Set(CAIP_ACCOUNT_ID_FIXTURES.map((value) => value.split(':')[2])),
3535
);
36+
37+
export const CAIP_ASSET_ID_FIXTURES = [
38+
'eip155:1/slip44:60',
39+
'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
40+
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769',
41+
'bip122:000000000019d6689c085ae165831e93/slip44:0',
42+
'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2',
43+
'cosmos:cosmoshub-3/slip44:118',
44+
'cosmos:Binance-Chain-Tigris/slip44:714',
45+
'lip9:9ee11e9df416b18b/slip44:134',
46+
'hedera:mainnet/nft:0.0.55492/12',
47+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w',
48+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
49+
] as const;
50+
51+
export const CAIP_ASSET_NAMESPACE_FIXTURES = Array.from(
52+
new Set(
53+
CAIP_ASSET_ID_FIXTURES.map((value) => value.split('/')[1]?.split(':')[0]),
54+
),
55+
);
56+
57+
export const CAIP_ASSET_REFERENCE_FIXTURES = Array.from(
58+
new Set(
59+
CAIP_ASSET_ID_FIXTURES.map((value) => value.split('/')[1]?.split(':')[1]),
60+
),
61+
);
62+
63+
export const CAIP_ASSET_TYPE_FIXTURES = Array.from(
64+
new Set(
65+
CAIP_ASSET_ID_FIXTURES.map((value) =>
66+
value.split('/').slice(0, 2).join('/'),
67+
),
68+
),
69+
);

‎src/caip-types.test-d.ts

+44
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { expectAssignable, expectNotAssignable } from 'tsd';
33
import type {
44
CaipAccountAddress,
55
CaipAccountId,
6+
CaipAssetId,
7+
CaipAssetNamespace,
8+
CaipAssetReference,
9+
CaipAssetType,
610
CaipChainId,
711
CaipNamespace,
812
CaipReference,
@@ -33,6 +37,36 @@ expectAssignable<CaipAccountId>(
3337
expectAssignable<CaipAccountAddress>('string');
3438
expectAssignable<CaipAccountAddress>(`${embeddedString}`);
3539

40+
expectAssignable<CaipAssetNamespace>('string');
41+
expectAssignable<CaipAssetNamespace>(`${embeddedString}`);
42+
43+
expectAssignable<CaipAssetReference>('string');
44+
expectAssignable<CaipAssetReference>(`${embeddedString}`);
45+
46+
expectAssignable<CaipAssetType>(
47+
'namespace:reference/assetNamespace:assetReference',
48+
);
49+
expectAssignable<CaipAssetType>('namespace:reference/:');
50+
expectAssignable<CaipAssetType>(':reference/assetNamespace:');
51+
expectAssignable<CaipAssetType>(
52+
`${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}`,
53+
);
54+
55+
expectAssignable<CaipAssetId>(
56+
'namespace:reference/assetNamespace:assetReference',
57+
);
58+
expectAssignable<CaipAssetId>(
59+
'namespace:reference/assetNamespace:assetReference/tokenId',
60+
);
61+
expectAssignable<CaipAssetId>('namespace:reference/:assetReference/');
62+
expectAssignable<CaipAssetId>(':reference/assetNamespace:');
63+
expectAssignable<CaipAssetId>(
64+
`${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}`,
65+
);
66+
expectAssignable<CaipAssetId>(
67+
`${embeddedString}:${embeddedString}/${embeddedString}:${embeddedString}/${embeddedString}`,
68+
);
69+
3670
// Not valid caip strings:
3771

3872
expectAssignable<CaipChainId>('namespace:😀');
@@ -50,3 +84,13 @@ expectNotAssignable<CaipAccountId>(0);
5084
expectNotAssignable<CaipAccountId>('🙃');
5185

5286
expectNotAssignable<CaipAccountAddress>(0);
87+
88+
expectNotAssignable<CaipAssetNamespace>(0);
89+
90+
expectNotAssignable<CaipAssetReference>(0);
91+
92+
expectNotAssignable<CaipAssetType>(0);
93+
expectNotAssignable<CaipAssetType>('🙃');
94+
95+
expectNotAssignable<CaipAssetId>(0);
96+
expectNotAssignable<CaipAssetId>('🙃');

‎src/caip-types.test.ts

+362-25
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,37 @@
11
import {
22
CAIP_ACCOUNT_ADDRESS_FIXTURES,
33
CAIP_ACCOUNT_ID_FIXTURES,
4+
CAIP_ASSET_ID_FIXTURES,
5+
CAIP_ASSET_NAMESPACE_FIXTURES,
6+
CAIP_ASSET_REFERENCE_FIXTURES,
7+
CAIP_ASSET_TYPE_FIXTURES,
48
CAIP_CHAIN_ID_FIXTURES,
59
CAIP_NAMESPACE_FIXTURES,
610
CAIP_REFERENCE_FIXTURES,
711
} from './__fixtures__';
812
import {
13+
CAIP_ACCOUNT_ADDRESS_REGEX,
14+
CAIP_ASSET_NAMESPACE_REGEX,
15+
CAIP_ASSET_REFERENCE_REGEX,
16+
CAIP_NAMESPACE_REGEX,
17+
CAIP_REFERENCE_REGEX,
18+
CAIP_TOKEN_ID_REGEX,
919
isCaipAccountAddress,
1020
isCaipAccountId,
21+
isCaipAssetId,
22+
isCaipAssetNamespace,
23+
isCaipAssetReference,
24+
isCaipAssetType,
1125
isCaipChainId,
1226
isCaipNamespace,
1327
isCaipReference,
14-
isCaipAssetType,
15-
isCaipAssetId,
28+
KnownCaipNamespace,
1629
parseCaipAccountId,
30+
parseCaipAssetId,
1731
parseCaipChainId,
32+
toCaipAccountId,
33+
toCaipAssetId,
1834
toCaipChainId,
19-
KnownCaipNamespace,
20-
CAIP_NAMESPACE_REGEX,
21-
CAIP_REFERENCE_REGEX,
2235
} from './caip-types';
2336

2437
describe('isCaipChainId', () => {
@@ -151,21 +164,53 @@ describe('isCaipAccountAddress', () => {
151164
});
152165
});
153166

154-
describe('isCaipAssetType', () => {
155-
// Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases
167+
describe('isCaipAssetNamespace', () => {
168+
it.each([...CAIP_ASSET_NAMESPACE_FIXTURES])(
169+
'returns true for a valid asset namespace %s',
170+
(assetNamespace) => {
171+
expect(isCaipAssetNamespace(assetNamespace)).toBe(true);
172+
},
173+
);
174+
175+
it.each([true, false, null, undefined, 1, {}, [], 'abC', '12', '123456789'])(
176+
'returns false for an invalid asset namespace %s',
177+
(assetNamespace) => {
178+
expect(isCaipAssetNamespace(assetNamespace)).toBe(false);
179+
},
180+
);
181+
});
182+
183+
describe('isCaipAssetReference', () => {
184+
it.each([...CAIP_ASSET_REFERENCE_FIXTURES])(
185+
'returns true for a valid asset reference %s',
186+
(assetReference) => {
187+
expect(isCaipAssetReference(assetReference)).toBe(true);
188+
},
189+
);
190+
156191
it.each([
157-
'eip155:1/slip44:60',
158-
'bip122:000000000019d6689c085ae165831e93/slip44:0',
159-
'cosmos:cosmoshub-3/slip44:118',
160-
'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2',
161-
'cosmos:Binance-Chain-Tigris/slip44:714',
162-
'cosmos:iov-mainnet/slip44:234',
163-
'lip9:9ee11e9df416b18b/slip44:134',
164-
'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
165-
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d',
166-
])('returns true for a valid asset type %s', (id) => {
167-
expect(isCaipAssetType(id)).toBe(true);
192+
true,
193+
false,
194+
null,
195+
undefined,
196+
1,
197+
{},
198+
[],
199+
'',
200+
'!@#$%^&*()',
201+
Array(129).fill('0').join(''),
202+
])('returns false for an invalid asset reference %s', (assetReference) => {
203+
expect(isCaipAssetReference(assetReference)).toBe(false);
168204
});
205+
});
206+
207+
describe('isCaipAssetType', () => {
208+
it.each([...CAIP_ASSET_TYPE_FIXTURES])(
209+
'returns true for a valid asset type %s',
210+
(assetType) => {
211+
expect(isCaipAssetType(assetType)).toBe(true);
212+
},
213+
);
169214

170215
it.each([
171216
true,
@@ -198,13 +243,12 @@ describe('isCaipAssetType', () => {
198243
});
199244

200245
describe('isCaipAssetId', () => {
201-
// Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases
202-
it.each([
203-
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769',
204-
'hedera:mainnet/nft:0.0.55492/12',
205-
])('returns true for a valid asset id %s', (id) => {
206-
expect(isCaipAssetId(id)).toBe(true);
207-
});
246+
it.each([...CAIP_ASSET_ID_FIXTURES])(
247+
'returns true for a valid asset id %s',
248+
(id) => {
249+
expect(isCaipAssetId(id)).toBe(true);
250+
},
251+
);
208252

209253
it.each([
210254
true,
@@ -366,6 +410,113 @@ describe('parseCaipAccountId', () => {
366410
});
367411
});
368412

413+
describe('parseCaipAssetId', () => {
414+
it('parses valid asset ids', () => {
415+
expect(parseCaipAssetId('eip155:1/slip44:60')).toMatchInlineSnapshot(`
416+
{
417+
"assetNamespace": "slip44",
418+
"assetReference": "60",
419+
"chain": {
420+
"namespace": "eip155",
421+
"reference": "1",
422+
},
423+
"chainId": "eip155:1",
424+
}
425+
`);
426+
427+
expect(
428+
parseCaipAssetId(
429+
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769',
430+
),
431+
).toMatchInlineSnapshot(`
432+
{
433+
"assetNamespace": "erc721",
434+
"assetReference": "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d",
435+
"chain": {
436+
"namespace": "eip155",
437+
"reference": "1",
438+
},
439+
"chainId": "eip155:1",
440+
"tokenId": "771769",
441+
}
442+
`);
443+
444+
expect(parseCaipAssetId('bip122:000000000019d6689c085ae165831e93/slip44:0'))
445+
.toMatchInlineSnapshot(`
446+
{
447+
"assetNamespace": "slip44",
448+
"assetReference": "0",
449+
"chain": {
450+
"namespace": "bip122",
451+
"reference": "000000000019d6689c085ae165831e93",
452+
},
453+
"chainId": "bip122:000000000019d6689c085ae165831e93",
454+
}
455+
`);
456+
457+
expect(parseCaipAssetId('cosmos:cosmoshub-3/slip44:118'))
458+
.toMatchInlineSnapshot(`
459+
{
460+
"assetNamespace": "slip44",
461+
"assetReference": "118",
462+
"chain": {
463+
"namespace": "cosmos",
464+
"reference": "cosmoshub-3",
465+
},
466+
"chainId": "cosmos:cosmoshub-3",
467+
}
468+
`);
469+
470+
expect(parseCaipAssetId('hedera:mainnet/nft:0.0.55492/12'))
471+
.toMatchInlineSnapshot(`
472+
{
473+
"assetNamespace": "nft",
474+
"assetReference": "0.0.55492",
475+
"chain": {
476+
"namespace": "hedera",
477+
"reference": "mainnet",
478+
},
479+
"chainId": "hedera:mainnet",
480+
"tokenId": "12",
481+
}
482+
`);
483+
484+
expect(
485+
parseCaipAssetId(
486+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w',
487+
),
488+
).toMatchInlineSnapshot(`
489+
{
490+
"assetNamespace": "nft",
491+
"assetReference": "Fz6LxeUg5qjesYX3BdmtTwyyzBtMxk644XiTqU5W3w9w",
492+
"chain": {
493+
"namespace": "solana",
494+
"reference": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
495+
},
496+
"chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
497+
}
498+
`);
499+
});
500+
501+
it.each([
502+
true,
503+
false,
504+
null,
505+
undefined,
506+
1,
507+
'foo',
508+
'foobarbazquz:1',
509+
'foo:',
510+
'foo:foobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquz',
511+
'eip155:1',
512+
'eip155:1:',
513+
])('throws for invalid input %s', (input) => {
514+
expect(() => parseCaipAssetId(input as any)).toThrow(
515+
'Invalid CAIP asset ID.',
516+
);
517+
});
518+
});
519+
369520
describe('toCaipChainId', () => {
370521
// This function relies on @metamask/utils CAIP helpers. Those are being
371522
// tested with a variety of inputs.
@@ -415,3 +566,189 @@ describe('toCaipChainId', () => {
415566
);
416567
});
417568
});
569+
570+
describe('toCaipAccountId', () => {
571+
it('returns a valid CAIP-10 account ID when given a valid namespace, reference, and accountAddress', () => {
572+
const namespace = 'eip';
573+
const reference = '1';
574+
const accountAddress = '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb';
575+
expect(toCaipAccountId(namespace, reference, accountAddress)).toBe(
576+
`${namespace}:${reference}:${accountAddress}`,
577+
);
578+
});
579+
580+
it.each([
581+
// Too short, must have 3 chars at least
582+
'',
583+
'xs',
584+
// Not matching
585+
'!@#$%^&*()',
586+
// Too long
587+
'namespacetoolong',
588+
])('throws for invalid namespaces: %s', (namespace) => {
589+
const reference = '1';
590+
const accountAddress = '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb';
591+
expect(() => toCaipAccountId(namespace, reference, accountAddress)).toThrow(
592+
`Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`,
593+
);
594+
});
595+
596+
it.each([
597+
// Too short, must have 1 char at least
598+
'',
599+
// Not matching
600+
'!@#$%^&*()',
601+
// Too long
602+
'012345678901234567890123456789012', // 33 chars
603+
])('throws for invalid reference: %s', (reference) => {
604+
const namespace = 'eip';
605+
const accountAddress = '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb';
606+
expect(() => toCaipAccountId(namespace, reference, accountAddress)).toThrow(
607+
`Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`,
608+
);
609+
});
610+
611+
it.each([
612+
// Too short, must have 1 char at least
613+
'',
614+
// Not matching
615+
'!@#$%^&*()',
616+
// Too long
617+
Array(129).fill('0').join(''),
618+
])('throws for invalid accountAddress: %s', (accountAddress) => {
619+
const namespace = 'eip';
620+
const reference = '1';
621+
expect(() => toCaipAccountId(namespace, reference, accountAddress)).toThrow(
622+
`Invalid "accountAddress", must match: ${CAIP_ACCOUNT_ADDRESS_REGEX.toString()}`,
623+
);
624+
});
625+
});
626+
627+
describe('toCaipAssetId', () => {
628+
it('returns a valid CAIP-19 asset ID when given a valid namespace, reference, assetNamespace, and assetReference', () => {
629+
const namespace = 'eip';
630+
const reference = '1';
631+
const assetNamespace = 'erc20';
632+
const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f';
633+
expect(
634+
toCaipAssetId(namespace, reference, assetNamespace, assetReference),
635+
).toBe(`${namespace}:${reference}/${assetNamespace}:${assetReference}`);
636+
});
637+
638+
it('returns a valid CAIP-19 asset ID when given a valid namespace, reference, assetNamespace, assetReference, and tokenId', () => {
639+
const namespace = 'eip';
640+
const reference = '1';
641+
const assetNamespace = 'erc721';
642+
const assetReference = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d';
643+
const tokenId = '771769';
644+
expect(
645+
toCaipAssetId(
646+
namespace,
647+
reference,
648+
assetNamespace,
649+
assetReference,
650+
tokenId,
651+
),
652+
).toBe(
653+
`${namespace}:${reference}/${assetNamespace}:${assetReference}/${tokenId}`,
654+
);
655+
});
656+
657+
it.each([
658+
// Too short, must have 3 chars at least
659+
'',
660+
'xs',
661+
// Not matching
662+
'!@#$%^&*()',
663+
// Too long
664+
'namespacetoolong',
665+
])('throws for invalid namespaces: %s', (namespace) => {
666+
const reference = '1';
667+
const assetNamespace = 'erc20';
668+
const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f';
669+
expect(() =>
670+
toCaipAssetId(namespace, reference, assetNamespace, assetReference),
671+
).toThrow(
672+
`Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`,
673+
);
674+
});
675+
676+
it.each([
677+
// Too short, must have 1 char at least
678+
'',
679+
// Not matching
680+
'!@#$%^&*()',
681+
// Too long
682+
'012345678901234567890123456789012', // 33 chars
683+
])('throws for invalid reference: %s', (reference) => {
684+
const namespace = 'eip';
685+
const assetNamespace = 'erc20';
686+
const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f';
687+
expect(() =>
688+
toCaipAssetId(namespace, reference, assetNamespace, assetReference),
689+
).toThrow(
690+
`Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`,
691+
);
692+
});
693+
694+
it.each([
695+
// Too short, must have 1 char at least
696+
'',
697+
// Not matching
698+
'!@#$%^&*',
699+
// Too long
700+
'012345789',
701+
])('throws for invalid assetNamespace: %s', (assetNamespace) => {
702+
const namespace = 'eip';
703+
const reference = '1';
704+
const assetReference = '0x6b175474e89094c44da98b954eedeac495271d0f';
705+
expect(() =>
706+
toCaipAssetId(namespace, reference, assetNamespace, assetReference),
707+
).toThrow(
708+
`Invalid "assetNamespace", must match: ${CAIP_ASSET_NAMESPACE_REGEX.toString()}`,
709+
);
710+
});
711+
712+
it.each([
713+
// Too short, must have 1 char at least
714+
'',
715+
// Not matching
716+
'!@#$%^&*()',
717+
// Too long
718+
Array(129).fill('0').join(''),
719+
])('throws for invalid assetReference: %s', (assetReference) => {
720+
const namespace = 'eip';
721+
const reference = '1';
722+
const assetNamespace = 'erc20';
723+
expect(() =>
724+
toCaipAssetId(namespace, reference, assetNamespace, assetReference),
725+
).toThrow(
726+
`Invalid "assetReference", must match: ${CAIP_ASSET_REFERENCE_REGEX.toString()}`,
727+
);
728+
});
729+
730+
it.each([
731+
// Too short, must have 1 char at least
732+
'',
733+
// Not matching
734+
'!@#$%^&*()',
735+
// Too long
736+
Array(79).fill('0').join(''),
737+
])('throws for invalid tokenId: %s', (tokenId) => {
738+
const namespace = 'eip';
739+
const reference = '1';
740+
const assetNamespace = 'erc721';
741+
const assetReference = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d';
742+
expect(() =>
743+
toCaipAssetId(
744+
namespace,
745+
reference,
746+
assetNamespace,
747+
assetReference,
748+
tokenId,
749+
),
750+
).toThrow(
751+
`Invalid "tokenId", must match: ${CAIP_TOKEN_ID_REGEX.toString()}`,
752+
);
753+
});
754+
});

‎src/caip-types.ts

+207-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { is, pattern, string } from '@metamask/superstruct';
21
import type { Infer, Struct } from '@metamask/superstruct';
2+
import { is, pattern, string } from '@metamask/superstruct';
33

44
export const CAIP_CHAIN_ID_REGEX =
55
/^(?<namespace>[-a-z0-9]{3,8}):(?<reference>[-_a-zA-Z0-9]{1,32})$/u;
@@ -13,11 +13,17 @@ export const CAIP_ACCOUNT_ID_REGEX =
1313

1414
export const CAIP_ACCOUNT_ADDRESS_REGEX = /^[-.%a-zA-Z0-9]{1,128}$/u;
1515

16+
export const CAIP_ASSET_NAMESPACE_REGEX = /^[-a-z0-9]{3,8}$/u;
17+
18+
export const CAIP_ASSET_REFERENCE_REGEX = /^[-.%a-zA-Z0-9]{1,128}$/u;
19+
20+
export const CAIP_TOKEN_ID_REGEX = /^[-.%a-zA-Z0-9]{1,78}$/u;
21+
1622
export const CAIP_ASSET_TYPE_REGEX =
1723
/^(?<chainId>(?<namespace>[-a-z0-9]{3,8}):(?<reference>[-_a-zA-Z0-9]{1,32}))\/(?<assetNamespace>[-a-z0-9]{3,8}):(?<assetReference>[-.%a-zA-Z0-9]{1,128})$/u;
1824

1925
export const CAIP_ASSET_ID_REGEX =
20-
/^(?<chainId>(?<namespace>[-a-z0-9]{3,8}):(?<reference>[-_a-zA-Z0-9]{1,32}))\/(?<assetNamespace>[-a-z0-9]{3,8}):(?<assetReference>[-.%a-zA-Z0-9]{1,128})\/(?<tokenId>[-.%a-zA-Z0-9]{1,78})$/u;
26+
/^(?<chainId>(?<namespace>[-a-z0-9]{3,8}):(?<reference>[-_a-zA-Z0-9]{1,32}))\/(?<assetNamespace>[-a-z0-9]{3,8}):(?<assetReference>[-.%a-zA-Z0-9]{1,128})(\/(?<tokenId>[-.%a-zA-Z0-9]{1,78}))?$/u;
2127

2228
/**
2329
* A CAIP-2 chain ID, i.e., a human-readable namespace and reference.
@@ -58,6 +64,30 @@ export const CaipAccountAddressStruct = pattern(
5864
);
5965
export type CaipAccountAddress = Infer<typeof CaipAccountAddressStruct>;
6066

67+
/**
68+
* A CAIP-19 asset namespace, i.e., a namespace domain of an asset.
69+
*/
70+
export const CaipAssetNamespaceStruct = pattern(
71+
string(),
72+
CAIP_ASSET_NAMESPACE_REGEX,
73+
);
74+
export type CaipAssetNamespace = Infer<typeof CaipAssetNamespaceStruct>;
75+
76+
/**
77+
* A CAIP-19 asset reference, i.e., an identifier for an asset within a given namespace.
78+
*/
79+
export const CaipAssetReferenceStruct = pattern(
80+
string(),
81+
CAIP_ASSET_REFERENCE_REGEX,
82+
);
83+
export type CaipAssetReference = Infer<typeof CaipAssetReferenceStruct>;
84+
85+
/**
86+
* A CAIP-19 asset token ID, i.e., a unique identifier for an addressable asset of a given type
87+
*/
88+
export const CaipTokenIdStruct = pattern(string(), CAIP_TOKEN_ID_REGEX);
89+
export type CaipTokenId = Infer<typeof CaipTokenIdStruct>;
90+
6191
/**
6292
* A CAIP-19 asset type identifier, i.e., a human-readable type of asset identifier.
6393
*/
@@ -74,7 +104,9 @@ export const CaipAssetIdStruct = pattern(
74104
string(),
75105
CAIP_ASSET_ID_REGEX,
76106
) as Struct<CaipAssetId, null>;
77-
export type CaipAssetId = `${string}:${string}/${string}:${string}/${string}`;
107+
export type CaipAssetId =
108+
| `${string}:${string}/${string}:${string}`
109+
| `${string}:${string}/${string}:${string}/${string}`;
78110

79111
/** Known CAIP namespaces. */
80112
export enum KnownCaipNamespace {
@@ -139,6 +171,40 @@ export function isCaipAccountAddress(
139171
return is(value, CaipAccountAddressStruct);
140172
}
141173

174+
/**
175+
* Check if the given value is a {@link CaipAssetNamespace}.
176+
*
177+
* @param value - The value to check.
178+
* @returns Whether the value is a {@link CaipAssetNamespace}.
179+
*/
180+
export function isCaipAssetNamespace(
181+
value: unknown,
182+
): value is CaipAssetNamespace {
183+
return is(value, CaipAssetNamespaceStruct);
184+
}
185+
186+
/**
187+
* Check if the given value is a {@link CaipAssetReference}.
188+
*
189+
* @param value - The value to check.
190+
* @returns Whether the value is a {@link CaipAssetReference}.
191+
*/
192+
export function isCaipAssetReference(
193+
value: unknown,
194+
): value is CaipAssetReference {
195+
return is(value, CaipAssetReferenceStruct);
196+
}
197+
198+
/**
199+
* Check if the given value is a {@link CaipTokenId}.
200+
*
201+
* @param value - The value to check.
202+
* @returns Whether the value is a {@link CaipTokenId}.
203+
*/
204+
export function isCaipTokenId(value: unknown): value is CaipTokenId {
205+
return is(value, CaipTokenIdStruct);
206+
}
207+
142208
/**
143209
* Check if the given value is a {@link CaipAssetType}.
144210
*
@@ -208,6 +274,41 @@ export function parseCaipAccountId(caipAccountId: CaipAccountId): {
208274
};
209275
}
210276

277+
/**
278+
* Parse a CAIP-19 asset ID to an object containing the chain ID, parsed chain ID,
279+
* asset namespace, asset reference, and token ID.
280+
*
281+
* This validates the CAIP-19 asset ID before parsing it.
282+
*
283+
* @param caipAssetId - The CAIP-19 asset ID to validate and parse.
284+
* @returns The parsed CAIP-19 asset ID.
285+
*/
286+
export function parseCaipAssetId(caipAssetId: CaipAssetId): {
287+
assetNamespace: CaipAssetNamespace;
288+
assetReference: CaipAssetReference;
289+
tokenId?: CaipTokenId;
290+
chainId: CaipChainId;
291+
chain: { namespace: CaipNamespace; reference: CaipReference };
292+
} {
293+
const match = CAIP_ASSET_ID_REGEX.exec(caipAssetId);
294+
if (!match?.groups) {
295+
throw new Error('Invalid CAIP asset ID.');
296+
}
297+
298+
const tokenId = match.groups.tokenId as CaipTokenId;
299+
300+
return {
301+
assetNamespace: match.groups.assetNamespace as CaipAssetNamespace,
302+
assetReference: match.groups.assetReference as CaipAssetReference,
303+
...(tokenId ? { tokenId } : {}),
304+
chainId: match.groups.chainId as CaipChainId,
305+
chain: {
306+
namespace: match.groups.namespace as CaipNamespace,
307+
reference: match.groups.reference as CaipReference,
308+
},
309+
};
310+
}
311+
211312
/**
212313
* Chain ID as defined per the CAIP-2
213314
* {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md}.
@@ -241,3 +342,106 @@ export function toCaipChainId(
241342

242343
return `${namespace}:${reference}`;
243344
}
345+
346+
/**
347+
* Account ID as defined per the CAIP-10
348+
* {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md}.
349+
*
350+
* It defines a way to uniquely identify any blockchain account in a human-readable
351+
* way.
352+
*
353+
* @param namespace - The standard (ecosystem) of similar blockchains.
354+
* @param reference - Identity of a blockchain within a given namespace.
355+
* @param accountAddress - The address of the blockchain account.
356+
* @throws {@link Error}
357+
* This exception is thrown if the inputs do not comply with the CAIP-10
358+
* syntax specification
359+
* {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md#syntax}.
360+
* @returns A CAIP account ID.
361+
*/
362+
export function toCaipAccountId(
363+
namespace: CaipNamespace,
364+
reference: CaipReference,
365+
accountAddress: CaipAccountAddress,
366+
): CaipAccountId {
367+
if (!isCaipNamespace(namespace)) {
368+
throw new Error(
369+
`Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`,
370+
);
371+
}
372+
373+
if (!isCaipReference(reference)) {
374+
throw new Error(
375+
`Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`,
376+
);
377+
}
378+
379+
if (!isCaipAccountAddress(accountAddress)) {
380+
throw new Error(
381+
`Invalid "accountAddress", must match: ${CAIP_ACCOUNT_ADDRESS_REGEX.toString()}`,
382+
);
383+
}
384+
385+
return `${namespace}:${reference}:${accountAddress}`;
386+
}
387+
388+
/**
389+
* Asset ID as defined per the CAIP-19
390+
* {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md}.
391+
*
392+
* It defines a way to uniquely identify any blockchain asset in a human-readable
393+
* way.
394+
*
395+
* @param namespace - The standard (ecosystem) of similar blockchains.
396+
* @param reference - Identity of a blockchain within a given namespace.
397+
* @param assetNamespace - The namespace domain of an asset.
398+
* @param assetReference - The identity of an asset within a given namespace.
399+
* @param tokenId - The unique identifier for an addressable asset of a given type.
400+
* @throws {@link Error}
401+
* This exception is thrown if the inputs do not comply with the CAIP-19
402+
* syntax specification
403+
* {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax}.
404+
* @returns A CAIP asset ID.
405+
*/
406+
export function toCaipAssetId(
407+
namespace: CaipNamespace,
408+
reference: CaipReference,
409+
assetNamespace: CaipAssetNamespace,
410+
assetReference: CaipAssetReference,
411+
tokenId?: CaipTokenId,
412+
): CaipAccountId {
413+
if (!isCaipNamespace(namespace)) {
414+
throw new Error(
415+
`Invalid "namespace", must match: ${CAIP_NAMESPACE_REGEX.toString()}`,
416+
);
417+
}
418+
419+
if (!isCaipReference(reference)) {
420+
throw new Error(
421+
`Invalid "reference", must match: ${CAIP_REFERENCE_REGEX.toString()}`,
422+
);
423+
}
424+
425+
if (!isCaipAssetNamespace(assetNamespace)) {
426+
throw new Error(
427+
`Invalid "assetNamespace", must match: ${CAIP_ASSET_NAMESPACE_REGEX.toString()}`,
428+
);
429+
}
430+
431+
if (!isCaipAssetReference(assetReference)) {
432+
throw new Error(
433+
`Invalid "assetReference", must match: ${CAIP_ASSET_REFERENCE_REGEX.toString()}`,
434+
);
435+
}
436+
437+
// do not throw if tokenId is falsy unless it is an empty string
438+
if ((tokenId && !isCaipTokenId(tokenId)) || tokenId === '') {
439+
throw new Error(
440+
`Invalid "tokenId", must match: ${CAIP_TOKEN_ID_REGEX.toString()}`,
441+
);
442+
}
443+
444+
return `${namespace}:${reference}/${assetNamespace}:${assetReference}${
445+
tokenId ? `/${tokenId}` : ''
446+
}`;
447+
}

‎src/index.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,23 @@ describe('index', () => {
88
"CAIP_ACCOUNT_ADDRESS_REGEX",
99
"CAIP_ACCOUNT_ID_REGEX",
1010
"CAIP_ASSET_ID_REGEX",
11+
"CAIP_ASSET_NAMESPACE_REGEX",
12+
"CAIP_ASSET_REFERENCE_REGEX",
1113
"CAIP_ASSET_TYPE_REGEX",
1214
"CAIP_CHAIN_ID_REGEX",
1315
"CAIP_NAMESPACE_REGEX",
1416
"CAIP_REFERENCE_REGEX",
17+
"CAIP_TOKEN_ID_REGEX",
1518
"CaipAccountAddressStruct",
1619
"CaipAccountIdStruct",
1720
"CaipAssetIdStruct",
21+
"CaipAssetNamespaceStruct",
22+
"CaipAssetReferenceStruct",
1823
"CaipAssetTypeStruct",
1924
"CaipChainIdStruct",
2025
"CaipNamespaceStruct",
2126
"CaipReferenceStruct",
27+
"CaipTokenIdStruct",
2228
"ChecksumStruct",
2329
"Duration",
2430
"ESCAPE_CHARACTERS_REGEXP",
@@ -100,10 +106,13 @@ describe('index', () => {
100106
"isCaipAccountAddress",
101107
"isCaipAccountId",
102108
"isCaipAssetId",
109+
"isCaipAssetNamespace",
110+
"isCaipAssetReference",
103111
"isCaipAssetType",
104112
"isCaipChainId",
105113
"isCaipNamespace",
106114
"isCaipReference",
115+
"isCaipTokenId",
107116
"isErrorWithCode",
108117
"isErrorWithMessage",
109118
"isErrorWithStack",
@@ -130,12 +139,15 @@ describe('index', () => {
130139
"numberToHex",
131140
"object",
132141
"parseCaipAccountId",
142+
"parseCaipAssetId",
133143
"parseCaipChainId",
134144
"remove0x",
135145
"satisfiesVersionRange",
136146
"signedBigIntToBytes",
137147
"stringToBytes",
138148
"timeSince",
149+
"toCaipAccountId",
150+
"toCaipAssetId",
139151
"toCaipChainId",
140152
"valueToBytes",
141153
"wrapError",

‎src/node.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,23 @@ describe('node', () => {
88
"CAIP_ACCOUNT_ADDRESS_REGEX",
99
"CAIP_ACCOUNT_ID_REGEX",
1010
"CAIP_ASSET_ID_REGEX",
11+
"CAIP_ASSET_NAMESPACE_REGEX",
12+
"CAIP_ASSET_REFERENCE_REGEX",
1113
"CAIP_ASSET_TYPE_REGEX",
1214
"CAIP_CHAIN_ID_REGEX",
1315
"CAIP_NAMESPACE_REGEX",
1416
"CAIP_REFERENCE_REGEX",
17+
"CAIP_TOKEN_ID_REGEX",
1518
"CaipAccountAddressStruct",
1619
"CaipAccountIdStruct",
1720
"CaipAssetIdStruct",
21+
"CaipAssetNamespaceStruct",
22+
"CaipAssetReferenceStruct",
1823
"CaipAssetTypeStruct",
1924
"CaipChainIdStruct",
2025
"CaipNamespaceStruct",
2126
"CaipReferenceStruct",
27+
"CaipTokenIdStruct",
2228
"ChecksumStruct",
2329
"Duration",
2430
"ESCAPE_CHARACTERS_REGEXP",
@@ -105,10 +111,13 @@ describe('node', () => {
105111
"isCaipAccountAddress",
106112
"isCaipAccountId",
107113
"isCaipAssetId",
114+
"isCaipAssetNamespace",
115+
"isCaipAssetReference",
108116
"isCaipAssetType",
109117
"isCaipChainId",
110118
"isCaipNamespace",
111119
"isCaipReference",
120+
"isCaipTokenId",
112121
"isErrorWithCode",
113122
"isErrorWithMessage",
114123
"isErrorWithStack",
@@ -135,6 +144,7 @@ describe('node', () => {
135144
"numberToHex",
136145
"object",
137146
"parseCaipAccountId",
147+
"parseCaipAssetId",
138148
"parseCaipChainId",
139149
"readFile",
140150
"readJsonFile",
@@ -143,6 +153,8 @@ describe('node', () => {
143153
"signedBigIntToBytes",
144154
"stringToBytes",
145155
"timeSince",
156+
"toCaipAccountId",
157+
"toCaipAssetId",
146158
"toCaipChainId",
147159
"valueToBytes",
148160
"wrapError",

0 commit comments

Comments
 (0)
Please sign in to comment.