Skip to content

Commit 5622413

Browse files
committed
feat: add KeyringClientV2 support
1 parent 6376bb7 commit 5622413

File tree

4 files changed

+500
-0
lines changed

4 files changed

+500
-0
lines changed

packages/keyring-api/src/api/v2/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type * from './keyring';
22
export * from './keyring-capabilities';
33
export * from './keyring-type';
4+
export * from './keyring-rpc';
45
export * from './create-account';
56
export * from './export-account';
67
export * from './private-key';
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { object, exactOptional, UuidStruct } from '@metamask/keyring-utils';
2+
import type { Infer } from '@metamask/superstruct';
3+
import { array, literal, number, string, union } from '@metamask/superstruct';
4+
import { JsonStruct } from '@metamask/utils';
5+
6+
import {
7+
ExportAccountOptionsStruct,
8+
PrivateKeyExportedAccountStruct,
9+
} from './export-account';
10+
import type { KeyringV2 } from './keyring';
11+
import { KeyringAccountStruct } from '../account';
12+
import { KeyringRequestStruct } from '../request';
13+
14+
/**
15+
* Keyring interface for keyring methods that can be invoked through
16+
* RPC calls.
17+
*/
18+
export type KeyringRpcV2 = {
19+
getAccounts: KeyringV2['getAccounts'];
20+
getAccount: KeyringV2['getAccount'];
21+
createAccounts: KeyringV2['createAccounts'];
22+
deleteAccount: KeyringV2['deleteAccount'];
23+
exportAccount: KeyringV2['exportAccount'];
24+
submitRequest: KeyringV2['submitRequest'];
25+
};
26+
27+
/**
28+
* Keyring RPC methods used by the API.
29+
*/
30+
export enum KeyringRpcV2Method {
31+
GetAccounts = 'keyring_v2_getAccounts',
32+
GetAccount = 'keyring_v2_getAccount',
33+
CreateAccounts = 'keyring_v2_createAccounts',
34+
DeleteAccount = 'keyring_v2_deleteAccount',
35+
ExportAccount = 'keyring_v2_exportAccount',
36+
SubmitRequest = 'keyring_v2_submitRequest',
37+
}
38+
39+
/**
40+
* Check if a method is a keyring RPC method.
41+
*
42+
* @param method - Method to check.
43+
* @returns Whether the method is a keyring RPC method.
44+
*/
45+
export function isKeyringRpcV2Method(method: string): boolean {
46+
return Object.values(KeyringRpcV2Method).includes(
47+
method as KeyringRpcV2Method,
48+
);
49+
}
50+
51+
// ----------------------------------------------------------------------------
52+
53+
const CommonHeader = {
54+
jsonrpc: literal('2.0'),
55+
id: union([string(), number(), literal(null)]),
56+
};
57+
58+
// ----------------------------------------------------------------------------
59+
// Get accounts
60+
61+
export const GetAccountsV2Struct = object({
62+
...CommonHeader,
63+
method: literal(`${KeyringRpcV2Method.GetAccounts}`),
64+
});
65+
66+
export type GetAccountsV2Request = Infer<typeof GetAccountsV2Struct>;
67+
68+
export const GetAccountsV2ResponseStruct = array(KeyringAccountStruct);
69+
70+
export type GetAccountsV2Response = Infer<typeof GetAccountsV2ResponseStruct>;
71+
72+
// ----------------------------------------------------------------------------
73+
// Get account
74+
75+
export const GetAccountV2Struct = object({
76+
...CommonHeader,
77+
method: literal(`${KeyringRpcV2Method.GetAccount}`),
78+
params: object({
79+
id: UuidStruct,
80+
}),
81+
});
82+
83+
export type GetAccountV2Request = Infer<typeof GetAccountV2Struct>;
84+
85+
export const GetAccountV2ResponseStruct = KeyringAccountStruct;
86+
87+
export type GetAccountV2Response = Infer<typeof GetAccountV2ResponseStruct>;
88+
89+
// ----------------------------------------------------------------------------
90+
// Create accounts
91+
92+
export const CreateAccountsV2Struct = object({
93+
...CommonHeader,
94+
method: literal(`${KeyringRpcV2Method.CreateAccounts}`),
95+
params: object({
96+
// TODO
97+
}),
98+
});
99+
100+
export type CreateAccountsV2Request = Infer<typeof CreateAccountsV2Struct>;
101+
102+
export const CreateAccountsV2ResponseStruct = array(KeyringAccountStruct);
103+
104+
export type CreateAccountsV2Response = Infer<
105+
typeof CreateAccountsV2ResponseStruct
106+
>;
107+
108+
// ----------------------------------------------------------------------------
109+
// Delete account
110+
111+
export const DeleteAccountV2RequestStruct = object({
112+
...CommonHeader,
113+
method: literal(`${KeyringRpcV2Method.DeleteAccount}`),
114+
params: object({
115+
id: UuidStruct,
116+
}),
117+
});
118+
119+
export type DeleteAccountV2Request = Infer<typeof DeleteAccountV2RequestStruct>;
120+
121+
export const DeleteAccountV2ResponseStruct = literal(null);
122+
123+
export type DeleteAccountV2Response = Infer<
124+
typeof DeleteAccountV2ResponseStruct
125+
>;
126+
127+
// ----------------------------------------------------------------------------
128+
// Export account
129+
130+
export const ExportAccountV2RequestStruct = object({
131+
...CommonHeader,
132+
method: literal(`${KeyringRpcV2Method.ExportAccount}`),
133+
params: object({
134+
id: UuidStruct,
135+
options: exactOptional(ExportAccountOptionsStruct),
136+
}),
137+
});
138+
139+
export type ExportAccountV2Request = Infer<typeof ExportAccountV2RequestStruct>;
140+
141+
export const ExportAccountV2ResponseStruct = PrivateKeyExportedAccountStruct;
142+
143+
export type ExportAccountV2Response = Infer<
144+
typeof ExportAccountV2ResponseStruct
145+
>;
146+
147+
// ----------------------------------------------------------------------------
148+
// Submit request
149+
150+
export const SubmitRequestV2RequestStruct = object({
151+
...CommonHeader,
152+
method: literal(`${KeyringRpcV2Method.SubmitRequest}`),
153+
params: KeyringRequestStruct,
154+
});
155+
156+
export type SubmitRequestV2Request = Infer<typeof SubmitRequestV2RequestStruct>;
157+
158+
export const SubmitRequestV2ResponseStruct = JsonStruct;
159+
160+
export type SubmitRequestV2Response = Infer<
161+
typeof SubmitRequestV2ResponseStruct
162+
>;
163+
164+
// ----------------------------------------------------------------------------
165+
166+
/**
167+
* Keyring RPC requests.
168+
*/
169+
export type KeyringRpcV2Requests =
170+
| GetAccountsV2Request
171+
| GetAccountV2Request
172+
| CreateAccountsV2Request
173+
| DeleteAccountV2Request
174+
| ExportAccountV2Request
175+
| SubmitRequestV2Request;
176+
177+
/**
178+
* Extract the proper request type for a given `KeyringRpcV2Method`.
179+
*/
180+
export type KeyringRpcV2Request<RpcMethod extends KeyringRpcV2Method> = Extract<
181+
KeyringRpcV2Requests,
182+
{ method: `${RpcMethod}` }
183+
>;
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import {
2+
KeyringRpcV2Method,
3+
PrivateKeyEncoding,
4+
type KeyringAccount,
5+
type KeyringRequest,
6+
} from '@metamask/keyring-api';
7+
import type { Json } from '@metamask/utils';
8+
9+
import { KeyringClientV2 } from './KeyringClientV2';
10+
11+
describe('KeyringClient', () => {
12+
const mockSender = {
13+
send: jest.fn(),
14+
};
15+
16+
beforeEach(() => {
17+
mockSender.send.mockClear();
18+
});
19+
20+
describe('KeyringClientV2', () => {
21+
const client = new KeyringClientV2(mockSender);
22+
23+
describe('getAccounts', () => {
24+
it('sends a request to get accounts and return the response', async () => {
25+
const expectedResponse: KeyringAccount[] = [
26+
{
27+
id: '49116980-0712-4fa5-b045-e4294f1d440e',
28+
address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae',
29+
options: {},
30+
methods: [],
31+
scopes: ['eip155:0'],
32+
type: 'eip155:eoa',
33+
},
34+
];
35+
36+
mockSender.send.mockResolvedValue(expectedResponse);
37+
const accounts = await client.getAccounts();
38+
expect(mockSender.send).toHaveBeenCalledWith({
39+
jsonrpc: '2.0',
40+
id: expect.any(String),
41+
method: `${KeyringRpcV2Method.GetAccounts}`,
42+
});
43+
expect(accounts).toStrictEqual(expectedResponse);
44+
});
45+
});
46+
47+
describe('getAccount', () => {
48+
it('sends a request to get an account by ID and return the response', async () => {
49+
const id = '49116980-0712-4fa5-b045-e4294f1d440e';
50+
const expectedResponse: KeyringAccount = {
51+
id: '49116980-0712-4fa5-b045-e4294f1d440e',
52+
address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae',
53+
options: {},
54+
methods: [],
55+
scopes: ['eip155:0'],
56+
type: 'eip155:eoa',
57+
};
58+
59+
mockSender.send.mockResolvedValue(expectedResponse);
60+
const account = await client.getAccount(id);
61+
expect(mockSender.send).toHaveBeenCalledWith({
62+
jsonrpc: '2.0',
63+
id: expect.any(String),
64+
method: `${KeyringRpcV2Method.GetAccount}`,
65+
params: { id },
66+
});
67+
expect(account).toStrictEqual(expectedResponse);
68+
});
69+
});
70+
71+
describe('createAccounts', () => {
72+
it('sends a request to create an account and return the response', async () => {
73+
const expectedResponse: KeyringAccount[] = [
74+
{
75+
id: '49116980-0712-4fa5-b045-e4294f1d440e',
76+
address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae',
77+
options: {},
78+
methods: [],
79+
scopes: ['eip155:0'],
80+
type: 'eip155:eoa',
81+
},
82+
];
83+
84+
mockSender.send.mockResolvedValue(expectedResponse);
85+
const account = await client.createAccounts();
86+
expect(mockSender.send).toHaveBeenCalledWith({
87+
jsonrpc: '2.0',
88+
id: expect.any(String),
89+
method: `${KeyringRpcV2Method.CreateAccounts}`,
90+
params: { options: {} },
91+
});
92+
expect(account).toStrictEqual(expectedResponse);
93+
});
94+
});
95+
96+
describe('deleteAccount', () => {
97+
it('sends a request to delete an account', async () => {
98+
const id = '49116980-0712-4fa5-b045-e4294f1d440e';
99+
100+
mockSender.send.mockResolvedValue(null);
101+
const response = await client.deleteAccount(id);
102+
expect(mockSender.send).toHaveBeenCalledWith({
103+
jsonrpc: '2.0',
104+
id: expect.any(String),
105+
method: `${KeyringRpcV2Method.DeleteAccount}`,
106+
params: { id },
107+
});
108+
expect(response).toBeUndefined();
109+
});
110+
});
111+
112+
describe('exportAccount', () => {
113+
it('sends a request to export an account', async () => {
114+
const id = '49116980-0712-4fa5-b045-e4294f1d440e';
115+
const expectedResponse = {
116+
type: 'private-key',
117+
privateKey: '0x000000000',
118+
encoding: 'hexadecimal',
119+
};
120+
121+
mockSender.send.mockResolvedValue(expectedResponse);
122+
const response = await client.exportAccount(id);
123+
expect(mockSender.send).toHaveBeenCalledWith({
124+
jsonrpc: '2.0',
125+
id: expect.any(String),
126+
method: `${KeyringRpcV2Method.ExportAccount}`,
127+
params: { id },
128+
});
129+
expect(response).toStrictEqual(expectedResponse);
130+
});
131+
132+
it('sends a request to export an account with options', async () => {
133+
const id = '49116980-0712-4fa5-b045-e4294f1d440e';
134+
const expectedResponse = {
135+
type: 'private-key',
136+
privateKey: '0x000000000',
137+
encoding: 'hexadecimal',
138+
};
139+
const options = {
140+
type: 'private-key' as const,
141+
encoding: PrivateKeyEncoding.Hexadecimal,
142+
};
143+
144+
mockSender.send.mockResolvedValue(expectedResponse);
145+
const response = await client.exportAccount(id, options);
146+
expect(mockSender.send).toHaveBeenCalledWith({
147+
jsonrpc: '2.0',
148+
id: expect.any(String),
149+
method: `${KeyringRpcV2Method.ExportAccount}`,
150+
params: {
151+
id,
152+
options,
153+
},
154+
});
155+
expect(response).toStrictEqual(expectedResponse);
156+
});
157+
});
158+
159+
describe('submitRequest', () => {
160+
it('sends a request to submit a request', async () => {
161+
const request: KeyringRequest = {
162+
id: '71621d8d-62a4-4bf4-97cc-fb8f243679b0',
163+
scope: 'eip155:1',
164+
origin: 'test',
165+
account: '46b5ccd3-4786-427c-89d2-cef626dffe9b',
166+
request: {
167+
method: 'personal_sign',
168+
params: ['0xe9a74aacd7df8112911ca93260fc5a046f8a64ae', '0x0'],
169+
},
170+
};
171+
const expectedResponse: Json = {
172+
result: 'success',
173+
};
174+
175+
mockSender.send.mockResolvedValue(expectedResponse);
176+
const response = await client.submitRequest(request);
177+
expect(mockSender.send).toHaveBeenCalledWith({
178+
jsonrpc: '2.0',
179+
id: expect.any(String),
180+
method: `${KeyringRpcV2Method.SubmitRequest}`,
181+
params: request,
182+
});
183+
expect(response).toStrictEqual(expectedResponse);
184+
});
185+
});
186+
});
187+
});

0 commit comments

Comments
 (0)