From 7e5ef646e791b9dee57a348f9095ea44075a6075 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 12 Jul 2023 14:29:32 -0700 Subject: [PATCH 01/12] add caip chain id --- src/caip-chaid-id.test-d.ts | 23 +++++++++ src/caip-chain-id.test.ts | 94 +++++++++++++++++++++++++++++++++++++ src/caip-chain-id.ts | 46 ++++++++++++++++++ src/index.ts | 1 + 4 files changed, 164 insertions(+) create mode 100644 src/caip-chaid-id.test-d.ts create mode 100644 src/caip-chain-id.test.ts create mode 100644 src/caip-chain-id.ts diff --git a/src/caip-chaid-id.test-d.ts b/src/caip-chaid-id.test-d.ts new file mode 100644 index 00000000..8f8ce007 --- /dev/null +++ b/src/caip-chaid-id.test-d.ts @@ -0,0 +1,23 @@ +import { expectAssignable, expectNotAssignable } from 'tsd'; + +import { CaipChainId } from '.'; + +// Valid caip chain id strings: + +expectAssignable('namespace:reference'); + +const embeddedString = 'test'; +expectAssignable(`${embeddedString}:${embeddedString}`); + +// Not valid caip chain id strings: + +expectAssignable('namespace:😀'); +expectAssignable('😀:reference'); + +expectNotAssignable(0); + +expectNotAssignable('namespace:'); + +expectNotAssignable(':reference'); + +expectNotAssignable('🙃'); diff --git a/src/caip-chain-id.test.ts b/src/caip-chain-id.test.ts new file mode 100644 index 00000000..2bd69b1d --- /dev/null +++ b/src/caip-chain-id.test.ts @@ -0,0 +1,94 @@ +import { + isCaipChainIdString, + assertIsCaipChainIdString, + getCaipChainIdString, +} from './caip-chain-id'; + +const validCaipChainIdStrings = [ + '123:a', + '12345678:a', + 'abc:1', + 'abc:1234567890abcdefghijklmnopqrst32', + '12345678:1234567890abcdefghijklmnopqrst32', + 'az-45678:abcxyz_1234567890-ABCXYZ', + 'eip155:1', + 'bip122:000000000019d6689c085ae165831e93', + 'bip122:12a765e31ffd4059bada1e25190f6e98', + 'bip122:fdbe99b90c90bae7505796461471d89a', + 'cosmos:cosmoshub-2', + 'cosmos:cosmoshub-3', + 'cosmos:Binance-Chain-Tigris', + 'cosmos:iov-mainnet', + 'starknet:SN_GOERLI', + 'lip9:9ee11e9df416b18b', + 'chainstd:8c3444cf8970a9e41a706fab93e7a6c4', +] as const; + +const invalidCaipChainIdStrings = [ + true, + false, + null, + undefined, + 0, + 1, + {}, + [], + '12:a', + '123456789:a', + 'abc:', + 'abc:1234567890abcdefghijklmnopqrstu33', + 'abc', + '123::a', + '123:a:a', + ':123:a', + 'Abc:1', + 'abc!@#$:1', + 'abc:!@#$%^&*()123', +] as const; + +describe('isCaipChainIdString', () => { + it.each(validCaipChainIdStrings)( + 'returns true for a valid caip chain id string', + (caipChainIdString) => { + expect(isCaipChainIdString(caipChainIdString)).toBe(true); + }, + ); + + it.each(invalidCaipChainIdStrings)( + 'returns false for an invalid caip chain id string', + (caipChainIdString) => { + expect(isCaipChainIdString(caipChainIdString)).toBe(false); + }, + ); +}); + +describe('assertIsCaipChainIdString', () => { + it.each(validCaipChainIdStrings)( + 'does not throw for a valid caip chain id string', + (caipChainIdString) => { + expect(() => assertIsCaipChainIdString(caipChainIdString)).not.toThrow(); + }, + ); + + it.each(invalidCaipChainIdStrings)( + 'throws for an invalid caip chain id string', + (caipChainIdString) => { + expect(() => assertIsCaipChainIdString(caipChainIdString)).toThrow( + 'Value must be a caip chain id string.', + ); + }, + ); +}); + +describe('getCaipChainIdString', () => { + it('returns the unvalidated caip chain id string', () => { + expect(getCaipChainIdString('eip155', '1')).toBe('eip155:1'); + expect(getCaipChainIdString('namespace', 'reference')).toBe( + 'namespace:reference', + ); + expect(getCaipChainIdString('', '')).toBe(':'); + expect(getCaipChainIdString('UNVALIDATED', '!@#$%^&*()')).toBe( + 'UNVALIDATED:!@#$%^&*()', + ); + }); +}); diff --git a/src/caip-chain-id.ts b/src/caip-chain-id.ts new file mode 100644 index 00000000..3ad7408b --- /dev/null +++ b/src/caip-chain-id.ts @@ -0,0 +1,46 @@ +import { is, pattern, string, Struct } from 'superstruct'; + +import { assert } from './assert'; + +export type CaipChainId = `${string}:${string}`; + +export const CaipChainIdStruct = pattern( + string(), + /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u, +) as Struct; + +/** + * Check if a string is a valid caip chain id string. + * + * @param value - The value to check. + * @returns Whether the value is a valid caip chain id string. + */ +export function isCaipChainIdString(value: unknown): value is CaipChainId { + return is(value, CaipChainIdStruct); +} + +/** + * Assert that a value is a valid caip chain id string. + * + * @param value - The value to check. + * @throws If the value is not a valid caip chain id string. + */ +export function assertIsCaipChainIdString( + value: unknown, +): asserts value is CaipChainId { + assert(isCaipChainIdString(value), 'Value must be a caip chain id string.'); +} + +/** + * Returns caip chain id string from namespace and reference. + * + * @param namespace - The caip chain id namespace string. + * @param reference - The caip chaid id reference string. + * @returns The unvalidated caip chaid id string. + */ +export function getCaipChainIdString( + namespace: string, + reference: string, +): string { + return `${namespace}:${reference}`; +} diff --git a/src/index.ts b/src/index.ts index 094c4669..7186f169 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './assert'; export * from './base64'; export * from './bytes'; +export * from './caip-chain-id'; export * from './checksum'; export * from './coercers'; export * from './collections'; From 05127fce45d45e40f05cd62abd80cb34fe9ace8e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 12 Jul 2023 14:59:19 -0700 Subject: [PATCH 02/12] add parsing --- src/caip-chaid-id.test-d.ts | 8 ++++---- src/caip-chain-id.test.ts | 33 +++++++++++++++++++++++++++++++++ src/caip-chain-id.ts | 19 +++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/caip-chaid-id.test-d.ts b/src/caip-chaid-id.test-d.ts index 8f8ce007..1a1fcdb9 100644 --- a/src/caip-chaid-id.test-d.ts +++ b/src/caip-chaid-id.test-d.ts @@ -6,6 +6,10 @@ import { CaipChainId } from '.'; expectAssignable('namespace:reference'); +expectAssignable('namespace:'); + +expectAssignable(':reference'); + const embeddedString = 'test'; expectAssignable(`${embeddedString}:${embeddedString}`); @@ -16,8 +20,4 @@ expectAssignable('😀:reference'); expectNotAssignable(0); -expectNotAssignable('namespace:'); - -expectNotAssignable(':reference'); - expectNotAssignable('🙃'); diff --git a/src/caip-chain-id.test.ts b/src/caip-chain-id.test.ts index 2bd69b1d..01e69f7f 100644 --- a/src/caip-chain-id.test.ts +++ b/src/caip-chain-id.test.ts @@ -2,6 +2,7 @@ import { isCaipChainIdString, assertIsCaipChainIdString, getCaipChainIdString, + parseCaipChainIdString, } from './caip-chain-id'; const validCaipChainIdStrings = [ @@ -92,3 +93,35 @@ describe('getCaipChainIdString', () => { ); }); }); + +describe('parseCaipChainIdString', () => { + it('returns the unvalidated caip chain id namespace and reference', () => { + expect(parseCaipChainIdString('eip155:1')).toStrictEqual({ + namespace: 'eip155', + reference: '1', + }); + expect(parseCaipChainIdString('namespace:reference')).toStrictEqual({ + namespace: 'namespace', + reference: 'reference', + }); + expect(parseCaipChainIdString('abc:123:xyz')).toStrictEqual({ + namespace: 'abc', + reference: '123', + }); + }); + + it('returns empty string for missing caip chain id namespace or reference', () => { + expect(parseCaipChainIdString(':')).toStrictEqual({ + namespace: '', + reference: '', + }); + expect(parseCaipChainIdString('')).toStrictEqual({ + namespace: '', + reference: '', + }); + expect(parseCaipChainIdString(':abc:123:xyz')).toStrictEqual({ + namespace: '', + reference: 'abc', + }); + }); +}); diff --git a/src/caip-chain-id.ts b/src/caip-chain-id.ts index 3ad7408b..adc54ec9 100644 --- a/src/caip-chain-id.ts +++ b/src/caip-chain-id.ts @@ -9,6 +9,11 @@ export const CaipChainIdStruct = pattern( /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u, ) as Struct; +export type ParsedCaipChainId = { + namespace: string; + reference: string; +}; + /** * Check if a string is a valid caip chain id string. * @@ -44,3 +49,17 @@ export function getCaipChainIdString( ): string { return `${namespace}:${reference}`; } + +/** + * Returns the namespace and reference strings from caip chain id string. + * + * @param caipChainId - The caip chain id string. + * @returns The {@link ParsedCaipChainId} object. + */ +export function parseCaipChainIdString(caipChainId: string): ParsedCaipChainId { + const [namespace, reference] = caipChainId.split(':'); + return { + namespace: namespace || '', + reference: reference || '', + }; +} From 0478ad23e782638abd1799e120116cb60fbb3e8f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 12 Jul 2023 16:48:55 -0700 Subject: [PATCH 03/12] Update parseCaipChainIdString behavior --- src/caip-chain-id.test.ts | 22 +++++++++++++++------- src/caip-chain-id.ts | 6 ++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/caip-chain-id.test.ts b/src/caip-chain-id.test.ts index 01e69f7f..950130fe 100644 --- a/src/caip-chain-id.test.ts +++ b/src/caip-chain-id.test.ts @@ -95,22 +95,30 @@ describe('getCaipChainIdString', () => { }); describe('parseCaipChainIdString', () => { - it('returns the unvalidated caip chain id namespace and reference', () => { + it('returns the namespace and reference for valid caip chain id', () => { expect(parseCaipChainIdString('eip155:1')).toStrictEqual({ namespace: 'eip155', reference: '1', }); - expect(parseCaipChainIdString('namespace:reference')).toStrictEqual({ - namespace: 'namespace', + expect(parseCaipChainIdString('name:reference')).toStrictEqual({ + namespace: 'name', reference: 'reference', }); - expect(parseCaipChainIdString('abc:123:xyz')).toStrictEqual({ + expect(parseCaipChainIdString('abc:123')).toStrictEqual({ namespace: 'abc', reference: '123', }); }); - it('returns empty string for missing caip chain id namespace or reference', () => { + it('returns empty strings for invalid caip chain id', () => { + expect(parseCaipChainIdString('12:a')).toStrictEqual({ + namespace: '', + reference: '', + }); + expect(parseCaipChainIdString('abc:')).toStrictEqual({ + namespace: '', + reference: '', + }); expect(parseCaipChainIdString(':')).toStrictEqual({ namespace: '', reference: '', @@ -119,9 +127,9 @@ describe('parseCaipChainIdString', () => { namespace: '', reference: '', }); - expect(parseCaipChainIdString(':abc:123:xyz')).toStrictEqual({ + expect(parseCaipChainIdString('abc:123:xyz')).toStrictEqual({ namespace: '', - reference: 'abc', + reference: '', }); }); }); diff --git a/src/caip-chain-id.ts b/src/caip-chain-id.ts index adc54ec9..3f55d24c 100644 --- a/src/caip-chain-id.ts +++ b/src/caip-chain-id.ts @@ -4,9 +4,11 @@ import { assert } from './assert'; export type CaipChainId = `${string}:${string}`; +const CAIP2_REGEX = /^([-a-z0-9]{3,8}):([-_a-zA-Z0-9]{1,32})$/u + export const CaipChainIdStruct = pattern( string(), - /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u, + CAIP2_REGEX, ) as Struct; export type ParsedCaipChainId = { @@ -57,7 +59,7 @@ export function getCaipChainIdString( * @returns The {@link ParsedCaipChainId} object. */ export function parseCaipChainIdString(caipChainId: string): ParsedCaipChainId { - const [namespace, reference] = caipChainId.split(':'); + const [_, namespace, reference] = caipChainId.match(CAIP2_REGEX) || [] return { namespace: namespace || '', reference: reference || '', From 0304a0911818e64072e7818ca72be4926fbfa4a6 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 12 Jul 2023 16:58:28 -0700 Subject: [PATCH 04/12] lint --- src/caip-chain-id.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/caip-chain-id.ts b/src/caip-chain-id.ts index 3f55d24c..7e36552e 100644 --- a/src/caip-chain-id.ts +++ b/src/caip-chain-id.ts @@ -4,12 +4,12 @@ import { assert } from './assert'; export type CaipChainId = `${string}:${string}`; -const CAIP2_REGEX = /^([-a-z0-9]{3,8}):([-_a-zA-Z0-9]{1,32})$/u +const CAIP2_REGEX = /^([-a-z0-9]{3,8}):([-_a-zA-Z0-9]{1,32})$/u; -export const CaipChainIdStruct = pattern( - string(), - CAIP2_REGEX, -) as Struct; +export const CaipChainIdStruct = pattern(string(), CAIP2_REGEX) as Struct< + CaipChainId, + null +>; export type ParsedCaipChainId = { namespace: string; @@ -59,9 +59,9 @@ export function getCaipChainIdString( * @returns The {@link ParsedCaipChainId} object. */ export function parseCaipChainIdString(caipChainId: string): ParsedCaipChainId { - const [_, namespace, reference] = caipChainId.match(CAIP2_REGEX) || [] + const [, namespace, reference] = caipChainId.match(CAIP2_REGEX) ?? []; return { - namespace: namespace || '', - reference: reference || '', + namespace: namespace ?? '', + reference: reference ?? '', }; } From 782e4fba023fdd148dc3be1eccd36614e06dc42e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 18 Jul 2023 15:00:55 -0700 Subject: [PATCH 05/12] drop string suffix --- src/caip-chain-id.test.ts | 70 ++++++++++++++++++--------------------- src/caip-chain-id.ts | 16 ++++----- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/caip-chain-id.test.ts b/src/caip-chain-id.test.ts index 950130fe..c10a1323 100644 --- a/src/caip-chain-id.test.ts +++ b/src/caip-chain-id.test.ts @@ -1,11 +1,11 @@ import { - isCaipChainIdString, - assertIsCaipChainIdString, - getCaipChainIdString, - parseCaipChainIdString, + isCaipChainId, + assertIsCaipChainId, + buildCaipChainId, + parseCaipChainId, } from './caip-chain-id'; -const validCaipChainIdStrings = [ +const validCaipChainIds = [ '123:a', '12345678:a', 'abc:1', @@ -25,7 +25,7 @@ const validCaipChainIdStrings = [ 'chainstd:8c3444cf8970a9e41a706fab93e7a6c4', ] as const; -const invalidCaipChainIdStrings = [ +const invalidCaipChainIds = [ true, false, null, @@ -47,87 +47,83 @@ const invalidCaipChainIdStrings = [ 'abc:!@#$%^&*()123', ] as const; -describe('isCaipChainIdString', () => { - it.each(validCaipChainIdStrings)( +describe('isCaipChainId', () => { + it.each(validCaipChainIds)( 'returns true for a valid caip chain id string', - (caipChainIdString) => { - expect(isCaipChainIdString(caipChainIdString)).toBe(true); + (caipChainId) => { + expect(isCaipChainId(caipChainId)).toBe(true); }, ); - it.each(invalidCaipChainIdStrings)( + it.each(invalidCaipChainIds)( 'returns false for an invalid caip chain id string', - (caipChainIdString) => { - expect(isCaipChainIdString(caipChainIdString)).toBe(false); + (caipChainId) => { + expect(isCaipChainId(caipChainId)).toBe(false); }, ); }); -describe('assertIsCaipChainIdString', () => { - it.each(validCaipChainIdStrings)( +describe('assertIsCaipChainId', () => { + it.each(validCaipChainIds)( 'does not throw for a valid caip chain id string', - (caipChainIdString) => { - expect(() => assertIsCaipChainIdString(caipChainIdString)).not.toThrow(); + (caipChainId) => { + expect(() => assertIsCaipChainId(caipChainId)).not.toThrow(); }, ); - it.each(invalidCaipChainIdStrings)( + it.each(invalidCaipChainIds)( 'throws for an invalid caip chain id string', - (caipChainIdString) => { - expect(() => assertIsCaipChainIdString(caipChainIdString)).toThrow( + (caipChainId) => { + expect(() => assertIsCaipChainId(caipChainId)).toThrow( 'Value must be a caip chain id string.', ); }, ); }); -describe('getCaipChainIdString', () => { +describe('buildCaipChainId', () => { it('returns the unvalidated caip chain id string', () => { - expect(getCaipChainIdString('eip155', '1')).toBe('eip155:1'); - expect(getCaipChainIdString('namespace', 'reference')).toBe( + expect(buildCaipChainId('eip155', '1')).toBe('eip155:1'); + expect(buildCaipChainId('namespace', 'reference')).toBe( 'namespace:reference', ); - expect(getCaipChainIdString('', '')).toBe(':'); - expect(getCaipChainIdString('UNVALIDATED', '!@#$%^&*()')).toBe( + expect(buildCaipChainId('', '')).toBe(':'); + expect(buildCaipChainId('UNVALIDATED', '!@#$%^&*()')).toBe( 'UNVALIDATED:!@#$%^&*()', ); }); }); -describe('parseCaipChainIdString', () => { +describe('parseCaipChainId', () => { it('returns the namespace and reference for valid caip chain id', () => { - expect(parseCaipChainIdString('eip155:1')).toStrictEqual({ + expect(parseCaipChainId('eip155:1')).toStrictEqual({ namespace: 'eip155', reference: '1', }); - expect(parseCaipChainIdString('name:reference')).toStrictEqual({ + expect(parseCaipChainId('name:reference')).toStrictEqual({ namespace: 'name', reference: 'reference', }); - expect(parseCaipChainIdString('abc:123')).toStrictEqual({ + expect(parseCaipChainId('abc:123')).toStrictEqual({ namespace: 'abc', reference: '123', }); }); it('returns empty strings for invalid caip chain id', () => { - expect(parseCaipChainIdString('12:a')).toStrictEqual({ + expect(parseCaipChainId('12:a')).toStrictEqual({ namespace: '', reference: '', }); - expect(parseCaipChainIdString('abc:')).toStrictEqual({ + expect(parseCaipChainId('abc:')).toStrictEqual({ namespace: '', reference: '', }); - expect(parseCaipChainIdString(':')).toStrictEqual({ + expect(parseCaipChainId(':')).toStrictEqual({ namespace: '', reference: '', }); - expect(parseCaipChainIdString('')).toStrictEqual({ - namespace: '', - reference: '', - }); - expect(parseCaipChainIdString('abc:123:xyz')).toStrictEqual({ + expect(parseCaipChainId('abc:123:xyz')).toStrictEqual({ namespace: '', reference: '', }); diff --git a/src/caip-chain-id.ts b/src/caip-chain-id.ts index 7e36552e..5d10a7b7 100644 --- a/src/caip-chain-id.ts +++ b/src/caip-chain-id.ts @@ -17,25 +17,25 @@ export type ParsedCaipChainId = { }; /** - * Check if a string is a valid caip chain id string. + * Check if a string is a valid caip chain id. * * @param value - The value to check. * @returns Whether the value is a valid caip chain id string. */ -export function isCaipChainIdString(value: unknown): value is CaipChainId { +export function isCaipChainId(value: unknown): value is CaipChainId { return is(value, CaipChainIdStruct); } /** - * Assert that a value is a valid caip chain id string. + * Assert that a value is a valid caip chain id. * * @param value - The value to check. * @throws If the value is not a valid caip chain id string. */ -export function assertIsCaipChainIdString( +export function assertIsCaipChainId( value: unknown, ): asserts value is CaipChainId { - assert(isCaipChainIdString(value), 'Value must be a caip chain id string.'); + assert(isCaipChainId(value), 'Value must be a caip chain id string.'); } /** @@ -45,10 +45,10 @@ export function assertIsCaipChainIdString( * @param reference - The caip chaid id reference string. * @returns The unvalidated caip chaid id string. */ -export function getCaipChainIdString( +export function buildCaipChainId( namespace: string, reference: string, -): string { +): CaipChainId { return `${namespace}:${reference}`; } @@ -58,7 +58,7 @@ export function getCaipChainIdString( * @param caipChainId - The caip chain id string. * @returns The {@link ParsedCaipChainId} object. */ -export function parseCaipChainIdString(caipChainId: string): ParsedCaipChainId { +export function parseCaipChainId(caipChainId: CaipChainId): ParsedCaipChainId { const [, namespace, reference] = caipChainId.match(CAIP2_REGEX) ?? []; return { namespace: namespace ?? '', From c2a686cf9ff4bbf5105120f7e79fcff222f10e82 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 26 Jul 2023 14:29:59 -0700 Subject: [PATCH 06/12] Switch to @metamask/snap-utils caip types --- src/caip-chaid-id.test-d.ts | 23 --- src/caip-chain-id.test.ts | 131 --------------- src/caip-chain-id.ts | 67 -------- src/caip-types.test-d.ts | 138 +++++++++++++++ src/caip-types.test.ts | 327 ++++++++++++++++++++++++++++++++++++ src/caip-types.ts | 165 ++++++++++++++++++ src/index.ts | 2 +- 7 files changed, 631 insertions(+), 222 deletions(-) delete mode 100644 src/caip-chaid-id.test-d.ts delete mode 100644 src/caip-chain-id.test.ts delete mode 100644 src/caip-chain-id.ts create mode 100644 src/caip-types.test-d.ts create mode 100644 src/caip-types.test.ts create mode 100644 src/caip-types.ts diff --git a/src/caip-chaid-id.test-d.ts b/src/caip-chaid-id.test-d.ts deleted file mode 100644 index 1a1fcdb9..00000000 --- a/src/caip-chaid-id.test-d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expectAssignable, expectNotAssignable } from 'tsd'; - -import { CaipChainId } from '.'; - -// Valid caip chain id strings: - -expectAssignable('namespace:reference'); - -expectAssignable('namespace:'); - -expectAssignable(':reference'); - -const embeddedString = 'test'; -expectAssignable(`${embeddedString}:${embeddedString}`); - -// Not valid caip chain id strings: - -expectAssignable('namespace:😀'); -expectAssignable('😀:reference'); - -expectNotAssignable(0); - -expectNotAssignable('🙃'); diff --git a/src/caip-chain-id.test.ts b/src/caip-chain-id.test.ts deleted file mode 100644 index c10a1323..00000000 --- a/src/caip-chain-id.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - isCaipChainId, - assertIsCaipChainId, - buildCaipChainId, - parseCaipChainId, -} from './caip-chain-id'; - -const validCaipChainIds = [ - '123:a', - '12345678:a', - 'abc:1', - 'abc:1234567890abcdefghijklmnopqrst32', - '12345678:1234567890abcdefghijklmnopqrst32', - 'az-45678:abcxyz_1234567890-ABCXYZ', - 'eip155:1', - 'bip122:000000000019d6689c085ae165831e93', - 'bip122:12a765e31ffd4059bada1e25190f6e98', - 'bip122:fdbe99b90c90bae7505796461471d89a', - 'cosmos:cosmoshub-2', - 'cosmos:cosmoshub-3', - 'cosmos:Binance-Chain-Tigris', - 'cosmos:iov-mainnet', - 'starknet:SN_GOERLI', - 'lip9:9ee11e9df416b18b', - 'chainstd:8c3444cf8970a9e41a706fab93e7a6c4', -] as const; - -const invalidCaipChainIds = [ - true, - false, - null, - undefined, - 0, - 1, - {}, - [], - '12:a', - '123456789:a', - 'abc:', - 'abc:1234567890abcdefghijklmnopqrstu33', - 'abc', - '123::a', - '123:a:a', - ':123:a', - 'Abc:1', - 'abc!@#$:1', - 'abc:!@#$%^&*()123', -] as const; - -describe('isCaipChainId', () => { - it.each(validCaipChainIds)( - 'returns true for a valid caip chain id string', - (caipChainId) => { - expect(isCaipChainId(caipChainId)).toBe(true); - }, - ); - - it.each(invalidCaipChainIds)( - 'returns false for an invalid caip chain id string', - (caipChainId) => { - expect(isCaipChainId(caipChainId)).toBe(false); - }, - ); -}); - -describe('assertIsCaipChainId', () => { - it.each(validCaipChainIds)( - 'does not throw for a valid caip chain id string', - (caipChainId) => { - expect(() => assertIsCaipChainId(caipChainId)).not.toThrow(); - }, - ); - - it.each(invalidCaipChainIds)( - 'throws for an invalid caip chain id string', - (caipChainId) => { - expect(() => assertIsCaipChainId(caipChainId)).toThrow( - 'Value must be a caip chain id string.', - ); - }, - ); -}); - -describe('buildCaipChainId', () => { - it('returns the unvalidated caip chain id string', () => { - expect(buildCaipChainId('eip155', '1')).toBe('eip155:1'); - expect(buildCaipChainId('namespace', 'reference')).toBe( - 'namespace:reference', - ); - expect(buildCaipChainId('', '')).toBe(':'); - expect(buildCaipChainId('UNVALIDATED', '!@#$%^&*()')).toBe( - 'UNVALIDATED:!@#$%^&*()', - ); - }); -}); - -describe('parseCaipChainId', () => { - it('returns the namespace and reference for valid caip chain id', () => { - expect(parseCaipChainId('eip155:1')).toStrictEqual({ - namespace: 'eip155', - reference: '1', - }); - expect(parseCaipChainId('name:reference')).toStrictEqual({ - namespace: 'name', - reference: 'reference', - }); - expect(parseCaipChainId('abc:123')).toStrictEqual({ - namespace: 'abc', - reference: '123', - }); - }); - - it('returns empty strings for invalid caip chain id', () => { - expect(parseCaipChainId('12:a')).toStrictEqual({ - namespace: '', - reference: '', - }); - expect(parseCaipChainId('abc:')).toStrictEqual({ - namespace: '', - reference: '', - }); - expect(parseCaipChainId(':')).toStrictEqual({ - namespace: '', - reference: '', - }); - expect(parseCaipChainId('abc:123:xyz')).toStrictEqual({ - namespace: '', - reference: '', - }); - }); -}); diff --git a/src/caip-chain-id.ts b/src/caip-chain-id.ts deleted file mode 100644 index 5d10a7b7..00000000 --- a/src/caip-chain-id.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { is, pattern, string, Struct } from 'superstruct'; - -import { assert } from './assert'; - -export type CaipChainId = `${string}:${string}`; - -const CAIP2_REGEX = /^([-a-z0-9]{3,8}):([-_a-zA-Z0-9]{1,32})$/u; - -export const CaipChainIdStruct = pattern(string(), CAIP2_REGEX) as Struct< - CaipChainId, - null ->; - -export type ParsedCaipChainId = { - namespace: string; - reference: string; -}; - -/** - * Check if a string is a valid caip chain id. - * - * @param value - The value to check. - * @returns Whether the value is a valid caip chain id string. - */ -export function isCaipChainId(value: unknown): value is CaipChainId { - return is(value, CaipChainIdStruct); -} - -/** - * Assert that a value is a valid caip chain id. - * - * @param value - The value to check. - * @throws If the value is not a valid caip chain id string. - */ -export function assertIsCaipChainId( - value: unknown, -): asserts value is CaipChainId { - assert(isCaipChainId(value), 'Value must be a caip chain id string.'); -} - -/** - * Returns caip chain id string from namespace and reference. - * - * @param namespace - The caip chain id namespace string. - * @param reference - The caip chaid id reference string. - * @returns The unvalidated caip chaid id string. - */ -export function buildCaipChainId( - namespace: string, - reference: string, -): CaipChainId { - return `${namespace}:${reference}`; -} - -/** - * Returns the namespace and reference strings from caip chain id string. - * - * @param caipChainId - The caip chain id string. - * @returns The {@link ParsedCaipChainId} object. - */ -export function parseCaipChainId(caipChainId: CaipChainId): ParsedCaipChainId { - const [, namespace, reference] = caipChainId.match(CAIP2_REGEX) ?? []; - return { - namespace: namespace ?? '', - reference: reference ?? '', - }; -} diff --git a/src/caip-types.test-d.ts b/src/caip-types.test-d.ts new file mode 100644 index 00000000..da0ecb0b --- /dev/null +++ b/src/caip-types.test-d.ts @@ -0,0 +1,138 @@ +import { expectAssignable, expectNotAssignable } from 'tsd'; + +import { + CaipAccountId, + CaipChain, + CaipChainId, + CaipNamespace, + CaipNamespaceId, +} from '.'; + +const embeddedString = 'test'; + +// Valid caip strings: + +expectAssignable('namespace:reference'); +expectAssignable('namespace:'); +expectAssignable(':reference'); +expectAssignable(`${embeddedString}:${embeddedString}`); + +expectAssignable('namespace:reference:accountAddress'); +expectAssignable('namespace:reference:'); +expectAssignable(':reference:accountAddress'); +expectAssignable( + `${embeddedString}:${embeddedString}:${embeddedString}`, +); + +expectAssignable('string'); +expectAssignable(`${embeddedString}`); + +// Not valid caip strings: + +expectAssignable('namespace:😀'); +expectAssignable('😀:reference'); +expectNotAssignable(0); +expectNotAssignable('🙃'); + +expectAssignable('namespace:reference:😀'); +expectAssignable('😀:reference:accountAddress'); +expectNotAssignable(0); +expectNotAssignable('🙃'); + +expectNotAssignable(0); + +// Valid caip objects: + +expectAssignable({ + id: 'string', + name: 'string', +}); +expectAssignable({ + id: `${embeddedString}`, + name: `${embeddedString}`, +}); + +expectAssignable({ + chains: [ + { + id: 'string', + name: 'string', + }, + { + id: `${embeddedString}`, + name: `${embeddedString}`, + }, + ], +}); +expectAssignable({ + chains: [ + { + id: 'string', + name: 'string', + }, + { + id: `${embeddedString}`, + name: `${embeddedString}`, + }, + ], + methods: ['string', `${embeddedString}`], + events: ['string', `${embeddedString}`], +}); + +// Not valid caip objects: +expectNotAssignable(''); +expectNotAssignable(0); +expectNotAssignable({}); +expectNotAssignable({ + id: 'string', +}); +expectNotAssignable({ + name: 'string', +}); +expectNotAssignable({ + id: 0, + name: 'string', +}); +expectNotAssignable({ + id: 'string', + name: 0, +}); + +expectNotAssignable(''); +expectNotAssignable(0); +expectNotAssignable({}); +expectNotAssignable({ + chains: [ + '', + 0, + {}, + { + id: 'string', + name: 0, + }, + { + id: 'string', + }, + { + name: 'string', + }, + ], +}); +expectNotAssignable({ + chains: [ + { + id: 'string', + name: 'string', + }, + ], + methods: [0], +}); +expectNotAssignable({ + chains: [ + { + id: 'string', + name: 'string', + }, + ], + events: [0], +}); diff --git a/src/caip-types.test.ts b/src/caip-types.test.ts new file mode 100644 index 00000000..a24aff46 --- /dev/null +++ b/src/caip-types.test.ts @@ -0,0 +1,327 @@ +import { + isCaipAccountId, + isCaipAccountIdArray, + isCaipChainId, + isCaipNamespace, + isCaipNamespaceId, + parseCaipAccountId, + parseCaipChainId, +} from './caip-types'; + +describe('parseCaipChainId', () => { + it('parses valid chain ids', () => { + expect(parseCaipChainId('eip155:1')).toMatchInlineSnapshot(` + { + "namespace": "eip155", + "reference": "1", + } + `); + + expect( + parseCaipChainId('bip122:000000000019d6689c085ae165831e93'), + ).toMatchInlineSnapshot( + ` + { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + } + `, + ); + + expect(parseCaipChainId('cosmos:cosmoshub-3')).toMatchInlineSnapshot( + ` + { + "namespace": "cosmos", + "reference": "cosmoshub-3", + } + `, + ); + + expect( + parseCaipChainId('polkadot:b0a8d493285c2df73290dfb7e61f870f'), + ).toMatchInlineSnapshot( + ` + { + "namespace": "polkadot", + "reference": "b0a8d493285c2df73290dfb7e61f870f", + } + `, + ); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + 'foo', + 'foobarbazquz:1', + 'foo:', + 'foo:foobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquz', + ])('throws for invalid input', (input) => { + expect(() => parseCaipChainId(input as any)).toThrow('Invalid chain ID.'); + }); +}); + +describe('parseCaipAccountId', () => { + it('parses valid account ids', () => { + expect( + parseCaipAccountId('eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb'), + ).toMatchInlineSnapshot( + ` + { + "address": "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb", + "chain": { + "namespace": "eip155", + "reference": "1", + }, + "chainId": "eip155:1", + } + `, + ); + + expect( + parseCaipAccountId( + 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + ), + ).toMatchInlineSnapshot( + ` + { + "address": "128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6", + "chain": { + "namespace": "bip122", + "reference": "000000000019d6689c085ae165831e93", + }, + "chainId": "bip122:000000000019d6689c085ae165831e93", + } + `, + ); + + expect( + parseCaipAccountId( + 'cosmos:cosmoshub-3:cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0', + ), + ).toMatchInlineSnapshot( + ` + { + "address": "cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0", + "chain": { + "namespace": "cosmos", + "reference": "cosmoshub-3", + }, + "chainId": "cosmos:cosmoshub-3", + } + `, + ); + + expect( + parseCaipAccountId( + 'polkadot:b0a8d493285c2df73290dfb7e61f870f:5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy', + ), + ).toMatchInlineSnapshot( + ` + { + "address": "5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy", + "chain": { + "namespace": "polkadot", + "reference": "b0a8d493285c2df73290dfb7e61f870f", + }, + "chainId": "polkadot:b0a8d493285c2df73290dfb7e61f870f", + } + `, + ); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + 'foo', + 'foobarbazquz:1', + 'foo:', + 'foo:foobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquz', + 'eip155:1', + 'eip155:1:', + ])('throws for invalid input', (input) => { + expect(() => parseCaipAccountId(input as any)).toThrow( + 'Invalid account ID.', + ); + }); +}); + +describe('isCaipNamespaceId', () => { + it.each(['eip155', 'bip122'])( + 'returns true for a valid namespace id', + (id) => { + expect(isCaipNamespaceId(id)).toBe(true); + }, + ); + + it.each([true, false, null, undefined, 1, {}, [], 'a', 'foobarbaz'])( + 'returns false for an invalid namespace id', + (id) => { + expect(isCaipNamespaceId(id)).toBe(false); + }, + ); +}); + +describe('isCaipChainId', () => { + it.each([ + 'eip155:1', + 'eip155:1337', + 'bip122:000000000019d6689c085ae165831e93', + ])('returns true for a valid chain id', (id) => { + expect(isCaipChainId(id)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + 'a', + 'eip155', + 'eip155:', + 'eip155:1:2', + 'bip122', + 'bip122:', + 'bip122:000000000019d6689c085ae165831e93:2', + ])('returns false for an invalid chain id', (id) => { + expect(isCaipChainId(id)).toBe(false); + }); +}); + +describe('isCaipAccountId', () => { + it.each([ + 'eip155:1:0x0000000000000000000000000000000000000000', + 'eip155:1337:0x0000000000000000000000000000000000000000', + 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000', + ])('returns true for a valid account id', (id) => { + expect(isCaipAccountId(id)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + 'foo', + 'eip155', + 'eip155:', + 'eip155:1', + 'eip155:1:', + 'eip155:1:0x0000000000000000000000000000000000000000:2', + 'bip122', + 'bip122:', + 'bip122:000000000019d6689c085ae165831e93', + 'bip122:000000000019d6689c085ae165831e93:', + 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000:2', + ])('returns false for an invalid account id', (id) => { + expect(isCaipAccountId(id)).toBe(false); + }); +}); + +describe('isCaipAccountIdArray', () => { + it.each([ + // `it.each` does not support nested arrays, so we nest them in objects. + { + accounts: [], + }, + { + accounts: [ + 'eip155:1:0x0000000000000000000000000000000000000000', + 'eip155:1337:0x0000000000000000000000000000000000000000', + 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000', + ], + }, + { + accounts: ['eip155:1:0x0000000000000000000000000000000000000000'], + }, + ])('returns true for a valid account id array', ({ accounts }) => { + expect(isCaipAccountIdArray(accounts)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + 'foo', + ['foo'], + ['eip155:1:0x0000000000000000000000000000000000000000:2'], + [ + 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000:2', + ], + ])('returns false for an invalid account id array', (accounts) => { + expect(isCaipAccountIdArray(accounts)).toBe(false); + }); +}); + +describe('isCaipNamespace', () => { + it.each([ + { + chains: [ + { + id: 'eip155:1', + name: 'Ethereum Mainnet', + }, + ], + methods: ['eth_signTransaction', 'eth_accounts'], + events: ['accountsChanged'], + }, + { + chains: [ + { + id: 'eip155:1', + name: 'Ethereum Mainnet', + }, + ], + methods: ['eth_signTransaction'], + }, + { + chains: [ + { + id: 'eip155:1', + name: 'Ethereum Mainnet', + }, + ], + events: ['accountsChanged'], + }, + { + chains: [ + { + id: 'eip155:1', + name: 'Ethereum Mainnet', + }, + ], + }, + ])('returns true for a valid namespace', (namespace) => { + expect(isCaipNamespace(namespace)).toBe(true); + }); + + it.each([ + {}, + [], + true, + false, + null, + undefined, + 1, + 'foo', + { methods: [], events: [] }, + { chains: ['foo'] }, + ])('returns false for an invalid namespace', (namespace) => { + expect(isCaipNamespace(namespace)).toBe(false); + }); +}); diff --git a/src/caip-types.ts b/src/caip-types.ts new file mode 100644 index 00000000..45fdfb0f --- /dev/null +++ b/src/caip-types.ts @@ -0,0 +1,165 @@ +import type { Infer } from 'superstruct'; +import { + array, + is, + object, + optional, + pattern, + size, + string, +} from 'superstruct'; + +export const CHAIN_ID_REGEX = + /^(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})$/u; + +export const ACCOUNT_ID_REGEX = + /^(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})):(?[a-zA-Z0-9]{1,64})$/u; + +/** + * Parse a chain ID string to an object containing the namespace and reference. + * This validates the chain ID before parsing it. + * + * @param caipChainId - The chain ID to validate and parse. + * @returns The parsed chain ID. + */ +export function parseCaipChainId(caipChainId: CaipChainId): { + namespace: CaipNamespaceId; + reference: string; +} { + const match = CHAIN_ID_REGEX.exec(caipChainId); + if (!match?.groups) { + throw new Error('Invalid chain ID.'); + } + + return { + namespace: match.groups.namespace as CaipNamespaceId, + reference: match.groups.reference as string, + }; +} + +/** + * Parse an account ID to an object containing the chain, chain ID and address. + * This validates the account ID before parsing it. + * + * @param accountId - The account ID to validate and parse. + * @returns The parsed account ID. + */ +export function parseCaipAccountId(accountId: CaipAccountId): { + chain: { namespace: CaipNamespaceId; reference: string }; + chainId: CaipChainId; + address: string; +} { + const match = ACCOUNT_ID_REGEX.exec(accountId); + if (!match?.groups) { + throw new Error('Invalid account ID.'); + } + + return { + address: match.groups.accountAddress as string, + chainId: match.groups.chainId as CaipChainId, + chain: { + namespace: match.groups.namespace as CaipNamespaceId, + reference: match.groups.reference as string, + }, + }; +} + +/** + * A helper struct for a string with a minimum length of 1 and a maximum length + * of 40. + */ +export const LimitedString = size(string(), 1, 40); + +/** + * A CAIP-2 chain ID, i.e., a human-readable namespace and reference. + */ +export const CaipChainIdStruct = pattern(string(), CHAIN_ID_REGEX); +export type CaipChainId = `${string}:${string}`; + +export const CaipAccountIdStruct = pattern(string(), ACCOUNT_ID_REGEX); +export type CaipAccountId = `${CaipChainId}:${string}`; + +export const CaipAccountIdArrayStruct = array(CaipAccountIdStruct); + +/** + * A chain descriptor. + */ +export const CaipChainStruct = object({ + id: CaipChainIdStruct, + name: LimitedString, +}); +export type CaipChain = Infer; + +export const CaipNamespaceStruct = object({ + /** + * A list of supported chains in the namespace. + */ + chains: array(CaipChainStruct), + + /** + * A list of supported RPC methods on the namespace, that a DApp can call. + */ + methods: optional(array(LimitedString)), + + /** + * A list of supported RPC events on the namespace, that a DApp can listen to. + */ + events: optional(array(LimitedString)), +}); +export type CaipNamespace = Infer; + +/** + * A CAIP-2 namespace, i.e., the first part of a chain ID. + */ +export const CaipNamespaceIdStruct = pattern(string(), /^[-a-z0-9]{3,8}$/u); +export type CaipNamespaceId = Infer; + +/** + * Check if the given value is a CAIP-2 namespace ID. + * + * @param value - The value to check. + * @returns Whether the value is a CAIP-2 namespace ID. + */ +export function isCaipNamespaceId(value: unknown): value is CaipNamespaceId { + return is(value, CaipNamespaceIdStruct); +} + +/** + * Check if the given value is a CAIP-2 chain ID. + * + * @param value - The value to check. + * @returns Whether the value is a CAIP-2 chain ID. + */ +export function isCaipChainId(value: unknown): value is CaipChainId { + return is(value, CaipChainIdStruct); +} + +/** + * Check if the given value is a CAIP-10 account ID. + * + * @param value - The value to check. + * @returns Whether the value is a CAIP-10 account ID. + */ +export function isCaipAccountId(value: unknown): value is CaipAccountId { + return is(value, CaipAccountIdStruct); +} + +/** + * Check if the given value is an array of CAIP-10 account IDs. + * + * @param value - The value to check. + * @returns Whether the value is an array of CAIP-10 account IDs. + */ +export function isCaipAccountIdArray(value: unknown): value is CaipAccountId[] { + return is(value, CaipAccountIdArrayStruct); +} + +/** + * Check if a value is a {@link CaipNamespace}. + * + * @param value - The value to validate. + * @returns True if the value is a valid {@link CaipNamespace}. + */ +export function isCaipNamespace(value: unknown): value is CaipNamespace { + return is(value, CaipNamespaceStruct); +} diff --git a/src/index.ts b/src/index.ts index 7186f169..7e44f26a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ export * from './assert'; export * from './base64'; export * from './bytes'; -export * from './caip-chain-id'; +export * from './caip-types'; export * from './checksum'; export * from './coercers'; export * from './collections'; From 488863c19de48e79953c7271440c929fe34d43f8 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 26 Jul 2023 14:38:52 -0700 Subject: [PATCH 07/12] lint --- src/caip-types.test-d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/caip-types.test-d.ts b/src/caip-types.test-d.ts index da0ecb0b..4eeace1b 100644 --- a/src/caip-types.test-d.ts +++ b/src/caip-types.test-d.ts @@ -1,6 +1,6 @@ import { expectAssignable, expectNotAssignable } from 'tsd'; -import { +import type { CaipAccountId, CaipChain, CaipChainId, From 621e8aea7cdc68352f370b83af5583501ec1e33a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 27 Jul 2023 14:56:32 -0700 Subject: [PATCH 08/12] Separate regex const. Add CaipAccountAddressStruct --- src/caip-types.ts | 58 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/src/caip-types.ts b/src/caip-types.ts index 45fdfb0f..7a587b01 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -9,11 +9,16 @@ import { string, } from 'superstruct'; -export const CHAIN_ID_REGEX = +export const CAIP_CHAIN_ID_REGEX = /^(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})$/u; -export const ACCOUNT_ID_REGEX = - /^(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})):(?[a-zA-Z0-9]{1,64})$/u; +export const CAIP_NAMESPACE_ID_REGEX = /^[-a-z0-9]{3,8}$/u; + +export const CAIP_ACCOUNT_ID_REGEX = + /^(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})):(?[a-zA-Z0-9]{1,128})$/u; + +export const CAIP_ACCOUNT_ADDRESS_REGEX = + /^(?[a-zA-Z0-9]{1,128})$/u; /** * Parse a chain ID string to an object containing the namespace and reference. @@ -26,7 +31,7 @@ export function parseCaipChainId(caipChainId: CaipChainId): { namespace: CaipNamespaceId; reference: string; } { - const match = CHAIN_ID_REGEX.exec(caipChainId); + const match = CAIP_CHAIN_ID_REGEX.exec(caipChainId); if (!match?.groups) { throw new Error('Invalid chain ID.'); } @@ -49,7 +54,7 @@ export function parseCaipAccountId(accountId: CaipAccountId): { chainId: CaipChainId; address: string; } { - const match = ACCOUNT_ID_REGEX.exec(accountId); + const match = CAIP_ACCOUNT_ID_REGEX.exec(accountId); if (!match?.groups) { throw new Error('Invalid account ID.'); } @@ -73,10 +78,10 @@ export const LimitedString = size(string(), 1, 40); /** * A CAIP-2 chain ID, i.e., a human-readable namespace and reference. */ -export const CaipChainIdStruct = pattern(string(), CHAIN_ID_REGEX); +export const CaipChainIdStruct = pattern(string(), CAIP_CHAIN_ID_REGEX); export type CaipChainId = `${string}:${string}`; -export const CaipAccountIdStruct = pattern(string(), ACCOUNT_ID_REGEX); +export const CaipAccountIdStruct = pattern(string(), CAIP_ACCOUNT_ID_REGEX); export type CaipAccountId = `${CaipChainId}:${string}`; export const CaipAccountIdArrayStruct = array(CaipAccountIdStruct); @@ -111,9 +116,28 @@ export type CaipNamespace = Infer; /** * A CAIP-2 namespace, i.e., the first part of a chain ID. */ -export const CaipNamespaceIdStruct = pattern(string(), /^[-a-z0-9]{3,8}$/u); +export const CaipNamespaceIdStruct = pattern(string(), CAIP_NAMESPACE_ID_REGEX); export type CaipNamespaceId = Infer; +/** + * A CAIP-10 account address, i.e., the last part of the account ID. + */ +export const CaipAccountAddressStruct = pattern( + string(), + CAIP_ACCOUNT_ADDRESS_REGEX, +); +export type CaipAccountAddress = Infer; + +/** + * Check if the given value is a CAIP-2 chain ID. + * + * @param value - The value to check. + * @returns Whether the value is a CAIP-2 chain ID. + */ +export function isCaipChainId(value: unknown): value is CaipChainId { + return is(value, CaipChainIdStruct); +} + /** * Check if the given value is a CAIP-2 namespace ID. * @@ -125,13 +149,13 @@ export function isCaipNamespaceId(value: unknown): value is CaipNamespaceId { } /** - * Check if the given value is a CAIP-2 chain ID. + * Check if a value is a {@link CaipNamespace}. * - * @param value - The value to check. - * @returns Whether the value is a CAIP-2 chain ID. + * @param value - The value to validate. + * @returns True if the value is a valid {@link CaipNamespace}. */ -export function isCaipChainId(value: unknown): value is CaipChainId { - return is(value, CaipChainIdStruct); +export function isCaipNamespace(value: unknown): value is CaipNamespace { + return is(value, CaipNamespaceStruct); } /** @@ -155,11 +179,11 @@ export function isCaipAccountIdArray(value: unknown): value is CaipAccountId[] { } /** - * Check if a value is a {@link CaipNamespace}. + * Check if a value is a {@link CaipAccountAddress}. * * @param value - The value to validate. - * @returns True if the value is a valid {@link CaipNamespace}. + * @returns True if the value is a valid {@link CaipAccountAddress}. */ -export function isCaipNamespace(value: unknown): value is CaipNamespace { - return is(value, CaipNamespaceStruct); +export function isCaipAccountAddress(value: unknown): value is CaipAccountAddress { + return is(value, CaipAccountAddressStruct); } From 192ebbf2f4090d0e953cc49dee11451f6f554491 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 27 Jul 2023 15:07:53 -0700 Subject: [PATCH 09/12] Add CaipAccountAddress specs --- src/caip-types.test-d.ts | 6 ++++++ src/caip-types.test.ts | 33 +++++++++++++++++++++++++++++++++ src/caip-types.ts | 12 +++++++----- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/caip-types.test-d.ts b/src/caip-types.test-d.ts index 4eeace1b..fcd81979 100644 --- a/src/caip-types.test-d.ts +++ b/src/caip-types.test-d.ts @@ -1,6 +1,7 @@ import { expectAssignable, expectNotAssignable } from 'tsd'; import type { + CaipAccountAddress, CaipAccountId, CaipChain, CaipChainId, @@ -27,6 +28,9 @@ expectAssignable( expectAssignable('string'); expectAssignable(`${embeddedString}`); +expectAssignable('string'); +expectAssignable(`${embeddedString}`); + // Not valid caip strings: expectAssignable('namespace:😀'); @@ -41,6 +45,8 @@ expectNotAssignable('🙃'); expectNotAssignable(0); +expectNotAssignable(0); + // Valid caip objects: expectAssignable({ diff --git a/src/caip-types.test.ts b/src/caip-types.test.ts index a24aff46..1d870f7c 100644 --- a/src/caip-types.test.ts +++ b/src/caip-types.test.ts @@ -1,4 +1,5 @@ import { + isCaipAccountAddress, isCaipAccountId, isCaipAccountIdArray, isCaipChainId, @@ -325,3 +326,35 @@ describe('isCaipNamespace', () => { expect(isCaipNamespace(namespace)).toBe(false); }); }); + +describe('isCaipAccountAddress', () => { + it.each([ + Array(128).fill('0').join(''), + '0', + '0x0', + '0x0000000000000000000000000000000000000000', + '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb', + '128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + 'cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0', + '5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy', + '0x02dd1b492765c064eac4039e3841aa5f382773b598097a40073bd8b48170ab57', + '6d9b0b4b9994e8a6afbd3dc3ed983cd51c755afb27cd1dc7825ef59c134a39f7', + '0.0.1234567890-zbhlt', + ])('returns true for a valid account address', (id) => { + expect(isCaipAccountAddress(id)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + '', + Array(129).fill('a').join(''), + ])('returns false for an invalid account address', (id) => { + expect(isCaipAccountAddress(id)).toBe(false); + }); +}); diff --git a/src/caip-types.ts b/src/caip-types.ts index 7a587b01..1313dc61 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -15,10 +15,10 @@ export const CAIP_CHAIN_ID_REGEX = export const CAIP_NAMESPACE_ID_REGEX = /^[-a-z0-9]{3,8}$/u; export const CAIP_ACCOUNT_ID_REGEX = - /^(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})):(?[a-zA-Z0-9]{1,128})$/u; + /^(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})):(?[-.%a-zA-Z0-9]{1,128})$/u; export const CAIP_ACCOUNT_ADDRESS_REGEX = - /^(?[a-zA-Z0-9]{1,128})$/u; + /^(?[-.%a-zA-Z0-9]{1,128})$/u; /** * Parse a chain ID string to an object containing the namespace and reference. @@ -79,10 +79,10 @@ export const LimitedString = size(string(), 1, 40); * A CAIP-2 chain ID, i.e., a human-readable namespace and reference. */ export const CaipChainIdStruct = pattern(string(), CAIP_CHAIN_ID_REGEX); -export type CaipChainId = `${string}:${string}`; +export type CaipChainId = Infer; export const CaipAccountIdStruct = pattern(string(), CAIP_ACCOUNT_ID_REGEX); -export type CaipAccountId = `${CaipChainId}:${string}`; +export type CaipAccountId = Infer; export const CaipAccountIdArrayStruct = array(CaipAccountIdStruct); @@ -184,6 +184,8 @@ export function isCaipAccountIdArray(value: unknown): value is CaipAccountId[] { * @param value - The value to validate. * @returns True if the value is a valid {@link CaipAccountAddress}. */ -export function isCaipAccountAddress(value: unknown): value is CaipAccountAddress { +export function isCaipAccountAddress( + value: unknown, +): value is CaipAccountAddress { return is(value, CaipAccountAddressStruct); } From 253c92b2bbb673a1cb5bf8203bc7f06c2700baf5 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 28 Jul 2023 10:10:27 -0700 Subject: [PATCH 10/12] tighten types --- src/caip-types.test-d.ts | 12 ++++++------ src/caip-types.ts | 15 +++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/caip-types.test-d.ts b/src/caip-types.test-d.ts index fcd81979..c4b8b630 100644 --- a/src/caip-types.test-d.ts +++ b/src/caip-types.test-d.ts @@ -50,22 +50,22 @@ expectNotAssignable(0); // Valid caip objects: expectAssignable({ - id: 'string', + id: 'namespace:reference', name: 'string', }); expectAssignable({ - id: `${embeddedString}`, + id: `${embeddedString}:${embeddedString}`, name: `${embeddedString}`, }); expectAssignable({ chains: [ { - id: 'string', + id: 'namespace:reference', name: 'string', }, { - id: `${embeddedString}`, + id: `${embeddedString}:${embeddedString}`, name: `${embeddedString}`, }, ], @@ -73,11 +73,11 @@ expectAssignable({ expectAssignable({ chains: [ { - id: 'string', + id: 'namespace:reference', name: 'string', }, { - id: `${embeddedString}`, + id: `${embeddedString}:${embeddedString}`, name: `${embeddedString}`, }, ], diff --git a/src/caip-types.ts b/src/caip-types.ts index 1313dc61..cd427126 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -79,10 +79,10 @@ export const LimitedString = size(string(), 1, 40); * A CAIP-2 chain ID, i.e., a human-readable namespace and reference. */ export const CaipChainIdStruct = pattern(string(), CAIP_CHAIN_ID_REGEX); -export type CaipChainId = Infer; +export type CaipChainId = `${string}:${string}`; export const CaipAccountIdStruct = pattern(string(), CAIP_ACCOUNT_ID_REGEX); -export type CaipAccountId = Infer; +export type CaipAccountId = `${string}:${string}:${string}`; export const CaipAccountIdArrayStruct = array(CaipAccountIdStruct); @@ -93,7 +93,10 @@ export const CaipChainStruct = object({ id: CaipChainIdStruct, name: LimitedString, }); -export type CaipChain = Infer; +export type CaipChain = { + id: CaipChainId; + name: string; +}; export const CaipNamespaceStruct = object({ /** @@ -111,7 +114,11 @@ export const CaipNamespaceStruct = object({ */ events: optional(array(LimitedString)), }); -export type CaipNamespace = Infer; +export type CaipNamespace = { + chains: CaipChain[]; + methods?: string[]; + events?: string[]; +}; /** * A CAIP-2 namespace, i.e., the first part of a chain ID. From 2668bb1ee476f700b535d89aab52c4dd7be2a625 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 28 Jul 2023 11:19:30 -0700 Subject: [PATCH 11/12] remove unused types. cleanup specs. add fixtures --- src/__fixtures__/caip-types.ts | 35 +++ src/__fixtures__/index.ts | 1 + src/caip-types.test-d.ts | 114 +--------- src/caip-types.test.ts | 404 +++++++++++++-------------------- src/caip-types.ts | 208 +++++++---------- 5 files changed, 286 insertions(+), 476 deletions(-) create mode 100644 src/__fixtures__/caip-types.ts diff --git a/src/__fixtures__/caip-types.ts b/src/__fixtures__/caip-types.ts new file mode 100644 index 00000000..b0af50db --- /dev/null +++ b/src/__fixtures__/caip-types.ts @@ -0,0 +1,35 @@ +export const CAIP_CHAIN_ID_FIXTURES = [ + 'eip155:1', + 'bip122:000000000019d6689c085ae165831e93', + 'bip122:12a765e31ffd4059bada1e25190f6e98', + 'bip122:fdbe99b90c90bae7505796461471d89a', + 'cosmos:cosmoshub-2', + 'cosmos:cosmoshub-3', + 'cosmos:Binance-Chain-Tigris', + 'cosmos:iov-mainnet', + 'starknet:SN_GOERLI', + 'lip9:9ee11e9df416b18b', + 'chainstd:8c3444cf8970a9e41a706fab93e7a6c4', +] as const; + +export const CAIP_NAMESPACE_FIXTURES = Array.from( + new Set(CAIP_CHAIN_ID_FIXTURES.map((value) => value.split(':')[0])), +); + +export const CAIP_REFERENCE_FIXTURES = Array.from( + new Set(CAIP_CHAIN_ID_FIXTURES.map((value) => value.split(':')[1])), +); + +export const CAIP_ACCOUNT_ID_FIXTURES = [ + 'eip155:1:0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb', + 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + 'cosmos:cosmoshub-3:cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0', + 'polkadot:b0a8d493285c2df73290dfb7e61f870f:5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy', + 'starknet:SN_GOERLI:0x02dd1b492765c064eac4039e3841aa5f382773b598097a40073bd8b48170ab57', + 'chainstd:8c3444cf8970a9e41a706fab93e7a6c4:6d9b0b4b9994e8a6afbd3dc3ed983cd51c755afb27cd1dc7825ef59c134a39f7', + 'hedera:mainnet:0.0.1234567890-zbhlt', +] as const; + +export const CAIP_ACCOUNT_ADDRESS_FIXTURES = Array.from( + new Set(CAIP_ACCOUNT_ID_FIXTURES.map((value) => value.split(':')[2])), +); diff --git a/src/__fixtures__/index.ts b/src/__fixtures__/index.ts index 54588564..39d2d2b4 100644 --- a/src/__fixtures__/index.ts +++ b/src/__fixtures__/index.ts @@ -1,4 +1,5 @@ export * from './bytes'; +export * from './caip-types'; export * from './coercions'; export * from './json'; export * from './numbers'; diff --git a/src/caip-types.test-d.ts b/src/caip-types.test-d.ts index c4b8b630..5b94402a 100644 --- a/src/caip-types.test-d.ts +++ b/src/caip-types.test-d.ts @@ -3,10 +3,9 @@ import { expectAssignable, expectNotAssignable } from 'tsd'; import type { CaipAccountAddress, CaipAccountId, - CaipChain, CaipChainId, CaipNamespace, - CaipNamespaceId, + CaipReference, } from '.'; const embeddedString = 'test'; @@ -18,6 +17,12 @@ expectAssignable('namespace:'); expectAssignable(':reference'); expectAssignable(`${embeddedString}:${embeddedString}`); +expectAssignable('string'); +expectAssignable(`${embeddedString}`); + +expectAssignable('string'); +expectAssignable(`${embeddedString}`); + expectAssignable('namespace:reference:accountAddress'); expectAssignable('namespace:reference:'); expectAssignable(':reference:accountAddress'); @@ -25,9 +30,6 @@ expectAssignable( `${embeddedString}:${embeddedString}:${embeddedString}`, ); -expectAssignable('string'); -expectAssignable(`${embeddedString}`); - expectAssignable('string'); expectAssignable(`${embeddedString}`); @@ -38,107 +40,13 @@ expectAssignable('😀:reference'); expectNotAssignable(0); expectNotAssignable('🙃'); +expectNotAssignable(0); + +expectNotAssignable(0); + expectAssignable('namespace:reference:😀'); expectAssignable('😀:reference:accountAddress'); expectNotAssignable(0); expectNotAssignable('🙃'); -expectNotAssignable(0); - expectNotAssignable(0); - -// Valid caip objects: - -expectAssignable({ - id: 'namespace:reference', - name: 'string', -}); -expectAssignable({ - id: `${embeddedString}:${embeddedString}`, - name: `${embeddedString}`, -}); - -expectAssignable({ - chains: [ - { - id: 'namespace:reference', - name: 'string', - }, - { - id: `${embeddedString}:${embeddedString}`, - name: `${embeddedString}`, - }, - ], -}); -expectAssignable({ - chains: [ - { - id: 'namespace:reference', - name: 'string', - }, - { - id: `${embeddedString}:${embeddedString}`, - name: `${embeddedString}`, - }, - ], - methods: ['string', `${embeddedString}`], - events: ['string', `${embeddedString}`], -}); - -// Not valid caip objects: -expectNotAssignable(''); -expectNotAssignable(0); -expectNotAssignable({}); -expectNotAssignable({ - id: 'string', -}); -expectNotAssignable({ - name: 'string', -}); -expectNotAssignable({ - id: 0, - name: 'string', -}); -expectNotAssignable({ - id: 'string', - name: 0, -}); - -expectNotAssignable(''); -expectNotAssignable(0); -expectNotAssignable({}); -expectNotAssignable({ - chains: [ - '', - 0, - {}, - { - id: 'string', - name: 0, - }, - { - id: 'string', - }, - { - name: 'string', - }, - ], -}); -expectNotAssignable({ - chains: [ - { - id: 'string', - name: 'string', - }, - ], - methods: [0], -}); -expectNotAssignable({ - chains: [ - { - id: 'string', - name: 'string', - }, - ], - events: [0], -}); diff --git a/src/caip-types.test.ts b/src/caip-types.test.ts index 1d870f7c..3b5acc9d 100644 --- a/src/caip-types.test.ts +++ b/src/caip-types.test.ts @@ -1,14 +1,150 @@ +import { + CAIP_ACCOUNT_ADDRESS_FIXTURES, + CAIP_ACCOUNT_ID_FIXTURES, + CAIP_CHAIN_ID_FIXTURES, + CAIP_NAMESPACE_FIXTURES, + CAIP_REFERENCE_FIXTURES, +} from './__fixtures__'; import { isCaipAccountAddress, isCaipAccountId, - isCaipAccountIdArray, isCaipChainId, isCaipNamespace, - isCaipNamespaceId, + isCaipReference, parseCaipAccountId, parseCaipChainId, } from './caip-types'; +describe('isCaipChainId', () => { + it.each(CAIP_CHAIN_ID_FIXTURES)( + 'returns true for a valid chain id %s', + (id) => { + expect(isCaipChainId(id)).toBe(true); + }, + ); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + '!@#$%^&*()', + 'a', + ':1', + '123:', + 'abC:1', + 'eip155', + 'eip155:', + 'eip155:1:2', + 'bip122', + 'bip122:', + 'bip122:000000000019d6689c085ae165831e93:2', + ])('returns false for an invalid chain id %s', (id) => { + expect(isCaipChainId(id)).toBe(false); + }); +}); + +describe('isCaipNamespace', () => { + it.each([...CAIP_NAMESPACE_FIXTURES])( + 'returns true for a valid namespace %s', + (id) => { + expect(isCaipNamespace(id)).toBe(true); + }, + ); + + it.each([true, false, null, undefined, 1, {}, [], 'abC', '12', '123456789'])( + 'returns false for an invalid namespace %s', + (id) => { + expect(isCaipNamespace(id)).toBe(false); + }, + ); +}); + +describe('isCaipReference', () => { + it.each([...CAIP_REFERENCE_FIXTURES])( + 'returns true for a valid reference %s', + (id) => { + expect(isCaipReference(id)).toBe(true); + }, + ); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + '', + '!@#$%^&*()', + Array(33).fill('0').join(''), + ])('returns false for an invalid reference %s', (id) => { + expect(isCaipReference(id)).toBe(false); + }); +}); + +describe('isCaipAccountId', () => { + it.each([...CAIP_ACCOUNT_ID_FIXTURES])( + 'returns true for a valid account id %s', + (id) => { + expect(isCaipAccountId(id)).toBe(true); + }, + ); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + '', + '!@#$%^&*()', + 'foo', + 'eip155', + 'eip155:', + 'eip155:1', + 'eip155:1:', + 'eip155:1:0x0000000000000000000000000000000000000000:2', + 'bip122', + 'bip122:', + 'bip122:000000000019d6689c085ae165831e93', + 'bip122:000000000019d6689c085ae165831e93:', + 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000:2', + ])('returns false for an invalid account id %s', (id) => { + expect(isCaipAccountId(id)).toBe(false); + }); +}); + +describe('isCaipAccountAddress', () => { + it.each([...CAIP_ACCOUNT_ADDRESS_FIXTURES])( + 'returns true for a valid account address %s', + (id) => { + expect(isCaipAccountAddress(id)).toBe(true); + }, + ); + + it.each([ + true, + false, + null, + undefined, + 1, + {}, + [], + '', + '!@#$%^&*()', + Array(129).fill('0').join(''), + ])('returns false for an invalid account address %s', (id) => { + expect(isCaipAccountAddress(id)).toBe(false); + }); +}); + describe('parseCaipChainId', () => { it('parses valid chain ids', () => { expect(parseCaipChainId('eip155:1')).toMatchInlineSnapshot(` @@ -18,36 +154,28 @@ describe('parseCaipChainId', () => { } `); - expect( - parseCaipChainId('bip122:000000000019d6689c085ae165831e93'), - ).toMatchInlineSnapshot( - ` + expect(parseCaipChainId('bip122:000000000019d6689c085ae165831e93')) + .toMatchInlineSnapshot(` { "namespace": "bip122", "reference": "000000000019d6689c085ae165831e93", } - `, - ); + `); - expect(parseCaipChainId('cosmos:cosmoshub-3')).toMatchInlineSnapshot( - ` + expect(parseCaipChainId('cosmos:cosmoshub-3')).toMatchInlineSnapshot(` { "namespace": "cosmos", "reference": "cosmoshub-3", } - `, - ); + `); - expect( - parseCaipChainId('polkadot:b0a8d493285c2df73290dfb7e61f870f'), - ).toMatchInlineSnapshot( - ` + expect(parseCaipChainId('polkadot:b0a8d493285c2df73290dfb7e61f870f')) + .toMatchInlineSnapshot(` { "namespace": "polkadot", "reference": "b0a8d493285c2df73290dfb7e61f870f", } - `, - ); + `); }); it.each([ @@ -60,8 +188,10 @@ describe('parseCaipChainId', () => { 'foobarbazquz:1', 'foo:', 'foo:foobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquz', - ])('throws for invalid input', (input) => { - expect(() => parseCaipChainId(input as any)).toThrow('Invalid chain ID.'); + ])('throws for invalid input %s', (input) => { + expect(() => parseCaipChainId(input as any)).toThrow( + 'Invalid CAIP chain ID.', + ); }); }); @@ -69,8 +199,7 @@ describe('parseCaipAccountId', () => { it('parses valid account ids', () => { expect( parseCaipAccountId('eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb'), - ).toMatchInlineSnapshot( - ` + ).toMatchInlineSnapshot(` { "address": "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb", "chain": { @@ -79,15 +208,13 @@ describe('parseCaipAccountId', () => { }, "chainId": "eip155:1", } - `, - ); + `); expect( parseCaipAccountId( 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', ), - ).toMatchInlineSnapshot( - ` + ).toMatchInlineSnapshot(` { "address": "128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6", "chain": { @@ -96,15 +223,13 @@ describe('parseCaipAccountId', () => { }, "chainId": "bip122:000000000019d6689c085ae165831e93", } - `, - ); + `); expect( parseCaipAccountId( 'cosmos:cosmoshub-3:cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0', ), - ).toMatchInlineSnapshot( - ` + ).toMatchInlineSnapshot(` { "address": "cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0", "chain": { @@ -113,15 +238,13 @@ describe('parseCaipAccountId', () => { }, "chainId": "cosmos:cosmoshub-3", } - `, - ); + `); expect( parseCaipAccountId( 'polkadot:b0a8d493285c2df73290dfb7e61f870f:5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy', ), - ).toMatchInlineSnapshot( - ` + ).toMatchInlineSnapshot(` { "address": "5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy", "chain": { @@ -130,8 +253,7 @@ describe('parseCaipAccountId', () => { }, "chainId": "polkadot:b0a8d493285c2df73290dfb7e61f870f", } - `, - ); + `); }); it.each([ @@ -146,215 +268,9 @@ describe('parseCaipAccountId', () => { 'foo:foobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquzfoobarbazquz', 'eip155:1', 'eip155:1:', - ])('throws for invalid input', (input) => { + ])('throws for invalid input %s', (input) => { expect(() => parseCaipAccountId(input as any)).toThrow( - 'Invalid account ID.', + 'Invalid CAIP account ID.', ); }); }); - -describe('isCaipNamespaceId', () => { - it.each(['eip155', 'bip122'])( - 'returns true for a valid namespace id', - (id) => { - expect(isCaipNamespaceId(id)).toBe(true); - }, - ); - - it.each([true, false, null, undefined, 1, {}, [], 'a', 'foobarbaz'])( - 'returns false for an invalid namespace id', - (id) => { - expect(isCaipNamespaceId(id)).toBe(false); - }, - ); -}); - -describe('isCaipChainId', () => { - it.each([ - 'eip155:1', - 'eip155:1337', - 'bip122:000000000019d6689c085ae165831e93', - ])('returns true for a valid chain id', (id) => { - expect(isCaipChainId(id)).toBe(true); - }); - - it.each([ - true, - false, - null, - undefined, - 1, - {}, - [], - 'a', - 'eip155', - 'eip155:', - 'eip155:1:2', - 'bip122', - 'bip122:', - 'bip122:000000000019d6689c085ae165831e93:2', - ])('returns false for an invalid chain id', (id) => { - expect(isCaipChainId(id)).toBe(false); - }); -}); - -describe('isCaipAccountId', () => { - it.each([ - 'eip155:1:0x0000000000000000000000000000000000000000', - 'eip155:1337:0x0000000000000000000000000000000000000000', - 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000', - ])('returns true for a valid account id', (id) => { - expect(isCaipAccountId(id)).toBe(true); - }); - - it.each([ - true, - false, - null, - undefined, - 1, - {}, - [], - 'foo', - 'eip155', - 'eip155:', - 'eip155:1', - 'eip155:1:', - 'eip155:1:0x0000000000000000000000000000000000000000:2', - 'bip122', - 'bip122:', - 'bip122:000000000019d6689c085ae165831e93', - 'bip122:000000000019d6689c085ae165831e93:', - 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000:2', - ])('returns false for an invalid account id', (id) => { - expect(isCaipAccountId(id)).toBe(false); - }); -}); - -describe('isCaipAccountIdArray', () => { - it.each([ - // `it.each` does not support nested arrays, so we nest them in objects. - { - accounts: [], - }, - { - accounts: [ - 'eip155:1:0x0000000000000000000000000000000000000000', - 'eip155:1337:0x0000000000000000000000000000000000000000', - 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000', - ], - }, - { - accounts: ['eip155:1:0x0000000000000000000000000000000000000000'], - }, - ])('returns true for a valid account id array', ({ accounts }) => { - expect(isCaipAccountIdArray(accounts)).toBe(true); - }); - - it.each([ - true, - false, - null, - undefined, - 1, - {}, - 'foo', - ['foo'], - ['eip155:1:0x0000000000000000000000000000000000000000:2'], - [ - 'bip122:000000000019d6689c085ae165831e93:0x0000000000000000000000000000000000000000:2', - ], - ])('returns false for an invalid account id array', (accounts) => { - expect(isCaipAccountIdArray(accounts)).toBe(false); - }); -}); - -describe('isCaipNamespace', () => { - it.each([ - { - chains: [ - { - id: 'eip155:1', - name: 'Ethereum Mainnet', - }, - ], - methods: ['eth_signTransaction', 'eth_accounts'], - events: ['accountsChanged'], - }, - { - chains: [ - { - id: 'eip155:1', - name: 'Ethereum Mainnet', - }, - ], - methods: ['eth_signTransaction'], - }, - { - chains: [ - { - id: 'eip155:1', - name: 'Ethereum Mainnet', - }, - ], - events: ['accountsChanged'], - }, - { - chains: [ - { - id: 'eip155:1', - name: 'Ethereum Mainnet', - }, - ], - }, - ])('returns true for a valid namespace', (namespace) => { - expect(isCaipNamespace(namespace)).toBe(true); - }); - - it.each([ - {}, - [], - true, - false, - null, - undefined, - 1, - 'foo', - { methods: [], events: [] }, - { chains: ['foo'] }, - ])('returns false for an invalid namespace', (namespace) => { - expect(isCaipNamespace(namespace)).toBe(false); - }); -}); - -describe('isCaipAccountAddress', () => { - it.each([ - Array(128).fill('0').join(''), - '0', - '0x0', - '0x0000000000000000000000000000000000000000', - '0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb', - '128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', - 'cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0', - '5hmuyxw9xdgbpptgypokw4thfyoe3ryenebr381z9iaegmfy', - '0x02dd1b492765c064eac4039e3841aa5f382773b598097a40073bd8b48170ab57', - '6d9b0b4b9994e8a6afbd3dc3ed983cd51c755afb27cd1dc7825ef59c134a39f7', - '0.0.1234567890-zbhlt', - ])('returns true for a valid account address', (id) => { - expect(isCaipAccountAddress(id)).toBe(true); - }); - - it.each([ - true, - false, - null, - undefined, - 1, - {}, - [], - '', - Array(129).fill('a').join(''), - ])('returns false for an invalid account address', (id) => { - expect(isCaipAccountAddress(id)).toBe(false); - }); -}); diff --git a/src/caip-types.ts b/src/caip-types.ts index cd427126..c0f4453b 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -1,133 +1,44 @@ import type { Infer } from 'superstruct'; -import { - array, - is, - object, - optional, - pattern, - size, - string, -} from 'superstruct'; +import { is, pattern, string } from 'superstruct'; export const CAIP_CHAIN_ID_REGEX = - /^(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})$/u; + /^(?[-a-z0-9]{3,8}):(?[-_a-zA-Z0-9]{1,32})$/u; -export const CAIP_NAMESPACE_ID_REGEX = /^[-a-z0-9]{3,8}$/u; +export const CAIP_NAMESPACE_REGEX = /^[-a-z0-9]{3,8}$/u; + +export const CAIP_REFERENCE_REGEX = /^[-_a-zA-Z0-9]{1,32}$/u; export const CAIP_ACCOUNT_ID_REGEX = - /^(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32})):(?[-.%a-zA-Z0-9]{1,128})$/u; + /^(?(?[-a-z0-9]{3,8}):(?[-_a-zA-Z0-9]{1,32})):(?[-.%a-zA-Z0-9]{1,128})$/u; -export const CAIP_ACCOUNT_ADDRESS_REGEX = - /^(?[-.%a-zA-Z0-9]{1,128})$/u; +export const CAIP_ACCOUNT_ADDRESS_REGEX = /^[-.%a-zA-Z0-9]{1,128}$/u; /** - * Parse a chain ID string to an object containing the namespace and reference. - * This validates the chain ID before parsing it. - * - * @param caipChainId - The chain ID to validate and parse. - * @returns The parsed chain ID. + * A CAIP-2 chain ID, i.e., a human-readable namespace and reference. */ -export function parseCaipChainId(caipChainId: CaipChainId): { - namespace: CaipNamespaceId; - reference: string; -} { - const match = CAIP_CHAIN_ID_REGEX.exec(caipChainId); - if (!match?.groups) { - throw new Error('Invalid chain ID.'); - } - - return { - namespace: match.groups.namespace as CaipNamespaceId, - reference: match.groups.reference as string, - }; -} +export const CaipChainIdStruct = pattern(string(), CAIP_CHAIN_ID_REGEX); +export type CaipChainId = `${string}:${string}`; /** - * Parse an account ID to an object containing the chain, chain ID and address. - * This validates the account ID before parsing it. - * - * @param accountId - The account ID to validate and parse. - * @returns The parsed account ID. + * A CAIP-2 namespace, i.e., the first part of a CAIP chain ID. */ -export function parseCaipAccountId(accountId: CaipAccountId): { - chain: { namespace: CaipNamespaceId; reference: string }; - chainId: CaipChainId; - address: string; -} { - const match = CAIP_ACCOUNT_ID_REGEX.exec(accountId); - if (!match?.groups) { - throw new Error('Invalid account ID.'); - } - - return { - address: match.groups.accountAddress as string, - chainId: match.groups.chainId as CaipChainId, - chain: { - namespace: match.groups.namespace as CaipNamespaceId, - reference: match.groups.reference as string, - }, - }; -} +export const CaipNamespaceStruct = pattern(string(), CAIP_NAMESPACE_REGEX); +export type CaipNamespace = Infer; /** - * A helper struct for a string with a minimum length of 1 and a maximum length - * of 40. + * A CAIP-2 reference, i.e., the second part of a CAIP chain ID. */ -export const LimitedString = size(string(), 1, 40); +export const CaipReferenceStruct = pattern(string(), CAIP_REFERENCE_REGEX); +export type CaipReference = Infer; /** - * A CAIP-2 chain ID, i.e., a human-readable namespace and reference. + * A CAIP-10 account ID, i.e., a human-readable namespace, reference, and account address. */ -export const CaipChainIdStruct = pattern(string(), CAIP_CHAIN_ID_REGEX); -export type CaipChainId = `${string}:${string}`; - export const CaipAccountIdStruct = pattern(string(), CAIP_ACCOUNT_ID_REGEX); export type CaipAccountId = `${string}:${string}:${string}`; -export const CaipAccountIdArrayStruct = array(CaipAccountIdStruct); - -/** - * A chain descriptor. - */ -export const CaipChainStruct = object({ - id: CaipChainIdStruct, - name: LimitedString, -}); -export type CaipChain = { - id: CaipChainId; - name: string; -}; - -export const CaipNamespaceStruct = object({ - /** - * A list of supported chains in the namespace. - */ - chains: array(CaipChainStruct), - - /** - * A list of supported RPC methods on the namespace, that a DApp can call. - */ - methods: optional(array(LimitedString)), - - /** - * A list of supported RPC events on the namespace, that a DApp can listen to. - */ - events: optional(array(LimitedString)), -}); -export type CaipNamespace = { - chains: CaipChain[]; - methods?: string[]; - events?: string[]; -}; - /** - * A CAIP-2 namespace, i.e., the first part of a chain ID. - */ -export const CaipNamespaceIdStruct = pattern(string(), CAIP_NAMESPACE_ID_REGEX); -export type CaipNamespaceId = Infer; - -/** - * A CAIP-10 account address, i.e., the last part of the account ID. + * A CAIP-10 account address, i.e., the third part of the CAIP account ID. */ export const CaipAccountAddressStruct = pattern( string(), @@ -136,53 +47,43 @@ export const CaipAccountAddressStruct = pattern( export type CaipAccountAddress = Infer; /** - * Check if the given value is a CAIP-2 chain ID. + * Check if the given value is a {@link CaipChainId}. * * @param value - The value to check. - * @returns Whether the value is a CAIP-2 chain ID. + * @returns Whether the value is a {@link CaipChainId}. */ export function isCaipChainId(value: unknown): value is CaipChainId { return is(value, CaipChainIdStruct); } /** - * Check if the given value is a CAIP-2 namespace ID. + * Check if the given value is a {@link CaipNamespace}. * * @param value - The value to check. - * @returns Whether the value is a CAIP-2 namespace ID. - */ -export function isCaipNamespaceId(value: unknown): value is CaipNamespaceId { - return is(value, CaipNamespaceIdStruct); -} - -/** - * Check if a value is a {@link CaipNamespace}. - * - * @param value - The value to validate. - * @returns True if the value is a valid {@link CaipNamespace}. + * @returns Whether the value is a {@link CaipNamespace}. */ export function isCaipNamespace(value: unknown): value is CaipNamespace { return is(value, CaipNamespaceStruct); } /** - * Check if the given value is a CAIP-10 account ID. + * Check if the given value is a {@link CaipReference}. * * @param value - The value to check. - * @returns Whether the value is a CAIP-10 account ID. + * @returns Whether the value is a {@link CaipReference}. */ -export function isCaipAccountId(value: unknown): value is CaipAccountId { - return is(value, CaipAccountIdStruct); +export function isCaipReference(value: unknown): value is CaipReference { + return is(value, CaipReferenceStruct); } /** - * Check if the given value is an array of CAIP-10 account IDs. + * Check if the given value is a {@link CaipAccountId}. * * @param value - The value to check. - * @returns Whether the value is an array of CAIP-10 account IDs. + * @returns Whether the value is a {@link CaipAccountId}. */ -export function isCaipAccountIdArray(value: unknown): value is CaipAccountId[] { - return is(value, CaipAccountIdArrayStruct); +export function isCaipAccountId(value: unknown): value is CaipAccountId { + return is(value, CaipAccountIdStruct); } /** @@ -196,3 +97,52 @@ export function isCaipAccountAddress( ): value is CaipAccountAddress { return is(value, CaipAccountAddressStruct); } + +/** + * Parse a CAIP-2 chain ID string to an object containing the namespace and reference. + * This validates the CAIP chain ID before parsing it. + * + * @param caipChainId - The CAIP-2 chain ID to validate and parse. + * @returns The parsed CAIP-2 chain ID. + */ +export function parseCaipChainId(caipChainId: CaipChainId): { + namespace: CaipNamespace; + reference: CaipReference; +} { + const match = CAIP_CHAIN_ID_REGEX.exec(caipChainId); + if (!match?.groups) { + throw new Error('Invalid CAIP chain ID.'); + } + + return { + namespace: match.groups.namespace as CaipNamespace, + reference: match.groups.reference as CaipReference, + }; +} + +/** + * Parse an CAIP-10 account ID to an object containing the chain, chain ID, and account address. + * This validates the CAIP account ID before parsing it. + * + * @param caipAccountId - The CAIP-10 account ID to validate and parse. + * @returns The parsed CAIP-10 account ID. + */ +export function parseCaipAccountId(caipAccountId: CaipAccountId): { + address: CaipAccountAddress; + chainId: CaipChainId; + chain: { namespace: CaipNamespace; reference: CaipReference }; +} { + const match = CAIP_ACCOUNT_ID_REGEX.exec(caipAccountId); + if (!match?.groups) { + throw new Error('Invalid CAIP account ID.'); + } + + return { + address: match.groups.accountAddress as CaipAccountAddress, + chainId: match.groups.chainId as CaipChainId, + chain: { + namespace: match.groups.namespace as CaipNamespace, + reference: match.groups.reference as CaipReference, + }, + }; +} From a70473acd0b6367692c47745a37315af45c47d74 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 28 Jul 2023 11:21:55 -0700 Subject: [PATCH 12/12] last bit --- src/caip-types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/caip-types.ts b/src/caip-types.ts index c0f4453b..bbd1b4d4 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -99,8 +99,8 @@ export function isCaipAccountAddress( } /** - * Parse a CAIP-2 chain ID string to an object containing the namespace and reference. - * This validates the CAIP chain ID before parsing it. + * Parse a CAIP-2 chain ID to an object containing the namespace and reference. + * This validates the CAIP-2 chain ID before parsing it. * * @param caipChainId - The CAIP-2 chain ID to validate and parse. * @returns The parsed CAIP-2 chain ID. @@ -121,8 +121,8 @@ export function parseCaipChainId(caipChainId: CaipChainId): { } /** - * Parse an CAIP-10 account ID to an object containing the chain, chain ID, and account address. - * This validates the CAIP account ID before parsing it. + * Parse an CAIP-10 account ID to an object containing the chain ID, parsed chain ID, and account address. + * This validates the CAIP-10 account ID before parsing it. * * @param caipAccountId - The CAIP-10 account ID to validate and parse. * @returns The parsed CAIP-10 account ID.