generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add KeyringClientV2 support
#408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ccharly
wants to merge
23
commits into
main
Choose a base branch
from
feat/keyring-client-v2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
5622413
feat: add KeyringClientV2 support
ccharly 0ed4812
fix: remove unused function
ccharly e59cd8f
fix: remove unused eslint directive
ccharly f52e5b6
fix: fix createAccounts params
ccharly 085b7f9
fix: properly name request structs
ccharly 058543e
feat: add rpc-handler for v2
ccharly 45e3ed5
feat: better typing for isKeyringRpcMethod
ccharly 84b6e95
feat: add isKeyringRpcV2Method
ccharly 6b260f0
feat: fix createAccounts for client v2
ccharly fac383e
chore: lint
ccharly 0d94203
fix: fix jsdocs
ccharly 4b72e8b
feat: add KeyringInternalSnapClientV2 + missing v2 exports
ccharly 2ae5c15
fix: add missing code
ccharly 7f68ec4
refactor: revert class split
ccharly 273e431
fix: fix test
ccharly b74cf08
fix: forward options for exportAccount
ccharly f83d807
chore: be more DRY
ccharly 7de9e3e
test: fix test titles
ccharly 383dfef
fix: re-use MethodNotSupportedError
ccharly ccd47f6
chore: add missing index.ts
ccharly 167f0f8
chore: typo
ccharly 0b75238
fix: make exportAccount optional
ccharly 533f9cf
test: better deleteAccount test
ccharly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,14 @@ | ||
| import { KeyringRpcV2Method, isKeyringRpcV2Method } from './keyring-rpc'; | ||
|
|
||
| describe('isKeyringRpcV2Method', () => { | ||
| it.each(Object.values(KeyringRpcV2Method))( | ||
| 'returns true for: "%s"', | ||
| (method) => { | ||
| expect(isKeyringRpcV2Method(method)).toBe(true); | ||
| }, | ||
| ); | ||
|
|
||
| it('returns false for unknown method', () => { | ||
| expect(isKeyringRpcV2Method('keyring_unknownMethod')).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or 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,186 @@ | ||
| import { object, exactOptional, UuidStruct } from '@metamask/keyring-utils'; | ||
| import type { Infer } from '@metamask/superstruct'; | ||
| import { array, literal, number, string, union } from '@metamask/superstruct'; | ||
| import { JsonStruct } from '@metamask/utils'; | ||
|
|
||
| import { CreateAccountOptionsStruct } from './create-account'; | ||
| import { | ||
| ExportAccountOptionsStruct, | ||
| PrivateKeyExportedAccountStruct, | ||
| } from './export-account'; | ||
| import type { KeyringV2 } from './keyring'; | ||
| import { KeyringAccountStruct } from '../account'; | ||
| import { KeyringRequestStruct } from '../request'; | ||
|
|
||
| /** | ||
| * Keyring interface for keyring methods that can be invoked through | ||
| * RPC calls. | ||
| */ | ||
| export type KeyringRpcV2 = { | ||
| getAccounts: KeyringV2['getAccounts']; | ||
| getAccount: KeyringV2['getAccount']; | ||
| createAccounts: KeyringV2['createAccounts']; | ||
| deleteAccount: KeyringV2['deleteAccount']; | ||
| exportAccount: KeyringV2['exportAccount']; | ||
| submitRequest: KeyringV2['submitRequest']; | ||
| }; | ||
|
|
||
| /** | ||
| * Keyring RPC methods used by the API. | ||
| */ | ||
| export enum KeyringRpcV2Method { | ||
| GetAccounts = 'keyring_v2_getAccounts', | ||
| GetAccount = 'keyring_v2_getAccount', | ||
| CreateAccounts = 'keyring_v2_createAccounts', | ||
| DeleteAccount = 'keyring_v2_deleteAccount', | ||
| ExportAccount = 'keyring_v2_exportAccount', | ||
| SubmitRequest = 'keyring_v2_submitRequest', | ||
| } | ||
|
|
||
| /** | ||
| * Check if a method is a keyring RPC method (v2). | ||
| * | ||
| * @param method - Method to check. | ||
| * @returns Whether the method is a keyring RPC method (v2). | ||
| */ | ||
| export function isKeyringRpcV2Method( | ||
| method: string, | ||
| ): method is KeyringRpcV2Method { | ||
| return Object.values(KeyringRpcV2Method).includes( | ||
| method as KeyringRpcV2Method, | ||
| ); | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
|
|
||
| const CommonHeader = { | ||
| jsonrpc: literal('2.0'), | ||
| id: union([string(), number(), literal(null)]), | ||
| }; | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| // Get accounts | ||
|
|
||
| export const GetAccountsV2RequestStruct = object({ | ||
| ...CommonHeader, | ||
| method: literal(`${KeyringRpcV2Method.GetAccounts}`), | ||
| }); | ||
|
|
||
| export type GetAccountsV2Request = Infer<typeof GetAccountsV2RequestStruct>; | ||
|
|
||
| export const GetAccountsV2ResponseStruct = array(KeyringAccountStruct); | ||
|
|
||
| export type GetAccountsV2Response = Infer<typeof GetAccountsV2ResponseStruct>; | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| // Get account | ||
|
|
||
| export const GetAccountV2RequestStruct = object({ | ||
| ...CommonHeader, | ||
| method: literal(`${KeyringRpcV2Method.GetAccount}`), | ||
| params: object({ | ||
| id: UuidStruct, | ||
| }), | ||
| }); | ||
|
|
||
| export type GetAccountV2Request = Infer<typeof GetAccountV2RequestStruct>; | ||
|
|
||
| export const GetAccountV2ResponseStruct = KeyringAccountStruct; | ||
|
|
||
| export type GetAccountV2Response = Infer<typeof GetAccountV2ResponseStruct>; | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| // Create accounts | ||
|
|
||
| export const CreateAccountsV2RequestStruct = object({ | ||
| ...CommonHeader, | ||
| method: literal(`${KeyringRpcV2Method.CreateAccounts}`), | ||
| params: CreateAccountOptionsStruct, | ||
| }); | ||
|
|
||
| export type CreateAccountsV2Request = Infer< | ||
| typeof CreateAccountsV2RequestStruct | ||
| >; | ||
|
|
||
| export const CreateAccountsV2ResponseStruct = array(KeyringAccountStruct); | ||
|
|
||
| export type CreateAccountsV2Response = Infer< | ||
| typeof CreateAccountsV2ResponseStruct | ||
| >; | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| // Delete account | ||
|
|
||
| export const DeleteAccountV2RequestStruct = object({ | ||
| ...CommonHeader, | ||
| method: literal(`${KeyringRpcV2Method.DeleteAccount}`), | ||
| params: object({ | ||
| id: UuidStruct, | ||
| }), | ||
| }); | ||
|
|
||
| export type DeleteAccountV2Request = Infer<typeof DeleteAccountV2RequestStruct>; | ||
|
|
||
| export const DeleteAccountV2ResponseStruct = literal(null); | ||
|
|
||
| export type DeleteAccountV2Response = Infer< | ||
| typeof DeleteAccountV2ResponseStruct | ||
| >; | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| // Export account | ||
|
|
||
| export const ExportAccountV2RequestStruct = object({ | ||
| ...CommonHeader, | ||
| method: literal(`${KeyringRpcV2Method.ExportAccount}`), | ||
| params: object({ | ||
| id: UuidStruct, | ||
| options: exactOptional(ExportAccountOptionsStruct), | ||
| }), | ||
| }); | ||
|
|
||
| export type ExportAccountV2Request = Infer<typeof ExportAccountV2RequestStruct>; | ||
|
|
||
| export const ExportAccountV2ResponseStruct = PrivateKeyExportedAccountStruct; | ||
|
|
||
| export type ExportAccountV2Response = Infer< | ||
| typeof ExportAccountV2ResponseStruct | ||
| >; | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| // Submit request | ||
|
|
||
| export const SubmitRequestV2RequestStruct = object({ | ||
| ...CommonHeader, | ||
| method: literal(`${KeyringRpcV2Method.SubmitRequest}`), | ||
| params: KeyringRequestStruct, | ||
| }); | ||
|
|
||
| export type SubmitRequestV2Request = Infer<typeof SubmitRequestV2RequestStruct>; | ||
|
|
||
| export const SubmitRequestV2ResponseStruct = JsonStruct; | ||
|
|
||
| export type SubmitRequestV2Response = Infer< | ||
| typeof SubmitRequestV2ResponseStruct | ||
| >; | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Keyring RPC requests. | ||
| */ | ||
| export type KeyringRpcV2Requests = | ||
| | GetAccountsV2Request | ||
| | GetAccountV2Request | ||
| | CreateAccountsV2Request | ||
| | DeleteAccountV2Request | ||
| | ExportAccountV2Request | ||
| | SubmitRequestV2Request; | ||
|
|
||
| /** | ||
| * Extract the proper request type for a given `KeyringRpcV2Method`. | ||
| */ | ||
| export type KeyringRpcV2Request<RpcMethod extends KeyringRpcV2Method> = Extract< | ||
| KeyringRpcV2Requests, | ||
| { method: `${RpcMethod}` } | ||
| >; | ||
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 +1,2 @@ | ||
| export * from './KeyringInternalSnapClient'; | ||
| export * from './v2'; |
82 changes: 82 additions & 0 deletions
82
packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts
This file contains hidden or 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,82 @@ | ||
| import { KeyringRpcV2Method, type KeyringAccount } from '@metamask/keyring-api'; | ||
| import type { SnapId } from '@metamask/snaps-sdk'; | ||
|
|
||
| import { KeyringInternalSnapClientV2 } from './KeyringInternalSnapClientV2'; | ||
| import type { KeyringInternalSnapClientMessenger } from '../KeyringInternalSnapClient'; | ||
|
|
||
| const MOCK_ACCOUNT: KeyringAccount = { | ||
| id: '13f94041-6ae6-451f-a0fe-afdd2fda18a7', | ||
| address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae', | ||
| options: {}, | ||
| methods: [], | ||
| scopes: ['eip155:0'], | ||
| type: 'eip155:eoa', | ||
| }; | ||
|
|
||
| describe('KeyringInternalSnapClientV2', () => { | ||
| const snapId = 'local:localhost:3000' as SnapId; | ||
|
|
||
| const accountsList: KeyringAccount[] = [MOCK_ACCOUNT]; | ||
|
|
||
| const messenger = { | ||
| call: jest.fn(), | ||
| }; | ||
|
|
||
| describe('getAccounts', () => { | ||
| const request = { | ||
| snapId, | ||
| origin: 'metamask', | ||
| handler: 'onKeyringRequest', | ||
| request: { | ||
| id: expect.any(String), | ||
| jsonrpc: '2.0', | ||
| method: KeyringRpcV2Method.GetAccounts, | ||
| }, | ||
| }; | ||
|
|
||
| it('calls the getAccounts method and return the result', async () => { | ||
| const client = new KeyringInternalSnapClientV2({ | ||
| messenger: messenger as unknown as KeyringInternalSnapClientMessenger, | ||
| snapId, | ||
| }); | ||
|
|
||
| messenger.call.mockResolvedValue(accountsList); | ||
| const accounts = await client.getAccounts(); | ||
| expect(messenger.call).toHaveBeenCalledWith( | ||
| 'SnapController:handleRequest', | ||
| request, | ||
| ); | ||
| expect(accounts).toStrictEqual(accountsList); | ||
| }); | ||
|
|
||
| it('calls the getAccounts method and return the result (withSnapId)', async () => { | ||
| const client = new KeyringInternalSnapClientV2({ | ||
| messenger: messenger as unknown as KeyringInternalSnapClientMessenger, | ||
| }); | ||
|
|
||
| messenger.call.mockResolvedValue(accountsList); | ||
| const accounts = await client.withSnapId(snapId).getAccounts(); | ||
| expect(messenger.call).toHaveBeenCalledWith( | ||
| 'SnapController:handleRequest', | ||
| request, | ||
| ); | ||
| expect(accounts).toStrictEqual(accountsList); | ||
| }); | ||
|
|
||
| it('calls the default snapId value ("undefined")', async () => { | ||
| const client = new KeyringInternalSnapClientV2({ | ||
| messenger: messenger as unknown as KeyringInternalSnapClientMessenger, | ||
| }); | ||
|
|
||
| messenger.call.mockResolvedValue(accountsList); | ||
| await client.getAccounts(); | ||
| expect(messenger.call).toHaveBeenCalledWith( | ||
| 'SnapController:handleRequest', | ||
| { | ||
| ...request, | ||
| snapId: 'undefined', | ||
| }, | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
59 changes: 59 additions & 0 deletions
59
packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts
This file contains hidden or 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,59 @@ | ||
| import { KeyringClientV2 } from '@metamask/keyring-snap-client'; | ||
| import type { SnapId } from '@metamask/snaps-sdk'; | ||
| import type { HandlerType } from '@metamask/snaps-utils'; | ||
|
|
||
| import type { KeyringInternalSnapClientMessenger } from '../KeyringInternalSnapClient'; | ||
| import { SnapControllerMessengerSender } from '../KeyringInternalSnapClient'; | ||
|
|
||
| /** | ||
| * A `KeyringClient` that allows the communication with a Snap through a | ||
| * `Messenger`. | ||
| */ | ||
| export class KeyringInternalSnapClientV2 extends KeyringClientV2 { | ||
| readonly #messenger: KeyringInternalSnapClientMessenger; | ||
|
|
||
| /** | ||
| * Create a new instance of `KeyringInternalSnapClient`. | ||
ccharly marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * The `handlerType` argument has a hard-coded default `string` value instead | ||
| * of a `HandlerType` value to prevent the `@metamask/snaps-utils` module | ||
| * from being required at runtime. | ||
| * | ||
| * @param args - Constructor arguments. | ||
| * @param args.messenger - The `KeyringInternalSnapClientMessenger` instance to use. | ||
| * @param args.snapId - The ID of the Snap to use (default: `'undefined'`). | ||
| * @param args.origin - The sender's origin (default: `'metamask'`). | ||
| * @param args.handler - The handler type (default: `'onKeyringRequest'`). | ||
| */ | ||
| constructor({ | ||
| messenger, | ||
| snapId = 'undefined' as SnapId, | ||
| origin = 'metamask', | ||
| handler = 'onKeyringRequest' as HandlerType, | ||
| }: { | ||
| messenger: KeyringInternalSnapClientMessenger; | ||
| snapId?: SnapId; | ||
| origin?: string; | ||
| handler?: HandlerType; | ||
| }) { | ||
| super( | ||
| new SnapControllerMessengerSender(messenger, snapId, origin, handler), | ||
| ); | ||
| this.#messenger = messenger; | ||
| } | ||
|
|
||
| /** | ||
| * Create a new instance of `KeyringInternalSnapClient` with the specified | ||
ccharly marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * `snapId`. | ||
| * | ||
| * @param snapId - The ID of the Snap to use in the new instance. | ||
| * @returns A new instance of `KeyringInternalSnapClient` with the | ||
ccharly marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * specified Snap ID. | ||
| */ | ||
| withSnapId(snapId: SnapId): KeyringInternalSnapClientV2 { | ||
| return new KeyringInternalSnapClientV2({ | ||
| messenger: this.#messenger, | ||
| snapId, | ||
| }); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.