Skip to content

Commit

Permalink
feat: add CAIP-19 types and split utils (#321)
Browse files Browse the repository at this point in the history
  • Loading branch information
danroc committed May 28, 2024
1 parent 4edbbd7 commit 800a7bb
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 54 deletions.
2 changes: 1 addition & 1 deletion src/KeyringClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from './internal/api';
import { KeyringRpcMethod } from './internal/rpc';
import type { JsonRpcRequest } from './JsonRpcRequest';
import { strictMask } from './utils';
import { strictMask } from './superstruct';

export type Sender = {
send(request: JsonRpcRequest): Promise<Json>;
Expand Down
3 changes: 2 additions & 1 deletion src/eth/erc4337/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Infer } from 'superstruct';

import { UrlStruct, exactOptional, object } from '../../superstruct';
import { exactOptional, object } from '../../superstruct';
import { UrlStruct } from '../../utils';
import { EthAddressStruct, EthBytesStruct, EthUint256Struct } from '../types';

/**
Expand Down
2 changes: 1 addition & 1 deletion src/eth/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UrlStruct } from '../superstruct';
import { UrlStruct } from '../utils';

describe('types', () => {
it('is a valid BundlerUrl', () => {
Expand Down
32 changes: 17 additions & 15 deletions src/superstruct.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Infer, Context } from 'superstruct';
import { Struct, define, object as stObject } from 'superstruct';
import { Struct, assert, define, object as stObject } from 'superstruct';
import type {
ObjectSchema,
OmitBy,
Expand Down Expand Up @@ -127,19 +127,21 @@ export function definePattern(
}

/**
* Validates if a given value is a valid URL.
* Assert that a value is valid according to a struct.
*
* @param value - The value to be validated.
* @returns A boolean indicating if the value is a valid URL.
* It is similar to superstruct's mask function, but it does not ignore extra
* properties.
*
* @param value - Value to check.
* @param struct - Struct to validate the value against.
* @param message - Error message to throw if the value is not valid.
* @returns The value if it is valid.
*/
export const UrlStruct = define<string>('Url', (value: unknown) => {
let url;

try {
url = new URL(value as string);
} catch (_) {
return false;
}

return url.protocol === 'http:' || url.protocol === 'https:';
});
export function strictMask<Type, Schema>(
value: unknown,
struct: Struct<Type, Schema>,
message?: string,
): Type {
assert(value, struct, message);
return value;
}
86 changes: 86 additions & 0 deletions src/utils/caip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { isCaipAssetId, isCaipAssetType } from './caip';

describe('isCaipAssetType', () => {
// Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases
it.each([
'eip155:1/slip44:60',
'bip122:000000000019d6689c085ae165831e93/slip44:0',
'cosmos:cosmoshub-3/slip44:118',
'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2',
'cosmos:Binance-Chain-Tigris/slip44:714',
'cosmos:iov-mainnet/slip44:234',
'lip9:9ee11e9df416b18b/slip44:134',
'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d',
])('returns true for a valid asset type %s', (id) => {
expect(isCaipAssetType(id)).toBe(true);
});

it.each([
true,
false,
null,
undefined,
1,
{},
[],
'',
'!@#$%^&*()',
'foo',
'eip155',
'eip155:',
'eip155:1',
'eip155:1:',
'eip155:1:0x0000000000000000000000000000000000000000:2',
'bip122',
'bip122:',
'bip122:000000000019d6689c085ae165831e93',
'bip122:000000000019d6689c085ae165831e93/',
'bip122:000000000019d6689c085ae165831e93/tooooooolong',
'bip122:000000000019d6689c085ae165831e93/tooooooolong:asset',
'eip155:1/erc721',
'eip155:1/erc721:',
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/',
])('returns false for an invalid asset type %s', (id) => {
expect(isCaipAssetType(id)).toBe(false);
});
});

describe('isCaipAssetId', () => {
// Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases
it.each([
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769',
'hedera:mainnet/nft:0.0.55492/12',
])('returns true for a valid asset id %s', (id) => {
expect(isCaipAssetId(id)).toBe(true);
});

it.each([
true,
false,
null,
undefined,
1,
{},
[],
'',
'!@#$%^&*()',
'foo',
'eip155',
'eip155:',
'eip155:1',
'eip155:1:',
'eip155:1:0x0000000000000000000000000000000000000000:2',
'bip122',
'bip122:',
'bip122:000000000019d6689c085ae165831e93',
'bip122:000000000019d6689c085ae165831e93/',
'bip122:000000000019d6689c085ae165831e93/tooooooolong',
'bip122:000000000019d6689c085ae165831e93/tooooooolong:asset',
'eip155:1/erc721',
'eip155:1/erc721:',
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/',
])('returns false for an invalid asset id %s', (id) => {
expect(isCaipAssetType(id)).toBe(false);
});
});
59 changes: 59 additions & 0 deletions src/utils/caip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { is, type Infer } from 'superstruct';

import { definePattern } from '../superstruct';

const CAIP_ASSET_TYPE_REGEX =
/^(?<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;

const CAIP_ASSET_ID_REGEX =
/^(?<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;

/**
* A CAIP-19 asset type identifier, i.e., a human-readable type of asset identifier.
*/
export const CaipAssetTypeStruct = definePattern(
'CaipAssetType',
CAIP_ASSET_TYPE_REGEX,
);
export type CaipAssetType = Infer<typeof CaipAssetTypeStruct>;

/**
* A CAIP-19 asset ID identifier, i.e., a human-readable type of asset ID.
*/
export const CaipAssetIdStruct = definePattern(
'CaipAssetId',
CAIP_ASSET_ID_REGEX,
);
export type CaipAssetId = Infer<typeof CaipAssetIdStruct>;

/**
* Check if the given value is a {@link CaipAssetType}.
*
* @param value - The value to check.
* @returns Whether the value is a {@link CaipAssetType}.
* @example
* ```ts
* isCaipAssetType('eip155:1/slip44:60'); // true
* isCaipAssetType('cosmos:cosmoshub-3/slip44:118'); // true
* isCaipAssetType('hedera:mainnet/nft:0.0.55492/12'); // false
* ```
*/
export function isCaipAssetType(value: unknown): value is CaipAssetType {
return is(value, CaipAssetTypeStruct);
}

/**
* Check if the given value is a {@link CaipAssetId}.
*
* @param value - The value to check.
* @returns Whether the value is a {@link CaipAssetId}.
* @example
* ```ts
* isCaipAssetType('eip155:1/slip44:60'); // false
* isCaipAssetType('cosmos:cosmoshub-3/slip44:118'); // false
* isCaipAssetType('hedera:mainnet/nft:0.0.55492/12'); // true
* ```
*/
export function isCaipAssetId(value: unknown): value is CaipAssetId {
return is(value, CaipAssetIdStruct);
}
4 changes: 4 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './caip';
export * from './typing';
export * from './url';
export * from './uuid';
4 changes: 2 additions & 2 deletions src/utils.test-d.ts → src/utils/typing.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Extends } from './utils';
import { expectTrue } from './utils';
import type { Extends } from './typing';
import { expectTrue } from './typing';

expectTrue<true>();

Expand Down
2 changes: 1 addition & 1 deletion src/utils.test.ts → src/utils/typing.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expectTrue } from './utils';
import { expectTrue } from './typing';

describe('expectTrue', () => {
it('does nothing since expectTrue is an empty function', () => {
Expand Down
33 changes: 0 additions & 33 deletions src/utils.ts → src/utils/typing.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
import { assert } from 'superstruct';
import type { Struct } from 'superstruct';

import { definePattern } from './superstruct';

/**
* UUIDv4 struct.
*/
export const UuidStruct = definePattern(
'UuidV4',
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,
);

/**
* Omit keys from a union type.
*
Expand All @@ -23,26 +10,6 @@ export type OmitUnion<Type, Key extends keyof any> = Type extends any
? Omit<Type, Key>
: never;

/**
* Assert that a value is valid according to a struct.
*
* It is similar to superstruct's mask function, but it does not ignore extra
* properties.
*
* @param value - Value to check.
* @param struct - Struct to validate the value against.
* @param message - Error message to throw if the value is not valid.
* @returns The value if it is valid.
*/
export function strictMask<Type, Schema>(
value: unknown,
struct: Struct<Type, Schema>,
message?: string,
): Type {
assert(value, struct, message);
return value;
}

/**
* Type that resolves to `true` if `Child` extends `Base`, otherwise `false`.
*
Expand Down
19 changes: 19 additions & 0 deletions src/utils/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { define } from 'superstruct';

/**
* Validates if a given value is a valid URL.
*
* @param value - The value to be validated.
* @returns A boolean indicating if the value is a valid URL.
*/
export const UrlStruct = define<string>('Url', (value: unknown) => {
let url;

try {
url = new URL(value as string);
} catch (_) {
return false;
}

return url.protocol === 'http:' || url.protocol === 'https:';
});
9 changes: 9 additions & 0 deletions src/utils/uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { definePattern } from '../superstruct';

/**
* UUIDv4 struct.
*/
export const UuidStruct = definePattern(
'UuidV4',
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,
);

0 comments on commit 800a7bb

Please sign in to comment.