Skip to content

Commit f10bc3f

Browse files
authored
chore: Add displayed property to stored interfaces (#3888)
This adds a `displayed` property to stored interfaces, which is used to determine if a Snap can read the interface state. If an interface has not been displayed, `snap_getInterfaceState` will throw an error. An interface is considered displayed when: - `snap_dialog` is called. - One of the UI entrypoints were called, such as `onHomePage`, `onTransaction`, etc. Extension implementation for these changes: MetaMask/metamask-extension#40713 Closes https://consensyssoftware.atlassian.net/browse/WPC-501. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes Snap UI lifecycle behavior and adds a new gating rule for reading interface state, which could affect existing snaps and interface-handling flows if marking-as-displayed is missed or mis-identified. > > **Overview** > Adds a `displayed` flag to `StoredInterface`, plus new controller actions `setInterfaceDisplayed` and `getInterfaceState` that **block `snap_getInterfaceState` until an interface has been displayed**. > > Automatically marks interfaces as displayed when UI entrypoint handlers return an interface (`onHomePage`, `onSettingsPage`, `onSignature`, `onTransaction`) and when `snap_dialog` is invoked (both content-created and ID-referenced dialogs), with corresponding updates across `SnapController`, rpc-method hooks, simulation hooks, and test/mocks. > > Updates the `send-flow` example/tests to stop relying on pre-reading interface state during initial render and bumps the example snap manifest `shasum`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b286fe6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8b6eeff commit f10bc3f

19 files changed

Lines changed: 611 additions & 24 deletions

File tree

packages/examples/packages/send-flow/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": "fHxddiV/u1kEdNZEz2PIb6yBks29PwmrAUAhWaSDEGc=",
10+
"shasum": "rhxlptZLJG7H5o28og8M5S9TaL1IaeyugbgQILjb05w=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/examples/packages/send-flow/src/index.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ describe('onRpcRequest', () => {
4444

4545
expect(sendFlowInterface).toRender(
4646
<SendFlow
47-
account={getStateFromAccount(MOCK_ACCOUNT_1)}
4847
asset={getStateFromAsset(
4948
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
5049
MOCK_ASSETS,
@@ -71,7 +70,6 @@ describe('onHomePage', () => {
7170

7271
expect(sendFlowInterface).toRender(
7372
<SendFlow
74-
account={getStateFromAccount(MOCK_ACCOUNT_1)}
7573
asset={getStateFromAsset(
7674
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
7775
MOCK_ASSETS,

packages/examples/packages/send-flow/src/utils.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ export async function generateSendFlow({
4545
},
4646
});
4747

48-
const { sendForm } = (await snap.request({
49-
method: 'snap_getInterfaceState',
50-
params: { id: interfaceId },
51-
})) as { sendForm: SendFormState };
52-
5348
await snap.request({
5449
method: 'snap_updateInterface',
5550
params: {
@@ -58,7 +53,6 @@ export async function generateSendFlow({
5853
<SendFlow
5954
fiatCurrency={fiatCurrency}
6055
useFiat={false}
61-
account={sendForm.account}
6256
total={{ amount: 0, fiat: 0 }}
6357
fees={fees}
6458
/>

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

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describe('SnapInterfaceController', () => {
104104
components,
105105
);
106106

107-
const { content, state } = rootMessenger.call(
107+
const { content, state, displayed } = rootMessenger.call(
108108
'SnapInterfaceController:getInterface',
109109
MOCK_SNAP_ID,
110110
id,
@@ -118,6 +118,7 @@ describe('SnapInterfaceController', () => {
118118

119119
expect(content).toStrictEqual(getJsxElementFromComponent(components));
120120
expect(state).toStrictEqual({ foo: { bar: null } });
121+
expect(displayed).toBe(false);
121122
});
122123

123124
it('calls the multichain asset controller to construct an asset selector state', async () => {
@@ -907,6 +908,221 @@ describe('SnapInterfaceController', () => {
907908
});
908909
});
909910

911+
describe('getInterfaceState', () => {
912+
it('gets the interface state if the interface has been displayed', async () => {
913+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
914+
const controllerMessenger =
915+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
916+
917+
// eslint-disable-next-line no-new
918+
new SnapInterfaceController({
919+
messenger: controllerMessenger,
920+
});
921+
922+
const content = (
923+
<Form name="foo">
924+
<Field label="Bar">
925+
<Input name="bar" type="text" />
926+
</Field>
927+
</Form>
928+
);
929+
930+
const id = rootMessenger.call(
931+
'SnapInterfaceController:createInterface',
932+
MOCK_SNAP_ID,
933+
content,
934+
);
935+
936+
rootMessenger.call(
937+
'SnapInterfaceController:setInterfaceDisplayed',
938+
MOCK_SNAP_ID,
939+
id,
940+
);
941+
942+
const state = rootMessenger.call(
943+
'SnapInterfaceController:getInterfaceState',
944+
MOCK_SNAP_ID,
945+
id,
946+
);
947+
948+
expect(state).toStrictEqual({ foo: { bar: null } });
949+
});
950+
951+
it('throws if the interface has not been displayed yet', async () => {
952+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
953+
const controllerMessenger =
954+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
955+
956+
// eslint-disable-next-line no-new
957+
new SnapInterfaceController({
958+
messenger: controllerMessenger,
959+
});
960+
961+
const content = (
962+
<Form name="foo">
963+
<Field label="Bar">
964+
<Input name="bar" type="text" />
965+
</Field>
966+
</Form>
967+
);
968+
969+
const id = rootMessenger.call(
970+
'SnapInterfaceController:createInterface',
971+
MOCK_SNAP_ID,
972+
content,
973+
);
974+
975+
expect(() =>
976+
rootMessenger.call(
977+
'SnapInterfaceController:getInterfaceState',
978+
MOCK_SNAP_ID,
979+
id,
980+
),
981+
).toThrow(
982+
`Interface with ID '${id}' has not been displayed yet. Interface state is not available until the interface has been displayed at least once.`,
983+
);
984+
});
985+
986+
it('throws if the interface does not exist', () => {
987+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
988+
const controllerMessenger =
989+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
990+
991+
// eslint-disable-next-line no-new
992+
new SnapInterfaceController({
993+
messenger: controllerMessenger,
994+
});
995+
996+
expect(() =>
997+
rootMessenger.call(
998+
'SnapInterfaceController:getInterfaceState',
999+
MOCK_SNAP_ID,
1000+
'test',
1001+
),
1002+
).toThrow(`Interface with id 'test' not found.`);
1003+
});
1004+
1005+
it('throws if the snap requesting the interface state is not the one that created it', async () => {
1006+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
1007+
const controllerMessenger =
1008+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
1009+
1010+
// eslint-disable-next-line no-new
1011+
new SnapInterfaceController({
1012+
messenger: controllerMessenger,
1013+
});
1014+
1015+
const content = (
1016+
<Form name="foo">
1017+
<Field label="Bar">
1018+
<Input name="bar" type="text" />
1019+
</Field>
1020+
</Form>
1021+
);
1022+
1023+
const id = rootMessenger.call(
1024+
'SnapInterfaceController:createInterface',
1025+
MOCK_SNAP_ID,
1026+
content,
1027+
);
1028+
1029+
rootMessenger.call(
1030+
'SnapInterfaceController:setInterfaceDisplayed',
1031+
MOCK_SNAP_ID,
1032+
id,
1033+
);
1034+
1035+
expect(() =>
1036+
rootMessenger.call(
1037+
'SnapInterfaceController:getInterfaceState',
1038+
'foo' as SnapId,
1039+
id,
1040+
),
1041+
).toThrow(`Interface not created by foo.`);
1042+
});
1043+
});
1044+
1045+
describe('setInterfaceDisplayed', () => {
1046+
it('sets the interface as displayed', async () => {
1047+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
1048+
const controllerMessenger =
1049+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
1050+
1051+
// eslint-disable-next-line no-new
1052+
new SnapInterfaceController({
1053+
messenger: controllerMessenger,
1054+
});
1055+
1056+
const id = rootMessenger.call(
1057+
'SnapInterfaceController:createInterface',
1058+
MOCK_SNAP_ID,
1059+
<Box>
1060+
<Text>foo</Text>
1061+
</Box>,
1062+
);
1063+
1064+
rootMessenger.call(
1065+
'SnapInterfaceController:setInterfaceDisplayed',
1066+
MOCK_SNAP_ID,
1067+
id,
1068+
);
1069+
1070+
const { displayed } = rootMessenger.call(
1071+
'SnapInterfaceController:getInterface',
1072+
MOCK_SNAP_ID,
1073+
id,
1074+
);
1075+
1076+
expect(displayed).toBe(true);
1077+
});
1078+
1079+
it('throws if the interface does not exist', () => {
1080+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
1081+
const controllerMessenger =
1082+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
1083+
1084+
// eslint-disable-next-line no-new
1085+
new SnapInterfaceController({
1086+
messenger: controllerMessenger,
1087+
});
1088+
1089+
expect(() =>
1090+
rootMessenger.call(
1091+
'SnapInterfaceController:setInterfaceDisplayed',
1092+
MOCK_SNAP_ID,
1093+
'test',
1094+
),
1095+
).toThrow(`Interface with id 'test' not found.`);
1096+
});
1097+
1098+
it('throws if the interface was not created by the Snap', async () => {
1099+
const rootMessenger = getRootSnapInterfaceControllerMessenger();
1100+
const controllerMessenger =
1101+
getRestrictedSnapInterfaceControllerMessenger(rootMessenger);
1102+
1103+
// eslint-disable-next-line no-new
1104+
new SnapInterfaceController({
1105+
messenger: controllerMessenger,
1106+
});
1107+
1108+
const id = rootMessenger.call(
1109+
'SnapInterfaceController:createInterface',
1110+
MOCK_SNAP_ID,
1111+
<Box>
1112+
<Text>foo</Text>
1113+
</Box>,
1114+
);
1115+
1116+
expect(() =>
1117+
rootMessenger.call(
1118+
'SnapInterfaceController:setInterfaceDisplayed',
1119+
'foo' as SnapId,
1120+
id,
1121+
),
1122+
).toThrow(`Interface not created by foo.`);
1123+
});
1124+
});
1125+
9101126
describe('updateInterface', () => {
9111127
it('can update an interface', async () => {
9121128
const rootMessenger = getRootSnapInterfaceControllerMessenger();

0 commit comments

Comments
 (0)