This repository has been archived by the owner on Jan 13, 2025. It is now read-only.
generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add Chain.getBalances method + add CAIP asset type * chore: import JsonRpcRequest + superstruct helpers from keyring-api Having an extra dependency to the keyring-api feels wrong... So for now, let's copy them here to avoid this dependency. * feat: add RPC dispatcher + chain_getBalances RPC * chore: lint * feat(caip): add support for asset id in CAIP asset type/id * refactor: properly use MethodNotSupportedError * test(caip): fix test cases * test: add test for StringNumber + fix regex * chore: import JsonRpcRequest + superstruct tests from keyring-api * test: add rpc-handler tests * test: exclude index.ts + *.test-d.ts from coverage * refactor: do not re-export caip helpers from @metamask/utils * chore: lint * feat(caip): add CaipAssetTypeOrId * feat(api): use CaipAssetTypeOrId for assets * chore: downgrade superstruct to 1.0.3 We do use a resolution for this to make sure @metamask/utils also uses the 1.0.3 (and avoid having different versions of superstruct, that might cause typing issues see like: https://github.com/MetaMask/chain-api/actions/runs/8836209841/job/24262243391?pr=1) * feat(rpc): use CaipAssetTypeOrId for assets * refactor: proper definition for AmountStruct * test: add test for undefined JsonRpcRequest.id * refactor: use JsonRpcRequest/superstruct helpers from @metamask/utils
- Loading branch information
Showing
14 changed files
with
669 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import type { CaipChainId } from '@metamask/utils'; | ||
|
||
import type { CaipAssetTypeOrId } from './caip-types'; | ||
import type { BalancesResult } from './types'; | ||
|
||
export type Chain = { | ||
getBalances( | ||
scope: CaipChainId, | ||
accounts: string[], | ||
assets: CaipAssetTypeOrId[], | ||
): Promise<BalancesResult>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { | ||
isCaipAssetType, | ||
isCaipAssetId, | ||
isCaipAssetTypeOrId, | ||
} from './caip-types'; | ||
|
||
// Imported from: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#test-cases | ||
const good = { | ||
assetTypes: [ | ||
'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', | ||
], | ||
assetIds: [ | ||
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769', | ||
'hedera:mainnet/nft:0.0.55492/12', | ||
], | ||
}; | ||
|
||
const badAssets = [ | ||
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/', | ||
]; | ||
const bad = { | ||
assetTypes: badAssets, | ||
assetIds: [ | ||
...badAssets, | ||
'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/tooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolongasset', | ||
], | ||
}; | ||
|
||
const uniq = (data1: any[], data2: any[]): any[] => { | ||
return Array.from(new Set(data1.concat(data2))); | ||
}; | ||
|
||
describe('isCaipAssetType', () => { | ||
it.each(good.assetTypes)( | ||
'returns true for a valid asset type %s', | ||
(asset) => { | ||
expect(isCaipAssetType(asset)).toBe(true); | ||
}, | ||
); | ||
|
||
it.each(bad.assetTypes)( | ||
'returns false for an invalid asset type %s', | ||
(asset) => { | ||
expect(isCaipAssetType(asset)).toBe(false); | ||
}, | ||
); | ||
}); | ||
|
||
describe('isCaipAssetId', () => { | ||
it.each(good.assetIds)('returns true for a valid asset id %s', (asset) => { | ||
expect(isCaipAssetId(asset)).toBe(true); | ||
}); | ||
|
||
it.each(bad.assetIds)('returns false for an invalid asset id %s', (asset) => { | ||
expect(isCaipAssetType(asset)).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('isCaipAssetTypeOrId', () => { | ||
it.each(uniq(good.assetIds, good.assetTypes))( | ||
'returns true for a valid asset %s', | ||
(asset) => { | ||
expect(isCaipAssetTypeOrId(asset)).toBe(true); | ||
}, | ||
); | ||
|
||
it.each(uniq(bad.assetIds, bad.assetIds))( | ||
'returns false for an invalid asset %s', | ||
(asset) => { | ||
expect(isCaipAssetTypeOrId(asset)).toBe(false); | ||
}, | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import type { Infer } from 'superstruct'; | ||
import { is, string, pattern } from 'superstruct'; | ||
|
||
export 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; | ||
|
||
export 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; | ||
|
||
export const CAIP_ASSET_TYPE_OR_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 type identifier. | ||
*/ | ||
export const CaipAssetTypeStruct = pattern(string(), 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 identifier. | ||
*/ | ||
export const CaipAssetIdStruct = pattern(string(), CAIP_ASSET_ID_REGEX); | ||
export type CaipAssetId = Infer<typeof CaipAssetIdStruct>; | ||
|
||
/** | ||
* A CAIP-19 asset type or asset ID identifier, i.e., a human-readable type of asset identifier. | ||
*/ | ||
export const CaipAssetTypeOrIdStruct = pattern( | ||
string(), | ||
CAIP_ASSET_TYPE_OR_ID_REGEX, | ||
); | ||
export type CaipAssetTypeOrId = Infer<typeof CaipAssetTypeOrIdStruct>; | ||
|
||
/** | ||
* Check if the given value is a {@link CaipAssetType}. | ||
* | ||
* @param value - The value to check. | ||
* @returns Whether the value is a {@link CaipAssetType}. | ||
*/ | ||
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}. | ||
*/ | ||
export function isCaipAssetId(value: unknown): value is CaipAssetId { | ||
return is(value, CaipAssetIdStruct); | ||
} | ||
|
||
/** | ||
* Check if the given value is a {@link CaipAssetTypeOrId}. | ||
* | ||
* @param value - The value to check. | ||
* @returns Whether the value is a {@link CaipAssetTypeOrId}. | ||
*/ | ||
export function isCaipAssetTypeOrId(value: unknown): value is CaipAssetId { | ||
return is(value, CaipAssetTypeOrIdStruct); | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,5 @@ | ||
/** | ||
* Example function that returns a greeting for the given name. | ||
* | ||
* @param name - The name to greet. | ||
* @returns The greeting. | ||
*/ | ||
export default function greeter(name: string): string { | ||
return `Hello, ${name}!`; | ||
} | ||
export * from './api'; | ||
export * from './types'; | ||
export * from './caip-types'; | ||
export * from './rpc-handler'; | ||
export * from './rpc-types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import type { JsonRpcRequest } from '@metamask/utils'; | ||
|
||
import { | ||
ChainRpcMethod, | ||
isChainRpcMethod, | ||
MethodNotSupportedError, | ||
handleChainRequest, | ||
} from './rpc-handler'; | ||
|
||
describe('rpc-handler', () => { | ||
const chain = { | ||
getBalances: jest.fn(), | ||
}; | ||
|
||
const params = { | ||
scope: 'bip122:000000000019d6689c085ae165831e93', | ||
accounts: [ | ||
'bc1qrp0yzgkf8rawkuvdlhnjfj2fnjwm0m8727kgah', | ||
'bc1qf5n2h6mgelkls4497pkpemew55xpew90td2qae', | ||
], | ||
assets: [ | ||
'bip122:000000000019d6689c085ae165831e93/asset:0', | ||
'bip122:000000000019d6689c085ae165831e93/asset:1', | ||
'bip122:000000000019d6689c085ae165831e93/asset:2', | ||
'bip122:000000000019d6689c085ae165831e93/asset:3', | ||
'bip122:000000000019d6689c085ae165831e93/asset:4', | ||
], | ||
}; | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should call chain_getBalances', async () => { | ||
const request: JsonRpcRequest = { | ||
jsonrpc: '2.0', | ||
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', | ||
method: 'chain_getBalances', | ||
params, | ||
}; | ||
|
||
chain.getBalances.mockResolvedValue('GetBalances result'); | ||
const result = await handleChainRequest(chain, request); | ||
|
||
expect(chain.getBalances).toHaveBeenCalled(); | ||
expect(result).toBe('GetBalances result'); | ||
}); | ||
|
||
it('should fail to call chainRpcDispatcher with a non-JSON-RPC request', async () => { | ||
const request = { | ||
jsonrpc: '2.0', | ||
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', | ||
// Missing method name. | ||
}; | ||
|
||
await expect( | ||
handleChainRequest(chain, request as unknown as JsonRpcRequest), | ||
).rejects.toThrow( | ||
'At path: method -- Expected a string, but received: undefined', | ||
); | ||
}); | ||
|
||
it('calls the chain with a number request ID', async () => { | ||
const request: JsonRpcRequest = { | ||
jsonrpc: '2.0', | ||
id: 1, | ||
method: 'chain_getBalances', | ||
params, | ||
}; | ||
|
||
chain.getBalances.mockResolvedValue([]); | ||
expect(await handleChainRequest(chain, request)).toStrictEqual([]); | ||
}); | ||
|
||
it('calls the chain with a null request ID', async () => { | ||
const request: JsonRpcRequest = { | ||
jsonrpc: '2.0', | ||
id: null, | ||
method: 'chain_getBalances', | ||
params, | ||
}; | ||
|
||
chain.getBalances.mockResolvedValue([]); | ||
expect(await handleChainRequest(chain, request)).toStrictEqual([]); | ||
}); | ||
|
||
it('fails to call the chain with a boolean request ID', async () => { | ||
const request: JsonRpcRequest = { | ||
jsonrpc: '2.0', | ||
id: true as any, | ||
method: 'chain_getBalances', | ||
}; | ||
|
||
chain.getBalances.mockResolvedValue([]); | ||
await expect(handleChainRequest(chain, request)).rejects.toThrow( | ||
'At path: id -- Expected the value to satisfy a union of `number | string`, but received: true', | ||
); | ||
}); | ||
|
||
it('should throw MethodNotSupportedError for an unknown method', async () => { | ||
const request: JsonRpcRequest = { | ||
jsonrpc: '2.0', | ||
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', | ||
method: 'unknown_method', | ||
params, | ||
}; | ||
|
||
await expect(handleChainRequest(chain, request)).rejects.toThrow( | ||
MethodNotSupportedError, | ||
); | ||
}); | ||
}); | ||
|
||
describe('isChainRequestMethod', () => { | ||
it.each([ | ||
[`${ChainRpcMethod.GetBalances}`, true], | ||
[`chain_invalid`, false], | ||
])(`%s should be %s`, (method, expected) => { | ||
expect(isChainRpcMethod(method)).toBe(expected); | ||
}); | ||
}); |
Oops, something went wrong.