Skip to content

Commit 45db6e7

Browse files
feat: Add snap_getInterfaceContext (#2902)
Expose a new RPC method `snap_getInterfaceContext` that makes the `context` easily accessible outside `onUserInput`. Also adds the necessary wiring to make this RPC method work in the simulation environment. This feature will require the addition of a `getInterfaceContext` method hook in the clients. Progresses #2901
1 parent 126990b commit 45db6e7

File tree

13 files changed

+294
-10
lines changed

13 files changed

+294
-10
lines changed

packages/examples/packages/browserify-plugin/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snaps.git"
88
},
99
"source": {
10-
"shasum": "8LxymXn6+X9URWzkmurIZEyCypzF3OUm53FLjlNW0/I=",
10+
"shasum": "IdAFrQlUYgQaMo/lbXgEJOMKTFbB9RYylXwPvUFT6As=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/examples/packages/browserify/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snaps.git"
88
},
99
"source": {
10-
"shasum": "hYGGCiIVhwOlDnwIyfpkscAd5bc2kVAyzXMq3UC6ORQ=",
10+
"shasum": "bzhrHkJoo2dRz2utZ10KRNL2X2mgRxkur3DrGXHbNOc=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/snaps-rpc-methods/jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
1010
],
1111
coverageThreshold: {
1212
global: {
13-
branches: 92.85,
14-
functions: 97.23,
15-
lines: 97.8,
16-
statements: 97.31,
13+
branches: 92.88,
14+
functions: 97.26,
15+
lines: 97.84,
16+
statements: 97.36,
1717
},
1818
},
1919
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
2+
import { type GetInterfaceContextResult } from '@metamask/snaps-sdk';
3+
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
4+
5+
import type { GetInterfaceContextParameters } from './getInterfaceContext';
6+
import { getInterfaceContextHandler } from './getInterfaceContext';
7+
8+
describe('snap_getInterfaceContext', () => {
9+
describe('getInterfaceContextHandler', () => {
10+
it('has the expected shape', () => {
11+
expect(getInterfaceContextHandler).toMatchObject({
12+
methodNames: ['snap_getInterfaceContext'],
13+
implementation: expect.any(Function),
14+
hookNames: {
15+
getInterfaceContext: true,
16+
},
17+
});
18+
});
19+
});
20+
21+
describe('implementation', () => {
22+
it('returns the result from the `getInterfaceContext` hook', async () => {
23+
const { implementation } = getInterfaceContextHandler;
24+
25+
const getInterfaceContext = jest.fn().mockReturnValue({ foo: 'bar' });
26+
27+
const hooks = {
28+
getInterfaceContext,
29+
};
30+
31+
const engine = new JsonRpcEngine();
32+
33+
engine.push((request, response, next, end) => {
34+
const result = implementation(
35+
request as JsonRpcRequest<GetInterfaceContextParameters>,
36+
response as PendingJsonRpcResponse<GetInterfaceContextResult>,
37+
next,
38+
end,
39+
hooks,
40+
);
41+
42+
result?.catch(end);
43+
});
44+
45+
const response = await engine.handle({
46+
jsonrpc: '2.0',
47+
id: 1,
48+
method: 'snap_getInterfaceContext',
49+
params: {
50+
id: 'foo',
51+
},
52+
});
53+
54+
expect(response).toStrictEqual({
55+
jsonrpc: '2.0',
56+
id: 1,
57+
result: { foo: 'bar' },
58+
});
59+
});
60+
61+
it('throws on invalid params', async () => {
62+
const { implementation } = getInterfaceContextHandler;
63+
64+
const getInterfaceContext = jest.fn().mockReturnValue({ foo: 'bar' });
65+
66+
const hooks = {
67+
getInterfaceContext,
68+
};
69+
70+
const engine = new JsonRpcEngine();
71+
72+
engine.push((request, response, next, end) => {
73+
const result = implementation(
74+
request as JsonRpcRequest<GetInterfaceContextParameters>,
75+
response as PendingJsonRpcResponse<GetInterfaceContextResult>,
76+
next,
77+
end,
78+
hooks,
79+
);
80+
81+
result?.catch(end);
82+
});
83+
84+
const response = await engine.handle({
85+
jsonrpc: '2.0',
86+
id: 1,
87+
method: 'snap_getInterfaceContext',
88+
params: {
89+
id: 42,
90+
},
91+
});
92+
93+
expect(response).toStrictEqual({
94+
error: {
95+
code: -32602,
96+
message:
97+
'Invalid params: At path: id -- Expected a string, but received: 42.',
98+
stack: expect.any(String),
99+
},
100+
id: 1,
101+
jsonrpc: '2.0',
102+
});
103+
});
104+
});
105+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
GetInterfaceContextParams,
6+
GetInterfaceContextResult,
7+
InterfaceContext,
8+
JsonRpcRequest,
9+
} from '@metamask/snaps-sdk';
10+
import { type InferMatching } from '@metamask/snaps-utils';
11+
import { StructError, create, object, string } from '@metamask/superstruct';
12+
import type { PendingJsonRpcResponse } from '@metamask/utils';
13+
14+
import type { MethodHooksObject } from '../utils';
15+
16+
const hookNames: MethodHooksObject<GetInterfaceContextMethodHooks> = {
17+
getInterfaceContext: true,
18+
};
19+
20+
export type GetInterfaceContextMethodHooks = {
21+
/**
22+
* @param id - The interface ID.
23+
* @returns The interface context.
24+
*/
25+
getInterfaceContext: (id: string) => InterfaceContext | null;
26+
};
27+
28+
export const getInterfaceContextHandler: PermittedHandlerExport<
29+
GetInterfaceContextMethodHooks,
30+
GetInterfaceContextParameters,
31+
GetInterfaceContextResult
32+
> = {
33+
methodNames: ['snap_getInterfaceContext'],
34+
implementation: getInterfaceContextImplementation,
35+
hookNames,
36+
};
37+
38+
const GetInterfaceContextParametersStruct = object({
39+
id: string(),
40+
});
41+
42+
export type GetInterfaceContextParameters = InferMatching<
43+
typeof GetInterfaceContextParametersStruct,
44+
GetInterfaceContextParams
45+
>;
46+
47+
/**
48+
* The `snap_getInterfaceContext` method implementation.
49+
*
50+
* @param req - The JSON-RPC request object.
51+
* @param res - The JSON-RPC response object.
52+
* @param _next - The `json-rpc-engine` "next" callback. Not used by this
53+
* function.
54+
* @param end - The `json-rpc-engine` "end" callback.
55+
* @param hooks - The RPC method hooks.
56+
* @param hooks.getInterfaceContext - The function to get the interface context.
57+
* @returns Noting.
58+
*/
59+
function getInterfaceContextImplementation(
60+
req: JsonRpcRequest<GetInterfaceContextParameters>,
61+
res: PendingJsonRpcResponse<GetInterfaceContextResult>,
62+
_next: unknown,
63+
end: JsonRpcEngineEndCallback,
64+
{ getInterfaceContext }: GetInterfaceContextMethodHooks,
65+
): void {
66+
const { params } = req;
67+
68+
try {
69+
const validatedParams = getValidatedParams(params);
70+
71+
const { id } = validatedParams;
72+
73+
res.result = getInterfaceContext(id);
74+
} catch (error) {
75+
return end(error);
76+
}
77+
78+
return end();
79+
}
80+
81+
/**
82+
* Validate the getInterfaceContext method `params` and returns them cast to the correct
83+
* type. Throws if validation fails.
84+
*
85+
* @param params - The unvalidated params object from the method request.
86+
* @returns The validated getInterfaceContext method parameter object.
87+
*/
88+
function getValidatedParams(params: unknown): GetInterfaceContextParameters {
89+
try {
90+
return create(params, GetInterfaceContextParametersStruct);
91+
} catch (error) {
92+
if (error instanceof StructError) {
93+
throw rpcErrors.invalidParams({
94+
message: `Invalid params: ${error.message}.`,
95+
});
96+
}
97+
/* istanbul ignore next */
98+
throw rpcErrors.internal();
99+
}
100+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getAllSnapsHandler } from './getAllSnaps';
44
import { getClientStatusHandler } from './getClientStatus';
55
import { getCurrencyRateHandler } from './getCurrencyRate';
66
import { getFileHandler } from './getFile';
7+
import { getInterfaceContextHandler } from './getInterfaceContext';
78
import { getInterfaceStateHandler } from './getInterfaceState';
89
import { getSnapsHandler } from './getSnaps';
910
import { invokeKeyringHandler } from './invokeKeyring';
@@ -24,6 +25,7 @@ export const methodHandlers = {
2425
snap_createInterface: createInterfaceHandler,
2526
snap_updateInterface: updateInterfaceHandler,
2627
snap_getInterfaceState: getInterfaceStateHandler,
28+
snap_getInterfaceContext: getInterfaceContextHandler,
2729
snap_resolveInterface: resolveInterfaceHandler,
2830
snap_getCurrencyRate: getCurrencyRateHandler,
2931
snap_experimentalProviderRequest: providerRequestHandler,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { InterfaceContext } from '../interface';
2+
3+
/**
4+
* The request parameters for the `snap_getInterfaceContext` method.
5+
*
6+
* @property id - The interface id.
7+
*/
8+
export type GetInterfaceContextParams = {
9+
id: string;
10+
};
11+
12+
/**
13+
* The result returned by the `snap_getInterfaceContext` method, which is the context for a given interface.
14+
*/
15+
export type GetInterfaceContextResult = InterfaceContext | null;

packages/snaps-sdk/src/types/methods/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './get-bip44-entropy';
66
export * from './get-client-status';
77
export * from './get-entropy';
88
export * from './get-file';
9+
export * from './get-interface-context';
910
export * from './get-interface-state';
1011
export * from './get-locale';
1112
export * from './get-preferences';

packages/snaps-sdk/src/types/methods/methods.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import type {
2626
} from './get-currency-rate';
2727
import type { GetEntropyParams, GetEntropyResult } from './get-entropy';
2828
import type { GetFileParams, GetFileResult } from './get-file';
29+
import type {
30+
GetInterfaceContextParams,
31+
GetInterfaceContextResult,
32+
} from './get-interface-context';
2933
import type {
3034
GetInterfaceStateParams,
3135
GetInterfaceStateResult,
@@ -79,6 +83,10 @@ export type SnapMethods = {
7983
snap_createInterface: [CreateInterfaceParams, CreateInterfaceResult];
8084
snap_updateInterface: [UpdateInterfaceParams, UpdateInterfaceResult];
8185
snap_getInterfaceState: [GetInterfaceStateParams, GetInterfaceStateResult];
86+
snap_getInterfaceContext: [
87+
GetInterfaceContextParams,
88+
GetInterfaceContextResult,
89+
];
8290
snap_resolveInterface: [ResolveInterfaceParams, ResolveInterfaceResult];
8391
wallet_getSnaps: [GetSnapsParams, GetSnapsResult];
8492
wallet_invokeKeyring: [InvokeKeyringParams, InvokeKeyringResult];

packages/snaps-simulation/src/simulation.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,46 @@ describe('getHooks', () => {
376376
await close();
377377
});
378378

379+
it('returns the `getInterfaceContext` hook', async () => {
380+
// eslint-disable-next-line no-new
381+
new SnapInterfaceController({
382+
messenger:
383+
getRestrictedSnapInterfaceControllerMessenger(controllerMessenger),
384+
});
385+
386+
jest.spyOn(controllerMessenger, 'call');
387+
388+
const { snapId, close } = await getMockServer({
389+
manifest: getSnapManifest(),
390+
});
391+
392+
const location = detectSnapLocation(snapId, {
393+
allowLocal: true,
394+
});
395+
const snapFiles = await fetchSnap(snapId, location);
396+
397+
const { createInterface, getInterfaceContext } = getHooks(
398+
getMockOptions(),
399+
snapFiles,
400+
snapId,
401+
controllerMessenger,
402+
);
403+
404+
const id = await createInterface(text('foo'), { bar: 'baz' });
405+
406+
const result = getInterfaceContext(id);
407+
408+
expect(controllerMessenger.call).toHaveBeenNthCalledWith(
409+
3,
410+
'SnapInterfaceController:getInterface',
411+
snapId,
412+
id,
413+
);
414+
415+
expect(result).toStrictEqual({ bar: 'baz' });
416+
await close();
417+
});
418+
379419
it('returns the `resolveInterface` hook', async () => {
380420
// eslint-disable-next-line no-new
381421
const snapInterfaceController = new SnapInterfaceController({

0 commit comments

Comments
 (0)