Skip to content

Commit d28ee8e

Browse files
committed
Move snap_manageAccounts to a gated permitted method
1 parent 45db6e7 commit d28ee8e

File tree

10 files changed

+345
-300
lines changed

10 files changed

+345
-300
lines changed

packages/snaps-rpc-methods/src/permissions.test.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,6 @@ describe('buildSnapRestrictedMethodSpecifications', () => {
211211
],
212212
"targetName": "snap_getPreferences",
213213
},
214-
"snap_manageAccounts": {
215-
"allowedCaveats": null,
216-
"methodImplementation": [Function],
217-
"permissionType": "RestrictedMethod",
218-
"subjectTypes": [
219-
"snap",
220-
],
221-
"targetName": "snap_manageAccounts",
222-
},
223214
"snap_manageState": {
224215
"allowedCaveats": null,
225216
"methodImplementation": [Function],

packages/snaps-rpc-methods/src/permitted/handlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getInterfaceStateHandler } from './getInterfaceState';
99
import { getSnapsHandler } from './getSnaps';
1010
import { invokeKeyringHandler } from './invokeKeyring';
1111
import { invokeSnapSugarHandler } from './invokeSnapSugar';
12+
import { manageAccountsHandler } from './manageAccounts';
1213
import { requestSnapsHandler } from './requestSnaps';
1314
import { resolveInterfaceHandler } from './resolveInterface';
1415
import { updateInterfaceHandler } from './updateInterface';
@@ -29,6 +30,7 @@ export const methodHandlers = {
2930
snap_resolveInterface: resolveInterfaceHandler,
3031
snap_getCurrencyRate: getCurrencyRateHandler,
3132
snap_experimentalProviderRequest: providerRequestHandler,
33+
snap_manageAccounts: manageAccountsHandler,
3234
};
3335
/* eslint-enable @typescript-eslint/naming-convention */
3436

packages/snaps-rpc-methods/src/permitted/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { GetClientStatusHooks } from './getClientStatus';
55
import type { GetCurrencyRateMethodHooks } from './getCurrencyRate';
66
import type { GetInterfaceStateMethodHooks } from './getInterfaceState';
77
import type { GetSnapsHooks } from './getSnaps';
8+
import type { ManageAccountsMethodHooks } from './manageAccounts';
89
import type { RequestSnapsHooks } from './requestSnaps';
910
import type { ResolveInterfaceMethodHooks } from './resolveInterface';
1011
import type { UpdateInterfaceMethodHooks } from './updateInterface';
@@ -18,7 +19,8 @@ export type PermittedRpcMethodHooks = GetAllSnapsHooks &
1819
GetInterfaceStateMethodHooks &
1920
ResolveInterfaceMethodHooks &
2021
GetCurrencyRateMethodHooks &
21-
ProviderRequestMethodHooks;
22+
ProviderRequestMethodHooks &
23+
ManageAccountsMethodHooks;
2224

2325
export * from './handlers';
2426
export * from './middleware';
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
2+
import { rpcErrors } from '@metamask/rpc-errors';
3+
import type { ManageAccountsResult } from '@metamask/snaps-sdk';
4+
import type {
5+
JsonRpcFailure,
6+
JsonRpcRequest,
7+
PendingJsonRpcResponse,
8+
} from '@metamask/utils';
9+
10+
import type { ManageAccountsParameters } from './manageAccounts';
11+
import { manageAccountsHandler } from './manageAccounts';
12+
13+
describe('snap_manageAccounts', () => {
14+
describe('manageAccountsHandler', () => {
15+
it('has the expected shape', () => {
16+
expect(manageAccountsHandler).toMatchObject({
17+
methodNames: ['snap_manageAccounts'],
18+
implementation: expect.any(Function),
19+
hookNames: {
20+
hasPermission: true,
21+
handleKeyringSnapMessage: true,
22+
},
23+
});
24+
});
25+
});
26+
27+
describe('implementation', () => {
28+
it('returns the result from the `handleKeyringSnapMessage` hook', async () => {
29+
const { implementation } = manageAccountsHandler;
30+
31+
const hasPermission = jest.fn().mockReturnValue(true);
32+
const handleKeyringSnapMessage = jest.fn().mockReturnValue('foo');
33+
34+
const hooks = {
35+
hasPermission,
36+
handleKeyringSnapMessage,
37+
};
38+
39+
const engine = new JsonRpcEngine();
40+
41+
engine.push((request, response, next, end) => {
42+
const result = implementation(
43+
request as JsonRpcRequest<ManageAccountsParameters>,
44+
response as PendingJsonRpcResponse<ManageAccountsResult>,
45+
next,
46+
end,
47+
hooks,
48+
);
49+
50+
result?.catch(end);
51+
});
52+
53+
const response = await engine.handle({
54+
jsonrpc: '2.0',
55+
id: 1,
56+
method: 'snap_manageAccounts',
57+
params: {
58+
method: 'foo',
59+
params: { bar: 'baz' },
60+
},
61+
});
62+
63+
expect(handleKeyringSnapMessage).toHaveBeenCalledWith({
64+
method: 'foo',
65+
params: { bar: 'baz' },
66+
});
67+
68+
expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: 'foo' });
69+
});
70+
71+
it('throws an error if the snap does not have permission', async () => {
72+
const { implementation } = manageAccountsHandler;
73+
74+
const hasPermission = jest.fn().mockReturnValue(false);
75+
const handleKeyringSnapMessage = jest.fn().mockReturnValue('foo');
76+
77+
const hooks = {
78+
hasPermission,
79+
handleKeyringSnapMessage,
80+
};
81+
82+
const engine = new JsonRpcEngine();
83+
84+
engine.push((request, response, next, end) => {
85+
const result = implementation(
86+
request as JsonRpcRequest<ManageAccountsParameters>,
87+
response as PendingJsonRpcResponse<ManageAccountsResult>,
88+
next,
89+
end,
90+
hooks,
91+
);
92+
93+
result?.catch(end);
94+
});
95+
96+
const response = (await engine.handle({
97+
jsonrpc: '2.0',
98+
id: 1,
99+
method: 'snap_manageAccounts',
100+
params: {
101+
method: 'foo',
102+
params: { bar: 'baz' },
103+
},
104+
})) as JsonRpcFailure;
105+
106+
expect(response.error).toStrictEqual({
107+
...rpcErrors.methodNotFound().serialize(),
108+
stack: expect.any(String),
109+
});
110+
});
111+
112+
it('throws an error if the `handleKeyringSnapMessage` hook throws', async () => {
113+
const { implementation } = manageAccountsHandler;
114+
115+
const hasPermission = jest.fn().mockReturnValue(true);
116+
const handleKeyringSnapMessage = jest
117+
.fn()
118+
.mockRejectedValue(new Error('foo'));
119+
120+
const hooks = {
121+
hasPermission,
122+
handleKeyringSnapMessage,
123+
};
124+
125+
const engine = new JsonRpcEngine();
126+
127+
engine.push((request, response, next, end) => {
128+
const result = implementation(
129+
request as JsonRpcRequest<ManageAccountsParameters>,
130+
response as PendingJsonRpcResponse<ManageAccountsResult>,
131+
next,
132+
end,
133+
hooks,
134+
);
135+
136+
result?.catch(end);
137+
});
138+
139+
const response = (await engine.handle({
140+
jsonrpc: '2.0',
141+
id: 1,
142+
method: 'snap_manageAccounts',
143+
params: {
144+
method: 'foo',
145+
params: { bar: 'baz' },
146+
},
147+
})) as JsonRpcFailure;
148+
149+
expect(response.error).toStrictEqual({
150+
code: -32603,
151+
message: 'foo',
152+
data: {
153+
cause: {
154+
message: 'foo',
155+
stack: expect.any(String),
156+
},
157+
},
158+
});
159+
});
160+
161+
it('throws on invalid params', async () => {
162+
const { implementation } = manageAccountsHandler;
163+
164+
const hasPermission = jest.fn().mockReturnValue(true);
165+
const handleKeyringSnapMessage = jest.fn().mockReturnValue('foo');
166+
167+
const hooks = {
168+
hasPermission,
169+
handleKeyringSnapMessage,
170+
};
171+
172+
const engine = new JsonRpcEngine();
173+
174+
engine.push((request, response, next, end) => {
175+
const result = implementation(
176+
request as JsonRpcRequest<ManageAccountsParameters>,
177+
response as PendingJsonRpcResponse<ManageAccountsResult>,
178+
next,
179+
end,
180+
hooks,
181+
);
182+
183+
result?.catch(end);
184+
});
185+
186+
const response = await engine.handle({
187+
jsonrpc: '2.0',
188+
id: 1,
189+
method: 'snap_manageAccounts',
190+
params: {
191+
method: 'foo',
192+
params: 42,
193+
},
194+
});
195+
196+
expect(response).toStrictEqual({
197+
jsonrpc: '2.0',
198+
id: 1,
199+
error: {
200+
code: -32602,
201+
message:
202+
'Invalid params: At path: params -- Expected the value to satisfy a union of `array | record`, but received: 42.',
203+
stack: expect.any(String),
204+
},
205+
});
206+
});
207+
});
208+
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';
2+
import type { PermittedHandlerExport } from '@metamask/permission-controller';
3+
import { rpcErrors } from '@metamask/rpc-errors';
4+
import type {
5+
ManageAccountsParams,
6+
ManageAccountsResult,
7+
} from '@metamask/snaps-sdk';
8+
import { type InferMatching } from '@metamask/snaps-utils';
9+
import {
10+
array,
11+
create,
12+
object,
13+
optional,
14+
record,
15+
string,
16+
StructError,
17+
union,
18+
} from '@metamask/superstruct';
19+
import type {
20+
Json,
21+
JsonRpcRequest,
22+
PendingJsonRpcResponse,
23+
} from '@metamask/utils';
24+
import { JsonStruct } from '@metamask/utils';
25+
26+
import { SnapEndowments } from '../endowments';
27+
import type { MethodHooksObject } from '../utils';
28+
29+
const hookNames: MethodHooksObject<ManageAccountsMethodHooks> = {
30+
hasPermission: true,
31+
handleKeyringSnapMessage: true,
32+
};
33+
34+
export type ManageAccountsMethodHooks = {
35+
/**
36+
* Checks if the current snap has a permission.
37+
*
38+
* @param permissionName - The name of the permission.
39+
* @returns Whether the snap has the permission.
40+
*/
41+
hasPermission: (permissionName: string) => boolean;
42+
/**
43+
* Handles the keyring snap message.
44+
*
45+
* @returns The snap keyring message result.
46+
*/
47+
handleKeyringSnapMessage: (
48+
message: ManageAccountsParameters,
49+
) => Promise<Json>;
50+
};
51+
52+
export const manageAccountsHandler: PermittedHandlerExport<
53+
ManageAccountsMethodHooks,
54+
ManageAccountsParams,
55+
ManageAccountsResult
56+
> = {
57+
methodNames: ['snap_manageAccounts'],
58+
implementation: getManageAccountsImplementation,
59+
hookNames,
60+
};
61+
62+
const ManageAccountsParametersStruct = object({
63+
method: string(),
64+
params: optional(union([array(JsonStruct), record(string(), JsonStruct)])),
65+
});
66+
67+
export type ManageAccountsParameters = InferMatching<
68+
typeof ManageAccountsParametersStruct,
69+
ManageAccountsParams
70+
>;
71+
72+
/**
73+
* The `snap_manageAccounts` method implementation.
74+
*
75+
* @param req - The JSON-RPC request object.
76+
* @param res - The JSON-RPC response object.
77+
* @param _next - The `json-rpc-engine` "next" callback. Not used by this
78+
* function.
79+
* @param end - The `json-rpc-engine` "end" callback.
80+
* @param hooks - The RPC method hooks.
81+
* @param hooks.hasPermission - The function to check if the snap has a permission.
82+
* @param hooks.handleKeyringSnapMessage - The function to handle the keyring snap message.
83+
* @returns Nothing.
84+
*/
85+
async function getManageAccountsImplementation(
86+
req: JsonRpcRequest<ManageAccountsParameters>,
87+
res: PendingJsonRpcResponse<ManageAccountsResult>,
88+
_next: unknown,
89+
end: JsonRpcEngineEndCallback,
90+
{ hasPermission, handleKeyringSnapMessage }: ManageAccountsMethodHooks,
91+
): Promise<void> {
92+
if (!hasPermission(SnapEndowments.Keyring)) {
93+
return end(rpcErrors.methodNotFound());
94+
}
95+
96+
const { params } = req;
97+
98+
try {
99+
const validatedParams = getValidatedParams(params);
100+
101+
res.result = await handleKeyringSnapMessage(validatedParams);
102+
} catch (error) {
103+
return end(error);
104+
}
105+
106+
return end();
107+
}
108+
109+
/**
110+
* Validate the manageAccounts method `params` and returns them cast to the correct
111+
* type. Throws if validation fails.
112+
*
113+
* @param params - The unvalidated params object from the method request.
114+
* @returns The validated manageAccounts method parameter object.
115+
*/
116+
function getValidatedParams(params: unknown): ManageAccountsParameters {
117+
try {
118+
return create(params, ManageAccountsParametersStruct);
119+
} catch (error) {
120+
if (error instanceof StructError) {
121+
throw rpcErrors.invalidParams({
122+
message: `Invalid params: ${error.message}.`,
123+
});
124+
}
125+
/* istanbul ignore next */
126+
throw rpcErrors.internal();
127+
}
128+
}

0 commit comments

Comments
 (0)