Skip to content

Commit 9aa68cd

Browse files
fix: Throw if Snap not installed (#3666)
When invoking Snaps directly, e.g. from within MetaMask, we were currently not validating that Snaps were installed. This means that misleading errors would be thrown about permissions when the Snap was not installed in the first place. Also removes this check from one RPC method, since it is now unnecessary. We leave the RPC method around in `invokeKeyring` to not change the error handling behavior. Closes #3658 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds an installation check to SnapController.handleRequest, removes redundant snap-installed checks from invokeSnap, aligns error messages, and updates tests and coverage thresholds. > > - **SnapController**: > - Validate Snap exists in `handleRequest` and throw a clear error if not installed. > - Refactor subsequent checks to use local `snap` (enabled/status). > - **RPC Methods**: > - `invokeSnap`: Remove `getSnap` hook and installed check; rely on controller for validation. > - `invokeKeyring`: Align "not installed" error message with controller. > - **Tests**: > - Add test for non-installed Snap in `SnapController.handleRequest`. > - Update `invokeSnap` tests to drop `getSnap` usage and non-installed error case. > - Update `invokeKeyring` tests to match new error message. > - **Config**: > - Slightly adjust Jest coverage thresholds in `snaps-rpc-methods`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 303b2b9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5d19c66 commit 9aa68cd

File tree

7 files changed

+36
-44
lines changed

7 files changed

+36
-44
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2192,6 +2192,27 @@ describe('SnapController', () => {
21922192
});
21932193

21942194
describe('handleRequest', () => {
2195+
it('throws if the Snap is not installed', async () => {
2196+
const snapController = getSnapController();
2197+
2198+
await expect(
2199+
snapController.handleRequest({
2200+
snapId: 'npm:foo' as SnapId,
2201+
origin: METAMASK_ORIGIN,
2202+
handler: HandlerType.OnUserInput,
2203+
request: {
2204+
jsonrpc: '2.0',
2205+
method: 'test',
2206+
params: { id: MOCK_INTERFACE_ID },
2207+
},
2208+
}),
2209+
).rejects.toThrow(
2210+
'The Snap "npm:foo" is not installed. Please install it before invoking it.',
2211+
);
2212+
2213+
snapController.destroy();
2214+
});
2215+
21952216
it.each(
21962217
Object.keys(handlerEndowments).filter(
21972218
(handler) => handlerEndowments[handler as HandlerType],

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3588,6 +3588,13 @@ export class SnapController extends BaseController<
35883588
}: SnapRpcHookArgs & { snapId: SnapId }): Promise<unknown> {
35893589
this.#assertCanUsePlatform();
35903590

3591+
const snap = this.get(snapId);
3592+
3593+
assert(
3594+
snap,
3595+
`The Snap "${snapId}" is not installed. Please install it before invoking it.`,
3596+
);
3597+
35913598
assert(
35923599
origin === METAMASK_ORIGIN || isValidUrl(origin),
35933600
"'origin' must be a valid URL or 'metamask'.",
@@ -3666,11 +3673,11 @@ export class SnapController extends BaseController<
36663673
throw new Error(`"${handlerType}" can only be invoked by MetaMask.`);
36673674
}
36683675

3669-
if (!this.state.snaps[snapId].enabled) {
3676+
if (!snap.enabled) {
36703677
throw new Error(`Snap "${snapId}" is disabled.`);
36713678
}
36723679

3673-
if (this.state.snaps[snapId].status === SnapStatus.Installing) {
3680+
if (snap.status === SnapStatus.Installing) {
36743681
throw new Error(
36753682
`Snap "${snapId}" is currently being installed. Please try again later.`,
36763683
);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ module.exports = deepmerge(baseConfig, {
1010
],
1111
coverageThreshold: {
1212
global: {
13-
branches: 95.7,
13+
branches: 95.68,
1414
functions: 98.75,
15-
lines: 98.99,
15+
lines: 98.98,
1616
statements: 98.69,
1717
},
1818
},

packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ describe('wallet_invokeKeyring', () => {
303303
expect(response.error).toStrictEqual({
304304
...rpcErrors
305305
.invalidRequest({
306-
message: `The snap "${MOCK_SNAP_ID}" is not installed. Please install it first, before invoking the snap.`,
306+
message: `The Snap "${MOCK_SNAP_ID}" is not installed. Please install it before invoking it.`,
307307
})
308308
.serialize(),
309309
stack: expect.any(String),

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,9 @@ async function invokeKeyringImplementation(
107107

108108
if (!getSnap(snapId)) {
109109
return end(
110+
// Mirror error message from SnapController.
110111
rpcErrors.invalidRequest({
111-
message: `The snap "${snapId}" is not installed. Please install it first, before invoking the snap.`,
112+
message: `The Snap "${snapId}" is not installed. Please install it before invoking it.`,
112113
}),
113114
);
114115
}

packages/snaps-rpc-methods/src/restricted/invokeSnap.test.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ describe('builder', () => {
2323
targetName: WALLET_SNAP_PERMISSION_KEY,
2424
specificationBuilder: expect.any(Function),
2525
methodHooks: {
26-
getSnap: true,
2726
handleSnapRpcRequest: true,
2827
},
2928
});
@@ -33,7 +32,6 @@ describe('builder', () => {
3332
expect(
3433
invokeSnapBuilder.specificationBuilder({
3534
methodHooks: {
36-
getSnap: jest.fn(),
3735
handleSnapRpcRequest: jest.fn(),
3836
},
3937
}),
@@ -53,7 +51,6 @@ describe('builder', () => {
5351
describe('specificationBuilder', () => {
5452
const specification = invokeSnapBuilder.specificationBuilder({
5553
methodHooks: {
56-
getSnap: jest.fn(),
5754
handleSnapRpcRequest: jest.fn(),
5855
},
5956
});
@@ -90,7 +87,6 @@ describe('implementation', () => {
9087
}) as any;
9188
it('calls handleSnapRpcRequest', async () => {
9289
const hooks = getMockHooks();
93-
hooks.getSnap.mockImplementation(getTruncatedSnap);
9490
const implementation = getInvokeSnapImplementation(hooks);
9591
await implementation({
9692
context: { origin: MOCK_ORIGIN },
@@ -101,8 +97,6 @@ describe('implementation', () => {
10197
},
10298
});
10399

104-
expect(hooks.getSnap).toHaveBeenCalledTimes(1);
105-
expect(hooks.getSnap).toHaveBeenCalledWith(MOCK_SNAP_ID);
106100
expect(hooks.handleSnapRpcRequest).toHaveBeenCalledWith({
107101
handler: 'onRpcRequest',
108102
origin: MOCK_ORIGIN,
@@ -113,27 +107,6 @@ describe('implementation', () => {
113107
snapId: MOCK_SNAP_ID,
114108
});
115109
});
116-
117-
it('throws if snap is not installed', async () => {
118-
const hooks = getMockHooks();
119-
const implementation = getInvokeSnapImplementation(hooks);
120-
await expect(
121-
implementation({
122-
context: { origin: MOCK_ORIGIN },
123-
method: WALLET_SNAP_PERMISSION_KEY,
124-
params: {
125-
snapId: MOCK_SNAP_ID,
126-
request: { method: 'hello', params: {} },
127-
},
128-
}),
129-
).rejects.toThrow(
130-
`The snap "${MOCK_SNAP_ID}" is not installed. Please install it first, before invoking the snap.`,
131-
);
132-
expect(hooks.getSnap).toHaveBeenCalledTimes(1);
133-
expect(hooks.getSnap).toHaveBeenCalledWith(MOCK_SNAP_ID);
134-
135-
expect(hooks.handleSnapRpcRequest).not.toHaveBeenCalled();
136-
});
137110
});
138111

139112
describe('handleSnapInstall', () => {

packages/snaps-rpc-methods/src/restricted/invokeSnap.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
RequestSnapsParams,
1313
RequestSnapsResult,
1414
} from '@metamask/snaps-sdk';
15-
import type { Snap, SnapRpcHookArgs } from '@metamask/snaps-utils';
15+
import type { SnapRpcHookArgs } from '@metamask/snaps-utils';
1616
import { HandlerType, SnapCaveatType } from '@metamask/snaps-utils';
1717
import type { Json, NonEmptyArray } from '@metamask/utils';
1818

@@ -37,7 +37,6 @@ export type GetPermittedSnaps = {
3737
type AllowedActions = InstallSnaps | GetPermittedSnaps;
3838

3939
export type InvokeSnapMethodHooks = {
40-
getSnap: (snapId: string) => Snap | undefined;
4140
handleSnapRpcRequest: ({
4241
snapId,
4342
origin,
@@ -139,7 +138,6 @@ const specificationBuilder: PermissionSpecificationBuilder<
139138
};
140139

141140
const methodHooks: MethodHooksObject<InvokeSnapMethodHooks> = {
142-
getSnap: true,
143141
handleSnapRpcRequest: true,
144142
};
145143

@@ -153,13 +151,11 @@ export const invokeSnapBuilder = Object.freeze({
153151
* Builds the method implementation for `wallet_snap_*`.
154152
*
155153
* @param hooks - The RPC method hooks.
156-
* @param hooks.getSnap - A function that retrieves all information stored about a snap.
157154
* @param hooks.handleSnapRpcRequest - A function that sends an RPC request to a snap's RPC handler or throws if that fails.
158155
* @returns The method implementation which returns the result of `handleSnapRpcRequest`.
159156
* @throws If the params are invalid.
160157
*/
161158
export function getInvokeSnapImplementation({
162-
getSnap,
163159
handleSnapRpcRequest,
164160
}: InvokeSnapMethodHooks) {
165161
return async function invokeSnap(
@@ -169,12 +165,6 @@ export function getInvokeSnapImplementation({
169165

170166
const { snapId, request } = params as InvokeSnapParams;
171167

172-
if (!getSnap(snapId)) {
173-
throw rpcErrors.invalidRequest({
174-
message: `The snap "${snapId}" is not installed. Please install it first, before invoking the snap.`,
175-
});
176-
}
177-
178168
const { origin } = context;
179169

180170
return (await handleSnapRpcRequest({

0 commit comments

Comments
 (0)