Skip to content

Commit 0ba61f0

Browse files
fix: Inject context into onUserInput calls (#3298)
Inject `context` into `onUserInput` requests, this reduces complexity on the client-side where the UI system no longer has to keep `context` up to date. Also fixes a problem where we weren't keeping the context properly up to date. Fixes #3069
1 parent 7e6ad03 commit 0ba61f0

File tree

4 files changed

+147
-19
lines changed

4 files changed

+147
-19
lines changed

packages/snaps-controllers/coverage.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"branches": 93.44,
2+
"branches": 93.48,
33
"functions": 97.38,
44
"lines": 98.34,
55
"statements": 98.08

packages/snaps-controllers/src/snaps/SnapController.test.tsx

+82-3
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ import {
1919
SnapEndowments,
2020
} from '@metamask/snaps-rpc-methods';
2121
import type { SnapId } from '@metamask/snaps-sdk';
22-
import { AuxiliaryFileEncoding, text } from '@metamask/snaps-sdk';
23-
import { Text } from '@metamask/snaps-sdk/jsx';
22+
import {
23+
AuxiliaryFileEncoding,
24+
text,
25+
UserInputEventType,
26+
} from '@metamask/snaps-sdk';
27+
import { Text, Box, Button } from '@metamask/snaps-sdk/jsx';
2428
import type { SnapPermissions, RpcOrigins } from '@metamask/snaps-utils';
2529
import {
2630
getPlatformVersion,
@@ -2242,7 +2246,11 @@ describe('SnapController', () => {
22422246
snapId: snap.id,
22432247
origin: MOCK_ORIGIN,
22442248
handler: HandlerType.OnUserInput,
2245-
request: { jsonrpc: '2.0', method: 'test' },
2249+
request: {
2250+
jsonrpc: '2.0',
2251+
method: 'test',
2252+
params: { id: MOCK_INTERFACE_ID },
2253+
},
22462254
}),
22472255
).toBeUndefined();
22482256

@@ -2770,6 +2778,77 @@ describe('SnapController', () => {
27702778
snapController.destroy();
27712779
});
27722780

2781+
it('injects context into onUserInput', async () => {
2782+
const rootMessenger = getControllerMessenger();
2783+
const messenger = getSnapControllerMessenger(rootMessenger);
2784+
const snapController = getSnapController(
2785+
getSnapControllerOptions({
2786+
messenger,
2787+
state: {
2788+
snaps: getPersistedSnapsState(),
2789+
},
2790+
}),
2791+
);
2792+
2793+
rootMessenger.registerActionHandler(
2794+
'SnapInterfaceController:getInterface',
2795+
() => ({
2796+
id: MOCK_INTERFACE_ID,
2797+
snapId: MOCK_SNAP_ID,
2798+
content: (
2799+
<Box>
2800+
<Button name="button">Click me</Button>
2801+
</Box>
2802+
),
2803+
state: {},
2804+
context: { foo: 'bar' },
2805+
contentType: null,
2806+
}),
2807+
);
2808+
2809+
await snapController.handleRequest({
2810+
snapId: MOCK_SNAP_ID,
2811+
origin: MOCK_ORIGIN,
2812+
handler: HandlerType.OnUserInput,
2813+
request: {
2814+
jsonrpc: '2.0',
2815+
method: ' ',
2816+
params: {
2817+
id: MOCK_INTERFACE_ID,
2818+
event: {
2819+
type: UserInputEventType.ButtonClickEvent,
2820+
name: 'button',
2821+
},
2822+
},
2823+
},
2824+
});
2825+
2826+
expect(rootMessenger.call).toHaveBeenNthCalledWith(
2827+
4,
2828+
'ExecutionService:handleRpcRequest',
2829+
MOCK_SNAP_ID,
2830+
{
2831+
origin: MOCK_ORIGIN,
2832+
handler: HandlerType.OnUserInput,
2833+
request: {
2834+
id: expect.any(String),
2835+
method: ' ',
2836+
jsonrpc: '2.0',
2837+
params: {
2838+
id: MOCK_INTERFACE_ID,
2839+
event: {
2840+
type: UserInputEventType.ButtonClickEvent,
2841+
name: 'button',
2842+
},
2843+
context: { foo: 'bar' },
2844+
},
2845+
},
2846+
},
2847+
);
2848+
2849+
snapController.destroy();
2850+
});
2851+
27732852
it('throws if onTransaction handler returns a phishing link', async () => {
27742853
const rootMessenger = getControllerMessenger();
27752854
const messenger = getSnapControllerMessenger(rootMessenger);

packages/snaps-controllers/src/snaps/SnapController.ts

+56-13
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import type {
111111
NonEmptyArray,
112112
SemVerRange,
113113
CaipAssetType,
114+
JsonRpcRequest,
114115
} from '@metamask/utils';
115116
import {
116117
assert,
@@ -213,7 +214,7 @@ export type PreinstalledSnap = {
213214
};
214215

215216
type SnapRpcHandler = (
216-
options: SnapRpcHookArgs & { timeout: number },
217+
options: SnapRpcHookArgs & { timeout: number; request: JsonRpcRequest },
217218
) => Promise<unknown>;
218219

219220
/**
@@ -3513,7 +3514,7 @@ export class SnapController extends BaseController<
35133514
handler: handlerType,
35143515
request,
35153516
timeout,
3516-
}: SnapRpcHookArgs & { timeout: number }) => {
3517+
}: SnapRpcHookArgs & { timeout: number; request: JsonRpcRequest }) => {
35173518
if (!this.state.snaps[snapId].enabled) {
35183519
throw new Error(`Snap "${snapId}" is disabled.`);
35193520
}
@@ -3547,13 +3548,19 @@ export class SnapController extends BaseController<
35473548
}
35483549
}
35493550

3551+
const transformedRequest = this.#transformSnapRpcRequest(
3552+
snapId,
3553+
handlerType,
3554+
request,
3555+
);
3556+
35503557
const timer = new Timer(timeout);
3551-
this.#recordSnapRpcRequestStart(snapId, request.id, timer);
3558+
this.#recordSnapRpcRequestStart(snapId, transformedRequest.id, timer);
35523559

35533560
const handleRpcRequestPromise = this.messagingSystem.call(
35543561
'ExecutionService:handleRpcRequest',
35553562
snapId,
3556-
{ origin, handler: handlerType, request },
3563+
{ origin, handler: handlerType, request: transformedRequest },
35573564
);
35583565

35593566
// This will either get the result or reject due to the timeout.
@@ -3566,21 +3573,21 @@ export class SnapController extends BaseController<
35663573
);
35673574
}
35683575

3569-
await this.#assertSnapRpcRequestResult(snapId, handlerType, result);
3576+
await this.#assertSnapRpcResponse(snapId, handlerType, result);
35703577

3571-
const transformedResult = await this.#transformSnapRpcRequestResult(
3578+
const transformedResult = await this.#transformSnapRpcResponse(
35723579
snapId,
35733580
handlerType,
3574-
request,
3581+
transformedRequest,
35753582
result,
35763583
);
35773584

3578-
this.#recordSnapRpcRequestFinish(snapId, request.id);
3585+
this.#recordSnapRpcRequestFinish(snapId, transformedRequest.id);
35793586

35803587
return transformedResult;
35813588
} catch (error) {
35823589
// We flag the RPC request as finished early since termination may affect pending requests
3583-
this.#recordSnapRpcRequestFinish(snapId, request.id);
3590+
this.#recordSnapRpcRequestFinish(snapId, transformedRequest.id);
35843591
const [jsonRpcError, handled] = unwrapError(error);
35853592

35863593
if (!handled) {
@@ -3629,15 +3636,15 @@ export class SnapController extends BaseController<
36293636
}
36303637

36313638
/**
3632-
* Transform a RPC request result if necessary.
3639+
* Transform a RPC response if necessary.
36333640
*
36343641
* @param snapId - The snap ID of the snap that produced the result.
36353642
* @param handlerType - The handler type that produced the result.
36363643
* @param request - The request that returned the result.
3637-
* @param result - The result.
3644+
* @param result - The response.
36383645
* @returns The transformed result if applicable, otherwise the original result.
36393646
*/
3640-
async #transformSnapRpcRequestResult(
3647+
async #transformSnapRpcResponse(
36413648
snapId: SnapId,
36423649
handlerType: HandlerType,
36433650
request: Record<string, unknown>,
@@ -3763,14 +3770,50 @@ export class SnapController extends BaseController<
37633770
return { conversionRates: filteredConversionRates };
37643771
}
37653772

3773+
/**
3774+
* Transforms a JSON-RPC request before sending it to the Snap, if required for a given handler.
3775+
*
3776+
* @param snapId - The Snap ID.
3777+
* @param handlerType - The handler being called.
3778+
* @param request - The JSON-RPC request.
3779+
* @returns The potentially transformed JSON-RPC request.
3780+
*/
3781+
#transformSnapRpcRequest(
3782+
snapId: SnapId,
3783+
handlerType: HandlerType,
3784+
request: JsonRpcRequest,
3785+
) {
3786+
switch (handlerType) {
3787+
// For onUserInput we inject context, so the client doesn't have to worry about keeping it in sync.
3788+
case HandlerType.OnUserInput: {
3789+
assert(request.params && hasProperty(request.params, 'id'));
3790+
3791+
const interfaceId = request.params.id as string;
3792+
const { context } = this.messagingSystem.call(
3793+
'SnapInterfaceController:getInterface',
3794+
snapId,
3795+
interfaceId,
3796+
);
3797+
3798+
return {
3799+
...request,
3800+
params: { ...request.params, context },
3801+
};
3802+
}
3803+
3804+
default:
3805+
return request;
3806+
}
3807+
}
3808+
37663809
/**
37673810
* Assert that the returned result of a Snap RPC call is the expected shape.
37683811
*
37693812
* @param snapId - The snap ID.
37703813
* @param handlerType - The handler type of the RPC Request.
37713814
* @param result - The result of the RPC request.
37723815
*/
3773-
async #assertSnapRpcRequestResult(
3816+
async #assertSnapRpcResponse(
37743817
snapId: SnapId,
37753818
handlerType: HandlerType,
37763819
result: unknown,

packages/snaps-controllers/src/test-utils/controller.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
SnapEndowments,
2323
WALLET_SNAP_PERMISSION_KEY,
2424
} from '@metamask/snaps-rpc-methods';
25-
import type { SnapId, text } from '@metamask/snaps-sdk';
25+
import type { SnapId } from '@metamask/snaps-sdk';
26+
import { text } from '@metamask/snaps-sdk';
2627
import { SnapCaveatType } from '@metamask/snaps-utils';
2728
import {
2829
MockControllerMessenger,
@@ -440,7 +441,12 @@ export const getControllerMessenger = (registry = new MockSnapsRegistry()) => {
440441
if (id !== MOCK_INTERFACE_ID) {
441442
throw new Error(`Interface with id '${id}' not found.`);
442443
}
443-
return { snapId, content: text('foo bar'), state: {} } as StoredInterface;
444+
return {
445+
snapId,
446+
content: text('foo bar'),
447+
state: {},
448+
context: null,
449+
} as StoredInterface;
444450
},
445451
);
446452

0 commit comments

Comments
 (0)