Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5622413
feat: add KeyringClientV2 support
ccharly Dec 4, 2025
0ed4812
fix: remove unused function
ccharly Dec 4, 2025
e59cd8f
fix: remove unused eslint directive
ccharly Dec 4, 2025
f52e5b6
fix: fix createAccounts params
ccharly Dec 4, 2025
085b7f9
fix: properly name request structs
ccharly Dec 4, 2025
058543e
feat: add rpc-handler for v2
ccharly Dec 4, 2025
45e3ed5
feat: better typing for isKeyringRpcMethod
ccharly Dec 5, 2025
84b6e95
feat: add isKeyringRpcV2Method
ccharly Dec 5, 2025
6b260f0
feat: fix createAccounts for client v2
ccharly Dec 5, 2025
fac383e
chore: lint
ccharly Dec 5, 2025
0d94203
fix: fix jsdocs
ccharly Dec 5, 2025
4b72e8b
feat: add KeyringInternalSnapClientV2 + missing v2 exports
ccharly Dec 5, 2025
2ae5c15
fix: add missing code
ccharly Dec 5, 2025
7f68ec4
refactor: revert class split
ccharly Dec 5, 2025
273e431
fix: fix test
ccharly Dec 5, 2025
b74cf08
fix: forward options for exportAccount
ccharly Dec 8, 2025
f83d807
chore: be more DRY
ccharly Dec 8, 2025
7de9e3e
test: fix test titles
ccharly Dec 8, 2025
383dfef
fix: re-use MethodNotSupportedError
ccharly Dec 8, 2025
ccd47f6
chore: add missing index.ts
ccharly Dec 8, 2025
167f0f8
chore: typo
ccharly Dec 8, 2025
0b75238
fix: make exportAccount optional
ccharly Dec 8, 2025
533f9cf
test: better deleteAccount test
ccharly Dec 10, 2025
47cae5a
refactor: use .v2 suffix
ccharly Dec 16, 2025
f142721
chore: fix lint
ccharly Jan 5, 2026
ab6a665
refactor: remove v2 prefix from KeyringRpcV2Method
ccharly Mar 20, 2026
2802610
fix: fix invalid only-throw-error
ccharly Mar 20, 2026
eee7698
Merge branch 'main' into feat/keyring-client-v2
ccharly Mar 20, 2026
1d5c494
fix: break circular deps
ccharly Mar 23, 2026
463f1d3
fix: add missing isSnapError
ccharly Mar 23, 2026
b5beb07
Merge branch 'main' into feat/keyring-client-v2
ccharly Mar 23, 2026
0248be4
chore: changelogs
ccharly Mar 23, 2026
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
8 changes: 8 additions & 0 deletions packages/keyring-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add keyring v2 RPC types and structs (`KeyringRpcV2`, `KeyringRpcV2Method`, `isKeyringRpcV2Method`, and request/response structs) ([#408](https://github.com/MetaMask/accounts/pull/408))

### Changed

- Improve return type of `isKeyringRpcMethod` to use type predicate `method is KeyringRpcMethod` ([#408](https://github.com/MetaMask/accounts/pull/408))

### Removed

- **BREAKING:** Remove `EthKeyringWrapper`, `EthKeyringMethod`, `KeyringWrapper`, and `KeyringAccountRegistry` exports ([#478](https://github.com/MetaMask/accounts/pull/478))
Expand Down
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';
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);
});
});
188 changes: 188 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,188 @@
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 = {
getAccount: KeyringV2['getAccount'];
getAccounts: KeyringV2['getAccounts'];
createAccounts: KeyringV2['createAccounts'];
deleteAccount: KeyringV2['deleteAccount'];
submitRequest: KeyringV2['submitRequest'];
exportAccount?: KeyringV2['exportAccount'];
};

/**
* Keyring RPC methods used by the API.
*/
export enum KeyringRpcV2Method {
GetAccounts = 'keyring_getAccounts',
CreateAccounts = 'keyring_createAccounts',
// Inherited from v1 (but method signatures may differ...):
// NOTE: We use literals here to avoid circular dependencies.
GetAccount = 'keyring_getAccount',
DeleteAccount = 'keyring_deleteAccount',
ExportAccount = 'keyring_exportAccount',
SubmitRequest = 'keyring_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 @@ -69,7 +69,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
4 changes: 4 additions & 0 deletions packages/keyring-internal-snap-client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `KeyringInternalSnapClientV2` class for communicating with a Snap using the keyring v2 RPC protocol ([#408](https://github.com/MetaMask/accounts/pull/408))

## [9.0.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import type {
KeyringResponseWithoutOrigin,
} 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