From 82893919e90f56f4953dd8cf322e365a5f1754bb Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 13 Aug 2024 09:11:54 +0200 Subject: [PATCH 01/10] Unblock `wallet_switchEthereumChain` --- packages/snaps-execution-environments/src/common/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/snaps-execution-environments/src/common/utils.ts b/packages/snaps-execution-environments/src/common/utils.ts index a683d2e6c4..205d46efef 100644 --- a/packages/snaps-execution-environments/src/common/utils.ts +++ b/packages/snaps-execution-environments/src/common/utils.ts @@ -97,7 +97,6 @@ export const BLOCKED_RPC_METHODS = Object.freeze([ 'eth_decrypt', 'eth_getEncryptionPublicKey', 'wallet_addEthereumChain', - 'wallet_switchEthereumChain', 'wallet_watchAsset', 'wallet_registerOnboarding', 'wallet_scanQRCode', From b7f3286e18d11aa2331b26432c4d2b7447030f27 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 13 Aug 2024 11:06:43 +0200 Subject: [PATCH 02/10] Add `isSnapId` helper function --- packages/snaps-utils/src/snaps.test.ts | 34 +++++++++++++++++++++++--- packages/snaps-utils/src/snaps.ts | 12 +++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/snaps-utils/src/snaps.test.ts b/packages/snaps-utils/src/snaps.test.ts index 20e642c690..94905842ab 100644 --- a/packages/snaps-utils/src/snaps.test.ts +++ b/packages/snaps-utils/src/snaps.test.ts @@ -2,6 +2,7 @@ import type { SubjectPermissions, PermissionConstraint, } from '@metamask/permission-controller'; +import type { SnapId } from '@metamask/snaps-sdk'; import { is } from '@metamask/superstruct'; import { SnapCaveatType } from './caveats'; @@ -14,9 +15,36 @@ import { assertIsValidSnapId, verifyRequestedSnapPermissions, stripSnapPrefix, + isSnapId, } from './snaps'; import { uri, WALLET_SNAP_PERMISSION_KEY } from './types'; +describe('isSnapId', () => { + it.each(['npm:@metamask/test-snap-bip44', 'local:http://localhost:8000'])( + 'returns `true` for "%s"', + () => { + expect(isSnapId('npm:@metamask/test-snap-bip44')).toBe(true); + }, + ); + + it.each([ + undefined, + {}, + null, + true, + 2, + 'foo:bar', + ' local:http://localhost:8000', + 'local:http://localhost:8000 ', + 'local:http://localhost:8000\n', + 'local:http://localhost:8000\r', + 'local:😎', + 'local:␡', + ])('returns `false` for "%s"', (value) => { + expect(isSnapId(value)).toBe(false); + }); +}); + describe('assertIsValidSnapId', () => { it.each([undefined, {}, null, true, 2])( 'throws for non-strings (#%#)', @@ -273,9 +301,9 @@ describe('isSnapPermitted', () => { }, }; - expect(isSnapPermitted(validPermissions, 'foo')).toBe(true); - expect(isSnapPermitted(invalidPermissions1, 'foo')).toBe(false); - expect(isSnapPermitted(invalidPermissions2, 'foo')).toBe(false); + expect(isSnapPermitted(validPermissions, 'foo' as SnapId)).toBe(true); + expect(isSnapPermitted(invalidPermissions1, 'foo' as SnapId)).toBe(false); + expect(isSnapPermitted(invalidPermissions2, 'foo' as SnapId)).toBe(false); }); describe('verifyRequestedSnapPermissions', () => { diff --git a/packages/snaps-utils/src/snaps.ts b/packages/snaps-utils/src/snaps.ts index 93a9d0032c..442bfa9c3b 100644 --- a/packages/snaps-utils/src/snaps.ts +++ b/packages/snaps-utils/src/snaps.ts @@ -7,6 +7,7 @@ import type { BlockReason } from '@metamask/snaps-registry'; import type { SnapId, Snap as TruncatedSnap } from '@metamask/snaps-sdk'; import type { Struct } from '@metamask/superstruct'; import { + is, empty, enums, intersection, @@ -306,6 +307,17 @@ export function stripSnapPrefix(snapId: string): string { return snapId.replace(getSnapPrefix(snapId), ''); } +/** + * Check if the given value is a valid snap ID. This function is a type guard, + * and will narrow the type of the value to `SnapId` if it returns `true`. + * + * @param value - The value to check. + * @returns `true` if the value is a valid snap ID, and `false` otherwise. + */ +export function isSnapId(value: unknown): value is SnapId { + return is(value, SnapIdStruct); +} + /** * Assert that the given value is a valid snap ID. * From eebe6a68eb11bbdae47148d09f0ab73cb8b97e89 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Fri, 16 Aug 2024 12:55:31 +0200 Subject: [PATCH 03/10] Update Ethereum provider example --- .../packages/ethereum-provider/src/index.ts | 17 ++++++++++++++++- .../packages/ethereum-provider/src/types.ts | 8 +++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/examples/packages/ethereum-provider/src/index.ts b/packages/examples/packages/ethereum-provider/src/index.ts index 50437699d7..8741725ccc 100644 --- a/packages/examples/packages/ethereum-provider/src/index.ts +++ b/packages/examples/packages/ethereum-provider/src/index.ts @@ -5,7 +5,19 @@ import { import type { Hex } from '@metamask/utils'; import { assert, stringToBytes, bytesToHex } from '@metamask/utils'; -import type { PersonalSignParams } from './types'; +import type { BaseParams, PersonalSignParams } from './types'; + +/** + * Set the active Ethereum chain for the Snap. + * + * @param chainId - The chain ID to switch to. + */ +async function switchChain(chainId: Hex) { + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }], + }); +} /** * Get the current gas price using the `ethereum` global. This is essentially @@ -106,6 +118,9 @@ async function personalSign(message: string, from: string) { * @see https://docs.metamask.io/snaps/reference/rpc-api/#wallet_invokesnap */ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { + const { chainId = '0x1' } = request.params as BaseParams; + await switchChain(chainId); + switch (request.method) { case 'getGasPrice': return await getGasPrice(); diff --git a/packages/examples/packages/ethereum-provider/src/types.ts b/packages/examples/packages/ethereum-provider/src/types.ts index 9175295002..7310c3b978 100644 --- a/packages/examples/packages/ethereum-provider/src/types.ts +++ b/packages/examples/packages/ethereum-provider/src/types.ts @@ -1,3 +1,9 @@ -export type PersonalSignParams = { +import type { Hex } from '@metamask/utils'; + +export type BaseParams = { + chainId?: Hex; +}; + +export type PersonalSignParams = BaseParams & { message: string; }; From 09bcf5887d32fdb54b9e67fe9c68944980988592 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Fri, 16 Aug 2024 13:51:31 +0200 Subject: [PATCH 04/10] Grant permitted chains permission on install/update --- packages/snaps-controllers/package.json | 2 + .../src/snaps/SnapController.ts | 58 +++++- yarn.lock | 165 ++++++++++++++++-- 3 files changed, 211 insertions(+), 14 deletions(-) diff --git a/packages/snaps-controllers/package.json b/packages/snaps-controllers/package.json index cec30b479c..df12c52272 100644 --- a/packages/snaps-controllers/package.json +++ b/packages/snaps-controllers/package.json @@ -57,11 +57,13 @@ "@metamask/base-controller": "^6.0.2", "@metamask/json-rpc-engine": "^9.0.2", "@metamask/json-rpc-middleware-stream": "^8.0.2", + "@metamask/network-controller": "^20.1.0", "@metamask/object-multiplex": "^2.0.0", "@metamask/permission-controller": "^11.0.0", "@metamask/phishing-controller": "^10.1.1", "@metamask/post-message-stream": "^8.1.0", "@metamask/rpc-errors": "^6.3.1", + "@metamask/selected-network-controller": "^17.0.0", "@metamask/snaps-registry": "^3.2.1", "@metamask/snaps-rpc-methods": "workspace:^", "@metamask/snaps-sdk": "workspace:^", diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 41e20cadc3..96550e2ab8 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -4,6 +4,7 @@ import type { } from '@metamask/approval-controller'; import type { RestrictedControllerMessenger } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; +import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/network-controller'; import type { Caveat, GetEndowments, @@ -26,6 +27,7 @@ import type { } from '@metamask/permission-controller'; import { SubjectType } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; +import type { SelectedNetworkControllerGetNetworkClientIdForDomainAction } from '@metamask/selected-network-controller'; import type { BlockReason } from '@metamask/snaps-registry'; import { WALLET_SNAP_PERMISSION_KEY, @@ -556,7 +558,9 @@ export type AllowedActions = | Update | ResolveVersion | CreateInterface - | GetInterface; + | GetInterface + | NetworkControllerGetNetworkClientByIdAction + | SelectedNetworkControllerGetNetworkClientIdForDomainAction; export type AllowedEvents = | ExecutionServiceEvents @@ -3624,7 +3628,50 @@ export class SnapController extends BaseController< } /** - * Updates the permissions for a snap following an install, update or rollback. + * Get the permissions to grant to a Snap following an install, update or + * rollback. + * + * @param snapId - The snap ID. + * @param newPermissions - The new permissions to be granted. + * @returns The permissions to grant to the Snap. + */ + #getPermissionsToGrant(snapId: SnapId, newPermissions: RequestedPermissions) { + if (Object.keys(newPermissions).includes(SnapEndowments.EthereumProvider)) { + const networkClientId = this.messagingSystem.call( + 'SelectedNetworkController:getNetworkClientIdForDomain', + snapId, + ); + + const { configuration } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + + // This needs to be assigned to have proper type inference. + const modifiedPermissions: RequestedPermissions = { + ...newPermissions, + permittedChains: { + caveats: [ + { + type: 'restrictNetworkSwitching', + value: [configuration.chainId], + }, + ], + date: Date.now(), + id: nanoid(), + invoker: snapId, + parentCapability: 'permittedChains', + }, + }; + + return modifiedPermissions; + } + + return newPermissions; + } + + /** + * Update the permissions for a snap following an install, update or rollback. * * Grants newly requested permissions and revokes unused/revoked permissions. * @@ -3657,8 +3704,13 @@ export class SnapController extends BaseController< } if (isNonEmptyArray(Object.keys(newPermissions))) { + const approvedPermissions = this.#getPermissionsToGrant( + snapId, + newPermissions, + ); + this.messagingSystem.call('PermissionController:grantPermissions', { - approvedPermissions: newPermissions, + approvedPermissions, subject: { origin: snapId }, requestData, }); diff --git a/yarn.lock b/yarn.lock index 994934591a..aa64c19d56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4422,6 +4422,49 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-block-tracker@npm:^9.0.3": + version: 9.0.3 + resolution: "@metamask/eth-block-tracker@npm:9.0.3" + dependencies: + "@metamask/eth-json-rpc-provider": ^3.0.2 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.1.0 + json-rpc-random-id: ^1.0.1 + pify: ^5.0.0 + checksum: edd3d59a0416752d90c8e2d8c10c31635dbe3eb323fcb054c401528afe4cbbb6a5a85aedd6ffee4a504d9779656bfab027f2274fd95981c90bf56b6f565dbca2 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-infura@npm:^9.1.0": + version: 9.1.0 + resolution: "@metamask/eth-json-rpc-infura@npm:9.1.0" + dependencies: + "@metamask/eth-json-rpc-provider": ^2.1.0 + "@metamask/json-rpc-engine": ^7.1.1 + "@metamask/rpc-errors": ^6.0.0 + "@metamask/utils": ^8.1.0 + node-fetch: ^2.7.0 + checksum: 58f2a6b6ce9c545c9210b2ab3f8c0946cc82ed02c82a096406d8c7146c89c1eba1a13e472048a6e252906dd5eb336e63238d9a5446407c1d46b1d6a40e2a64f4 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-middleware@npm:^12.1.1": + version: 12.1.2 + resolution: "@metamask/eth-json-rpc-middleware@npm:12.1.2" + dependencies: + "@metamask/eth-block-tracker": ^9.0.3 + "@metamask/eth-json-rpc-provider": ^3.0.2 + "@metamask/eth-sig-util": ^7.0.0 + "@metamask/json-rpc-engine": ^8.0.2 + "@metamask/rpc-errors": ^6.0.0 + "@metamask/utils": ^8.1.0 + klona: ^2.0.6 + pify: ^5.0.0 + safe-stable-stringify: ^2.4.3 + checksum: 0334fa8e51d73488e42e1cd663e90012f4055c5cd04cb4ff371ecb3552b82cd271f27a88ff0187ad23f195cfbbba467126711c08b20c1124083a706a85524a82 + languageName: node + linkType: hard + "@metamask/eth-json-rpc-middleware@npm:^13.0.0": version: 13.0.0 resolution: "@metamask/eth-json-rpc-middleware@npm:13.0.0" @@ -4441,16 +4484,38 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-provider@npm:^4.0.0": - version: 4.1.2 - resolution: "@metamask/eth-json-rpc-provider@npm:4.1.2" +"@metamask/eth-json-rpc-provider@npm:^2.1.0": + version: 2.3.2 + resolution: "@metamask/eth-json-rpc-provider@npm:2.3.2" + dependencies: + "@metamask/json-rpc-engine": ^7.3.2 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + checksum: e6731271aad3b972d85b9230c26d35a9b88722f3bd3024675ad2f568e634e9fdfef4717ef2892f3cc512d381cf17a4e20dbd5eb808ced765082bea3379ad6ddc + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-provider@npm:^3.0.2": + version: 3.0.2 + resolution: "@metamask/eth-json-rpc-provider@npm:3.0.2" + dependencies: + "@metamask/json-rpc-engine": ^8.0.2 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + checksum: 0321eaad6fa205a9d3ddcfaf28e63c05291614893cb2e116151185a4acbd6bb6a508d6e556b3cb8bc4d3caef4bf0a638202d9b6bdc127fbcb81715eb2660a809 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-provider@npm:^4.0.0, @metamask/eth-json-rpc-provider@npm:^4.1.2": + version: 4.1.3 + resolution: "@metamask/eth-json-rpc-provider@npm:4.1.3" dependencies: "@metamask/json-rpc-engine": ^9.0.2 "@metamask/rpc-errors": ^6.3.1 "@metamask/safe-event-emitter": ^3.0.0 "@metamask/utils": ^9.1.0 uuid: ^8.3.2 - checksum: d7092ce64fc185796a0be3f339da1718e280159f06e8fdf29a002b4573abd0903219a527a4c5890952d5e66dbe56b5c3e53d42aa8a4bbc25acbdf6efadcff6ea + checksum: 788c1f983d8021a10922f414f7c5aa93f79e14000219bd5155f027c1964e73d08e931cb98749057f57fb4001441098724aedacbe2198ab5a0b28cc3fcb4d262d languageName: node linkType: hard @@ -4881,6 +4946,28 @@ __metadata: languageName: unknown linkType: soft +"@metamask/json-rpc-engine@npm:^7.1.1, @metamask/json-rpc-engine@npm:^7.3.2": + version: 7.3.3 + resolution: "@metamask/json-rpc-engine@npm:7.3.3" + dependencies: + "@metamask/rpc-errors": ^6.2.1 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + checksum: 7bab8b4d2341a6243ba451bc58283f0a6905b09f7257857859848a51a795444ca6899b1a6908b15f8ed236fb574ab85a630c9cb28d127ab52c4630e496c16006 + languageName: node + linkType: hard + +"@metamask/json-rpc-engine@npm:^8.0.2": + version: 8.0.2 + resolution: "@metamask/json-rpc-engine@npm:8.0.2" + dependencies: + "@metamask/rpc-errors": ^6.2.1 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + checksum: c240d298ad503d93922a94a62cf59f0344b6d6644a523bc8ea3c0f321bea7172b89f2747a5618e2861b2e8152ae5086b76f391a10e4566529faa50b8850c051d + languageName: node + linkType: hard + "@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2": version: 9.0.2 resolution: "@metamask/json-rpc-engine@npm:9.0.2" @@ -5135,6 +5222,29 @@ __metadata: languageName: unknown linkType: soft +"@metamask/network-controller@npm:^20.1.0": + version: 20.1.0 + resolution: "@metamask/network-controller@npm:20.1.0" + dependencies: + "@metamask/base-controller": ^6.0.2 + "@metamask/controller-utils": ^11.0.2 + "@metamask/eth-block-tracker": ^9.0.3 + "@metamask/eth-json-rpc-infura": ^9.1.0 + "@metamask/eth-json-rpc-middleware": ^12.1.1 + "@metamask/eth-json-rpc-provider": ^4.1.2 + "@metamask/eth-query": ^4.0.0 + "@metamask/json-rpc-engine": ^9.0.2 + "@metamask/rpc-errors": ^6.3.1 + "@metamask/swappable-obj-proxy": ^2.2.0 + "@metamask/utils": ^9.1.0 + async-mutex: ^0.5.0 + immer: ^9.0.6 + loglevel: ^1.8.1 + uuid: ^8.3.2 + checksum: 41aa7f2cd693850b3f3ce3a0937b67bdca482bf573b1b96fa36cbf75728c3593c2082b2cd99497cf2cb182912f73e784a6cc8c4db0357bf452caa2d5890329c7 + languageName: node + linkType: hard + "@metamask/network-example-snap@workspace:^, @metamask/network-example-snap@workspace:packages/examples/packages/network-access": version: 0.0.0-use.local resolution: "@metamask/network-example-snap@workspace:packages/examples/packages/network-access" @@ -5337,7 +5447,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.3.1": +"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1": version: 6.3.1 resolution: "@metamask/rpc-errors@npm:6.3.1" dependencies: @@ -5364,6 +5474,21 @@ __metadata: languageName: node linkType: hard +"@metamask/selected-network-controller@npm:^17.0.0": + version: 17.0.0 + resolution: "@metamask/selected-network-controller@npm:17.0.0" + dependencies: + "@metamask/base-controller": ^6.0.2 + "@metamask/json-rpc-engine": ^9.0.2 + "@metamask/swappable-obj-proxy": ^2.2.0 + "@metamask/utils": ^9.1.0 + peerDependencies: + "@metamask/network-controller": ^20.0.0 + "@metamask/permission-controller": ^11.0.0 + checksum: 8ad6a64c4e51a7538e85b81065e1b2fc3648bc771f8406eafd2d53064b09739b2c6d1b2b6d6b1e8e17bad5400cee0206235f82cad4e51308263306d8fac32845 + languageName: node + linkType: hard + "@metamask/signature-insights-example-snap@workspace:^, @metamask/signature-insights-example-snap@workspace:packages/examples/packages/signature-insights": version: 0.0.0-use.local resolution: "@metamask/signature-insights-example-snap@workspace:packages/examples/packages/signature-insights" @@ -5562,11 +5687,13 @@ __metadata: "@metamask/eslint-config-typescript": ^12.1.0 "@metamask/json-rpc-engine": ^9.0.2 "@metamask/json-rpc-middleware-stream": ^8.0.2 + "@metamask/network-controller": ^20.1.0 "@metamask/object-multiplex": ^2.0.0 "@metamask/permission-controller": ^11.0.0 "@metamask/phishing-controller": ^10.1.1 "@metamask/post-message-stream": ^8.1.0 "@metamask/rpc-errors": ^6.3.1 + "@metamask/selected-network-controller": ^17.0.0 "@metamask/snaps-registry": ^3.2.1 "@metamask/snaps-rpc-methods": "workspace:^" "@metamask/snaps-sdk": "workspace:^" @@ -6162,6 +6289,13 @@ __metadata: languageName: node linkType: hard +"@metamask/swappable-obj-proxy@npm:^2.2.0": + version: 2.2.0 + resolution: "@metamask/swappable-obj-proxy@npm:2.2.0" + checksum: 343c95f72c96776980ef3e70600f7fa312be9a75683c132404a66ddd3c507abadee9c4deba1385246f73bded1938a7958e5a89fc407c19dfc352dd9b398e216f + languageName: node + linkType: hard + "@metamask/template-snap@npm:^0.7.0": version: 0.7.0 resolution: "@metamask/template-snap@npm:0.7.0" @@ -6255,7 +6389,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^8.1.0, @metamask/utils@npm:^8.2.0, @metamask/utils@npm:^8.2.1": +"@metamask/utils@npm:^8.1.0, @metamask/utils@npm:^8.2.0, @metamask/utils@npm:^8.2.1, @metamask/utils@npm:^8.3.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" dependencies: @@ -9442,6 +9576,15 @@ __metadata: languageName: node linkType: hard +"async-mutex@npm:^0.5.0": + version: 0.5.0 + resolution: "async-mutex@npm:0.5.0" + dependencies: + tslib: ^2.4.0 + checksum: be1587f4875f3bb15e34e9fcce82eac2966daef4432c8d0046e61947fb9a1b95405284601bc7ce4869319249bc07c75100880191db6af11d1498931ac2a2f9ea + languageName: node + linkType: hard + "async@npm:^3.2.3, async@npm:^3.2.4": version: 3.2.4 resolution: "async@npm:3.2.4" @@ -16904,10 +17047,10 @@ __metadata: languageName: node linkType: hard -"loglevel@npm:^1.6.0": - version: 1.8.1 - resolution: "loglevel@npm:1.8.1" - checksum: a1a62db40291aaeaef2f612334c49e531bff71cc1d01a2acab689ab80d59e092f852ab164a5aedc1a752fdc46b7b162cb097d8a9eb2cf0b299511106c29af61d +"loglevel@npm:^1.6.0, loglevel@npm:^1.8.1": + version: 1.9.1 + resolution: "loglevel@npm:1.9.1" + checksum: e1c8586108c4d566122e91f8a79c8df728920e3a714875affa5120566761a24077ec8ec9e5fc388b022e39fc411ec6e090cde1b5775871241b045139771eeb06 languageName: node linkType: hard @@ -17822,7 +17965,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.12": +"node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.12, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From 031019a35ca6abc5603c2fc2bdf1f3128da767aa Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 20 Aug 2024 12:36:04 +0200 Subject: [PATCH 05/10] Add a test --- .../src/snaps/SnapController.test.tsx | 59 +++++++++++++++++++ .../src/snaps/SnapController.ts | 2 + .../src/test-utils/controller.ts | 2 + 3 files changed, 63 insertions(+) diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index cd755ab439..9d2ea4efde 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -4628,6 +4628,65 @@ describe('SnapController', () => { snapController.destroy(); }); + it('grants permitted chains permission to Snaps with `endowment:ethereum-provider`', async () => { + const rootMessenger = getControllerMessenger(); + const messenger = getSnapControllerMessenger(rootMessenger); + + rootMessenger.registerActionHandler( + 'PermissionController:getPermissions', + () => ({}), + ); + + rootMessenger.registerActionHandler( + 'SelectedNetworkController:getNetworkClientIdForDomain', + () => 'mainnet', + ); + + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + () => ({ + // @ts-expect-error - Partial network client. + configuration: { + chainId: '0x1', + }, + }), + ); + + const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ + manifest: getSnapManifest({ + initialPermissions: { + 'endowment:page-home': {}, + 'endowment:ethereum-provider': {}, + }, + }), + }); + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + detectSnapLocation: loopbackDetect({ manifest }), + }), + ); + + await snapController.installSnaps(MOCK_ORIGIN, { + [MOCK_SNAP_ID]: {}, + }); + + const approvedPermissions = { + 'endowment:ethereum-provider': { + caveats: [], + }, + permittedChains: {}, + }; + + expect(messenger.call).toHaveBeenCalledWith( + 'PermissionController:grantPermissions', + { approvedPermissions, subject: { origin: MOCK_SNAP_ID } }, + ); + + snapController.destroy(); + }); + it('supports preinstalled snaps', async () => { const rootMessenger = getControllerMessenger(); jest.spyOn(rootMessenger, 'call'); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 96550e2ab8..297514eba4 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -3637,6 +3637,8 @@ export class SnapController extends BaseController< */ #getPermissionsToGrant(snapId: SnapId, newPermissions: RequestedPermissions) { if (Object.keys(newPermissions).includes(SnapEndowments.EthereumProvider)) { + // This will return the globally selected network if the Snap doesn't have + // one set. const networkClientId = this.messagingSystem.call( 'SelectedNetworkController:getNetworkClientIdForDomain', snapId, diff --git a/packages/snaps-controllers/src/test-utils/controller.ts b/packages/snaps-controllers/src/test-utils/controller.ts index bcb7e27708..55eafb2269 100644 --- a/packages/snaps-controllers/src/test-utils/controller.ts +++ b/packages/snaps-controllers/src/test-utils/controller.ts @@ -469,6 +469,7 @@ export const getSnapControllerMessenger = ( 'ExecutionService:terminateAllSnaps', 'ExecutionService:terminateSnap', 'ExecutionService:handleRpcRequest', + 'NetworkController:getNetworkClientById', 'PermissionController:getEndowments', 'PermissionController:hasPermission', 'PermissionController:hasPermissions', @@ -481,6 +482,7 @@ export const getSnapControllerMessenger = ( 'PermissionController:getSubjectNames', 'PhishingController:maybeUpdateState', 'PhishingController:testOrigin', + 'SelectedNetworkController:getNetworkClientIdForDomain', 'SnapController:get', 'SnapController:handleRequest', 'SnapController:getSnapState', From 832eb69a9a4bf941b65a85a3b20de904360e3e33 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 21 Aug 2024 12:13:52 +0200 Subject: [PATCH 06/10] Use PERMITTED_CHAINS_ENDOWMENT constant --- .../src/snaps/SnapController.test.tsx | 7 +++++-- .../snaps-controllers/src/snaps/SnapController.ts | 11 +++++------ packages/snaps-controllers/src/snaps/constants.ts | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index 9d2ea4efde..c0e409c64a 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -96,7 +96,10 @@ import { sleep, } from '../test-utils'; import { delay } from '../utils'; -import { LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS } from './constants'; +import { + LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS, + PERMITTED_CHAINS_ENDOWMENT, +} from './constants'; import { SnapsRegistryStatus } from './registry'; import type { SnapControllerState } from './SnapController'; import { @@ -4676,7 +4679,7 @@ describe('SnapController', () => { 'endowment:ethereum-provider': { caveats: [], }, - permittedChains: {}, + [PERMITTED_CHAINS_ENDOWMENT]: {}, }; expect(messenger.call).toHaveBeenCalledWith( diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 297514eba4..5f6e39bebe 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -118,8 +118,8 @@ import type { TerminateAllSnapsAction, TerminateSnapAction, } from '../services'; -import type { EncryptionResult } from '../types'; -import { +import type { + EncryptionResult, type ExportableKeyEncryptor, type KeyDerivationOptions, } from '../types'; @@ -133,6 +133,7 @@ import { import { ALLOWED_PERMISSIONS, LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS, + PERMITTED_CHAINS_ENDOWMENT, } from './constants'; import type { SnapLocation } from './location'; import { detectSnapLocation } from './location'; @@ -3652,17 +3653,15 @@ export class SnapController extends BaseController< // This needs to be assigned to have proper type inference. const modifiedPermissions: RequestedPermissions = { ...newPermissions, - permittedChains: { + [PERMITTED_CHAINS_ENDOWMENT]: { caveats: [ { type: 'restrictNetworkSwitching', value: [configuration.chainId], }, ], - date: Date.now(), - id: nanoid(), invoker: snapId, - parentCapability: 'permittedChains', + parentCapability: PERMITTED_CHAINS_ENDOWMENT, }, }; diff --git a/packages/snaps-controllers/src/snaps/constants.ts b/packages/snaps-controllers/src/snaps/constants.ts index 2125d9976d..4877ee521a 100644 --- a/packages/snaps-controllers/src/snaps/constants.ts +++ b/packages/snaps-controllers/src/snaps/constants.ts @@ -20,3 +20,5 @@ export const LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS = { iterations: 10_000, }, }; + +export const PERMITTED_CHAINS_ENDOWMENT = 'endowment:permitted-chains'; From fd11f167a332cffa26e377f463dfcec357397f50 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 21 Aug 2024 13:00:16 +0200 Subject: [PATCH 07/10] Add some more tests --- .../src/snaps/SnapController.test.tsx | 166 +++++++++++++++++- .../src/snaps/SnapController.ts | 4 +- 2 files changed, 163 insertions(+), 7 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index c0e409c64a..84302e37d5 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -4631,7 +4631,7 @@ describe('SnapController', () => { snapController.destroy(); }); - it('grants permitted chains permission to Snaps with `endowment:ethereum-provider`', async () => { + it('grants the `endowment:permitted-chains` permission to a Snap with `endowment:ethereum-provider`', async () => { const rootMessenger = getControllerMessenger(); const messenger = getSnapControllerMessenger(rootMessenger); @@ -4676,15 +4676,171 @@ describe('SnapController', () => { }); const approvedPermissions = { - 'endowment:ethereum-provider': { - caveats: [], + 'endowment:page-home': { + caveats: null, + }, + 'endowment:ethereum-provider': {}, + [PERMITTED_CHAINS_ENDOWMENT]: { + caveats: [ + { + type: 'restrictNetworkSwitching', + value: ['0x1'], + }, + ], + invoker: MOCK_SNAP_ID, + parentCapability: PERMITTED_CHAINS_ENDOWMENT, + }, + }; + + expect(messenger.call).toHaveBeenCalledWith( + 'PermissionController:grantPermissions', + { + approvedPermissions, + subject: { origin: MOCK_SNAP_ID }, + requestData: expect.any(Object), + }, + ); + + snapController.destroy(); + }); + + it('overrides the `endowment:permitted-chains` permission if the Snap specifies it in its manifest', async () => { + const rootMessenger = getControllerMessenger(); + const messenger = getSnapControllerMessenger(rootMessenger); + + rootMessenger.registerActionHandler( + 'PermissionController:getPermissions', + () => ({}), + ); + + rootMessenger.registerActionHandler( + 'SelectedNetworkController:getNetworkClientIdForDomain', + () => 'mainnet', + ); + + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + () => ({ + // @ts-expect-error - Partial network client. + configuration: { + chainId: '0x1', + }, + }), + ); + + const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ + manifest: getSnapManifest({ + initialPermissions: { + 'endowment:page-home': {}, + 'endowment:ethereum-provider': {}, + // @ts-expect-error - There is no type definition for this. + 'endowment:permitted-chains': { + caveats: [ + { + type: 'restrictNetworkSwitching', + value: ['0x5'], + }, + ], + }, + }, + }), + }); + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + detectSnapLocation: loopbackDetect({ manifest }), + }), + ); + + await snapController.installSnaps(MOCK_ORIGIN, { + [MOCK_SNAP_ID]: {}, + }); + + const approvedPermissions = { + 'endowment:page-home': { + caveats: null, + }, + 'endowment:ethereum-provider': {}, + [PERMITTED_CHAINS_ENDOWMENT]: { + caveats: [ + { + type: 'restrictNetworkSwitching', + value: ['0x1'], + }, + ], + invoker: MOCK_SNAP_ID, + parentCapability: PERMITTED_CHAINS_ENDOWMENT, + }, + }; + + expect(messenger.call).toHaveBeenCalledWith( + 'PermissionController:grantPermissions', + { + approvedPermissions, + subject: { origin: MOCK_SNAP_ID }, + requestData: expect.any(Object), + }, + ); + + snapController.destroy(); + }); + + it('does not grant the `endowment:permitted-chains` permission if the Snap does not have the `endowment:ethereum-provider` permission', async () => { + const rootMessenger = getControllerMessenger(); + const messenger = getSnapControllerMessenger(rootMessenger); + + rootMessenger.registerActionHandler( + 'PermissionController:getPermissions', + () => ({}), + ); + + rootMessenger.registerActionHandler( + 'SelectedNetworkController:getNetworkClientIdForDomain', + () => { + throw new Error('This should not be called.'); + }, + ); + + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + () => { + throw new Error('This should not be called.'); + }, + ); + + const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ + manifest: getSnapManifest({ + initialPermissions: { + 'endowment:page-home': {}, + }, + }), + }); + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + detectSnapLocation: loopbackDetect({ manifest }), + }), + ); + + await snapController.installSnaps(MOCK_ORIGIN, { + [MOCK_SNAP_ID]: {}, + }); + + const approvedPermissions = { + 'endowment:page-home': { + caveats: null, }, - [PERMITTED_CHAINS_ENDOWMENT]: {}, }; expect(messenger.call).toHaveBeenCalledWith( 'PermissionController:grantPermissions', - { approvedPermissions, subject: { origin: MOCK_SNAP_ID } }, + { + approvedPermissions, + subject: { origin: MOCK_SNAP_ID }, + requestData: expect.any(Object), + }, ); snapController.destroy(); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 5f6e39bebe..72a29e9969 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -120,8 +120,8 @@ import type { } from '../services'; import type { EncryptionResult, - type ExportableKeyEncryptor, - type KeyDerivationOptions, + ExportableKeyEncryptor, + KeyDerivationOptions, } from '../types'; import { fetchSnap, From c3bcda08e589faec9f7396a2e5b9202e05917621 Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Wed, 21 Aug 2024 11:05:16 +0000 Subject: [PATCH 08/10] Update example snaps --- packages/examples/packages/ethereum-provider/snap.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/packages/ethereum-provider/snap.manifest.json b/packages/examples/packages/ethereum-provider/snap.manifest.json index f86cffb746..198bd7a519 100644 --- a/packages/examples/packages/ethereum-provider/snap.manifest.json +++ b/packages/examples/packages/ethereum-provider/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "pAw1i97eZQAkVu1vghgoOKBdA5EImKc8HuC3rQyLo7k=", + "shasum": "vdxQxaJ4dDVhuIu8cvKJkKFC9YeIvaRORuCdbtX5qLM=", "location": { "npm": { "filePath": "dist/bundle.js", From c0b30ff7c17165f6b5a3b6388c71ca6b88806ca3 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 21 Aug 2024 13:34:12 +0200 Subject: [PATCH 09/10] Update coverages --- packages/snaps-controllers/coverage.json | 6 +++--- packages/snaps-utils/coverage.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index d03ef020c3..a12a1f5eba 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -1,6 +1,6 @@ { - "branches": 92.54, + "branches": 92.56, "functions": 96.91, - "lines": 98.01, - "statements": 97.71 + "lines": 98.02, + "statements": 97.72 } diff --git a/packages/snaps-utils/coverage.json b/packages/snaps-utils/coverage.json index fab111efc3..7c789ce487 100644 --- a/packages/snaps-utils/coverage.json +++ b/packages/snaps-utils/coverage.json @@ -2,5 +2,5 @@ "branches": 99.73, "functions": 98.9, "lines": 99.43, - "statements": 96.32 + "statements": 96.33 } From f2aa83e526b19fac11ae4bfda6d3e0837e63f13a Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 21 Aug 2024 14:54:43 +0200 Subject: [PATCH 10/10] Add `wallet_switchEthereumChain` support to `snaps-jest` --- .../ethereum-provider/snap.manifest.json | 2 +- .../packages/ethereum-provider/src/index.ts | 2 +- .../simulation/methods/specifications.test.ts | 2 ++ .../middleware/internal-methods/middleware.ts | 2 ++ .../switch-ethereum-chain.test.ts | 28 +++++++++++++++++ .../internal-methods/switch-ethereum-chain.ts | 31 +++++++++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.test.ts create mode 100644 packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.ts diff --git a/packages/examples/packages/ethereum-provider/snap.manifest.json b/packages/examples/packages/ethereum-provider/snap.manifest.json index 198bd7a519..ace9e35c7b 100644 --- a/packages/examples/packages/ethereum-provider/snap.manifest.json +++ b/packages/examples/packages/ethereum-provider/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "vdxQxaJ4dDVhuIu8cvKJkKFC9YeIvaRORuCdbtX5qLM=", + "shasum": "u/BaDj7fzOZvB+bPRLbkBie65a5oxunLABteu1+qWns=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethereum-provider/src/index.ts b/packages/examples/packages/ethereum-provider/src/index.ts index 8741725ccc..192164ceb1 100644 --- a/packages/examples/packages/ethereum-provider/src/index.ts +++ b/packages/examples/packages/ethereum-provider/src/index.ts @@ -118,7 +118,7 @@ async function personalSign(message: string, from: string) { * @see https://docs.metamask.io/snaps/reference/rpc-api/#wallet_invokesnap */ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { - const { chainId = '0x1' } = request.params as BaseParams; + const { chainId = '0x1' } = (request.params as BaseParams) ?? {}; await switchChain(chainId); switch (request.method) { diff --git a/packages/snaps-jest/src/internals/simulation/methods/specifications.test.ts b/packages/snaps-jest/src/internals/simulation/methods/specifications.test.ts index 8517da8304..0ec6db3539 100644 --- a/packages/snaps-jest/src/internals/simulation/methods/specifications.test.ts +++ b/packages/snaps-jest/src/internals/simulation/methods/specifications.test.ts @@ -20,6 +20,8 @@ const MOCK_HOOKS: MiddlewareHooks = { createInterface: jest.fn(), updateInterface: jest.fn(), getInterfaceState: jest.fn(), + getIsLocked: jest.fn(), + resolveInterface: jest.fn(), }; describe('resolve', () => { diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.ts b/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.ts index 72859120de..d144df0fb7 100644 --- a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.ts +++ b/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/middleware.ts @@ -4,6 +4,7 @@ import type { Json, JsonRpcParams } from '@metamask/utils'; import { getAccountsHandler } from './accounts'; import { getProviderStateHandler } from './provider-state'; +import { getSwitchEthereumChainHandler } from './switch-ethereum-chain'; export type InternalMethodsMiddlewareHooks = { /** @@ -19,6 +20,7 @@ const methodHandlers = { metamask_getProviderState: getProviderStateHandler, eth_requestAccounts: getAccountsHandler, eth_accounts: getAccountsHandler, + wallet_switchEthereumChain: getSwitchEthereumChainHandler, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.test.ts b/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.test.ts new file mode 100644 index 0000000000..436213c61b --- /dev/null +++ b/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.test.ts @@ -0,0 +1,28 @@ +import type { Json, PendingJsonRpcResponse } from '@metamask/utils'; + +import { getSwitchEthereumChainHandler } from './switch-ethereum-chain'; + +describe('getSwitchEthereumChainHandler', () => { + it('returns `null`', async () => { + const end = jest.fn(); + const result: PendingJsonRpcResponse = { + jsonrpc: '2.0' as const, + id: 1, + }; + + await getSwitchEthereumChainHandler( + { + jsonrpc: '2.0', + id: 1, + method: 'wallet_switchEthereumChain', + params: [], + }, + result, + jest.fn(), + end, + ); + + expect(end).toHaveBeenCalled(); + expect(result.result).toBeNull(); + }); +}); diff --git a/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.ts b/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.ts new file mode 100644 index 0000000000..56eb1174cd --- /dev/null +++ b/packages/snaps-jest/src/internals/simulation/middleware/internal-methods/switch-ethereum-chain.ts @@ -0,0 +1,31 @@ +import type { + JsonRpcEngineEndCallback, + JsonRpcEngineNextCallback, +} from '@metamask/json-rpc-engine'; +import type { + Json, + JsonRpcRequest, + PendingJsonRpcResponse, +} from '@metamask/utils'; + +/** + * A mock handler for the `wallet_switchEthereumChain` method that always + * returns `null`. + * + * @param _request - Incoming JSON-RPC request. This is ignored for this + * specific handler. + * @param response - The outgoing JSON-RPC response, modified to return the + * result. + * @param _next - The `json-rpc-engine` middleware next handler. + * @param end - The `json-rpc-engine` middleware end handler. + */ +export async function getSwitchEthereumChainHandler( + _request: JsonRpcRequest, + response: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + // hooks: GetAccountsHandlerHooks, +) { + response.result = null; + return end(); +}