Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/keyring-api/src/api/v2/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type * from './keyring';
export * from './keyring-capabilities';
export * from './keyring-type';
export * from './keyring-rpc';
export * from './create-account';
export * from './export-account';
export * from './private-key';
Expand Down
14 changes: 14 additions & 0 deletions packages/keyring-api/src/api/v2/keyring-rpc.test.ts
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);
});
});
186 changes: 186 additions & 0 deletions packages/keyring-api/src/api/v2/keyring-rpc.ts
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}` }
>;
2 changes: 1 addition & 1 deletion packages/keyring-api/src/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { KeyringRpcMethod, isKeyringRpcMethod } from './rpc';

describe('isKeyringRpcMethod', () => {
it.each(Object.values(KeyringRpcMethod))(
'returns true for: KeyringRpcMethod.$s',
'returns true for: "%s"',
(method) => {
expect(isKeyringRpcMethod(method)).toBe(true);
},
Expand Down
2 changes: 1 addition & 1 deletion packages/keyring-api/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export enum KeyringRpcMethod {
* @param method - Method to check.
* @returns Whether the method is a keyring RPC method.
*/
export function isKeyringRpcMethod(method: string): boolean {
export function isKeyringRpcMethod(method: string): method is KeyringRpcMethod {
return Object.values(KeyringRpcMethod).includes(method as KeyringRpcMethod);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import type {
KeyringResponseV1,
} from '@metamask/keyring-internal-api';
import { SubmitRequestResponseV1Struct } from '@metamask/keyring-internal-api';
import { KeyringClient, type Sender } from '@metamask/keyring-snap-client';
import { strictMask, type JsonRpcRequest } from '@metamask/keyring-utils';
import type { Sender } from '@metamask/keyring-snap-client';
import { KeyringClient } from '@metamask/keyring-snap-client';
import { strictMask } from '@metamask/keyring-utils';
import type { Messenger } from '@metamask/messenger';
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
import type { JsonRpcRequest, SnapId } from '@metamask/snaps-sdk';
import type { HandlerType } from '@metamask/snaps-utils';
import type { Json } from '@metamask/utils';

Expand All @@ -28,7 +29,7 @@ export type KeyringInternalSnapClientMessenger = Messenger<
* Implementation of the `Sender` interface that can be used to send requests
* to a Snap through a `Messenger`.
*/
class SnapControllerMessengerSender implements Sender {
export class SnapControllerMessengerSender implements Sender {
readonly #snapId: SnapId;

readonly #origin: string;
Expand Down
1 change: 1 addition & 0 deletions packages/keyring-internal-snap-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './KeyringInternalSnapClient';
export * from './v2';
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',
},
);
});
});
});
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`.
*
* 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
* `snapId`.
*
* @param snapId - The ID of the Snap to use in the new instance.
* @returns A new instance of `KeyringInternalSnapClient` with the
* specified Snap ID.
*/
withSnapId(snapId: SnapId): KeyringInternalSnapClientV2 {
return new KeyringInternalSnapClientV2({
messenger: this.#messenger,
snapId,
});
}
}
Loading
Loading