Skip to content

Commit 90784af

Browse files
feat: Allow updating interface context (#2809)
Allows updating the `context` object in the `SnapInterfaceController` via `snap_updateInterface`. This is a non-breaking change as `context` is optional. Closes #2804
1 parent a80db22 commit 90784af

File tree

6 files changed

+183
-7
lines changed

6 files changed

+183
-7
lines changed

packages/snaps-controllers/coverage.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"branches": 92.6,
2+
"branches": 92.62,
33
"functions": 96.65,
44
"lines": 97.97,
55
"statements": 97.67

packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx

+105
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,111 @@ describe('SnapInterfaceController', () => {
545545
expect(state).toStrictEqual({ foo: { baz: null } });
546546
});
547547

548+
it('can update an interface and context', async () => {
549+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
550+
const controllerMessenger =
551+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
552+
553+
/* eslint-disable-next-line no-new */
554+
new SnapInterfaceController({
555+
messenger: controllerMessenger,
556+
});
557+
558+
const components = form({
559+
name: 'foo',
560+
children: [input({ name: 'bar' })],
561+
});
562+
563+
const newContent = form({
564+
name: 'foo',
565+
children: [input({ name: 'baz' })],
566+
});
567+
568+
const context = { foo: 'bar' };
569+
570+
const id = await rootMessenger.call(
571+
'SnapInterfaceController:createInterface',
572+
MOCK_SNAP_ID,
573+
components,
574+
context,
575+
);
576+
577+
const newContext = { foo: 'baz' };
578+
579+
await rootMessenger.call(
580+
'SnapInterfaceController:updateInterface',
581+
MOCK_SNAP_ID,
582+
id,
583+
newContent,
584+
newContext,
585+
);
586+
587+
const {
588+
content,
589+
state,
590+
context: interfaceContext,
591+
} = rootMessenger.call(
592+
'SnapInterfaceController:getInterface',
593+
MOCK_SNAP_ID,
594+
id,
595+
);
596+
597+
expect(content).toStrictEqual(getJsxElementFromComponent(newContent));
598+
expect(state).toStrictEqual({ foo: { baz: null } });
599+
expect(interfaceContext).toStrictEqual(newContext);
600+
});
601+
602+
it('does not replace context if none is provided', async () => {
603+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
604+
const controllerMessenger =
605+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
606+
607+
/* eslint-disable-next-line no-new */
608+
new SnapInterfaceController({
609+
messenger: controllerMessenger,
610+
});
611+
612+
const components = form({
613+
name: 'foo',
614+
children: [input({ name: 'bar' })],
615+
});
616+
617+
const newContent = form({
618+
name: 'foo',
619+
children: [input({ name: 'baz' })],
620+
});
621+
622+
const context = { foo: 'bar' };
623+
624+
const id = await rootMessenger.call(
625+
'SnapInterfaceController:createInterface',
626+
MOCK_SNAP_ID,
627+
components,
628+
context,
629+
);
630+
631+
await rootMessenger.call(
632+
'SnapInterfaceController:updateInterface',
633+
MOCK_SNAP_ID,
634+
id,
635+
newContent,
636+
);
637+
638+
const {
639+
content,
640+
state,
641+
context: interfaceContext,
642+
} = rootMessenger.call(
643+
'SnapInterfaceController:getInterface',
644+
MOCK_SNAP_ID,
645+
id,
646+
);
647+
648+
expect(content).toStrictEqual(getJsxElementFromComponent(newContent));
649+
expect(state).toStrictEqual({ foo: { baz: null } });
650+
expect(interfaceContext).toStrictEqual(context);
651+
});
652+
548653
it('throws if a link is on the phishing list', async () => {
549654
const rootMessenger = getRootSnapInterfaceControllerMessenger();
550655
const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(

packages/snaps-controllers/src/interface/SnapInterfaceController.ts

+6
Original file line numberDiff line numberDiff line change
@@ -230,22 +230,28 @@ export class SnapInterfaceController extends BaseController<
230230
* @param snapId - The snap id requesting the update.
231231
* @param id - The interface id.
232232
* @param content - The new content.
233+
* @param context - An optional interface context object.
233234
*/
234235
async updateInterface(
235236
snapId: SnapId,
236237
id: string,
237238
content: ComponentOrElement,
239+
context?: InterfaceContext,
238240
) {
239241
this.#validateArgs(snapId, id);
240242
const element = getJsxInterface(content);
241243
await this.#validateContent(element);
244+
validateInterfaceContext(context);
242245

243246
const oldState = this.state.interfaces[id].state;
244247
const newState = constructState(oldState, element);
245248

246249
this.update((draftState) => {
247250
draftState.interfaces[id].state = newState;
248251
draftState.interfaces[id].content = castDraft(element);
252+
if (context) {
253+
draftState.interfaces[id].context = context;
254+
}
249255
});
250256
}
251257

packages/snaps-rpc-methods/src/permitted/updateInterface.test.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,58 @@ describe('snap_updateInterface', () => {
9898
<Box>
9999
<Text>Hello, world!</Text>
100100
</Box>,
101+
undefined,
101102
);
102103
});
103104
});
104105

106+
it('updates the interface context', async () => {
107+
const { implementation } = updateInterfaceHandler;
108+
109+
const updateInterface = jest.fn();
110+
111+
const hooks = {
112+
updateInterface,
113+
};
114+
115+
const engine = new JsonRpcEngine();
116+
117+
engine.push((request, response, next, end) => {
118+
const result = implementation(
119+
request as JsonRpcRequest<UpdateInterfaceParams>,
120+
response as PendingJsonRpcResponse<UpdateInterfaceResult>,
121+
next,
122+
end,
123+
hooks,
124+
);
125+
126+
result?.catch(end);
127+
});
128+
129+
await engine.handle({
130+
jsonrpc: '2.0',
131+
id: 1,
132+
method: 'snap_updateInterface',
133+
params: {
134+
id: 'foo',
135+
ui: (
136+
<Box>
137+
<Text>Hello, world!</Text>
138+
</Box>
139+
) as JSXElement,
140+
context: { foo: 'bar' },
141+
},
142+
});
143+
144+
expect(updateInterface).toHaveBeenCalledWith(
145+
'foo',
146+
<Box>
147+
<Text>Hello, world!</Text>
148+
</Box>,
149+
{ foo: 'bar' },
150+
);
151+
});
152+
105153
it('throws on invalid params', async () => {
106154
const { implementation } = updateInterfaceHandler;
107155

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

+21-5
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@ import type {
66
UpdateInterfaceResult,
77
JsonRpcRequest,
88
ComponentOrElement,
9+
InterfaceContext,
10+
} from '@metamask/snaps-sdk';
11+
import {
12+
ComponentOrElementStruct,
13+
InterfaceContextStruct,
914
} from '@metamask/snaps-sdk';
10-
import { ComponentOrElementStruct } from '@metamask/snaps-sdk';
1115
import { type InferMatching } from '@metamask/snaps-utils';
12-
import { StructError, create, object, string } from '@metamask/superstruct';
16+
import {
17+
StructError,
18+
create,
19+
object,
20+
optional,
21+
string,
22+
} from '@metamask/superstruct';
1323
import type { PendingJsonRpcResponse } from '@metamask/utils';
1424

1525
import type { MethodHooksObject } from '../utils';
@@ -22,8 +32,13 @@ export type UpdateInterfaceMethodHooks = {
2232
/**
2333
* @param id - The interface ID.
2434
* @param ui - The UI components.
35+
* @param context - The optional interface context object.
2536
*/
26-
updateInterface: (id: string, ui: ComponentOrElement) => Promise<void>;
37+
updateInterface: (
38+
id: string,
39+
ui: ComponentOrElement,
40+
context?: InterfaceContext,
41+
) => Promise<void>;
2742
};
2843

2944
export const updateInterfaceHandler: PermittedHandlerExport<
@@ -39,6 +54,7 @@ export const updateInterfaceHandler: PermittedHandlerExport<
3954
const UpdateInterfaceParametersStruct = object({
4055
id: string(),
4156
ui: ComponentOrElementStruct,
57+
context: optional(InterfaceContextStruct),
4258
});
4359

4460
export type UpdateInterfaceParameters = InferMatching<
@@ -70,9 +86,9 @@ async function getUpdateInterfaceImplementation(
7086
try {
7187
const validatedParams = getValidatedParams(params);
7288

73-
const { id, ui } = validatedParams;
89+
const { id, ui, context } = validatedParams;
7490

75-
await updateInterface(id, ui);
91+
await updateInterface(id, ui, context);
7692
res.result = null;
7793
} catch (error) {
7894
return end(error);

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ComponentOrElement } from '..';
1+
import type { ComponentOrElement, InterfaceContext } from '..';
22

33
/**
44
* The request parameters for the `snap_createInterface` method.
@@ -9,6 +9,7 @@ import type { ComponentOrElement } from '..';
99
export type UpdateInterfaceParams = {
1010
id: string;
1111
ui: ComponentOrElement;
12+
context?: InterfaceContext;
1213
};
1314

1415
/**

0 commit comments

Comments
 (0)