Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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);
});
});
193 changes: 193 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,193 @@
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 { KeyringRpcMethod } from '../../rpc';
import { KeyringAccountStruct } from '../account';
import { KeyringRequestStruct } from '../request';

/**
* Keyring interface for keyring methods that can be invoked through
* RPC calls.
*/
export type KeyringRpcV2 = {
getAccount: KeyringV2['getAccount'];
getAccounts: KeyringV2['getAccounts'];
createAccounts: KeyringV2['createAccounts'];
deleteAccount: KeyringV2['deleteAccount'];
submitRequest: KeyringV2['submitRequest'];
exportAccount?: KeyringV2['exportAccount'];
};

/**
* Keyring RPC version identifier.
*/
export const KEYRING_RPC_V2 = 'v2';
const V2 = KEYRING_RPC_V2; // Local alias for easier usage.

/**
* Keyring RPC methods used by the API.
*/
export enum KeyringRpcV2Method {
GetAccount = `${KeyringRpcMethod.GetAccount}.${V2}`,
GetAccounts = `keyring_getAccounts.${V2}`,
CreateAccounts = `keyring_createAccounts.${V2}`,
DeleteAccount = `${KeyringRpcMethod.DeleteAccount}.${V2}`,
ExportAccount = `${KeyringRpcMethod.ExportAccount}.${V2}`,
SubmitRequest = `${KeyringRpcMethod.SubmitRequest}.${V2}`,
}

/**
* 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',
},
);
});
});
});
Loading
Loading