From b6174c16801fad13d14ac497c3788685c3583af6 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Mon, 23 Mar 2026 14:08:42 +0100 Subject: [PATCH 01/17] refactor!: Standardise `SnapController` action/event names and types (#3907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This renames all `SnapController` action and event names and types to follow the `Controller...Action` pattern used in most other controllers. I've also added the `generate-method-actions` script used in `MetaMask/core` to automatically generate these types. I found numerous unrelated type errors in test files that I fixed in this pull request as well, since it was a bit difficult to determine if a type error was caused by this refactor or not, in some cases. ## Breaking changes - All `SnapController` action types were renamed from `DoSomething` to `SnapControllerDoSomethingAction`. - `GetSnap` is now `SnapControllerGetSnapAction`. - Note: The method is now called `getSnap` instead of `get`. - `HandleSnapRequest` is now `SnapControllerHandleRequestAction`. - `GetSnapState` is now `SnapControllerGetSnapStateAction`. - `HasSnap` is now `SnapControllerHasSnapAction`. - Note: The method is now called `hasSnap` instead of `has`. - `UpdateSnapState` is now `SnapControllerUpdateSnapStateAction`. - `ClearSnapState` is now `SnapControllerClearSnapStateAction`. - `UpdateRegistry` is now `SnapControllerUpdateRegistryAction`. - `EnableSnap` is now `SnapControllerEnableSnapAction`. - Note: The method is now called `enableSnap` instead of `enable`. - `DisableSnap` is now `SnapControllerDisableSnapAction`. - Note: The method is now called `disableSnap` instead of `disable`. - `RemoveSnap` is now `SnapControllerRemoveSnapAction`. - Note: The method is now called `removeSnap` instead of `remove`. - `GetPermittedSnaps` is now `SnapControllerGetPermittedSnapsAction`. - Note: The method is now called `getPermittedSnaps` instead of `getPermitted`. - `GetAllSnaps` is now `SnapControllerGetAllSnapsAction`. - Note: The method is now called `getAllSnaps` instead of `getAll`. - `GetRunnableSnaps` is now `SnapControllerGetRunnableSnapsAction`. - `StopAllSnaps` is now `SnapControllerStopAllSnapsAction`. - `IncrementActiveReferences` is now `SnapControllerIncrementActiveReferencesAction`. - `DecrementActiveReferences` is now `SnapControllerDecrementActiveReferencesAction`. - `InstallSnaps` is now `SnapControllerInstallSnapsAction`. - Note: The method is now called `installSnaps` instead of `install`. - `DisconnectOrigin` is now `SnapControllerRemoveSnapFromSubjectAction`. - `RevokeDynamicPermissions` is now `SnapControllerRevokeDynamicSnapPermissionsAction`. - `GetSnapFile` is now `SnapControllerGetSnapFileAction`. - `IsMinimumPlatformVersion` is now `SnapControllerIsMinimumPlatformVersionAction`. - `SetClientActive` is now `SnapControllerSetClientActiveAction`. - All `SnapController` event types were renamed from `OnSomething` to `SnapControllerOnSomethingEvent`. - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. - `SnapBlocked` is now `SnapControllerSnapBlockedEvent`. - `SnapInstallStarted` is now `SnapControllerSnapInstallStartedEvent`. - `SnapInstallFailed` is now `SnapControllerSnapInstallFailedEvent`. - `SnapInstalled` is now `SnapControllerSnapInstalledEvent`. - `SnapUninstalled` is now `SnapControllerSnapUninstalledEvent`. - `SnapUnblocked` is now `SnapControllerSnapUnblockedEvent. - `SnapUpdated` is now `SnapControllerSnapUpdatedEvent`. - `SnapRolledback` is now `SnapControllerSnapRolledbackEvent`. - `SnapTerminated` is now `SnapControllerSnapTerminatedEvent`. - `SnapEnabled` is now `SnapControllerSnapEnabledEvent`. - `SnapDisabled` is now `SnapControllerSnapDisabledEvent`. --- > [!NOTE] > **High Risk** > Large breaking rename of `SnapController` messenger action/event names (e.g., `get`→`getSnap`, `getAll`→`getAllSnaps`) and their exported TypeScript types across multiple controllers/tests, which can easily break downstream integrations. Adds a generated action-type source file and enforces it via lint, so CI failures are likely if regeneration is missed. > > **Overview** > **Standardizes `SnapController` messenger API naming** by renaming action/event type aliases to the `SnapController…Action` / `SnapController…Event` pattern and updating call sites (notably `SnapController:get`→`SnapController:getSnap` and `SnapController:getAll`→`SnapController:getAllSnaps`) across controllers and tests. > > **Introduces generated method action types** by adding `SnapController-method-action-types.ts` (auto-generated union of method action types), wiring workspace scripts (`generate-method-action-types`) and enforcing it in root `lint`. > > Also includes small cleanup/consistency fixes in tests (e.g., metadata key `anonymous`→`includeInDebugSnapshot`) and removes now-unneeded lint suppression comments in execution services. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7386c7f867e62a50c405ccdbbc0ba6b6900e61af. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- package.json | 6 +- packages/snaps-controllers/CHANGELOG.md | 50 ++ packages/snaps-controllers/package.json | 1 + .../src/cronjob/CronjobController.test.ts | 2 +- .../src/cronjob/CronjobController.ts | 27 +- .../insights/SnapInsightsController.test.ts | 14 +- .../src/insights/SnapInsightsController.ts | 11 +- .../SnapInterfaceController.test.tsx | 6 +- .../src/interface/SnapInterfaceController.ts | 6 +- .../src/multichain/MultichainRouter.test.ts | 48 +- .../src/multichain/MultichainRouter.ts | 11 +- .../node-js/NodeThreadExecutionService.ts | 3 - .../services/proxy/ProxyExecutionService.ts | 3 - .../SnapController-method-action-types.ts | 335 ++++++++ .../src/snaps/SnapController.test.tsx | 544 ++++++------ .../src/snaps/SnapController.ts | 428 ++++------ packages/snaps-controllers/src/snaps/index.ts | 48 +- .../src/test-utils/controller.tsx | 202 +++-- .../src/test-utils/execution-environment.ts | 9 +- .../src/test-utils/registry.ts | 5 +- .../src/websocket/WebSocketService.ts | 16 +- .../src/restricted/invokeSnap.test.ts | 38 +- .../src/restricted/invokeSnap.ts | 16 +- scripts/generate-method-action-types.mts | 773 ++++++++++++++++++ yarn.lock | 1 + 25 files changed, 1832 insertions(+), 771 deletions(-) create mode 100644 packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts create mode 100644 scripts/generate-method-action-types.mts diff --git a/package.json b/package.json index 9d109bdf4c..a7cd8198e8 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,10 @@ "changelog:validate": "yarn workspaces foreach --all --parallel --interlaced --verbose run changelog:validate", "child-workspace-package-names-as-json": "ts-node scripts/child-workspace-package-names-as-json.ts", "clean": "yarn workspaces foreach --all --parallel --verbose run clean", + "generate-method-action-types": "yarn workspaces foreach --all --parallel --interlaced --verbose run generate-method-action-types", "get-release-tag": "ts-node --swc scripts/get-release-tag.ts", "install-chrome": "./scripts/install-chrome.sh", - "lint": "yarn lint:eslint && yarn lint:misc --check && yarn lint:tsconfig && yarn constraints && yarn lint:dependencies", + "lint": "yarn lint:eslint && yarn lint:misc --check && yarn lint:tsconfig && yarn constraints && yarn lint:dependencies && yarn generate-method-action-types --check", "lint:dependencies": "yarn workspaces foreach --all --parallel --verbose run lint:dependencies && yarn dedupe --check", "lint:eslint": "eslint . --cache", "lint:fix": "yarn workspaces foreach --all --parallel run lint:eslint --fix && yarn lint:misc --write && yarn lint:tsconfig && yarn constraints --fix && yarn dedupe", @@ -120,7 +121,8 @@ "tsx": "^4.20.3", "typescript": "~5.3.3", "typescript-eslint": "^8.6.0", - "vite": "^6.4.1" + "vite": "^6.4.1", + "yargs": "^17.7.1" }, "packageManager": "yarn@4.10.3", "engines": { diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index 320e4fa6e0..79c95c4809 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** All `SnapController` action types were renamed from `DoSomething` to `SnapControllerDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907)) + - `GetSnap` is now `SnapControllerGetSnapAction`. + - Note: The method is now called `getSnap` instead of `get`. + - `HandleSnapRequest` is now `SnapControllerHandleRequestAction`. + - `GetSnapState` is now `SnapControllerGetSnapStateAction`. + - `HasSnap` is now `SnapControllerHasSnapAction`. + - Note: The method is now called `hasSnap` instead of `has`. + - `UpdateSnapState` is now `SnapControllerUpdateSnapStateAction`. + - `ClearSnapState` is now `SnapControllerClearSnapStateAction`. + - `UpdateRegistry` is now `SnapControllerUpdateRegistryAction`. + - `EnableSnap` is now `SnapControllerEnableSnapAction`. + - Note: The method is now called `enableSnap` instead of `enable`. + - `DisableSnap` is now `SnapControllerDisableSnapAction`. + - Note: The method is now called `disableSnap` instead of `disable`. + - `RemoveSnap` is now `SnapControllerRemoveSnapAction`. + - Note: The method is now called `removeSnap` instead of `remove`. + - `GetPermittedSnaps` is now `SnapControllerGetPermittedSnapsAction`. + - Note: The method is now called `getPermittedSnaps` instead of `getPermitted`. + - `GetAllSnaps` is now `SnapControllerGetAllSnapsAction`. + - Note: The method is now called `getAllSnaps` instead of `getAll`. + - `GetRunnableSnaps` is now `SnapControllerGetRunnableSnapsAction`. + - `StopAllSnaps` is now `SnapControllerStopAllSnapsAction`. + - `InstallSnaps` is now `SnapControllerInstallSnapsAction`. + - Note: The method is now called `installSnaps` instead of `install`. + - `DisconnectOrigin` is now `SnapControllerDisconnectOriginAction`. + - Note: The method is now called `disconnectOrigin` instead of `removeSnapFromSubject`. + - `RevokeDynamicPermissions` is now `SnapControllerRevokeDynamicSnapPermissionsAction`. + - `GetSnapFile` is now `SnapControllerGetSnapFileAction`. + - `IsMinimumPlatformVersion` is now `SnapControllerIsMinimumPlatformVersionAction`. + - `SetClientActive` is now `SnapControllerSetClientActiveAction`. +- **BREAKING:** All `SnapController` event types were renamed from `OnSomething` to `SnapControllerOnSomethingEvent` ([#3907](https://github.com/MetaMask/snaps/pull/3907)) + - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. + - `SnapBlocked` is now `SnapControllerSnapBlockedEvent`. + - `SnapInstallStarted` is now `SnapControllerSnapInstallStartedEvent`. + - `SnapInstallFailed` is now `SnapControllerSnapInstallFailedEvent`. + - `SnapInstalled` is now `SnapControllerSnapInstalledEvent`. + - `SnapUninstalled` is now `SnapControllerSnapUninstalledEvent`. + - `SnapUnblocked` is now `SnapControllerSnapUnblockedEvent. + - `SnapUpdated` is now `SnapControllerSnapUpdatedEvent`. + - `SnapRolledback` is now `SnapControllerSnapRolledbackEvent`. + - `SnapTerminated` is now `SnapControllerSnapTerminatedEvent`. + - `SnapEnabled` is now `SnapControllerSnapEnabledEvent`. + - `SnapDisabled` is now `SnapControllerSnapDisabledEvent`. + +### Removed + +- **BREAKING:** `incrementActiveReferences` and `decrementActiveReferences` actions were removed ([#3907](https://github.com/MetaMask/snaps/pull/3907)) + ## [18.0.4] ### Fixed diff --git a/packages/snaps-controllers/package.json b/packages/snaps-controllers/package.json index f981211c60..73b90f4bc3 100644 --- a/packages/snaps-controllers/package.json +++ b/packages/snaps-controllers/package.json @@ -62,6 +62,7 @@ "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", "changelog:update": "../../scripts/update-changelog.sh @metamask/snaps-controllers", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/snaps-controllers", + "generate-method-action-types": "tsx ../../scripts/generate-method-action-types.mts", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn changelog:validate && yarn lint:dependencies", "lint:ci": "yarn lint", "lint:dependencies": "depcheck", diff --git a/packages/snaps-controllers/src/cronjob/CronjobController.test.ts b/packages/snaps-controllers/src/cronjob/CronjobController.test.ts index d682f05a70..8389151178 100644 --- a/packages/snaps-controllers/src/cronjob/CronjobController.test.ts +++ b/packages/snaps-controllers/src/cronjob/CronjobController.test.ts @@ -1139,7 +1139,7 @@ describe('CronjobController', () => { deriveStateFromMetadata( controller.state, controller.metadata, - 'anonymous', + 'includeInDebugSnapshot', ), ).toMatchInlineSnapshot(`{}`); }); diff --git a/packages/snaps-controllers/src/cronjob/CronjobController.ts b/packages/snaps-controllers/src/cronjob/CronjobController.ts index df12e9fbb7..c5914ca608 100644 --- a/packages/snaps-controllers/src/cronjob/CronjobController.ts +++ b/packages/snaps-controllers/src/cronjob/CronjobController.ts @@ -23,13 +23,13 @@ import { nanoid } from 'nanoid'; import { getCronjobSpecificationSchedule, getExecutionDate } from './utils'; import type { - HandleSnapRequest, - SnapDisabled, - SnapEnabled, - SnapInstalled, - SnapUninstalled, - SnapUpdated, -} from '..'; + SnapControllerHandleRequestAction, + SnapControllerSnapDisabledEvent, + SnapControllerSnapEnabledEvent, + SnapControllerSnapInstalledEvent, + SnapControllerSnapUninstalledEvent, + SnapControllerSnapUpdatedEvent, +} from '../snaps'; import { METAMASK_ORIGIN } from '../snaps/constants'; import { Timer } from '../snaps/Timer'; @@ -37,6 +37,7 @@ export type CronjobControllerGetStateAction = ControllerGetStateAction< typeof controllerName, CronjobControllerState >; + export type CronjobControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, CronjobControllerState @@ -68,7 +69,7 @@ export type Get = { export type CronjobControllerActions = | CronjobControllerGetStateAction - | HandleSnapRequest + | SnapControllerHandleRequestAction | GetPermissions | Schedule | Cancel @@ -77,11 +78,11 @@ export type CronjobControllerActions = export type CronjobControllerEvents = | CronjobControllerStateChangeEvent - | SnapInstalled - | SnapUninstalled - | SnapUpdated - | SnapEnabled - | SnapDisabled; + | SnapControllerSnapInstalledEvent + | SnapControllerSnapUninstalledEvent + | SnapControllerSnapUpdatedEvent + | SnapControllerSnapEnabledEvent + | SnapControllerSnapDisabledEvent; export type CronjobControllerMessenger = Messenger< typeof controllerName, diff --git a/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts index cf934e930f..5dfee842aa 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts @@ -31,7 +31,7 @@ describe('SnapInsightsController', () => { }, ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; }); @@ -157,7 +157,7 @@ describe('SnapInsightsController', () => { }, ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; }); @@ -285,7 +285,7 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; }); @@ -388,7 +388,7 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; }); @@ -456,7 +456,7 @@ describe('SnapInsightsController', () => { it('ignores insight if transaction has already been signed', async () => { const rootMessenger = getRootSnapInsightsControllerMessenger(); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; }); @@ -556,7 +556,7 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; }); @@ -661,7 +661,7 @@ describe('SnapInsightsController', () => { deriveStateFromMetadata( controller.state, controller.metadata, - 'anonymous', + 'includeInDebugSnapshot', ), ).toMatchInlineSnapshot(`{}`); }); diff --git a/packages/snaps-controllers/src/insights/SnapInsightsController.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.ts index 62287a4600..69dda216e8 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.ts @@ -19,7 +19,10 @@ import { HandlerType } from '@metamask/snaps-utils'; import { hasProperty, hexToBigInt } from '@metamask/utils'; import type { DeleteInterface } from '../interface'; -import type { GetAllSnaps, HandleSnapRequest } from '../snaps'; +import type { + SnapControllerGetAllSnapsAction, + SnapControllerHandleRequestAction, +} from '../snaps'; import { getRunnableSnaps } from '../snaps'; import type { TransactionControllerUnapprovedTransactionAddedEvent, @@ -33,8 +36,8 @@ import type { const controllerName = 'SnapInsightsController'; export type SnapInsightsControllerAllowedActions = - | HandleSnapRequest - | GetAllSnaps + | SnapControllerHandleRequestAction + | SnapControllerGetAllSnapsAction | GetPermissions | DeleteInterface; @@ -143,7 +146,7 @@ export class SnapInsightsController extends BaseController< * @returns A list of objects containing Snap IDs and the permission object. */ #getSnapsWithPermission(permissionName: string) { - const allSnaps = this.messenger.call('SnapController:getAll'); + const allSnaps = this.messenger.call('SnapController:getAllSnaps'); const filteredSnaps = getRunnableSnaps(allSnaps); return filteredSnaps.reduce((accumulator, snap) => { diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx index 0699bff4a8..ece1306f40 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx @@ -646,7 +646,7 @@ describe('SnapInterfaceController', () => { ); rootMessenger.registerActionHandler( - 'SnapController:get', + 'SnapController:getSnap', () => undefined, ); @@ -673,7 +673,7 @@ describe('SnapInterfaceController', () => { expect(controllerMessenger.call).toHaveBeenNthCalledWith( 1, - 'SnapController:get', + 'SnapController:getSnap', MOCK_SNAP_ID, ); }); @@ -2024,7 +2024,7 @@ describe('SnapInterfaceController', () => { deriveStateFromMetadata( controller.state, controller.metadata, - 'anonymous', + 'includeInDebugSnapshot', ), ).toMatchInlineSnapshot(`{}`); }); diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts index f80e17c7ad..574eccf619 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts @@ -40,7 +40,7 @@ import { isMatchingChainId, validateInterfaceContext, } from './utils'; -import type { GetSnap } from '../snaps'; +import type { SnapControllerGetSnapAction } from '../snaps'; const MAX_UI_CONTENT_SIZE = 10_000_000; // 10 mb @@ -125,7 +125,7 @@ export type SnapInterfaceControllerAllowedActions = | PhishingControllerTestOrigin | ApprovalControllerHasRequestAction | ApprovalControllerAcceptRequestAction - | GetSnap + | SnapControllerGetSnapAction | MultichainAssetsControllerGetStateAction | AccountsControllerGetSelectedMultichainAccountAction | AccountsControllerGetAccountByAddressAction @@ -598,7 +598,7 @@ export class SnapInterfaceController extends BaseController< * @returns The snap. */ #getSnap(id: string) { - return this.messenger.call('SnapController:get', id); + return this.messenger.call('SnapController:getSnap', id); } #hasPermission(snapId: SnapId, permission: string) { diff --git a/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts b/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts index 8988c4dee3..b0a0895edd 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts @@ -7,7 +7,7 @@ import { import { MultichainRouter } from './MultichainRouter'; import { METAMASK_ORIGIN } from '../snaps/constants'; import { - getRootMultichainRouterMessenger, + getMultichainRouterRootMessenger, getRestrictedMultichainRouterMessenger, BTC_CAIP2, BTC_CONNECTED_ACCOUNTS, @@ -22,7 +22,7 @@ import { describe('MultichainRouter', () => { describe('handleRequest', () => { it('can route signing requests to account Snaps without address resolution', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring({ submitRequest: jest.fn().mockResolvedValue({ @@ -71,7 +71,7 @@ describe('MultichainRouter', () => { }); it('can route signing requests to account Snaps using address resolution', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring({ submitRequest: jest.fn().mockResolvedValue({ @@ -123,7 +123,7 @@ describe('MultichainRouter', () => { }); it('disallows routing to unconnected accounts', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -166,7 +166,7 @@ describe('MultichainRouter', () => { }); it('can route protocol requests to protocol Snaps', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -181,7 +181,7 @@ describe('MultichainRouter', () => { () => [], ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap()]; }); @@ -237,7 +237,7 @@ describe('MultichainRouter', () => { }); it('throws if no suitable Snaps are found', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -252,7 +252,7 @@ describe('MultichainRouter', () => { () => [], ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return []; }); @@ -271,7 +271,7 @@ describe('MultichainRouter', () => { }); it('throws if address resolution fails', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -320,7 +320,7 @@ describe('MultichainRouter', () => { }); it('throws if address resolution returns an address that isnt available', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -372,7 +372,7 @@ describe('MultichainRouter', () => { }); it(`throws if address resolution returns a lower case address that isn't available`, async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -425,7 +425,7 @@ describe('MultichainRouter', () => { describe('getSupportedMethods', () => { it('returns a set of both protocol and account Snap methods', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -435,7 +435,7 @@ describe('MultichainRouter', () => { withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap()]; }); @@ -455,7 +455,7 @@ describe('MultichainRouter', () => { }); it('handles lack of protocol Snaps', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -465,7 +465,7 @@ describe('MultichainRouter', () => { withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap()]; }); @@ -485,7 +485,7 @@ describe('MultichainRouter', () => { }); it('handles lack of account Snaps', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -495,7 +495,7 @@ describe('MultichainRouter', () => { withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap()]; }); @@ -517,7 +517,7 @@ describe('MultichainRouter', () => { describe('getSupportedAccounts', () => { it('returns a set of accounts for the requested scope', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -542,7 +542,7 @@ describe('MultichainRouter', () => { describe('isSupportedScope', () => { it('returns true if an account Snap exists', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -552,7 +552,7 @@ describe('MultichainRouter', () => { withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap()]; }); @@ -572,7 +572,7 @@ describe('MultichainRouter', () => { }); it('returns true if a protocol Snap exists', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -582,7 +582,7 @@ describe('MultichainRouter', () => { withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return [getTruncatedSnap()]; }); @@ -602,7 +602,7 @@ describe('MultichainRouter', () => { }); it('returns false if no Snap is found', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); + const rootMessenger = getMultichainRouterRootMessenger(); const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); @@ -612,7 +612,7 @@ describe('MultichainRouter', () => { withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { + rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { return []; }); diff --git a/packages/snaps-controllers/src/multichain/MultichainRouter.ts b/packages/snaps-controllers/src/multichain/MultichainRouter.ts index 87df9a634e..fefa0aa408 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRouter.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRouter.ts @@ -22,8 +22,11 @@ import { } from '@metamask/utils'; import { nanoid } from 'nanoid'; +import type { + SnapControllerGetAllSnapsAction, + SnapControllerHandleRequestAction, +} from '../snaps'; import { getRunnableSnaps } from '../snaps'; -import type { GetAllSnaps, HandleSnapRequest } from '../snaps'; export type MultichainRouterHandleRequestAction = { type: `${typeof name}:handleRequest`; @@ -72,8 +75,8 @@ export type MultichainRouterActions = | MultichainRouterIsSupportedScopeAction; export type MultichainRouterAllowedActions = - | GetAllSnaps - | HandleSnapRequest + | SnapControllerGetAllSnapsAction + | SnapControllerHandleRequestAction | GetPermissions | AccountsControllerListMultichainAccountsAction; @@ -260,7 +263,7 @@ export class MultichainRouter { * @returns A list of all the protocol Snaps available and their RPC methods. */ #getProtocolSnaps(scope: CaipChainId) { - const allSnaps = this.#messenger.call('SnapController:getAll'); + const allSnaps = this.#messenger.call('SnapController:getAllSnaps'); const filteredSnaps = getRunnableSnaps(allSnaps); return filteredSnaps.reduce((accumulator, snap) => { diff --git a/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts b/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts index 223506acf5..9165986784 100644 --- a/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts +++ b/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts @@ -36,9 +36,6 @@ export class NodeThreadExecutionService extends AbstractExecutionService return Promise.resolve({ worker, stream }); } - // TODO: Either fix this lint violation or explain why it's necessary to - // ignore. - // eslint-disable-next-line @typescript-eslint/no-misused-promises protected async terminateJob( jobWrapper: TerminateJobArgs, ): Promise { diff --git a/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts b/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts index 251f7131b9..03d4fe531d 100644 --- a/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts +++ b/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts @@ -47,9 +47,6 @@ export class ProxyExecutionService extends AbstractExecutionService { * * @param job - The job to terminate. */ - // TODO: Either fix this lint violation or explain why it's necessary to - // ignore. - // eslint-disable-next-line @typescript-eslint/no-misused-promises protected async terminateJob(job: TerminateJobArgs) { // The `AbstractExecutionService` will have already closed the job stream, // so we write to the runtime stream directly. diff --git a/packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts b/packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts new file mode 100644 index 0000000000..8c2bcdfb26 --- /dev/null +++ b/packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts @@ -0,0 +1,335 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { SnapController } from './SnapController'; + +/** + * Initialise the SnapController. + * + * Currently this method sets up the controller and calls the `onStart` lifecycle hook for all + * runnable Snaps. + * + * @param waitForPlatform - Whether to wait for the platform to be ready before returning. + */ +export type SnapControllerInitAction = { + type: `SnapController:init`; + handler: SnapController['init']; +}; + +/** + * Trigger an update of the registry. + * + * As a side-effect of this, preinstalled Snaps may be updated and Snaps may be blocked/unblocked. + */ +export type SnapControllerUpdateRegistryAction = { + type: `SnapController:updateRegistry`; + handler: SnapController['updateRegistry']; +}; + +/** + * Enables the given snap. A snap can only be started if it is enabled. A snap + * can only be enabled if it isn't blocked. + * + * @param snapId - The id of the Snap to enable. + */ +export type SnapControllerEnableSnapAction = { + type: `SnapController:enableSnap`; + handler: SnapController['enableSnap']; +}; + +/** + * Disables the given snap. A snap can only be started if it is enabled. + * + * @param snapId - The id of the Snap to disable. + * @returns A promise that resolves once the snap has been disabled. + */ +export type SnapControllerDisableSnapAction = { + type: `SnapController:disableSnap`; + handler: SnapController['disableSnap']; +}; + +/** + * Stops the given snap, removes all hooks, closes all connections, and + * terminates its worker. + * + * @param snapId - The id of the Snap to stop. + * @param statusEvent - The Snap status event that caused the snap to be + * stopped. + */ +export type SnapControllerStopSnapAction = { + type: `SnapController:stopSnap`; + handler: SnapController['stopSnap']; +}; + +/** + * Stops all running snaps, removes all hooks, closes all connections, and + * terminates their workers. + * + * @param statusEvent - The Snap status event that caused the snap to be + * stopped. + */ +export type SnapControllerStopAllSnapsAction = { + type: `SnapController:stopAllSnaps`; + handler: SnapController['stopAllSnaps']; +}; + +/** + * Returns whether the given snap is running. + * Throws an error if the snap doesn't exist. + * + * @param snapId - The id of the Snap to check. + * @returns `true` if the snap is running, otherwise `false`. + */ +export type SnapControllerIsSnapRunningAction = { + type: `SnapController:isSnapRunning`; + handler: SnapController['isSnapRunning']; +}; + +/** + * Returns whether the given snap has been added to state. + * + * @param snapId - The id of the Snap to check for. + * @returns `true` if the snap exists in the controller state, otherwise `false`. + */ +export type SnapControllerHasSnapAction = { + type: `SnapController:hasSnap`; + handler: SnapController['hasSnap']; +}; + +/** + * Gets the snap with the given id if it exists, including all data. + * This should not be used if the snap is to be serializable, as e.g. + * the snap sourceCode may be quite large. + * + * @param snapId - The id of the Snap to get. + * @returns The entire snap object from the controller state. + */ +export type SnapControllerGetSnapAction = { + type: `SnapController:getSnap`; + handler: SnapController['getSnap']; +}; + +/** + * Updates the own state of the snap with the given id. + * This is distinct from the state MetaMask uses to manage snaps. + * + * @param snapId - The id of the Snap whose state should be updated. + * @param newSnapState - The new state of the snap. + * @param encrypted - A flag to indicate whether to use encrypted storage or not. + */ +export type SnapControllerUpdateSnapStateAction = { + type: `SnapController:updateSnapState`; + handler: SnapController['updateSnapState']; +}; + +/** + * Clears the state of the snap with the given id. + * This is distinct from the state MetaMask uses to manage snaps. + * + * @param snapId - The id of the Snap whose state should be cleared. + * @param encrypted - A flag to indicate whether to use encrypted storage or not. + */ +export type SnapControllerClearSnapStateAction = { + type: `SnapController:clearSnapState`; + handler: SnapController['clearSnapState']; +}; + +/** + * Gets the own state of the snap with the given id. + * This is distinct from the state MetaMask uses to manage snaps. + * + * @param snapId - The id of the Snap whose state to get. + * @param encrypted - A flag to indicate whether to use encrypted storage or not. + * @returns The requested snap state or null if no state exists. + */ +export type SnapControllerGetSnapStateAction = { + type: `SnapController:getSnapState`; + handler: SnapController['getSnapState']; +}; + +/** + * Gets a static auxiliary snap file in a chosen file encoding. + * + * @param snapId - The id of the Snap whose state to get. + * @param path - The path to the requested file. + * @param encoding - An optional requested file encoding. + * @returns The file requested in the chosen file encoding or null if the file is not found. + */ +export type SnapControllerGetSnapFileAction = { + type: `SnapController:getSnapFile`; + handler: SnapController['getSnapFile']; +}; + +/** + * Determine if a given Snap ID supports a given minimum version of the Snaps platform + * by inspecting the platformVersion in the Snap manifest. + * + * @param snapId - The Snap ID. + * @param version - The version. + * @returns True if the platform version is equal or greater to the passed version, false otherwise. + */ +export type SnapControllerIsMinimumPlatformVersionAction = { + type: `SnapController:isMinimumPlatformVersion`; + handler: SnapController['isMinimumPlatformVersion']; +}; + +/** + * Completely clear the controller's state: delete all associated data, + * handlers, event listeners, and permissions; tear down all snap providers. + * Also re-initializes the controller after clearing the state. + */ +export type SnapControllerClearStateAction = { + type: `SnapController:clearState`; + handler: SnapController['clearState']; +}; + +/** + * Removes the given snap from state, and clears all associated handlers + * and listeners. + * + * @param snapId - The id of the Snap. + * @returns A promise that resolves once the snap has been removed. + */ +export type SnapControllerRemoveSnapAction = { + type: `SnapController:removeSnap`; + handler: SnapController['removeSnap']; +}; + +/** + * Stops the given snaps, removes them from state, and clears all associated + * permissions, handlers, and listeners. + * + * @param snapIds - The ids of the Snaps. + */ +export type SnapControllerRemoveSnapsAction = { + type: `SnapController:removeSnaps`; + handler: SnapController['removeSnaps']; +}; + +/** + * Disconnect the Snap from the given origin, meaning the origin can no longer + * interact with the Snap until it is reconnected. + * + * @param origin - The origin from which to remove the Snap. + * @param snapId - The id of the snap to remove. + */ +export type SnapControllerDisconnectOriginAction = { + type: `SnapController:disconnectOrigin`; + handler: SnapController['disconnectOrigin']; +}; + +/** + * Checks if a list of permissions are dynamic and allowed to be revoked, if they are they will all be revoked. + * + * @param snapId - The snap ID. + * @param permissionNames - The names of the permissions. + * @throws If non-dynamic permissions are passed. + */ +export type SnapControllerRevokeDynamicSnapPermissionsAction = { + type: `SnapController:revokeDynamicSnapPermissions`; + handler: SnapController['revokeDynamicSnapPermissions']; +}; + +/** + * Gets all snaps in their truncated format. + * + * @returns All installed snaps in their truncated format. + */ +export type SnapControllerGetAllSnapsAction = { + type: `SnapController:getAllSnaps`; + handler: SnapController['getAllSnaps']; +}; + +/** + * Gets all runnable snaps. + * + * @returns All runnable snaps. + */ +export type SnapControllerGetRunnableSnapsAction = { + type: `SnapController:getRunnableSnaps`; + handler: SnapController['getRunnableSnaps']; +}; + +/** + * Gets the serialized permitted snaps of the given origin, if any. + * + * @param origin - The origin whose permitted snaps to retrieve. + * @returns The serialized permitted snaps for the origin. + */ +export type SnapControllerGetPermittedSnapsAction = { + type: `SnapController:getPermittedSnaps`; + handler: SnapController['getPermittedSnaps']; +}; + +/** + * Installs the snaps requested by the given origin, returning the snap + * object if the origin is permitted to install it, and an authorization error + * otherwise. + * + * @param origin - The origin that requested to install the snaps. + * @param requestedSnaps - The snaps to install. + * @returns An object of snap ids and snap objects, or errors if a + * snap couldn't be installed. + */ +export type SnapControllerInstallSnapsAction = { + type: `SnapController:installSnaps`; + handler: SnapController['installSnaps']; +}; + +/** + * Passes a JSON-RPC request object to the RPC handler function of a snap. + * + * @param options - A bag of options. + * @param options.snapId - The ID of the recipient snap. + * @param options.origin - The origin of the RPC request. + * @param options.handler - The handler to trigger on the snap for the request. + * @param options.request - The JSON-RPC request object. + * @returns The result of the JSON-RPC request. + */ +export type SnapControllerHandleRequestAction = { + type: `SnapController:handleRequest`; + handler: SnapController['handleRequest']; +}; + +/** + * Set the active state of the client. This will trigger the `onActive` or + * `onInactive` lifecycle hooks for all Snaps. + * + * @param active - A boolean indicating whether the client is active or not. + */ +export type SnapControllerSetClientActiveAction = { + type: `SnapController:setClientActive`; + handler: SnapController['setClientActive']; +}; + +/** + * Union of all SnapController action types. + */ +export type SnapControllerMethodActions = + | SnapControllerInitAction + | SnapControllerUpdateRegistryAction + | SnapControllerEnableSnapAction + | SnapControllerDisableSnapAction + | SnapControllerStopSnapAction + | SnapControllerStopAllSnapsAction + | SnapControllerIsSnapRunningAction + | SnapControllerHasSnapAction + | SnapControllerGetSnapAction + | SnapControllerUpdateSnapStateAction + | SnapControllerClearSnapStateAction + | SnapControllerGetSnapStateAction + | SnapControllerGetSnapFileAction + | SnapControllerIsMinimumPlatformVersionAction + | SnapControllerClearStateAction + | SnapControllerRemoveSnapAction + | SnapControllerRemoveSnapsAction + | SnapControllerDisconnectOriginAction + | SnapControllerRevokeDynamicSnapPermissionsAction + | SnapControllerGetAllSnapsAction + | SnapControllerGetRunnableSnapsAction + | SnapControllerGetPermittedSnapsAction + | SnapControllerInstallSnapsAction + | SnapControllerHandleRequestAction + | SnapControllerSetClientActiveAction; diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index cfc481a078..94ec443b68 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -106,7 +106,7 @@ import { approvalControllerMock, DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS, ExecutionEnvironmentStub, - getControllerMessenger, + getRootMessenger, getNodeEES, getNodeEESMessenger, getPersistedSnapsState, @@ -177,7 +177,7 @@ describe('SnapController', () => { }), ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); const result = await snapController.handleRequest({ @@ -198,7 +198,7 @@ describe('SnapController', () => { }); it('adds a snap and uses its JSON-RPC API', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const executionEnvironmentStub = new ExecutionEnvironmentStub( getNodeEESMessenger(rootMessenger), ) as unknown as NodeThreadExecutionService; @@ -213,7 +213,7 @@ describe('SnapController', () => { executionEnvironmentStub, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); const result = await snapController.handleRequest({ @@ -233,7 +233,7 @@ describe('SnapController', () => { }); it('passes endowments to a snap when executing it', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -252,7 +252,7 @@ describe('SnapController', () => { }, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); @@ -321,7 +321,7 @@ describe('SnapController', () => { const { rootMessenger } = options; const [snapController, service] = await getSnapControllerWithEES(options); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); // defer @@ -354,7 +354,7 @@ describe('SnapController', () => { }), ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); await snapController.handleRequest({ @@ -371,14 +371,14 @@ describe('SnapController', () => { await delay(100); - expect(snapController.isRunning(snap.id)).toBe(false); + expect(snapController.isSnapRunning(snap.id)).toBe(false); snapController.destroy(); await service.terminateAllSnaps(); }); it('terminates a snap even if connection to worker has failed', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const [snapController, service] = await getSnapControllerWithEES( getSnapControllerOptions({ @@ -398,7 +398,7 @@ describe('SnapController', () => { }, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); // @ts-expect-error `maxRequestTime` is a private property. @@ -440,7 +440,7 @@ describe('SnapController', () => { }), ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); expect(snapController.state.snaps[snap.id].status).toBe('running'); @@ -463,7 +463,7 @@ describe('SnapController', () => { }), ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); expect(snapController.state.snaps[snap.id].status).toBe('running'); @@ -489,7 +489,7 @@ describe('SnapController', () => { }); it('includes the initialConnections data in the approval requestState when installing a Snap', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -535,7 +535,7 @@ describe('SnapController', () => { }); it('includes the initialConnections data in the requestState when updating a Snap without pre-existing connections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -596,7 +596,7 @@ describe('SnapController', () => { }); it('includes the initialConnections data in the requestState when updating a Snap with pre-existing connections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -645,6 +645,7 @@ describe('SnapController', () => { state: { snaps: getPersistedSnapsState( getPersistedSnapObject({ + // @ts-expect-error: Partial mock. manifest: { initialConnections: { 'https://snaps.metamask.io': {}, @@ -693,7 +694,7 @@ describe('SnapController', () => { }); it('includes the initialConnections data in the requestState when updating a Snap with pre-existing connections where some are revoked', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); // Simulate all permissions being revoked. rootMessenger.registerActionHandler( @@ -723,6 +724,7 @@ describe('SnapController', () => { state: { snaps: getPersistedSnapsState( getPersistedSnapObject({ + // @ts-expect-error: Partial mock. manifest: { initialConnections: { 'https://snaps.metamask.io': {}, @@ -1045,7 +1047,7 @@ describe('SnapController', () => { }); it('throws an error if snap is not on allowlist and allowlisting is required but resolve succeeds', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const controller = await getSnapController( @@ -1074,7 +1076,7 @@ describe('SnapController', () => { }); it('throws an error if the registry is unavailable and allowlisting is required but resolve succeeds', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const controller = await getSnapController( @@ -1138,7 +1140,7 @@ describe('SnapController', () => { }); it('resolves to allowlisted version when allowlisting is required', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const { manifest, sourceCode, svgIcon } = @@ -1171,14 +1173,14 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '^1.0.0' }, }); - expect(controller.get(MOCK_SNAP_ID)?.version).toBe('1.1.0'); + expect(controller.getSnap(MOCK_SNAP_ID)?.version).toBe('1.1.0'); expect(registry.resolveVersion).toHaveBeenCalled(); controller.destroy(); }); it('does not use registry resolving when allowlist is not required', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const controller = await getSnapController( @@ -1211,7 +1213,7 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '>0.9.0 <1.1.0' }, }); - const newSnap = controller.get(MOCK_SNAP_ID); + const newSnap = controller.getSnap(MOCK_SNAP_ID); expect(newSnap).toStrictEqual(getSnapObject()); expect(options.messenger.call).toHaveBeenCalledTimes(1); @@ -1226,7 +1228,7 @@ describe('SnapController', () => { }); it('fails to install snap if user rejects installation', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, detectSnapLocation: loopbackDetect(), @@ -1297,7 +1299,7 @@ describe('SnapController', () => { MOCK_SNAP_ID, ); - expect(controller.get(MOCK_SNAP_ID)).toBeUndefined(); + expect(controller.getSnap(MOCK_SNAP_ID)).toBeUndefined(); expect(options.messenger.publish).not.toHaveBeenCalledWith( 'SnapController:snapUninstalled', @@ -1308,7 +1310,7 @@ describe('SnapController', () => { }); it('removes a snap that errors during installation after being added', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, detectSnapLocation: loopbackDetect(), @@ -1384,7 +1386,7 @@ describe('SnapController', () => { }), ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await expect( snapController.handleRequest({ @@ -1456,7 +1458,7 @@ describe('SnapController', () => { }); it('times out an RPC request that takes too long', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, idleTimeCheckInterval: 30000, @@ -1468,7 +1470,7 @@ describe('SnapController', () => { }); const snapController = await getSnapController(options); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); rootMessenger.registerActionHandler( 'ExecutionService:handleRpcRequest', @@ -1517,7 +1519,7 @@ describe('SnapController', () => { }); const snapController = await getSnapController(options); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); expect(snapController.state.snaps[snap.id].status).toBe('running'); @@ -1530,7 +1532,7 @@ describe('SnapController', () => { }); it('uses the execution timeout specified by the snap', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, idleTimeCheckInterval: 30000, @@ -1542,7 +1544,7 @@ describe('SnapController', () => { }); const snapController = await getSnapController(options); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); rootMessenger.registerActionHandler( 'ExecutionService:handleRpcRequest', @@ -1637,7 +1639,7 @@ describe('SnapController', () => { setupSnapProvider, ); const [snapController] = await getSnapControllerWithEES(options, service); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); expect(snapController.state.snaps[snap.id].status).toBe('running'); @@ -1712,7 +1714,7 @@ describe('SnapController', () => { setupSnapProvider, ); const [snapController] = await getSnapControllerWithEES(options, service); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); rootMessenger.registerActionHandler( 'PermissionController:hasPermission', @@ -1766,7 +1768,7 @@ describe('SnapController', () => { }); const [snapController] = await getSnapControllerWithEES(options); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); const results = (await Promise.allSettled([ snapController.handleRequest({ @@ -1830,7 +1832,7 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: {}, }); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect(snapController.state.snaps[snap.id].status).toBe('running'); @@ -1862,7 +1864,7 @@ describe('SnapController', () => { // This test also ensures that we do not throw "Premature close" it('throws if the execution environment fails', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, state: { snaps: getPersistedSnapsState() }, @@ -1933,7 +1935,7 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: {}, }); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect(snapController.state.snaps[snap.id].status).toBe('running'); @@ -1979,7 +1981,7 @@ describe('SnapController', () => { `, }); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const [snapController, service] = await getSnapControllerWithEES( getSnapControllerOptions({ maxRequestTime: 50, @@ -1997,14 +1999,14 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: {}, }); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect(snapController.state.snaps[snap.id].status).toBe('running'); // @ts-expect-error Accessing protected value. const originalTerminateFunction = service.terminateJob.bind(service); - let promise: Promise; + let promise: Promise = Promise.resolve(); // Cause a request at termination time. // @ts-expect-error Accessing protected value. @@ -2054,7 +2056,7 @@ describe('SnapController', () => { module.exports.onRpcRequest = () => 'foo bar'; `; - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2073,15 +2075,12 @@ describe('SnapController', () => { }); const [snapController, service] = await getSnapControllerWithEES(options); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); expect(snapController.state.snaps[snap.id].status).toBe('running'); - rootMessenger.call( - 'SnapController:incrementActiveReferences', - MOCK_SNAP_ID, - ); + snapController.incrementActiveReferences(snap.id); expect( await snapController.handleRequest({ @@ -2102,10 +2101,7 @@ describe('SnapController', () => { // Should still be running after idle timeout expect(snapController.state.snaps[snap.id].status).toBe('running'); - options.rootMessenger.call( - 'SnapController:decrementActiveReferences', - MOCK_SNAP_ID, - ); + snapController.decrementActiveReferences(snap.id); await sleep(100); @@ -2117,7 +2113,7 @@ describe('SnapController', () => { }); it(`shouldn't time out a long running snap on start up`, async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2134,7 +2130,7 @@ describe('SnapController', () => { async () => await sleep(300), ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); const startPromise = snapController.startSnap(snap.id); const timeoutPromise = sleep(50).then(() => true); @@ -2148,7 +2144,7 @@ describe('SnapController', () => { }); it('removes a snap that is stopped without errors', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2166,7 +2162,7 @@ describe('SnapController', () => { getNodeEESMessenger(options.rootMessenger), ) as unknown as NodeThreadExecutionService, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); rootMessenger.registerActionHandler( 'ExecutionService:handleRpcRequest', @@ -2218,7 +2214,7 @@ describe('SnapController', () => { }); it('clears encrypted state of Snaps when the client is locked', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const state = { myVariable: 1 }; @@ -2306,7 +2302,7 @@ describe('SnapController', () => { )( 'throws if the snap does not have permission for the handler', async (handler) => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2322,7 +2318,7 @@ describe('SnapController', () => { () => ({}), ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await expect( snapController.handleRequest({ snapId: snap.id, @@ -2341,7 +2337,7 @@ describe('SnapController', () => { ); it('does not throw if the snap uses a permitted handler', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2357,7 +2353,7 @@ describe('SnapController', () => { () => false, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2375,7 +2371,7 @@ describe('SnapController', () => { }); it('allows MetaMask to send a JSON-RPC request', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2408,7 +2404,7 @@ describe('SnapController', () => { () => undefined, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2422,7 +2418,7 @@ describe('SnapController', () => { }); it('allows MetaMask to send a keyring request', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2455,7 +2451,7 @@ describe('SnapController', () => { () => undefined, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2469,7 +2465,7 @@ describe('SnapController', () => { }); it('allows a website origin if it is in the `allowedOrigins` list', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2502,7 +2498,7 @@ describe('SnapController', () => { () => MOCK_DAPP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2516,7 +2512,7 @@ describe('SnapController', () => { }); it('allows a website origin if it is in the `allowedOrigins` list for keyring requests', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2549,7 +2545,7 @@ describe('SnapController', () => { () => MOCK_DAPP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2563,7 +2559,7 @@ describe('SnapController', () => { }); it('allows a website origin if `dapps` is `true`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2597,7 +2593,7 @@ describe('SnapController', () => { () => MOCK_DAPP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2611,7 +2607,7 @@ describe('SnapController', () => { }); it('allows a Snap origin if it is in the `allowedOrigins` list', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2644,7 +2640,7 @@ describe('SnapController', () => { () => MOCK_SNAP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2658,7 +2654,7 @@ describe('SnapController', () => { }); it('allows a Snap origin if `snaps` is `true`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2692,7 +2688,7 @@ describe('SnapController', () => { () => MOCK_SNAP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); expect( await snapController.handleRequest({ snapId: snap.id, @@ -2719,7 +2715,7 @@ describe('SnapController', () => { ])( 'throws if the origin is not in the `allowedOrigins` list (%p)', async (value: RpcOrigins) => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2750,7 +2746,7 @@ describe('SnapController', () => { () => MOCK_DAPP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await expect( snapController.handleRequest({ snapId: snap.id, @@ -2767,7 +2763,7 @@ describe('SnapController', () => { ); it('ensures onboarding has completed before processing requests', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const { promise, resolve } = createDeferredPromise(); const ensureOnboardingComplete = jest.fn().mockReturnValue(promise); @@ -2786,7 +2782,7 @@ describe('SnapController', () => { const initPromise = options.messenger.call('SnapController:init'); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); const requestPromise = snapController.handleRequest({ snapId: snap.id, @@ -2820,7 +2816,7 @@ describe('SnapController', () => { }); it('throws if the snap does not have permission to handle JSON-RPC requests from dapps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -2844,7 +2840,7 @@ describe('SnapController', () => { () => MOCK_DAPP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await expect( snapController.handleRequest({ snapId: snap.id, @@ -2860,7 +2856,7 @@ describe('SnapController', () => { }); it('throws if the snap does not have permission to handle JSON-RPC requests from snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -2884,7 +2880,7 @@ describe('SnapController', () => { () => MOCK_SNAP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await expect( snapController.handleRequest({ snapId: snap.id, @@ -2900,7 +2896,7 @@ describe('SnapController', () => { }); it('throws if the website origin is not in the `allowedOrigins` list for keyring requests', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -2933,7 +2929,7 @@ describe('SnapController', () => { () => MOCK_DAPP_SUBJECT_METADATA, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await expect( snapController.handleRequest({ snapId: snap.id, @@ -2949,7 +2945,7 @@ describe('SnapController', () => { }); it('injects context into onUserInput', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -3021,7 +3017,7 @@ describe('SnapController', () => { }); it('calls `SnapInterfaceController:setInterfaceDisplayed` if the response includes content', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -3078,7 +3074,7 @@ describe('SnapController', () => { }); it('throws if onTransaction handler returns a phishing link', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3143,7 +3139,7 @@ describe('SnapController', () => { }); it('throws if onTransaction returns an invalid value', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3204,7 +3200,7 @@ describe('SnapController', () => { }); it("doesn't throw if onTransaction return value is valid", async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3263,7 +3259,7 @@ describe('SnapController', () => { }); it('throws if onTransaction return value is an invalid id', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3317,7 +3313,7 @@ describe('SnapController', () => { }); it("doesn't throw if onTransaction return value is an id", async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3355,6 +3351,7 @@ describe('SnapController', () => { rootMessenger.registerActionHandler( 'SnapInterfaceController:getInterface', + // @ts-expect-error: Partial mock. () => ({ snapId: MOCK_SNAP_ID, content: foo, state: {} }), ); @@ -3376,7 +3373,7 @@ describe('SnapController', () => { }); it('throws if onSignature handler returns a phishing link', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3441,7 +3438,7 @@ describe('SnapController', () => { }); it('throws if onSignature returns an invalid value', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3502,7 +3499,7 @@ describe('SnapController', () => { }); it('throws if onSignature return value is an invalid id', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3556,7 +3553,7 @@ describe('SnapController', () => { }); it("doesn't throw if onSignature return value is valid", async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3616,7 +3613,7 @@ describe('SnapController', () => { }); it(`doesn't throw if onTransaction handler returns null`, async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3668,7 +3665,7 @@ describe('SnapController', () => { }); it(`doesn't throw if onSignature handler returns null`, async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3720,7 +3717,7 @@ describe('SnapController', () => { }); it('throws if onHomePage handler returns a phishing link', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3785,7 +3782,7 @@ describe('SnapController', () => { }); it('throws if onHomePage return value is an invalid id', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3839,7 +3836,7 @@ describe('SnapController', () => { }); it("doesn't throw if onHomePage return value is valid", async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3898,7 +3895,7 @@ describe('SnapController', () => { }); it('throws if onSettingsPage handler returns a phishing link', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -3963,7 +3960,7 @@ describe('SnapController', () => { }); it('throws if onSettingsPage return value is an invalid id', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4017,7 +4014,7 @@ describe('SnapController', () => { }); it("doesn't throw if onSettingsPage return value is valid", async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4076,7 +4073,7 @@ describe('SnapController', () => { }); it('throws if onNameLookup returns an invalid value', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4133,7 +4130,7 @@ describe('SnapController', () => { }); it("doesn't throw if onNameLookup return value is valid", async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4195,7 +4192,7 @@ describe('SnapController', () => { }); it(`doesn't throw if onNameLookup handler returns null`, async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4248,7 +4245,7 @@ describe('SnapController', () => { describe('onAssetsLookup', () => { it('throws if `onAssetsLookup` handler returns an invalid response', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4310,7 +4307,7 @@ describe('SnapController', () => { }); it('filters out assets that are out of scope for `onAssetsLookup`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4386,7 +4383,7 @@ describe('SnapController', () => { }); it('returns the value when `onAssetsLookup` returns a valid response for fungible assets', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4478,7 +4475,7 @@ describe('SnapController', () => { }); it('returns the value when `onAssetsLookup` returns a valid response for non-fungible assets', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4600,7 +4597,7 @@ describe('SnapController', () => { describe('onAssetsConversion', () => { it('throws if `onAssetsConversion` handler returns an invalid response', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4662,7 +4659,7 @@ describe('SnapController', () => { }); it('filters out assets that are out of scope for `onAssetsConversion`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4736,7 +4733,7 @@ describe('SnapController', () => { }); it('returns the value when `onAssetsConversion` returns a valid response', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4821,7 +4818,7 @@ describe('SnapController', () => { describe('onAssetsMarketData', () => { it('throws if `onAssetsMarketData` handler returns an invalid response', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4883,7 +4880,7 @@ describe('SnapController', () => { }); it('filters out assets that are out of scope for `onAssetsMarketData`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -4956,7 +4953,7 @@ describe('SnapController', () => { }); it('returns the value when `onAssetsMarketData` returns a valid response for fungible assets', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -5041,7 +5038,7 @@ describe('SnapController', () => { }); it('returns the value when `onAssetsMarketData` returns a valid response for non-fungible assets', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -5165,7 +5162,7 @@ describe('SnapController', () => { describe('onAssetHistoricalPrice', () => { it('throws if `onAssetHistoricalPrice` handler returns an invalid response', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -5227,7 +5224,7 @@ describe('SnapController', () => { }); it('returns the value when `onAssetHistoricalPrice` returns a valid response', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -5304,7 +5301,7 @@ describe('SnapController', () => { describe('onClientRequest', () => { it('returns the value when `onClientRequest` returns a valid response', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -5347,7 +5344,7 @@ describe('SnapController', () => { }); it('throws if the origin is not "metamask"', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapController = await getSnapController( getSnapControllerOptions({ @@ -5391,7 +5388,7 @@ describe('SnapController', () => { describe('getRpcRequestHandler', () => { it('handlers populate the "jsonrpc" property if missing', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -5640,7 +5637,7 @@ describe('SnapController', () => { }); it('reinstalls local snaps even if they are already installed (already stopped)', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapObject = getPersistedSnapObject({ id: MOCK_LOCAL_SNAP_ID, }); @@ -5780,7 +5777,7 @@ describe('SnapController', () => { }); it('reinstalls local snaps even if they are already installed (running)', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const version = '0.0.1'; const newVersion = '0.0.2'; @@ -5822,7 +5819,7 @@ describe('SnapController', () => { await snapController.installSnaps(MOCK_ORIGIN, { [MOCK_LOCAL_SNAP_ID]: {}, }); - expect(snapController.isRunning(MOCK_LOCAL_SNAP_ID)).toBe(true); + expect(snapController.isSnapRunning(MOCK_LOCAL_SNAP_ID)).toBe(true); const result = await snapController.installSnaps(MOCK_ORIGIN, { [MOCK_LOCAL_SNAP_ID]: {}, @@ -6022,7 +6019,7 @@ describe('SnapController', () => { }); it('does not get stuck when re-installing a local snap that fails to install', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const snapObject = getPersistedSnapObject({ id: MOCK_LOCAL_SNAP_ID, }); @@ -6133,7 +6130,7 @@ describe('SnapController', () => { }); it('grants connection permission to initialConnections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -6192,7 +6189,7 @@ describe('SnapController', () => { }); it('updates existing caveats to satisfy initialConnections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const initialConnections = { 'npm:filsnap': {}, @@ -6241,7 +6238,7 @@ describe('SnapController', () => { }); it('supports preinstalled snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); jest.spyOn(rootMessenger, 'publish'); @@ -6333,11 +6330,12 @@ describe('SnapController', () => { }); it('supports preinstalled snaps with two-way initial connections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', + // @ts-expect-error: Partial mock. (origin) => { if (origin === `${MOCK_SNAP_ID}2`) { return { @@ -6423,7 +6421,7 @@ describe('SnapController', () => { }); it('supports preinstalled snaps with initial connections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); // The snap should not have permission initially @@ -6507,7 +6505,7 @@ describe('SnapController', () => { }); it('supports preinstalled snaps when Snap installation is disabled', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); // The snap should not have permission initially @@ -6596,7 +6594,7 @@ describe('SnapController', () => { }); it('supports updating preinstalled snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); jest.spyOn(rootMessenger, 'publish'); @@ -6713,7 +6711,7 @@ describe('SnapController', () => { }); it('skips preinstalling a Snap if a newer version is already installed', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); const preinstalledSnaps = [ @@ -6746,7 +6744,7 @@ describe('SnapController', () => { }); it('supports localized preinstalled snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); // The snap should not have permission initially @@ -6854,7 +6852,7 @@ describe('SnapController', () => { }); it('disallows manual updates of preinstalled snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); // The snap should not have permissions initially @@ -6915,7 +6913,7 @@ describe('SnapController', () => { }); it('supports preinstalled Snaps specifying the hidden flag', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); // The snap should not have permission initially @@ -6963,13 +6961,13 @@ describe('SnapController', () => { snapControllerOptions, ); - expect(snapController.get(MOCK_SNAP_ID)?.hidden).toBe(true); + expect(snapController.getSnap(MOCK_SNAP_ID)?.hidden).toBe(true); snapController.destroy(); }); it('supports preinstalled Snaps specifying the hideSnapBranding flag', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); // The snap should not have permission initially @@ -7017,13 +7015,13 @@ describe('SnapController', () => { snapControllerOptions, ); - expect(snapController.get(MOCK_SNAP_ID)?.hideSnapBranding).toBe(true); + expect(snapController.getSnap(MOCK_SNAP_ID)?.hideSnapBranding).toBe(true); snapController.destroy(); }); it('recovers if preinstalled permissions are out of sync', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); const log = jest.spyOn(console, 'warn').mockImplementation(); @@ -7114,7 +7112,7 @@ describe('SnapController', () => { }); it('recovers if preinstalled permissions are out of sync when Snap has limited information', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); const log = jest.spyOn(console, 'warn').mockImplementation(); @@ -7204,7 +7202,7 @@ describe('SnapController', () => { }); it('supports onInstall for preinstalled Snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); rootMessenger.registerActionHandler( @@ -7270,7 +7268,7 @@ describe('SnapController', () => { }); it('recovers if preinstalled source code is missing', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); const log = jest.spyOn(console, 'warn').mockImplementation(); @@ -7337,7 +7335,7 @@ describe('SnapController', () => { }); it('supports onUpdate for preinstalled Snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); rootMessenger.registerActionHandler( @@ -7410,7 +7408,7 @@ describe('SnapController', () => { it('authorizes permissions needed for snaps', async () => { const manifest = getSnapManifest(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -7919,7 +7917,7 @@ describe('SnapController', () => { manifest: manifest.result, }); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -7999,7 +7997,7 @@ describe('SnapController', () => { manifest: manifest.result, }); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -8073,7 +8071,7 @@ describe('SnapController', () => { const newVersion = '1.0.2'; const newVersionRange = '>=1.0.1'; - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ manifest: getSnapManifest({ @@ -8425,7 +8423,7 @@ describe('SnapController', () => { }), ); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); let revokedConnection = false; @@ -8474,8 +8472,8 @@ describe('SnapController', () => { await controller.stopSnap(snapId1); await controller.stopSnap(snapId2); - expect(controller.get(snapId1)).toBeDefined(); - expect(controller.get(snapId2)).toBeDefined(); + expect(controller.getSnap(snapId1)).toBeDefined(); + expect(controller.getSnap(snapId2)).toBeDefined(); ( options.messenger.publish as jest.MockedFn< @@ -8493,11 +8491,11 @@ describe('SnapController', () => { expect(detect).toHaveBeenCalledTimes(5); - expect(controller.get(snapId3)).toBeUndefined(); - expect(controller.get(snapId1)?.manifest.version).toBe(oldVersion); - expect(controller.get(snapId2)?.manifest.version).toBe(oldVersion); - expect(controller.get(snapId1)?.status).toBe('stopped'); - expect(controller.get(snapId2)?.status).toBe('stopped'); + expect(controller.getSnap(snapId3)).toBeUndefined(); + expect(controller.getSnap(snapId1)?.manifest.version).toBe(oldVersion); + expect(controller.getSnap(snapId2)?.manifest.version).toBe(oldVersion); + expect(controller.getSnap(snapId1)?.status).toBe('stopped'); + expect(controller.getSnap(snapId2)?.status).toBe('stopped'); expect(options.messenger.publish).not.toHaveBeenCalledWith( 'SnapController:snapInstalled', @@ -8630,8 +8628,8 @@ describe('SnapController', () => { await controller.stopSnap(snapId1); await controller.stopSnap(snapId2); - expect(controller.get(snapId1)).toBeDefined(); - expect(controller.get(snapId2)).toBeDefined(); + expect(controller.getSnap(snapId1)).toBeDefined(); + expect(controller.getSnap(snapId2)).toBeDefined(); await expect( controller.installSnaps(MOCK_ORIGIN, { @@ -8645,9 +8643,9 @@ describe('SnapController', () => { expect(detect).toHaveBeenCalledTimes(4); - expect(controller.get(snapId3)).toBeUndefined(); - expect(controller.get(snapId1)?.manifest.version).toBe(oldVersion); - expect(controller.get(snapId2)?.manifest.version).toBe(oldVersion); + expect(controller.getSnap(snapId3)).toBeUndefined(); + expect(controller.getSnap(snapId1)?.manifest.version).toBe(oldVersion); + expect(controller.getSnap(snapId2)?.manifest.version).toBe(oldVersion); expect(listener).toHaveBeenCalledTimes(0); controller.destroy(); @@ -8780,12 +8778,12 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: {}, }); - const localSnap = snapController.getExpect(MOCK_LOCAL_SNAP_ID); + const localSnap = snapController.getSnapExpect(MOCK_LOCAL_SNAP_ID); expect(localSnap.preinstalled).toBe(true); expect(localSnap.hideSnapBranding).toBe(true); expect(localSnap.hidden).toBe(false); - const npmSnap = snapController.getExpect(MOCK_SNAP_ID); + const npmSnap = snapController.getSnapExpect(MOCK_SNAP_ID); expect(npmSnap.preinstalled).toBeUndefined(); expect(npmSnap.hideSnapBranding).toBeUndefined(); expect(npmSnap.hidden).toBeUndefined(); @@ -8819,7 +8817,7 @@ describe('SnapController', () => { ); const localSnapBeforeUpdate = - snapController.getExpect(MOCK_LOCAL_SNAP_ID); + snapController.getSnapExpect(MOCK_LOCAL_SNAP_ID); expect(localSnapBeforeUpdate.preinstalled).toBeUndefined(); expect(localSnapBeforeUpdate.hideSnapBranding).toBeUndefined(); expect(localSnapBeforeUpdate.hidden).toBeUndefined(); @@ -8828,7 +8826,7 @@ describe('SnapController', () => { [MOCK_LOCAL_SNAP_ID]: {}, }); - const localSnap = snapController.getExpect(MOCK_LOCAL_SNAP_ID); + const localSnap = snapController.getSnapExpect(MOCK_LOCAL_SNAP_ID); expect(localSnap.preinstalled).toBe(true); expect(localSnap.hideSnapBranding).toBe(true); expect(localSnap.hidden).toBe(false); @@ -8884,11 +8882,11 @@ describe('SnapController', () => { const controller = await getSnapController(options); const onSnapUpdated = jest.fn(); - const snap = controller.getExpect(MOCK_SNAP_ID); + const snap = controller.getSnapExpect(MOCK_SNAP_ID); options.messenger.subscribe('SnapController:snapUpdated', onSnapUpdated); - const newSnap = controller.get(MOCK_SNAP_ID); + const newSnap = controller.getSnap(MOCK_SNAP_ID); await expect( controller.installSnaps(MOCK_ORIGIN, { @@ -8904,7 +8902,7 @@ describe('SnapController', () => { }); it('throws an error if the new version of the snap is blocked', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ @@ -8959,13 +8957,13 @@ describe('SnapController', () => { const controller = await getSnapController(options); const onSnapUpdated = jest.fn(); - const snap = controller.getExpect(MOCK_SNAP_ID); + const snap = controller.getSnapExpect(MOCK_SNAP_ID); options.messenger.subscribe('SnapController:snapUpdated', onSnapUpdated); const publishSpy = jest.spyOn(options.messenger, 'publish'); - const newSnap = controller.get(MOCK_SNAP_ID); + const newSnap = controller.getSnap(MOCK_SNAP_ID); const errorMessage = `Snap "${MOCK_SNAP_ID}@${snap.version}" is already installed. Couldn't update to a version inside requested "0.9.0" range.`; @@ -9027,9 +9025,9 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.1.0' }, }); - const newSnapTruncated = controller.getTruncated(MOCK_SNAP_ID); + const newSnapTruncated = controller.getTruncatedSnap(MOCK_SNAP_ID); - const newSnap = controller.get(MOCK_SNAP_ID); + const newSnap = controller.getSnap(MOCK_SNAP_ID); expect(result).toStrictEqual({ [MOCK_SNAP_ID]: newSnapTruncated }); expect(newSnap?.version).toBe('1.1.0'); @@ -9168,7 +9166,7 @@ describe('SnapController', () => { }), ); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -9283,7 +9281,7 @@ describe('SnapController', () => { }), ); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -9410,9 +9408,9 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.1.0' }, }); - const newSnapTruncated = controller.getTruncated(MOCK_SNAP_ID); + const newSnapTruncated = controller.getTruncatedSnap(MOCK_SNAP_ID); - const newSnap = controller.get(MOCK_SNAP_ID); + const newSnap = controller.getSnap(MOCK_SNAP_ID); expect(result).toStrictEqual({ [MOCK_SNAP_ID]: newSnapTruncated }); expect(newSnap?.version).toBe('1.1.0'); @@ -9460,7 +9458,7 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.1.0' }, }); - const isRunning = controller.isRunning(MOCK_SNAP_ID); + const isRunning = controller.isSnapRunning(MOCK_SNAP_ID); expect(callActionSpy).toHaveBeenCalledTimes(15); @@ -9562,7 +9560,7 @@ describe('SnapController', () => { }); it('throws on update request denied', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ manifest: getSnapManifest({ version: '1.1.0' as SemVerVersion, @@ -9622,7 +9620,7 @@ describe('SnapController', () => { }), ).rejects.toThrow('User rejected the request.'); - const newSnap = controller.get(MOCK_SNAP_ID); + const newSnap = controller.getSnap(MOCK_SNAP_ID); expect(newSnap?.version).toBe('1.0.0'); expect(callActionSpy).toHaveBeenCalledTimes(6); @@ -9678,7 +9676,7 @@ describe('SnapController', () => { }); it('requests approval for new and already approved permissions and revoke unused permissions', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); /* eslint-disable @typescript-eslint/naming-convention */ const initialPermissions = { @@ -9921,7 +9919,7 @@ describe('SnapController', () => { }); it('supports initialConnections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -10037,7 +10035,7 @@ describe('SnapController', () => { it('assigns the same id to the approval request and the request metadata', async () => { expect.assertions(4); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); /* eslint-disable @typescript-eslint/naming-convention */ const initialPermissions = { @@ -10191,7 +10189,7 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.2.0' }, }); - const newSnap = controller.get(MOCK_SNAP_ID); + const newSnap = controller.getSnap(MOCK_SNAP_ID); expect(newSnap?.version).toBe('1.2.0'); controller.destroy(); @@ -10410,10 +10408,10 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - expect(snapController.get(MOCK_SNAP_ID)?.enabled).toBe(false); + expect(snapController.getSnap(MOCK_SNAP_ID)?.enabled).toBe(false); snapController.enableSnap(MOCK_SNAP_ID); - expect(snapController.get(MOCK_SNAP_ID)?.enabled).toBe(true); + expect(snapController.getSnap(MOCK_SNAP_ID)?.enabled).toBe(true); expect(options.messenger.publish).toHaveBeenCalledWith( 'SnapController:snapEnabled', getTruncatedSnap(), @@ -10460,10 +10458,10 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - expect(snapController.get(MOCK_SNAP_ID)?.enabled).toBe(true); + expect(snapController.getSnap(MOCK_SNAP_ID)?.enabled).toBe(true); await snapController.disableSnap(MOCK_SNAP_ID); - expect(snapController.get(MOCK_SNAP_ID)?.enabled).toBe(false); + expect(snapController.getSnap(MOCK_SNAP_ID)?.enabled).toBe(false); expect(options.messenger.publish).toHaveBeenCalledWith( 'SnapController:snapDisabled', getTruncatedSnap({ enabled: false }), @@ -10481,14 +10479,14 @@ describe('SnapController', () => { }), ); - expect(snapController.get(MOCK_SNAP_ID)?.enabled).toBe(true); + expect(snapController.getSnap(MOCK_SNAP_ID)?.enabled).toBe(true); await snapController.startSnap(MOCK_SNAP_ID); - expect(snapController.isRunning(MOCK_SNAP_ID)).toBe(true); + expect(snapController.isSnapRunning(MOCK_SNAP_ID)).toBe(true); await snapController.disableSnap(MOCK_SNAP_ID); - expect(snapController.get(MOCK_SNAP_ID)?.enabled).toBe(false); - expect(snapController.isRunning(MOCK_SNAP_ID)).toBe(false); + expect(snapController.getSnap(MOCK_SNAP_ID)?.enabled).toBe(false); + expect(snapController.isSnapRunning(MOCK_SNAP_ID)).toBe(false); snapController.destroy(); }); @@ -10505,7 +10503,7 @@ describe('SnapController', () => { describe('updateRegistry', () => { it('updates the registry database', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const snapController = await getSnapController( @@ -10524,7 +10522,7 @@ describe('SnapController', () => { }); it('blocks snaps as expected', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const mockSnapA = getMockSnapData({ @@ -10575,12 +10573,12 @@ describe('SnapController', () => { }); // A is blocked and disabled - expect(snapController.get(mockSnapA.id)?.blocked).toBe(true); - expect(snapController.get(mockSnapA.id)?.enabled).toBe(false); + expect(snapController.getSnap(mockSnapA.id)?.blocked).toBe(true); + expect(snapController.getSnap(mockSnapA.id)?.enabled).toBe(false); // B is unblocked and enabled - expect(snapController.get(mockSnapB.id)?.blocked).toBe(false); - expect(snapController.get(mockSnapB.id)?.enabled).toBe(true); + expect(snapController.getSnap(mockSnapB.id)?.blocked).toBe(false); + expect(snapController.getSnap(mockSnapB.id)?.enabled).toBe(true); expect(publishMock).toHaveBeenLastCalledWith( 'SnapController:snapBlocked', @@ -10595,7 +10593,7 @@ describe('SnapController', () => { }); it('stops running snaps when they are blocked', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const mockSnap = getMockSnapData({ @@ -10622,15 +10620,15 @@ describe('SnapController', () => { await waitForStateChange(options.messenger); // The snap is blocked, disabled, and stopped - expect(snapController.get(mockSnap.id)?.blocked).toBe(true); - expect(snapController.get(mockSnap.id)?.enabled).toBe(false); - expect(snapController.isRunning(mockSnap.id)).toBe(false); + expect(snapController.getSnap(mockSnap.id)?.blocked).toBe(true); + expect(snapController.getSnap(mockSnap.id)?.enabled).toBe(false); + expect(snapController.isSnapRunning(mockSnap.id)).toBe(false); snapController.destroy(); }); it('unblocks snaps as expected', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const mockSnapA = getMockSnapData({ @@ -10660,12 +10658,12 @@ describe('SnapController', () => { const snapController = await getSnapController(options); // A is blocked and disabled - expect(snapController.get(mockSnapA.id)?.blocked).toBe(true); - expect(snapController.get(mockSnapA.id)?.enabled).toBe(false); + expect(snapController.getSnap(mockSnapA.id)?.blocked).toBe(true); + expect(snapController.getSnap(mockSnapA.id)?.enabled).toBe(false); // B is unblocked and enabled - expect(snapController.get(mockSnapB.id)?.blocked).toBe(false); - expect(snapController.get(mockSnapB.id)?.enabled).toBe(true); + expect(snapController.getSnap(mockSnapB.id)?.blocked).toBe(false); + expect(snapController.getSnap(mockSnapB.id)?.enabled).toBe(true); // Indicate that both snaps A and B are unblocked, and update blocked // states. @@ -10676,12 +10674,12 @@ describe('SnapController', () => { await snapController.updateRegistry(); // A is unblocked, but still disabled - expect(snapController.get(mockSnapA.id)?.blocked).toBe(false); - expect(snapController.get(mockSnapA.id)?.enabled).toBe(false); + expect(snapController.getSnap(mockSnapA.id)?.blocked).toBe(false); + expect(snapController.getSnap(mockSnapA.id)?.enabled).toBe(false); // B remains unblocked and enabled - expect(snapController.get(mockSnapB.id)?.blocked).toBe(false); - expect(snapController.get(mockSnapB.id)?.enabled).toBe(true); + expect(snapController.getSnap(mockSnapB.id)?.blocked).toBe(false); + expect(snapController.getSnap(mockSnapB.id)?.enabled).toBe(true); expect(publishMock).toHaveBeenLastCalledWith( 'SnapController:snapUnblocked', @@ -10693,7 +10691,7 @@ describe('SnapController', () => { it('updating blocked snaps does not throw if a snap is removed while fetching the blocklist', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const mockSnap = getMockSnapData({ @@ -10728,7 +10726,7 @@ describe('SnapController', () => { await updateBlockList; // The snap was removed, no errors were thrown - expect(snapController.has(mockSnap.id)).toBe(false); + expect(snapController.hasSnap(mockSnap.id)).toBe(false); expect(consoleErrorSpy).not.toHaveBeenCalled(); snapController.destroy(); @@ -10736,7 +10734,7 @@ describe('SnapController', () => { it('logs but does not throw unexpected errors while blocking', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const mockSnap = getMockSnapData({ @@ -10766,8 +10764,8 @@ describe('SnapController', () => { await snapController.updateRegistry(); // A is blocked and disabled - expect(snapController.get(mockSnap.id)?.blocked).toBe(true); - expect(snapController.get(mockSnap.id)?.enabled).toBe(false); + expect(snapController.getSnap(mockSnap.id)?.blocked).toBe(true); + expect(snapController.getSnap(mockSnap.id)?.enabled).toBe(false); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); expect(consoleErrorSpy).toHaveBeenCalledWith( @@ -10779,7 +10777,7 @@ describe('SnapController', () => { }); it('updates preinstalled Snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); // Simulate previous permissions, some of which will be removed @@ -10834,7 +10832,7 @@ describe('SnapController', () => { await waitForStateChange(options.messenger); await sleep(100); - const updatedSnap = snapController.get(snapId); + const updatedSnap = snapController.getSnap(snapId); assert(updatedSnap); expect(updatedSnap.version).toStrictEqual(updateVersion); @@ -10875,7 +10873,7 @@ describe('SnapController', () => { }); it('does not update preinstalled Snaps when the feature flag is off', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const registry = new MockSnapsRegistry(rootMessenger); const snapId = 'npm:@metamask/jsx-example-snap' as SnapId; @@ -10903,7 +10901,7 @@ describe('SnapController', () => { await snapController.updateRegistry(); - const snap = snapController.get(snapId); + const snap = snapController.getSnap(snapId); assert(snap); expect(snap.version).toStrictEqual(mockSnap.version); @@ -10940,7 +10938,7 @@ describe('SnapController', () => { const callActionSpy = jest.spyOn(messenger, 'call'); - expect(snapController.has(MOCK_SNAP_ID)).toBe(true); + expect(snapController.hasSnap(MOCK_SNAP_ID)).toBe(true); const requestPromise = snapController.handleRequest({ snapId: MOCK_SNAP_ID, @@ -10953,13 +10951,13 @@ describe('SnapController', () => { await waitForStateChange(messenger); - expect(snapController.isRunning(MOCK_SNAP_ID)).toBe(true); + expect(snapController.isSnapRunning(MOCK_SNAP_ID)).toBe(true); await new Promise((resolve) => setTimeout(resolve, 100)); await snapController.clearState(); - expect(snapController.has(MOCK_SNAP_ID)).toBe(false); + expect(snapController.hasSnap(MOCK_SNAP_ID)).toBe(false); expect(callActionSpy).toHaveBeenCalledWith( 'ExecutionService:terminateSnap', @@ -11010,7 +11008,7 @@ describe('SnapController', () => { }, ]; - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:revokeAllPermissions', @@ -11050,16 +11048,16 @@ describe('SnapController', () => { const callActionSpy = jest.spyOn(options.messenger, 'call'); - expect(snapController.has(MOCK_SNAP_ID)).toBe(true); - expect(snapController.has(preinstalledSnapId)).toBe(true); + expect(snapController.hasSnap(MOCK_SNAP_ID)).toBe(true); + expect(snapController.hasSnap(preinstalledSnapId)).toBe(true); await snapController.startSnap(MOCK_SNAP_ID); await snapController.startSnap(preinstalledSnapId); await snapController.clearState(); - expect(snapController.has(MOCK_SNAP_ID)).toBe(false); - expect(snapController.has(preinstalledSnapId)).toBe(true); + expect(snapController.hasSnap(MOCK_SNAP_ID)).toBe(false); + expect(snapController.hasSnap(preinstalledSnapId)).toBe(true); expect(callActionSpy).toHaveBeenCalledWith( 'ExecutionService:terminateSnap', @@ -11104,7 +11102,7 @@ describe('SnapController', () => { describe('SnapController actions', () => { describe('SnapController:init', () => { it('populates `isReady`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -11122,15 +11120,13 @@ describe('SnapController', () => { }); it('calls `onStart` for all Snaps with the `endowment:lifecycle-hooks` permission', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', ( origin, - ): SubjectPermissions< - ValidPermission - > => { + ): SubjectPermissions> => { if (origin === MOCK_SNAP_ID) { return { [SnapEndowments.LifecycleHooks]: @@ -11205,7 +11201,7 @@ describe('SnapController', () => { .spyOn(console, 'error') .mockImplementation(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:hasPermission', @@ -11261,9 +11257,9 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - const getSpy = jest.spyOn(snapController, 'get'); + const getSpy = jest.spyOn(snapController, 'getSnap'); const result = options.messenger.call( - 'SnapController:get', + 'SnapController:getSnap', MOCK_SNAP_ID, ); @@ -11303,7 +11299,7 @@ describe('SnapController', () => { it('should track event for allowed handler', async () => { const mockTrackEvent = jest.fn(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const executionEnvironmentStub = new ExecutionEnvironmentStub( getNodeEESMessenger(rootMessenger), ) as unknown as NodeThreadExecutionService; @@ -11319,7 +11315,7 @@ describe('SnapController', () => { executionEnvironmentStub, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); await snapController.handleRequest({ @@ -11353,7 +11349,7 @@ describe('SnapController', () => { it('should not track event for disallowed handler', async () => { const mockTrackEvent = jest.fn(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -11386,7 +11382,7 @@ describe('SnapController', () => { executionEnvironmentStub, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); await snapController.handleRequest({ @@ -11411,7 +11407,7 @@ describe('SnapController', () => { const mockTrackEvent = jest.fn().mockImplementation(() => { throw error; }); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const executionEnvironmentStub = new ExecutionEnvironmentStub( getNodeEESMessenger(rootMessenger), ) as unknown as NodeThreadExecutionService; @@ -11427,7 +11423,7 @@ describe('SnapController', () => { executionEnvironmentStub, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); await snapController.handleRequest({ @@ -11453,7 +11449,7 @@ describe('SnapController', () => { it('should not track event for preinstalled snap', async () => { const mockTrackEvent = jest.fn(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const executionEnvironmentStub = new ExecutionEnvironmentStub( getNodeEESMessenger(rootMessenger), ) as unknown as NodeThreadExecutionService; @@ -11471,7 +11467,7 @@ describe('SnapController', () => { executionEnvironmentStub, ); - const snap = snapController.getExpect(MOCK_SNAP_ID); + const snap = snapController.getSnapExpect(MOCK_SNAP_ID); await snapController.startSnap(snap.id); await snapController.handleRequest({ @@ -11888,8 +11884,8 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - const hasSpy = jest.spyOn(snapController, 'has'); - const result = options.messenger.call('SnapController:has', id); + const hasSpy = jest.spyOn(snapController, 'hasSnap'); + const result = options.messenger.call('SnapController:hasSnap', id); expect(hasSpy).toHaveBeenCalledTimes(1); expect(result).toBe(true); @@ -12267,7 +12263,7 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - options.messenger.call('SnapController:enable', mockSnap.id); + options.messenger.call('SnapController:enableSnap', mockSnap.id); expect(snapController.state.snaps[mockSnap.id].enabled).toBe(true); snapController.destroy(); @@ -12290,7 +12286,7 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - await options.messenger.call('SnapController:disable', mockSnap.id); + await options.messenger.call('SnapController:disableSnap', mockSnap.id); expect(snapController.state.snaps[mockSnap.id].enabled).toBe(false); snapController.destroy(); @@ -12313,7 +12309,7 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - await options.messenger.call('SnapController:remove', mockSnap.id); + await options.messenger.call('SnapController:removeSnap', mockSnap.id); expect(snapController.state.snaps[mockSnap.id]).toBeUndefined(); snapController.destroy(); @@ -12322,7 +12318,7 @@ describe('SnapController', () => { describe('SnapController:getPermitted', () => { it('calls SnapController.getPermittedSnaps()', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const mockSnap = getMockSnapData({ id: MOCK_SNAP_ID, origin: MOCK_ORIGIN, @@ -12338,7 +12334,7 @@ describe('SnapController', () => { const snapController = await getSnapController(options); const result = options.messenger.call( - 'SnapController:getPermitted', + 'SnapController:getPermittedSnaps', mockSnap.origin, ); expect(result).toStrictEqual({ @@ -12364,7 +12360,7 @@ describe('SnapController', () => { const snapController = await getSnapController(options); - const result = options.messenger.call('SnapController:getAll'); + const result = options.messenger.call('SnapController:getAllSnaps'); expect(result).toStrictEqual([getTruncatedSnap()]); snapController.destroy(); @@ -12412,7 +12408,7 @@ describe('SnapController', () => { .mockImplementation(); const snaps = { [MOCK_SNAP_ID]: {} }; - await options.messenger.call('SnapController:install', 'foo', snaps); + await options.messenger.call('SnapController:installSnaps', 'foo', snaps); expect(installSnapsSpy).toHaveBeenCalledTimes(1); expect(installSnapsSpy).toHaveBeenCalledWith('foo', snaps); @@ -12421,7 +12417,7 @@ describe('SnapController', () => { }); describe('SnapController:disconnectOrigin', () => { - it('calls SnapController.removeSnapFromSubject()', async () => { + it('calls SnapController.disconnectOrigin()', async () => { const permittedSnaps = [ MOCK_SNAP_ID, MOCK_LOCAL_SNAP_ID, @@ -12445,7 +12441,7 @@ describe('SnapController', () => { const removeSnapFromSubjectSpy = jest.spyOn( snapController, - 'removeSnapFromSubject', + 'disconnectOrigin', ); const callActionSpy = jest.spyOn(options.messenger, 'call'); @@ -12487,7 +12483,7 @@ describe('SnapController', () => { const callActionSpy = jest.spyOn(options.messenger, 'call'); options.messenger.call( - 'SnapController:revokeDynamicPermissions', + 'SnapController:revokeDynamicSnapPermissions', MOCK_SNAP_ID, ['endowment:caip25'], ); @@ -12507,7 +12503,7 @@ describe('SnapController', () => { expect(() => options.messenger.call( - 'SnapController:revokeDynamicPermissions', + 'SnapController:revokeDynamicSnapPermissions', MOCK_SNAP_ID, ['snap_notify'], ), @@ -12545,7 +12541,7 @@ describe('SnapController', () => { expect( await options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './src/foo.json', ), @@ -12595,7 +12591,7 @@ describe('SnapController', () => { expect( await options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './src/foo.json', AuxiliaryFileEncoding.Hex, @@ -12621,7 +12617,7 @@ describe('SnapController', () => { expect( await options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './foo.json', ), @@ -12668,7 +12664,7 @@ describe('SnapController', () => { await expect( options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './src/foo.json', AuxiliaryFileEncoding.Hex, @@ -12683,7 +12679,7 @@ describe('SnapController', () => { describe('SnapController:snapInstalled', () => { it('calls the `onInstall` lifecycle hook', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -12714,6 +12710,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12750,7 +12747,7 @@ describe('SnapController', () => { }); it('does not call the `onInstall` lifecycle hook if the snap does not have the `endowment:lifecycle-hooks` permission', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -12770,6 +12767,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12795,7 +12793,7 @@ describe('SnapController', () => { it('logs an error if the lifecycle hook call fails', async () => { const log = jest.spyOn(console, 'error').mockImplementation(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -12818,6 +12816,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12832,7 +12831,7 @@ describe('SnapController', () => { describe('SnapController:snapUpdated', () => { it('calls the `onUpdate` lifecycle hook', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -12864,6 +12863,7 @@ describe('SnapController', () => { getTruncatedSnap(), '0.9.0', MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12900,7 +12900,7 @@ describe('SnapController', () => { }); it('does not call the `onUpdate` lifecycle hook if the snap does not have the `endowment:lifecycle-hooks` permission', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -12920,6 +12920,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12945,7 +12946,7 @@ describe('SnapController', () => { it('logs an error if the lifecycle hook call fails', async () => { const log = jest.spyOn(console, 'error').mockImplementation(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -12969,6 +12970,7 @@ describe('SnapController', () => { getTruncatedSnap(), '0.9.0', MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -13112,7 +13114,7 @@ describe('SnapController', () => { describe('SnapController:setClientActive', () => { it('calls the `onActive` lifecycle hook for all Snaps when called with `true`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:hasPermission', @@ -13199,7 +13201,7 @@ describe('SnapController', () => { }); it('calls the `onInactive` lifecycle hook for all Snaps when called with `false`', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:hasPermission', @@ -13445,11 +13447,11 @@ describe('SnapController', () => { // create a new controller const secondSnapController = await getSnapController(options); - expect(secondSnapController.isRunning(id)).toBe(false); + expect(secondSnapController.isSnapRunning(id)).toBe(false); await secondSnapController.startSnap(id); expect(secondSnapController.state.snaps[id]).toBeDefined(); - expect(secondSnapController.isRunning(id)).toBe(true); + expect(secondSnapController.isSnapRunning(id)).toBe(true); firstSnapController.destroy(); secondSnapController.destroy(); }); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 344fe8c012..af4aa5e5da 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -171,6 +171,7 @@ import type { } from './registry'; import { SnapsRegistryStatus } from './registry'; import { getRunnableSnaps } from './selectors'; +import type { SnapControllerMethodActions } from './SnapController-method-action-types'; import { Timer } from './Timer'; import { forceStrict, validateMachine } from '../fsm'; import type { @@ -205,7 +206,34 @@ import { export const controllerName = 'SnapController'; -// TODO: Figure out how to name these +export const MESSENGER_EXPOSED_METHODS = [ + 'init', + 'updateRegistry', + 'enableSnap', + 'disableSnap', + 'stopSnap', + 'stopAllSnaps', + 'isSnapRunning', + 'hasSnap', + 'getSnap', + 'updateSnapState', + 'clearSnapState', + 'getSnapState', + 'getSnapFile', + 'isMinimumPlatformVersion', + 'clearState', + 'removeSnap', + 'removeSnaps', + 'disconnectOrigin', + 'revokeDynamicSnapPermissions', + 'getAllSnaps', + 'getRunnableSnaps', + 'getPermittedSnaps', + 'installSnaps', + 'handleRequest', + 'setClientActive', +] as const; + export const SNAP_APPROVAL_INSTALL = 'wallet_installSnap'; export const SNAP_APPROVAL_UPDATE = 'wallet_updateSnap'; export const SNAP_APPROVAL_RESULT = 'wallet_installSnapResult'; @@ -318,12 +346,6 @@ export type SnapRuntimeData = { getStateMutex: Mutex; }; -export type SnapError = { - message: string; - code: number; - data?: Json; -}; - // Types that probably should be defined elsewhere in prod type StoredSnaps = Record; @@ -359,188 +381,21 @@ type PendingApproval = { // Controller Messenger Actions -/** - * Initialise the SnapController. This should be called after all controllers - * are created. - */ -export type SnapControllerInitAction = { - type: `${typeof controllerName}:init`; - handler: SnapController['init']; -}; - -/** - * Gets the specified Snap from state. - */ -export type GetSnap = { - type: `${typeof controllerName}:get`; - handler: SnapController['get']; -}; - -/** - * Handles sending an inbound request to a snap and returns its result. - */ -export type HandleSnapRequest = { - type: `${typeof controllerName}:handleRequest`; - handler: SnapController['handleRequest']; -}; - -/** - * Gets the specified Snap's persisted state. - */ -export type GetSnapState = { - type: `${typeof controllerName}:getSnapState`; - handler: SnapController['getSnapState']; -}; - -/** - * Checks if the specified snap exists in state. - */ -export type HasSnap = { - type: `${typeof controllerName}:has`; - handler: SnapController['has']; -}; - -/** - * Updates the specified Snap's persisted state. - */ -export type UpdateSnapState = { - type: `${typeof controllerName}:updateSnapState`; - handler: SnapController['updateSnapState']; -}; - -/** - * Clears the specified Snap's persisted state. - */ -export type ClearSnapState = { - type: `${typeof controllerName}:clearSnapState`; - handler: SnapController['clearSnapState']; -}; - -/** - * Checks all installed snaps against the blocklist. - */ -export type UpdateRegistry = { - type: `${typeof controllerName}:updateRegistry`; - handler: SnapController['updateRegistry']; -}; - -export type EnableSnap = { - type: `${typeof controllerName}:enable`; - handler: SnapController['enableSnap']; -}; - -export type DisableSnap = { - type: `${typeof controllerName}:disable`; - handler: SnapController['disableSnap']; -}; - -export type RemoveSnap = { - type: `${typeof controllerName}:remove`; - handler: SnapController['removeSnap']; -}; - -export type GetPermittedSnaps = { - type: `${typeof controllerName}:getPermitted`; - handler: SnapController['getPermittedSnaps']; -}; - -export type GetAllSnaps = { - type: `${typeof controllerName}:getAll`; - handler: SnapController['getAllSnaps']; -}; - -export type GetRunnableSnaps = { - type: `${typeof controllerName}:getRunnableSnaps`; - handler: SnapController['getRunnableSnaps']; -}; - -export type StopAllSnaps = { - type: `${typeof controllerName}:stopAllSnaps`; - handler: SnapController['stopAllSnaps']; -}; - -export type IncrementActiveReferences = { - type: `${typeof controllerName}:incrementActiveReferences`; - handler: SnapController['incrementActiveReferences']; -}; - -export type DecrementActiveReferences = { - type: `${typeof controllerName}:decrementActiveReferences`; - handler: SnapController['decrementActiveReferences']; -}; - -export type InstallSnaps = { - type: `${typeof controllerName}:install`; - handler: SnapController['installSnaps']; -}; - -export type DisconnectOrigin = { - type: `${typeof controllerName}:disconnectOrigin`; - handler: SnapController['removeSnapFromSubject']; -}; - -export type RevokeDynamicPermissions = { - type: `${typeof controllerName}:revokeDynamicPermissions`; - handler: SnapController['revokeDynamicSnapPermissions']; -}; - -export type GetSnapFile = { - type: `${typeof controllerName}:getFile`; - handler: SnapController['getSnapFile']; -}; - -export type IsMinimumPlatformVersion = { - type: `${typeof controllerName}:isMinimumPlatformVersion`; - handler: SnapController['isMinimumPlatformVersion']; -}; - -export type SetClientActive = { - type: `${typeof controllerName}:setClientActive`; - handler: SnapController['setClientActive']; -}; - export type SnapControllerGetStateAction = ControllerGetStateAction< typeof controllerName, SnapControllerState >; export type SnapControllerActions = - | SnapControllerInitAction - | ClearSnapState - | GetSnap - | GetSnapState - | HandleSnapRequest - | HasSnap - | UpdateRegistry - | UpdateSnapState - | EnableSnap - | DisableSnap - | RemoveSnap - | GetPermittedSnaps - | InstallSnaps - | GetAllSnaps - | GetRunnableSnaps - | IncrementActiveReferences - | DecrementActiveReferences - | DisconnectOrigin - | RevokeDynamicPermissions - | GetSnapFile | SnapControllerGetStateAction - | StopAllSnaps - | IsMinimumPlatformVersion - | SetClientActive; + | SnapControllerMethodActions; // Controller Messenger Events -export type SnapStateChange = { - type: `${typeof controllerName}:stateChange`; - payload: [SnapControllerState, Patch[]]; -}; - /** * Emitted when an installed snap has been blocked. */ -export type SnapBlocked = { +export type SnapControllerSnapBlockedEvent = { type: `${typeof controllerName}:snapBlocked`; payload: [snapId: string, blockedSnapInfo?: BlockReason]; }; @@ -548,7 +403,7 @@ export type SnapBlocked = { /** * Emitted when a snap installation or update is started. */ -export type SnapInstallStarted = { +export type SnapControllerSnapInstallStartedEvent = { type: `${typeof controllerName}:snapInstallStarted`; payload: [snapId: SnapId, origin: string, isUpdate: boolean]; }; @@ -556,7 +411,7 @@ export type SnapInstallStarted = { /** * Emitted when a snap installation or update failed. */ -export type SnapInstallFailed = { +export type SnapControllerSnapInstallFailedEvent = { type: `${typeof controllerName}:snapInstallFailed`; payload: [snapId: SnapId, origin: string, isUpdate: boolean, error: string]; }; @@ -565,7 +420,7 @@ export type SnapInstallFailed = { * Emitted when a snap has been started after being added and authorized during * installation. */ -export type SnapInstalled = { +export type SnapControllerSnapInstalledEvent = { type: `${typeof controllerName}:snapInstalled`; payload: [snap: TruncatedSnap, origin: string, preinstalled: boolean]; }; @@ -573,7 +428,7 @@ export type SnapInstalled = { /** * Emitted when a snap that has previously been fully installed, is uninstalled. */ -export type SnapUninstalled = { +export type SnapControllerSnapUninstalledEvent = { type: `${typeof controllerName}:snapUninstalled`; payload: [snap: TruncatedSnap]; }; @@ -581,7 +436,7 @@ export type SnapUninstalled = { /** * Emitted when an installed snap has been unblocked. */ -export type SnapUnblocked = { +export type SnapControllerSnapUnblockedEvent = { type: `${typeof controllerName}:snapUnblocked`; payload: [snapId: string]; }; @@ -589,7 +444,7 @@ export type SnapUnblocked = { /** * Emitted when a snap is updated. */ -export type SnapUpdated = { +export type SnapControllerSnapUpdatedEvent = { type: `${typeof controllerName}:snapUpdated`; payload: [ snap: TruncatedSnap, @@ -602,7 +457,7 @@ export type SnapUpdated = { /** * Emitted when a snap is rolled back. */ -export type SnapRolledback = { +export type SnapControllerSnapRolledbackEvent = { type: `${typeof controllerName}:snapRolledback`; payload: [snap: TruncatedSnap, failedVersion: string]; }; @@ -611,7 +466,7 @@ export type SnapRolledback = { * Emitted when a Snap is terminated. This is different from the snap being * stopped as it can also be triggered when a snap fails initialization. */ -export type SnapTerminated = { +export type SnapControllerSnapTerminatedEvent = { type: `${typeof controllerName}:snapTerminated`; payload: [snap: TruncatedSnap]; }; @@ -620,7 +475,7 @@ export type SnapTerminated = { * Emitted when a Snap is enabled by a user. * This is not emitted by default when installing a snap. */ -export type SnapEnabled = { +export type SnapControllerSnapEnabledEvent = { type: `${typeof controllerName}:snapEnabled`; payload: [snap: TruncatedSnap]; }; @@ -628,7 +483,7 @@ export type SnapEnabled = { /** * Emitted when a Snap is disabled by a user. */ -export type SnapDisabled = { +export type SnapControllerSnapDisabledEvent = { type: `${typeof controllerName}:snapDisabled`; payload: [snap: TruncatedSnap]; }; @@ -641,24 +496,23 @@ export type SnapControllerStateChangeEvent = ControllerStateChangeEvent< SnapControllerState >; -type KeyringControllerLock = { +type KeyringControllerLockEvent = { type: 'KeyringController:lock'; payload: []; }; export type SnapControllerEvents = - | SnapBlocked - | SnapInstalled - | SnapUninstalled - | SnapInstallStarted - | SnapInstallFailed - | SnapStateChange - | SnapUnblocked - | SnapUpdated - | SnapRolledback - | SnapTerminated - | SnapEnabled - | SnapDisabled + | SnapControllerSnapBlockedEvent + | SnapControllerSnapInstalledEvent + | SnapControllerSnapUninstalledEvent + | SnapControllerSnapInstallStartedEvent + | SnapControllerSnapInstallFailedEvent + | SnapControllerSnapUnblockedEvent + | SnapControllerSnapUpdatedEvent + | SnapControllerSnapRolledbackEvent + | SnapControllerSnapTerminatedEvent + | SnapControllerSnapEnabledEvent + | SnapControllerSnapDisabledEvent | SnapControllerStateChangeEvent; export type AllowedActions = @@ -693,12 +547,12 @@ export type AllowedActions = export type AllowedEvents = | ExecutionServiceEvents - | SnapInstalled - | SnapUpdated - | KeyringControllerLock + | SnapControllerSnapInstalledEvent + | SnapControllerSnapUpdatedEvent + | KeyringControllerLockEvent | SnapsRegistryStateChangeEvent; -type SnapControllerMessenger = Messenger< +export type SnapControllerMessenger = Messenger< typeof controllerName, SnapControllerActions | AllowedActions, SnapControllerEvents | AllowedEvents @@ -1178,7 +1032,7 @@ export class SnapController extends BaseController< // In the future, side-effects could be added to the machine during transitions. #initializeStateMachine() { const disableGuard = ({ snapId }: StatusContext) => { - return this.getExpect(snapId).enabled; + return this.getSnapExpect(snapId).enabled; }; const statusConfig: StateMachine.Config< @@ -1250,8 +1104,9 @@ export class SnapController extends BaseController< (...args) => this.clearSnapState(...args), ); - this.messenger.registerActionHandler(`${controllerName}:get`, (...args) => - this.get(...args), + this.messenger.registerActionHandler( + `${controllerName}:getSnap`, + (...args) => this.getSnap(...args), ); this.messenger.registerActionHandler( @@ -1264,8 +1119,9 @@ export class SnapController extends BaseController< async (...args) => this.handleRequest(...args), ); - this.messenger.registerActionHandler(`${controllerName}:has`, (...args) => - this.has(...args), + this.messenger.registerActionHandler( + `${controllerName}:hasSnap`, + (...args) => this.hasSnap(...args), ); this.messenger.registerActionHandler( @@ -1279,32 +1135,32 @@ export class SnapController extends BaseController< ); this.messenger.registerActionHandler( - `${controllerName}:enable`, + `${controllerName}:enableSnap`, (...args) => this.enableSnap(...args), ); this.messenger.registerActionHandler( - `${controllerName}:disable`, + `${controllerName}:disableSnap`, async (...args) => this.disableSnap(...args), ); this.messenger.registerActionHandler( - `${controllerName}:remove`, + `${controllerName}:removeSnap`, async (...args) => this.removeSnap(...args), ); this.messenger.registerActionHandler( - `${controllerName}:getPermitted`, + `${controllerName}:getPermittedSnaps`, (...args) => this.getPermittedSnaps(...args), ); this.messenger.registerActionHandler( - `${controllerName}:install`, + `${controllerName}:installSnaps`, async (...args) => this.installSnaps(...args), ); this.messenger.registerActionHandler( - `${controllerName}:getAll`, + `${controllerName}:getAllSnaps`, (...args) => this.getAllSnaps(...args), ); @@ -1313,28 +1169,18 @@ export class SnapController extends BaseController< (...args) => this.getRunnableSnaps(...args), ); - this.messenger.registerActionHandler( - `${controllerName}:incrementActiveReferences`, - (...args) => this.incrementActiveReferences(...args), - ); - - this.messenger.registerActionHandler( - `${controllerName}:decrementActiveReferences`, - (...args) => this.decrementActiveReferences(...args), - ); - this.messenger.registerActionHandler( `${controllerName}:disconnectOrigin`, - (...args) => this.removeSnapFromSubject(...args), + (...args) => this.disconnectOrigin(...args), ); this.messenger.registerActionHandler( - `${controllerName}:revokeDynamicPermissions`, + `${controllerName}:revokeDynamicSnapPermissions`, (...args) => this.revokeDynamicSnapPermissions(...args), ); this.messenger.registerActionHandler( - `${controllerName}:getFile`, + `${controllerName}:getSnapFile`, async (...args) => this.getSnapFile(...args), ); @@ -1393,7 +1239,7 @@ export class SnapController extends BaseController< hidden, hideSnapBranding, } of preinstalledSnaps) { - const existingSnap = this.get(snapId); + const existingSnap = this.getSnap(snapId); const isAlreadyInstalled = existingSnap !== undefined; const isUpdate = isAlreadyInstalled && gtVersion(manifest.version, existingSnap.version); @@ -1505,7 +1351,7 @@ export class SnapController extends BaseController< if (isUpdate) { this.messenger.publish( 'SnapController:snapUpdated', - this.getTruncatedExpect(snapId), + this.getTruncatedSnapExpect(snapId), existingSnap.version, METAMASK_ORIGIN, true, @@ -1513,7 +1359,7 @@ export class SnapController extends BaseController< } else if (!isMissingSource) { this.messenger.publish( 'SnapController:snapInstalled', - this.getTruncatedExpect(snapId), + this.getTruncatedSnapExpect(snapId), METAMASK_ORIGIN, true, ); @@ -1684,7 +1530,7 @@ export class SnapController extends BaseController< /** * Blocks an installed snap and prevents it from being started again. Emits - * {@link SnapBlocked}. Does nothing if the snap is not installed. + * {@link SnapControllerSnapBlockedEvent}. Does nothing if the snap is not installed. * * @param snapId - The snap to block. * @param blockedSnapInfo - Information detailing why the snap is blocked. @@ -1693,7 +1539,7 @@ export class SnapController extends BaseController< snapId: SnapId, blockedSnapInfo?: BlockReason, ): Promise { - if (!this.has(snapId)) { + if (!this.hasSnap(snapId)) { return; } @@ -1720,13 +1566,13 @@ export class SnapController extends BaseController< /** * Unblocks a snap so that it can be enabled and started again. Emits - * {@link SnapUnblocked}. Does nothing if the snap is not installed or already + * {@link SnapControllerSnapUnblockedEvent}. Does nothing if the snap is not installed or already * unblocked. * * @param snapId - The id of the snap to unblock. */ #unblockSnap(snapId: SnapId) { - if (!this.has(snapId) || !this.state.snaps[snapId].blocked) { + if (!this.hasSnap(snapId) || !this.state.snaps[snapId].blocked) { return; } @@ -1918,7 +1764,7 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to enable. */ enableSnap(snapId: SnapId): void { - this.getExpect(snapId); + this.getSnapExpect(snapId); if (this.state.snaps[snapId].blocked) { throw new Error(`Snap "${snapId}" is blocked and cannot be enabled.`); @@ -1930,7 +1776,7 @@ export class SnapController extends BaseController< this.messenger.publish( 'SnapController:snapEnabled', - this.getTruncatedExpect(snapId), + this.getTruncatedSnapExpect(snapId), ); } @@ -1941,7 +1787,7 @@ export class SnapController extends BaseController< * @returns A promise that resolves once the snap has been disabled. */ async disableSnap(snapId: SnapId): Promise { - if (!this.has(snapId)) { + if (!this.hasSnap(snapId)) { throw new Error(`Snap "${snapId}" not found.`); } @@ -1949,13 +1795,13 @@ export class SnapController extends BaseController< state.snaps[snapId].enabled = false; }); - if (this.isRunning(snapId)) { + if (this.isSnapRunning(snapId)) { await this.stopSnap(snapId, SnapStatusEvents.Stop); } this.messenger.publish( 'SnapController:snapDisabled', - this.getTruncatedExpect(snapId), + this.getTruncatedSnapExpect(snapId), ); } @@ -1990,7 +1836,7 @@ export class SnapController extends BaseController< runtime.stopPromise = promise; try { - if (this.isRunning(snapId)) { + if (this.isSnapRunning(snapId)) { await this.#terminateSnap(snapId); } } finally { @@ -1999,7 +1845,7 @@ export class SnapController extends BaseController< runtime.pendingInboundRequests = []; runtime.pendingOutboundRequests = 0; runtime.stopPromise = null; - if (this.isRunning(snapId)) { + if (this.isSnapRunning(snapId)) { this.#transition(snapId, statusEvent); } resolve(); @@ -2019,7 +1865,7 @@ export class SnapController extends BaseController< | SnapStatusEvents.Crash = SnapStatusEvents.Stop, ): Promise { const snaps = Object.values(this.state.snaps).filter((snap) => - this.isRunning(snap.id), + this.isSnapRunning(snap.id), ); const promises = snaps.map(async (snap) => this.stopSnap(snap.id, statusEvent), @@ -2049,7 +1895,7 @@ export class SnapController extends BaseController< this.messenger.publish( 'SnapController:snapTerminated', - this.getTruncatedExpect(snapId), + this.getTruncatedSnapExpect(snapId), ); } @@ -2060,8 +1906,8 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to check. * @returns `true` if the snap is running, otherwise `false`. */ - isRunning(snapId: SnapId): boolean { - return this.getExpect(snapId).status === 'running'; + isSnapRunning(snapId: SnapId): boolean { + return this.getSnapExpect(snapId).status === 'running'; } /** @@ -2070,8 +1916,8 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to check for. * @returns `true` if the snap exists in the controller state, otherwise `false`. */ - has(snapId: SnapId): boolean { - return Boolean(this.get(snapId)); + hasSnap(snapId: SnapId): boolean { + return Boolean(this.getSnap(snapId)); } /** @@ -2082,7 +1928,7 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to get. * @returns The entire snap object from the controller state. */ - get(snapId: string): Snap | undefined { + getSnap(snapId: string): Snap | undefined { return this.state.snaps[snapId as SnapId]; } @@ -2091,13 +1937,13 @@ export class SnapController extends BaseController< * This should not be used if the snap is to be serializable, as e.g. * the snap sourceCode may be quite large. * - * @see {@link SnapController.get} + * @see {@link SnapController.getSnap} * @throws {@link Error}. If the snap doesn't exist * @param snapId - The id of the snap to get. * @returns The entire snap object. */ - getExpect(snapId: SnapId): Snap { - const snap = this.get(snapId); + getSnapExpect(snapId: SnapId): Snap { + const snap = this.getSnap(snapId); assert(snap !== undefined, `Snap "${snapId}" not found.`); return snap; } @@ -2110,8 +1956,8 @@ export class SnapController extends BaseController< * @returns A truncated version of the snap state, that is less expensive to serialize. */ // TODO(ritave): this.get returns undefined, this.getTruncated returns null - getTruncated(snapId: SnapId): TruncatedSnap | null { - const snap = this.get(snapId); + getTruncatedSnap(snapId: SnapId): TruncatedSnap | null { + const snap = this.getSnap(snapId); return snap ? truncateSnap(snap) : null; } @@ -2123,8 +1969,8 @@ export class SnapController extends BaseController< * @param snapId - The id of the snap to get. * @returns A truncated version of the snap state, that is less expensive to serialize. */ - getTruncatedExpect(snapId: SnapId): TruncatedSnap { - return truncateSnap(this.getExpect(snapId)); + getTruncatedSnapExpect(snapId: SnapId): TruncatedSnap { + return truncateSnap(this.getSnapExpect(snapId)); } /** @@ -2430,7 +2276,7 @@ export class SnapController extends BaseController< path: string, encoding: AuxiliaryFileEncoding = AuxiliaryFileEncoding.Base64, ): Promise { - const snap = this.getExpect(snapId); + const snap = this.getSnapExpect(snapId); const normalizedPath = normalizeRelative(path); const value = snap.auxiliaryFiles?.find( (file) => file.path === normalizedPath, @@ -2459,7 +2305,7 @@ export class SnapController extends BaseController< * @returns True if the platform version is equal or greater to the passed version, false otherwise. */ isMinimumPlatformVersion(snapId: SnapId, version: SemVerVersion): boolean { - const snap = this.getExpect(snapId); + const snap = this.getSnapExpect(snapId); const { platformVersion } = snap.manifest; @@ -2522,14 +2368,14 @@ export class SnapController extends BaseController< } snapIds.forEach((snapId) => { - const snap = this.getExpect(snapId); + const snap = this.getSnapExpect(snapId); assert(snap.removable !== false, `${snapId} is not removable.`); }); await Promise.all( snapIds.map(async (snapId) => { - const snap = this.getExpect(snapId); - const truncated = this.getTruncatedExpect(snapId); + const snap = this.getSnapExpect(snapId); + const truncated = this.getTruncatedSnapExpect(snapId); // Disable the snap and revoke all of its permissions before deleting // it. This ensures that the snap will not be restarted or otherwise // affect the host environment while we are deleting it. @@ -2568,7 +2414,7 @@ export class SnapController extends BaseController< ); for (const origin of Object.keys(revokedInitialConnections)) { - this.removeSnapFromSubject(origin, snapId); + this.disconnectOrigin(origin, snapId); } } @@ -2628,12 +2474,13 @@ export class SnapController extends BaseController< } /** - * Removes a snap's permission (caveat) from the specified subject. + * Disconnect the Snap from the given origin, meaning the origin can no longer + * interact with the Snap until it is reconnected. * - * @param origin - The origin from which to remove the snap. + * @param origin - The origin from which to remove the Snap. * @param snapId - The id of the snap to remove. */ - removeSnapFromSubject(origin: string, snapId: SnapId) { + disconnectOrigin(origin: string, snapId: SnapId) { const subjectPermissions = this.messenger.call( 'PermissionController:getPermissions', origin, @@ -2705,7 +2552,7 @@ export class SnapController extends BaseController< 'PermissionController:getSubjectNames', ); for (const subject of subjects) { - this.removeSnapFromSubject(subject, snapId); + this.disconnectOrigin(subject, snapId); } } @@ -2777,8 +2624,8 @@ export class SnapController extends BaseController< )?.value ?? {}; return Object.keys(snaps).reduce( (permittedSnaps, snapId) => { - const snap = this.get(snapId); - const truncatedSnap = this.getTruncated(snapId as SnapId); + const snap = this.getSnap(snapId); + const truncatedSnap = this.getTruncatedSnap(snapId as SnapId); if (truncatedSnap && snap?.status !== SnapStatus.Installing) { permittedSnaps[snapId] = truncatedSnap; @@ -2840,10 +2687,10 @@ export class SnapController extends BaseController< // Existing snaps may need to be updated, unless they should be re-installed (e.g. local snaps) // Everything else is treated as an install - const isUpdate = this.has(snapId) && !location.shouldAlwaysReload; + const isUpdate = this.hasSnap(snapId) && !location.shouldAlwaysReload; if (isUpdate && this.#isValidUpdate(snapId, version)) { - const existingSnap = this.getExpect(snapId); + const existingSnap = this.getSnapExpect(snapId); pendingUpdates.push({ snapId, oldVersion: existingSnap.version }); let rollbackSnapshot = this.#getRollbackSnapshot(snapId); if (rollbackSnapshot === undefined) { @@ -2868,7 +2715,7 @@ export class SnapController extends BaseController< pendingInstalls.forEach((snapId) => this.messenger.publish( `SnapController:snapInstalled`, - this.getTruncatedExpect(snapId), + this.getTruncatedSnapExpect(snapId), origin, false, ), @@ -2877,7 +2724,7 @@ export class SnapController extends BaseController< pendingUpdates.forEach(({ snapId, oldVersion }) => this.messenger.publish( `SnapController:snapUpdated`, - this.getTruncatedExpect(snapId), + this.getTruncatedSnapExpect(snapId), oldVersion, origin, false, @@ -2886,7 +2733,9 @@ export class SnapController extends BaseController< snapIds.forEach((snapId) => this.#rollbackSnapshots.delete(snapId)); } catch (error) { - const installed = pendingInstalls.filter((snapId) => this.has(snapId)); + const installed = pendingInstalls.filter((snapId) => + this.hasSnap(snapId), + ); await this.removeSnaps(installed); const snapshottedSnaps = [...this.#rollbackSnapshots.keys()]; const snapsToRollback = pendingUpdates @@ -2916,7 +2765,7 @@ export class SnapController extends BaseController< location: SnapLocation, versionRange: SemVerRange, ): Promise { - const existingSnap = this.getTruncated(snapId); + const existingSnap = this.getTruncatedSnap(snapId); // For devX we always re-install local snaps. if (existingSnap && !location.shouldAlwaysReload) { @@ -2948,7 +2797,7 @@ export class SnapController extends BaseController< ); // Existing snaps must be stopped before overwriting - if (existingSnap && this.isRunning(snapId)) { + if (existingSnap && this.isSnapRunning(snapId)) { await this.stopSnap(snapId, SnapStatusEvents.Stop); } @@ -2978,7 +2827,7 @@ export class SnapController extends BaseController< sourceCode, }); - const truncated = this.getTruncatedExpect(snapId); + const truncated = this.getTruncatedSnapExpect(snapId); this.#updateApproval(pendingApproval.id, { loading: false, @@ -3091,7 +2940,7 @@ export class SnapController extends BaseController< } await this.#ensureCanUsePlatform(); - const snap = this.getExpect(snapId); + const snap = this.getSnapExpect(snapId); const { preinstalled, removable, hidden, hideSnapBranding } = snap; @@ -3190,7 +3039,7 @@ export class SnapController extends BaseController< approvedNewPermissions = newPermissions; } - if (this.isRunning(snapId)) { + if (this.isSnapRunning(snapId)) { await this.stopSnap(snapId, SnapStatusEvents.Stop); } @@ -3245,7 +3094,7 @@ export class SnapController extends BaseController< throw new Error(`Snap ${snapId} crashed with updated source code.`); } - const truncatedSnap = this.getTruncatedExpect(snapId); + const truncatedSnap = this.getTruncatedSnapExpect(snapId); if (pendingApproval) { this.#updateApproval(pendingApproval.id, { @@ -3357,7 +3206,7 @@ export class SnapController extends BaseController< async #startSnap(snapData: { snapId: SnapId; sourceCode: string }) { const { snapId } = snapData; - if (this.isRunning(snapId)) { + if (this.isSnapRunning(snapId)) { throw new Error(`Snap "${snapId}" is already started.`); } @@ -3733,7 +3582,7 @@ export class SnapController extends BaseController< }: SnapRpcHookArgs & { snapId: SnapId }): Promise { await this.#ensureCanUsePlatform(); - const snap = this.get(snapId); + const snap = this.getSnap(snapId); assert( snap, @@ -3836,7 +3685,7 @@ export class SnapController extends BaseController< await runtime.stopPromise; } - if (!this.isRunning(snapId)) { + if (!this.isSnapRunning(snapId)) { if (!runtime.startPromise) { runtime.startPromise = this.startSnap(snapId); } @@ -3869,7 +3718,7 @@ export class SnapController extends BaseController< if (result === hasTimedOut) { const stopping = - runtime.stopPromise !== null || !this.isRunning(snapId); + runtime.stopPromise !== null || !this.isSnapRunning(snapId); throw new Error( stopping ? `${snapId} was stopped and the request was cancelled. This is likely because the Snap crashed.` @@ -3925,7 +3774,8 @@ export class SnapController extends BaseController< const [jsonRpcError, handled] = unwrapError(error); - const stopping = runtime.stopPromise !== null || !this.isRunning(snapId); + const stopping = + runtime.stopPromise !== null || !this.isSnapRunning(snapId); if (!handled) { if (!stopping) { @@ -4299,7 +4149,7 @@ export class SnapController extends BaseController< runtime.lastRequest = Date.now(); } - const snap = this.get(snapId); + const snap = this.getSnap(snapId); if (isTrackableHandler(handlerType) && !snap?.preinstalled) { try { @@ -4372,7 +4222,7 @@ export class SnapController extends BaseController< await this.stopSnap(snapId, SnapStatusEvents.Stop); // Always set to stopped even if it wasn't running initially - if (this.get(snapId)?.status !== SnapStatus.Stopped) { + if (this.getSnap(snapId)?.status !== SnapStatus.Stopped) { this.#transition(snapId, SnapStatusEvents.Stop); } @@ -4396,7 +4246,7 @@ export class SnapController extends BaseController< // Reset snap status, as we may have been in another state when we stored state patches // But now we are 100% in a stopped state - if (this.get(snapId)?.status !== SnapStatus.Stopped) { + if (this.getSnap(snapId)?.status !== SnapStatus.Stopped) { this.update((state) => { state.snaps[snapId].status = SnapStatus.Stopped; }); @@ -4416,7 +4266,7 @@ export class SnapController extends BaseController< previousInitialConnections ?? {}, ); - const truncatedSnap = this.getTruncatedExpect(snapId); + const truncatedSnap = this.getTruncatedSnapExpect(snapId); this.messenger.publish( 'SnapController:snapRolledback', @@ -4454,7 +4304,7 @@ export class SnapController extends BaseController< return; } - const snap = this.get(snapId); + const snap = this.getSnap(snapId); const interpreter = interpret(this.#statusMachine); interpreter.start({ context: { snapId }, @@ -4678,7 +4528,7 @@ export class SnapController extends BaseController< * @returns `true` if validation checks pass and `false` if they do not. */ #isValidUpdate(snapId: SnapId, newVersionRange: SemVerRange): boolean { - const existingSnap = this.getExpect(snapId); + const existingSnap = this.getSnapExpect(snapId); if (satisfiesVersionRange(existingSnap.version, newVersionRange)) { return false; diff --git a/packages/snaps-controllers/src/snaps/index.ts b/packages/snaps-controllers/src/snaps/index.ts index 27c90e70fd..d744095d96 100644 --- a/packages/snaps-controllers/src/snaps/index.ts +++ b/packages/snaps-controllers/src/snaps/index.ts @@ -1,5 +1,51 @@ export * from './constants'; export * from './location'; -export * from './SnapController'; +export type { + SnapControllerGetStateAction, + SnapControllerSnapBlockedEvent, + SnapControllerSnapDisabledEvent, + SnapControllerSnapEnabledEvent, + SnapControllerSnapInstalledEvent, + SnapControllerSnapInstallFailedEvent, + SnapControllerSnapInstallStartedEvent, + SnapControllerSnapRolledbackEvent, + SnapControllerSnapTerminatedEvent, + SnapControllerSnapUnblockedEvent, + SnapControllerSnapUninstalledEvent, + SnapControllerSnapUpdatedEvent, + SnapControllerState, + SnapControllerStateChangeEvent, + PreinstalledSnapFile, + PreinstalledSnap, + PersistedSnapControllerState, +} from './SnapController'; +export { SnapController } from './SnapController'; +export type { + SnapControllerInitAction, + SnapControllerUpdateRegistryAction, + SnapControllerEnableSnapAction, + SnapControllerDisableSnapAction, + SnapControllerStopSnapAction, + SnapControllerStopAllSnapsAction, + SnapControllerIsSnapRunningAction, + SnapControllerHasSnapAction, + SnapControllerGetSnapAction, + SnapControllerUpdateSnapStateAction, + SnapControllerClearSnapStateAction, + SnapControllerGetSnapStateAction, + SnapControllerGetSnapFileAction, + SnapControllerIsMinimumPlatformVersionAction, + SnapControllerClearStateAction, + SnapControllerRemoveSnapAction, + SnapControllerRemoveSnapsAction, + SnapControllerDisconnectOriginAction, + SnapControllerRevokeDynamicSnapPermissionsAction, + SnapControllerGetAllSnapsAction, + SnapControllerGetRunnableSnapsAction, + SnapControllerGetPermittedSnapsAction, + SnapControllerInstallSnapsAction, + SnapControllerHandleRequestAction, + SnapControllerSetClientActiveAction, +} from './SnapController-method-action-types'; export * from './selectors'; export * from './registry'; diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index d8423cc939..ef26f2925d 100644 --- a/packages/snaps-controllers/src/test-utils/controller.tsx +++ b/packages/snaps-controllers/src/test-utils/controller.tsx @@ -8,6 +8,11 @@ import { isVaultUpdated, keyFromPassword, } from '@metamask/browser-passworder'; +import type { + MessengerActions, + MessengerEvents, + MockAnyNamespace, +} from '@metamask/messenger'; import { Messenger } from '@metamask/messenger'; import type { Caveat, @@ -49,37 +54,28 @@ import type { Json } from '@metamask/utils'; import { MOCK_CRONJOB_PERMISSION } from './cronjob'; import { getNodeEES, getNodeEESMessenger } from './execution-environment'; import { MockSnapsRegistry } from './registry'; +import type { CronjobControllerMessenger } from '../cronjob'; +import type { SnapInsightsControllerMessenger } from '../insights'; import type { - CronjobControllerActions, - CronjobControllerEvents, -} from '../cronjob'; -import type { - SnapInsightsControllerAllowedActions, - SnapInsightsControllerAllowedEvents, -} from '../insights'; -import type { - SnapInterfaceControllerActions, - SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerEvents, + SnapInterfaceControllerMessenger, StoredInterface, } from '../interface/SnapInterfaceController'; +import type { MultichainRouterMessenger } from '../multichain'; import type { - MultichainRouterActions, - MultichainRouterAllowedActions, - MultichainRouterEvents, -} from '../multichain'; -import type { AbstractExecutionService } from '../services'; + AbstractExecutionService, + ExecutionServiceMessenger, +} from '../services'; import type { - AllowedActions, - AllowedEvents, - PersistedSnapControllerState, - SnapControllerActions, - SnapControllerEvents, - SnapControllerStateChangeEvent, SnapsRegistryActions, SnapsRegistryEvents, + SnapsRegistryMessenger, } from '../snaps'; import { SnapController } from '../snaps'; +import type { + PersistedSnapControllerState, + SnapControllerMessenger, + SnapControllerStateChangeEvent, +} from '../snaps/SnapController'; import type { KeyDerivationOptions } from '../types'; import type { WebSocketServiceActions, @@ -329,11 +325,22 @@ export const MOCK_INSIGHTS_PERMISSIONS_NO_ORIGINS: Record< }, }; -export const getControllerMessenger = () => { - const messenger = new MockControllerMessenger< - SnapControllerActions | AllowedActions, - SnapControllerEvents | AllowedEvents - >(); +/** + * The type of the messenger populated with all external actions and events + * required by the controller under test. + */ +export type RootMessenger = Messenger< + MockAnyNamespace, + MessengerActions< + SnapControllerMessenger | SnapsRegistryMessenger | ExecutionServiceMessenger + >, + MessengerEvents< + SnapControllerMessenger | SnapsRegistryMessenger | ExecutionServiceMessenger + > +>; + +export const getRootMessenger = () => { + const messenger: RootMessenger = new MockControllerMessenger(); messenger.registerActionHandler( 'PermissionController:hasPermission', @@ -463,16 +470,9 @@ export const getControllerMessenger = () => { }; export const getSnapControllerMessenger = ( - messenger: ReturnType< - typeof getControllerMessenger - > = getControllerMessenger(), + messenger: RootMessenger = getRootMessenger(), ) => { - const snapControllerMessenger = new Messenger< - 'SnapController', - SnapControllerActions | AllowedActions, - SnapControllerEvents | AllowedEvents, - any - >({ + const snapControllerMessenger: SnapControllerMessenger = new Messenger({ namespace: 'SnapController', parent: messenger, }); @@ -582,10 +582,10 @@ export const getSnapControllerEncryptor = () => { export type GetSnapControllerOptionsParam = Omit< PartialSnapControllerConstructorParamsWithStorage, 'messenger' -> & { rootMessenger?: ReturnType }; +> & { rootMessenger?: ReturnType }; export const getSnapControllerOptions = ({ - rootMessenger = getControllerMessenger(), + rootMessenger = getRootMessenger(), ...opts }: GetSnapControllerOptionsParam = {}) => { const snapControllerMessenger = getSnapControllerMessenger(rootMessenger); @@ -608,7 +608,7 @@ export const getSnapControllerOptions = ({ ensureOnboardingComplete: jest.fn().mockResolvedValue(undefined), ...opts, } as SnapControllerConstructorParamsWithStorage & { - rootMessenger: ReturnType; + rootMessenger: ReturnType; }; options.state = { @@ -643,7 +643,7 @@ export const extractSourceCodeFromSnapsState = ( }; export const getStorageService = ( - messenger: ReturnType, + messenger: ReturnType, initialData?: InitialStorageData, ) => { const storageServiceMessenger = new Messenger< @@ -687,6 +687,7 @@ export const getSnapController = async ( if (init) { await controller.init(); } + return controller; }; @@ -696,7 +697,6 @@ export const getSnapControllerWithEES = async ( init = true, ) => { const _service = - // @ts-expect-error: TODO: Investigate type mismatch. service ?? getNodeEES(getNodeEESMessenger(options.rootMessenger)); const { snaps, snapsData } = extractSourceCodeFromSnapsState( @@ -733,12 +733,16 @@ export const getPersistedSnapsState = ( }, {}); }; +type CronjobControllerRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + // Mock controller messenger for Cronjob Controller export const getRootCronjobControllerMessenger = () => { - const messenger = new MockControllerMessenger< - CronjobControllerActions | AllowedActions, - CronjobControllerEvents | AllowedEvents - >(); + const messenger: CronjobControllerRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); @@ -746,24 +750,16 @@ export const getRootCronjobControllerMessenger = () => { }; export const getRestrictedCronjobControllerMessenger = ( - messenger: ReturnType< - typeof getRootCronjobControllerMessenger - > = getRootCronjobControllerMessenger(), + messenger: CronjobControllerRootMessenger = getRootCronjobControllerMessenger(), mocked = true, ) => { - const cronjobControllerMessenger = new Messenger< - 'CronjobController', - CronjobControllerActions | AllowedActions, - CronjobControllerEvents | AllowedEvents, - any - >({ + const cronjobControllerMessenger: CronjobControllerMessenger = new Messenger({ namespace: 'CronjobController', parent: messenger, }); messenger.delegate({ actions: [ - 'PermissionController:hasPermission', 'PermissionController:getPermissions', 'SnapController:handleRequest', ], @@ -778,13 +774,6 @@ export const getRestrictedCronjobControllerMessenger = ( }); if (mocked) { - messenger.registerActionHandler( - 'PermissionController:hasPermission', - () => { - return true; - }, - ); - messenger.registerActionHandler( 'PermissionController:getPermissions', () => { @@ -825,12 +814,20 @@ export const getRestrictedSnapsRegistryControllerMessenger = ( >({ namespace: 'SnapsRegistry', parent: messenger }); }; +/** + * The type of the messenger populated with all external actions and events + * required by the controller under test. + */ +type SnapInterfaceControllerRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + // Mock controller messenger for Interface Controller export const getRootSnapInterfaceControllerMessenger = () => { - const messenger = new MockControllerMessenger< - SnapInterfaceControllerActions | SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerEvents - >(); + const messenger: SnapInterfaceControllerRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); @@ -843,12 +840,8 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( > = getRootSnapInterfaceControllerMessenger(), mocked = true, ) => { - const snapInterfaceControllerMessenger = new Messenger< - 'SnapInterfaceController', - SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerEvents, - any - >({ namespace: 'SnapInterfaceController', parent: messenger }); + const snapInterfaceControllerMessenger: SnapInterfaceControllerMessenger = + new Messenger({ namespace: 'SnapInterfaceController', parent: messenger }); messenger.delegate({ actions: [ @@ -857,7 +850,7 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( 'ApprovalController:acceptRequest', 'MultichainAssetsController:getState', 'AccountsController:getAccountByAddress', - 'SnapController:get', + 'SnapController:getSnap', 'AccountsController:getSelectedMultichainAccount', 'AccountsController:listMultichainAccounts', 'PermissionController:hasPermission', @@ -920,9 +913,12 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( ], ); - messenger.registerActionHandler('SnapController:get', (snapId: string) => { - return getSnapObject({ id: snapId as SnapId }); - }); + messenger.registerActionHandler( + 'SnapController:getSnap', + (snapId: string) => { + return getSnapObject({ id: snapId as SnapId }); + }, + ); } jest.spyOn(snapInterfaceControllerMessenger, 'call'); @@ -930,12 +926,16 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( return snapInterfaceControllerMessenger; }; +type RootSnapInsightsControllerMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + // Mock controller messenger for Insight Controller export const getRootSnapInsightsControllerMessenger = () => { - const messenger = new MockControllerMessenger< - SnapInsightsControllerAllowedActions, - SnapInsightsControllerAllowedEvents - >(); + const messenger: RootSnapInsightsControllerMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); @@ -943,16 +943,9 @@ export const getRootSnapInsightsControllerMessenger = () => { }; export const getRestrictedSnapInsightsControllerMessenger = ( - messenger: ReturnType< - typeof getRootSnapInsightsControllerMessenger - > = getRootSnapInsightsControllerMessenger(), + messenger: RootSnapInsightsControllerMessenger = getRootSnapInsightsControllerMessenger(), ) => { - const controllerMessenger = new Messenger< - 'SnapInsightsController', - SnapInsightsControllerAllowedActions, - SnapInsightsControllerAllowedEvents, - any - >({ + const controllerMessenger: SnapInsightsControllerMessenger = new Messenger({ namespace: 'SnapInsightsController', parent: messenger, }); @@ -960,7 +953,7 @@ export const getRestrictedSnapInsightsControllerMessenger = ( messenger.delegate({ actions: [ 'PermissionController:getPermissions', - 'SnapController:getAll', + 'SnapController:getAllSnaps', 'SnapController:handleRequest', 'SnapInterfaceController:deleteInterface', ], @@ -993,12 +986,15 @@ export async function waitForStateChange( }); } +type MultichainRouterRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions +>; + // Mock controller messenger for Multichain Router -export const getRootMultichainRouterMessenger = () => { - const messenger = new MockControllerMessenger< - MultichainRouterActions | MultichainRouterAllowedActions, - MultichainRouterEvents - >(); +export const getMultichainRouterRootMessenger = () => { + const messenger: MultichainRouterRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); @@ -1006,21 +1002,17 @@ export const getRootMultichainRouterMessenger = () => { }; export const getRestrictedMultichainRouterMessenger = ( - messenger: ReturnType< - typeof getRootMultichainRouterMessenger - > = getRootMultichainRouterMessenger(), + messenger: MultichainRouterRootMessenger = getMultichainRouterRootMessenger(), ) => { - const controllerMessenger = new Messenger< - 'MultichainRouter', - MultichainRouterActions | MultichainRouterAllowedActions, - never, - any - >({ namespace: 'MultichainRouter', parent: messenger }); + const controllerMessenger: MultichainRouterMessenger = new Messenger({ + namespace: 'MultichainRouter', + parent: messenger, + }); messenger.delegate({ actions: [ 'PermissionController:getPermissions', - 'SnapController:getAll', + 'SnapController:getAllSnaps', 'SnapController:handleRequest', 'AccountsController:listMultichainAccounts', ], diff --git a/packages/snaps-controllers/src/test-utils/execution-environment.ts b/packages/snaps-controllers/src/test-utils/execution-environment.ts index f0126f179c..575ebce491 100644 --- a/packages/snaps-controllers/src/test-utils/execution-environment.ts +++ b/packages/snaps-controllers/src/test-utils/execution-environment.ts @@ -2,10 +2,10 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { createEngineStream } from '@metamask/json-rpc-middleware-stream'; import { Messenger } from '@metamask/messenger'; import { logError, type SnapRpcHookArgs } from '@metamask/snaps-utils'; -import type { MockControllerMessenger } from '@metamask/snaps-utils/test-utils'; import { pipeline } from 'readable-stream'; import { MOCK_BLOCK_NUMBER } from './constants'; +import type { RootMessenger } from './controller'; import type { ExecutionService, ExecutionServiceActions, @@ -15,12 +15,7 @@ import type { } from '../services'; import { NodeThreadExecutionService, setupMultiplex } from '../services/node'; -export const getNodeEESMessenger = ( - messenger: MockControllerMessenger< - ExecutionServiceActions, - ExecutionServiceEvents - >, -) => { +export const getNodeEESMessenger = (messenger: RootMessenger) => { const executionServiceMessenger = new Messenger< 'ExecutionService', ExecutionServiceActions, diff --git a/packages/snaps-controllers/src/test-utils/registry.ts b/packages/snaps-controllers/src/test-utils/registry.ts index 086c3ffade..ef44f797ff 100644 --- a/packages/snaps-controllers/src/test-utils/registry.ts +++ b/packages/snaps-controllers/src/test-utils/registry.ts @@ -1,12 +1,11 @@ -import type { MockControllerMessenger } from '@metamask/snaps-utils/test-utils'; - +import type { RootMessenger } from './controller'; import type { SnapsRegistry } from '../snaps'; import { SnapsRegistryStatus } from '../snaps'; export class MockSnapsRegistry implements SnapsRegistry { readonly #messenger; - constructor(messenger: MockControllerMessenger) { + constructor(messenger: RootMessenger) { this.#messenger = messenger; this.#messenger.registerActionHandler( diff --git a/packages/snaps-controllers/src/websocket/WebSocketService.ts b/packages/snaps-controllers/src/websocket/WebSocketService.ts index 15addd7c4f..0b077e9ca0 100644 --- a/packages/snaps-controllers/src/websocket/WebSocketService.ts +++ b/packages/snaps-controllers/src/websocket/WebSocketService.ts @@ -10,10 +10,10 @@ import { assert, createDeferredPromise } from '@metamask/utils'; import { nanoid } from 'nanoid'; import type { - HandleSnapRequest, - SnapInstalled, - SnapUninstalled, - SnapUpdated, + SnapControllerHandleRequestAction, + SnapControllerSnapInstalledEvent, + SnapControllerSnapUninstalledEvent, + SnapControllerSnapUpdatedEvent, } from '../snaps'; import { METAMASK_ORIGIN } from '../snaps'; @@ -53,12 +53,12 @@ export type WebSocketServiceActions = | WebSocketServiceSendMessageAction | WebSocketServiceGetAllAction; -export type WebSocketServiceAllowedActions = HandleSnapRequest; +export type WebSocketServiceAllowedActions = SnapControllerHandleRequestAction; export type WebSocketServiceEvents = - | SnapUninstalled - | SnapUpdated - | SnapInstalled; + | SnapControllerSnapUninstalledEvent + | SnapControllerSnapUpdatedEvent + | SnapControllerSnapInstalledEvent; export type WebSocketServiceMessenger = Messenger< 'WebSocketService', diff --git a/packages/snaps-rpc-methods/src/restricted/invokeSnap.test.ts b/packages/snaps-rpc-methods/src/restricted/invokeSnap.test.ts index e36ebf07d5..44968b375a 100644 --- a/packages/snaps-rpc-methods/src/restricted/invokeSnap.test.ts +++ b/packages/snaps-rpc-methods/src/restricted/invokeSnap.test.ts @@ -10,7 +10,10 @@ import { MOCK_LOCAL_SNAP_ID, } from '@metamask/snaps-utils/test-utils'; -import type { InstallSnaps, GetPermittedSnaps } from './invokeSnap'; +import type { + SnapControllerInstallSnapsAction, + SnapControllerGetPermittedSnapsAction, +} from './invokeSnap'; import { invokeSnapBuilder, getInvokeSnapImplementation, @@ -113,20 +116,23 @@ describe('implementation', () => { describe('handleSnapInstall', () => { it('calls SnapController:install with the right parameters', async () => { const messenger = new MockControllerMessenger< - InstallSnaps | GetPermittedSnaps, + SnapControllerGetPermittedSnapsAction | SnapControllerInstallSnapsAction, never >(); const sideEffectMessenger = new Messenger< 'PermissionController', - InstallSnaps | GetPermittedSnaps, + SnapControllerGetPermittedSnapsAction | SnapControllerInstallSnapsAction, never, any >({ namespace: 'PermissionController', parent: messenger }); messenger.delegate({ messenger: sideEffectMessenger, - actions: ['SnapController:install', 'SnapController:getPermitted'], + actions: [ + 'SnapController:installSnaps', + 'SnapController:getPermittedSnaps', + ], }); const expectedResult = { @@ -134,11 +140,14 @@ describe('handleSnapInstall', () => { }; messenger.registerActionHandler( - 'SnapController:install', + 'SnapController:installSnaps', async () => expectedResult, ); - messenger.registerActionHandler('SnapController:getPermitted', () => ({})); + messenger.registerActionHandler( + 'SnapController:getPermittedSnaps', + () => ({}), + ); jest.spyOn(sideEffectMessenger, 'call'); @@ -166,7 +175,7 @@ describe('handleSnapInstall', () => { }); expect(sideEffectMessenger.call).toHaveBeenCalledWith( - 'SnapController:install', + 'SnapController:installSnaps', MOCK_ORIGIN, requestedSnaps, ); @@ -176,20 +185,23 @@ describe('handleSnapInstall', () => { it('dedupes snaps before calling installSnaps', async () => { const messenger = new MockControllerMessenger< - InstallSnaps | GetPermittedSnaps, + SnapControllerGetPermittedSnapsAction | SnapControllerInstallSnapsAction, never >(); const sideEffectMessenger = new Messenger< 'PermissionController', - InstallSnaps | GetPermittedSnaps, + SnapControllerGetPermittedSnapsAction | SnapControllerInstallSnapsAction, never, any >({ namespace: 'PermissionController', parent: messenger }); messenger.delegate({ messenger: sideEffectMessenger, - actions: ['SnapController:install', 'SnapController:getPermitted'], + actions: [ + 'SnapController:installSnaps', + 'SnapController:getPermittedSnaps', + ], }); const expectedResult = { @@ -197,11 +209,11 @@ describe('handleSnapInstall', () => { }; messenger.registerActionHandler( - 'SnapController:install', + 'SnapController:installSnaps', async () => expectedResult, ); - messenger.registerActionHandler('SnapController:getPermitted', () => ({ + messenger.registerActionHandler('SnapController:getPermittedSnaps', () => ({ [MOCK_SNAP_ID]: getTruncatedSnap(), })); @@ -232,7 +244,7 @@ describe('handleSnapInstall', () => { }); expect(sideEffectMessenger.call).toHaveBeenCalledWith( - 'SnapController:install', + 'SnapController:installSnaps', MOCK_ORIGIN, { [MOCK_LOCAL_SNAP_ID]: {} }, ); diff --git a/packages/snaps-rpc-methods/src/restricted/invokeSnap.ts b/packages/snaps-rpc-methods/src/restricted/invokeSnap.ts index f7448fbe2b..177d4537c7 100644 --- a/packages/snaps-rpc-methods/src/restricted/invokeSnap.ts +++ b/packages/snaps-rpc-methods/src/restricted/invokeSnap.ts @@ -22,20 +22,22 @@ import type { MethodHooksObject } from '../utils'; export const WALLET_SNAP_PERMISSION_KEY = 'wallet_snap'; // Redeclare installSnaps action type to avoid circular dependencies -export type InstallSnaps = { - type: `SnapController:install`; +export type SnapControllerInstallSnapsAction = { + type: `SnapController:installSnaps`; handler: ( origin: string, requestedSnaps: RequestSnapsParams, ) => Promise; }; -export type GetPermittedSnaps = { - type: `SnapController:getPermitted`; +export type SnapControllerGetPermittedSnapsAction = { + type: `SnapController:getPermittedSnaps`; handler: (origin: string) => RequestSnapsResult; }; -type AllowedActions = InstallSnaps | GetPermittedSnaps; +type AllowedActions = + | SnapControllerInstallSnapsAction + | SnapControllerGetPermittedSnapsAction; export type InvokeSnapMethodHooks = { handleSnapRpcRequest: ({ @@ -78,7 +80,7 @@ export const handleSnapInstall: PermissionSideEffect< .value as RequestSnapsParams; const permittedSnaps = messenger.call( - `SnapController:getPermitted`, + `SnapController:getPermittedSnaps`, requestData.metadata.origin, ); @@ -93,7 +95,7 @@ export const handleSnapInstall: PermissionSideEffect< ); return messenger.call( - `SnapController:install`, + `SnapController:installSnaps`, requestData.metadata.origin, dedupedSnaps, ); diff --git a/scripts/generate-method-action-types.mts b/scripts/generate-method-action-types.mts new file mode 100644 index 0000000000..27f3a5a436 --- /dev/null +++ b/scripts/generate-method-action-types.mts @@ -0,0 +1,773 @@ +#!yarn tsx + +// ESLint is saying `ts` can be replaced with named imports, but this doesn't +// seem to actually work with the current TypeScript version. +/* eslint-disable no-console, import-x/no-named-as-default-member */ + +import { assert, hasProperty, isObject } from '@metamask/utils'; +import { ESLint } from 'eslint'; +import * as fs from 'fs'; +import * as path from 'path'; +import ts from 'typescript'; +import yargs from 'yargs'; + +type MethodInfo = { + name: string; + jsDoc: string; + signature: string; +}; + +type ControllerInfo = { + name: string; + filePath: string; + exposedMethods: string[]; + methods: MethodInfo[]; +}; + +/** + * The parsed command-line arguments. + */ +type CommandLineArguments = { + /** + * Whether to check if the action types files are up to date. + */ + check: boolean; + /** + * Whether to fix the action types files. + */ + fix: boolean; + /** + * Optional path to a specific controller to process. + */ + controllerPath: string; +}; + +/** + * Uses `yargs` to parse the arguments given to the script. + * + * @returns The command line arguments. + */ +async function parseCommandLineArguments(): Promise { + const { + check, + fix, + path: controllerPath, + } = await yargs(process.argv.slice(2)) + .command( + '$0 [path]', + 'Generate method action types for a controller messenger', + (yargsInstance) => { + yargsInstance.positional('path', { + type: 'string', + description: 'Path to the folder where controllers are located', + default: 'src', + }); + }, + ) + .option('check', { + type: 'boolean', + description: 'Check if generated action type files are up to date', + default: false, + }) + .option('fix', { + type: 'boolean', + description: 'Generate/update action type files', + default: false, + }) + .help() + .check((argv) => { + if (!argv.check && !argv.fix) { + throw new Error('Either --check or --fix must be provided.\n'); + } + return true; + }).argv; + + return { + check, + fix, + // TypeScript doesn't narrow the type of `controllerPath` even though we defined it as a string in yargs, so we need to cast it here. + controllerPath: controllerPath as string, + }; +} + +/** + * Checks if generated action types files are up to date. + * + * @param controllers - Array of controller information objects. + * @param eslint - The ESLint instance to use for formatting. + */ +async function checkActionTypesFiles( + controllers: ControllerInfo[], + eslint: ESLint, +): Promise { + let hasErrors = false; + + // Track files that exist and their corresponding temp files + const fileComparisonJobs: { + expectedTempFile: string; + actualFile: string; + baseFileName: string; + }[] = []; + + try { + // Check each controller and prepare comparison jobs + for (const controller of controllers) { + console.log(`\n🔧 Checking ${controller.name}...`); + const outputDir = path.dirname(controller.filePath); + const baseFileName = path.basename(controller.filePath, '.ts'); + const actualFile = path.join( + outputDir, + `${baseFileName}-method-action-types.ts`, + ); + + const expectedContent = generateActionTypesContent(controller); + const expectedTempFile = actualFile.replace('.ts', '.tmp.ts'); + + try { + // Check if actual file exists first + await fs.promises.access(actualFile); + + // Write expected content to temp file + await fs.promises.writeFile(expectedTempFile, expectedContent, 'utf8'); + + // Add to comparison jobs + fileComparisonJobs.push({ + expectedTempFile, + actualFile, + baseFileName, + }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + console.error( + `❌ ${baseFileName}-method-action-types.ts does not exist`, + ); + } else { + console.error( + `❌ Error reading ${baseFileName}-method-action-types.ts:`, + error, + ); + } + hasErrors = true; + } + } + + // Run ESLint on all files at once if we have comparisons to make + if (fileComparisonJobs.length > 0) { + console.log('\n📝 Running ESLint to compare files...'); + + const results = await eslint.lintFiles( + fileComparisonJobs.map((job) => job.expectedTempFile), + ); + await ESLint.outputFixes(results); + + // Compare expected vs actual content + for (const job of fileComparisonJobs) { + const expectedContent = await fs.promises.readFile( + job.expectedTempFile, + 'utf8', + ); + const actualContent = await fs.promises.readFile( + job.actualFile, + 'utf8', + ); + + if (expectedContent === actualContent) { + console.log( + `✅ ${job.baseFileName}-method-action-types.ts is up to date`, + ); + } else { + console.error( + `❌ ${job.baseFileName}-method-action-types.ts is out of date`, + ); + hasErrors = true; + } + } + } + } finally { + // Clean up temp files + for (const job of fileComparisonJobs) { + try { + await fs.promises.unlink(job.expectedTempFile); + } catch { + // Ignore cleanup errors + } + } + } + + if (hasErrors) { + console.error('\n💥 Some action type files are out of date or missing.'); + console.error( + 'Run `yarn generate-method-action-types --fix` to update them.', + ); + process.exitCode = 1; + } else { + console.log('\n🎉 All action type files are up to date!'); + } +} + +/** + * Main entry point for the script. + */ +async function main(): Promise { + const { fix, controllerPath } = await parseCommandLineArguments(); + + console.log('🔍 Searching for controllers with MESSENGER_EXPOSED_METHODS...'); + + const controllers = await findControllersWithExposedMethods(controllerPath); + + if (controllers.length === 0) { + console.log('⚠️ No controllers found with MESSENGER_EXPOSED_METHODS'); + return; + } + + console.log( + `📦 Found ${controllers.length} controller(s) with exposed methods`, + ); + + const eslint = new ESLint({ + fix: true, + errorOnUnmatchedPattern: false, + }); + + if (fix) { + await generateAllActionTypesFiles(controllers, eslint); + console.log('\n🎉 All action types generated successfully!'); + } else { + // -check mode: check files + await checkActionTypesFiles(controllers, eslint); + } +} + +/** + * Check if a path is a directory. + * + * @param pathValue - The path to check. + * @returns True if the path is a directory, false otherwise. + * @throws If an error occurs other than the path not existing. + */ +async function isDirectory(pathValue: string): Promise { + try { + const stats = await fs.promises.stat(pathValue); + return stats.isDirectory(); + } catch (error) { + if ( + isObject(error) && + hasProperty(error, 'code') && + error.code === 'ENOENT' + ) { + return false; + } + + throw error; + } +} + +/** + * Recursively get all files in a directory and its subdirectories. + * + * @param directory - The directory to search. + * @returns An array of file paths. + */ +async function getFiles(directory: string): Promise { + const entries = await fs.promises.readdir(directory, { withFileTypes: true }); + const files = await Promise.all( + entries.map(async (entry) => { + const fullPath = path.join(directory, entry.name); + return entry.isDirectory() ? await getFiles(fullPath) : fullPath; + }), + ); + + return files.flat(); +} + +/** + * Finds all controller files that have MESSENGER_EXPOSED_METHODS constants. + * + * @param controllerPath - Path to the folder where controllers are located. + * @returns A list of controller information objects. + */ +async function findControllersWithExposedMethods( + controllerPath: string, +): Promise { + const srcPath = path.resolve(process.cwd(), controllerPath); + const controllers: ControllerInfo[] = []; + + if (!(await isDirectory(srcPath))) { + throw new Error(`The specified path is not a directory: ${srcPath}`); + } + + const srcFiles = await getFiles(srcPath); + + for (const file of srcFiles) { + if (!file.endsWith('.ts') || file.endsWith('.test.ts')) { + continue; + } + + const content = await fs.promises.readFile(file, 'utf8'); + + if (content.includes('MESSENGER_EXPOSED_METHODS')) { + const controllerInfo = await parseControllerFile(file); + if (controllerInfo) { + controllers.push(controllerInfo); + } + } + } + + return controllers; +} + +/** + * Context for AST visiting. + */ +type VisitorContext = { + exposedMethods: string[]; + className: string; + methods: MethodInfo[]; + sourceFile: ts.SourceFile; +}; + +/** + * Visits AST nodes to find exposed methods and controller class. + * + * @param context - The visitor context. + * @returns A function to visit nodes. + */ +function createASTVisitor(context: VisitorContext): (node: ts.Node) => void { + /** + * Visits AST nodes to find exposed methods and controller class. + * + * @param node - The AST node to visit. + */ + function visitNode(node: ts.Node): void { + if (ts.isVariableStatement(node)) { + const declaration = node.declarationList.declarations[0]; + if ( + ts.isIdentifier(declaration.name) && + declaration.name.text === 'MESSENGER_EXPOSED_METHODS' + ) { + if (declaration.initializer) { + let arrayExpression: ts.ArrayLiteralExpression | undefined; + + // Handle direct array literal + if (ts.isArrayLiteralExpression(declaration.initializer)) { + arrayExpression = declaration.initializer; + } + // Handle "as const" assertion: expression is wrapped in type assertion + else if ( + ts.isAsExpression(declaration.initializer) && + ts.isArrayLiteralExpression(declaration.initializer.expression) + ) { + arrayExpression = declaration.initializer.expression; + } + + if (arrayExpression) { + context.exposedMethods = arrayExpression.elements + .filter(ts.isStringLiteral) + .map((element) => element.text); + } + } + } + } + + // Find the controller or service class + if (ts.isClassDeclaration(node) && node.name) { + const classText = node.name.text; + if (classText.includes('Controller') || classText.includes('Service')) { + context.className = classText; + + // Extract method info for exposed methods + const seenMethods = new Set(); + for (const member of node.members) { + if ( + ts.isMethodDeclaration(member) && + member.name && + ts.isIdentifier(member.name) + ) { + const methodName = member.name.text; + if ( + context.exposedMethods.includes(methodName) && + !seenMethods.has(methodName) + ) { + seenMethods.add(methodName); + const jsDoc = extractJSDoc(member, context.sourceFile); + const signature = extractMethodSignature(member); + context.methods.push({ + name: methodName, + jsDoc, + signature, + }); + } + } + } + } + } + + ts.forEachChild(node, visitNode); + } + + return visitNode; +} + +/** + * Create a TypeScript program for the given file by locating the nearest + * tsconfig.json. + * + * @param filePath - Absolute path to the source file. + * @returns A TypeScript program, or null if no tsconfig was found. + */ +function createProgramForFile(filePath: string): ts.Program | null { + const configPath = ts.findConfigFile( + path.dirname(filePath), + ts.sys.fileExists.bind(ts.sys), + 'tsconfig.json', + ); + if (!configPath) { + return null; + } + + const { config, error } = ts.readConfigFile( + configPath, + ts.sys.readFile.bind(ts.sys), + ); + + if (error) { + return null; + } + + const parsedConfig = ts.parseJsonConfigFileContent( + config, + ts.sys, + path.dirname(configPath), + ); + + return ts.createProgram({ + rootNames: parsedConfig.fileNames, + options: parsedConfig.options, + }); +} + +/** + * Find a class declaration with the given name in a source file. + * + * @param sourceFile - The source file to search. + * @param className - The class name to look for. + * @returns The class declaration node, or null if not found. + */ +function findClassInSourceFile( + sourceFile: ts.SourceFile, + className: string, +): ts.ClassDeclaration | null { + return ( + sourceFile.statements.find( + (node): node is ts.ClassDeclaration => + ts.isClassDeclaration(node) && node.name?.text === className, + ) ?? null + ); +} + +/** + * Search through the class hierarchy of a TypeScript type to find the + * declaration of a method with the given name. + * + * @param classType - The class type to search. + * @param methodName - The method name to look for. + * @returns The method declaration node, or null if not found. + */ +function findMethodInHierarchy( + classType: ts.Type, + methodName: string, +): ts.MethodDeclaration | null { + const symbol = classType.getProperty(methodName); + if (!symbol) { + return null; + } + + const declarations = symbol.getDeclarations(); + if (!declarations) { + return null; + } + + for (const declaration of declarations) { + if (ts.isMethodDeclaration(declaration)) { + return declaration; + } + } + + return null; +} + +/** + * Parses a controller file to extract exposed methods and their metadata. + * + * @param filePath - Path to the controller file to parse. + * @returns Controller information or null if parsing fails. + */ +async function parseControllerFile( + filePath: string, +): Promise { + try { + const content = await fs.promises.readFile(filePath, 'utf8'); + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true, + ); + + const context: VisitorContext = { + exposedMethods: [], + className: '', + methods: [], + sourceFile, + }; + + createASTVisitor(context)(sourceFile); + + if (context.exposedMethods.length === 0 || !context.className) { + return null; + } + + // For exposed methods not found directly in the class body, attempt to + // locate them in the inheritance hierarchy using the type checker. + const foundMethodNames = new Set( + context.methods.map((method) => method.name), + ); + + const inheritedMethodNames = context.exposedMethods.filter( + (name) => !foundMethodNames.has(name), + ); + + if (inheritedMethodNames.length > 0) { + const program = createProgramForFile(filePath); + const checker = program?.getTypeChecker(); + const programSourceFile = program?.getSourceFile(filePath); + + assert( + checker, + `Type checker could not be created for "${filePath}". Ensure a valid tsconfig.json is present.`, + ); + + assert( + programSourceFile, + `Source file "${filePath}" not found in program.`, + ); + + const classNode = findClassInSourceFile( + programSourceFile, + context.className, + ); + + assert( + classNode, + `Class "${context.className}" not found in "${filePath}".`, + ); + + const classType = checker.getTypeAtLocation(classNode); + for (const methodName of inheritedMethodNames) { + const methodDeclaration = findMethodInHierarchy(classType, methodName); + + const jsDoc = methodDeclaration + ? extractJSDoc(methodDeclaration, methodDeclaration.getSourceFile()) + : ''; + context.methods.push({ name: methodName, jsDoc, signature: '' }); + } + } + + return { + name: context.className, + filePath, + exposedMethods: context.exposedMethods, + methods: context.methods, + }; + } catch (error) { + console.error(`Error parsing ${filePath}:`, error); + return null; + } +} + +/** + * Extracts JSDoc comment from a method declaration. + * + * @param node - The method declaration node. + * @param sourceFile - The source file. + * @returns The JSDoc comment. + */ +function extractJSDoc( + node: ts.MethodDeclaration, + sourceFile: ts.SourceFile, +): string { + const jsDocTags = ts.getJSDocCommentsAndTags(node); + if (jsDocTags.length === 0) { + return ''; + } + + const jsDoc = jsDocTags[0]; + if (ts.isJSDoc(jsDoc)) { + const fullText = sourceFile.getFullText(); + const start = jsDoc.getFullStart(); + const end = jsDoc.getEnd(); + const rawJsDoc = fullText.substring(start, end).trim(); + return formatJSDoc(rawJsDoc); + } + + return ''; +} + +/** + * Formats JSDoc comments to have consistent indentation for the generated file. + * + * @param rawJsDoc - The raw JSDoc comment from the source. + * @returns The formatted JSDoc comment. + */ +function formatJSDoc(rawJsDoc: string): string { + const lines = rawJsDoc.split('\n'); + const formattedLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (i === 0) { + // First line should be /** + formattedLines.push('/**'); + } else if (i === lines.length - 1) { + // Last line should be */ + formattedLines.push(' */'); + } else { + // Middle lines should start with ' * ' + const trimmed = line.trim(); + if (trimmed.startsWith('*')) { + // Remove existing * and normalize + const content = trimmed.substring(1).trim(); + formattedLines.push(content ? ` * ${content}` : ' *'); + } else { + // Handle lines that don't start with * + formattedLines.push(trimmed ? ` * ${trimmed}` : ' *'); + } + } + } + + return formattedLines.join('\n'); +} + +/** + * Extracts method signature as a string for the handler type. + * + * @param node - The method declaration node. + * @returns The method signature. + */ +function extractMethodSignature(node: ts.MethodDeclaration): string { + // Since we're just using the method reference in the handler type, + // we don't need the full signature - just return the method name + // The actual signature will be inferred from the controller class + return node.name ? (node.name as ts.Identifier).text : ''; +} + +/** + * Generates action types files for all controllers. + * + * @param controllers - Array of controller information objects. + * @param eslint - The ESLint instance to use for formatting. + */ +async function generateAllActionTypesFiles( + controllers: ControllerInfo[], + eslint: ESLint, +): Promise { + const outputFiles: string[] = []; + + // Write all files first + for (const controller of controllers) { + console.log(`\n🔧 Processing ${controller.name}...`); + const outputDir = path.dirname(controller.filePath); + const baseFileName = path.basename(controller.filePath, '.ts'); + const outputFile = path.join( + outputDir, + `${baseFileName}-method-action-types.ts`, + ); + + const generatedContent = generateActionTypesContent(controller); + await fs.promises.writeFile(outputFile, generatedContent, 'utf8'); + outputFiles.push(outputFile); + console.log(`✅ Generated action types for ${controller.name}`); + } + + // Run ESLint on all the actual files + if (outputFiles.length > 0) { + console.log('\n📝 Running ESLint on generated files...'); + + const results = await eslint.lintFiles(outputFiles); + await ESLint.outputFixes(results); + const errors = ESLint.getErrorResults(results); + if (errors.length > 0) { + console.error('❌ ESLint errors:', errors); + process.exitCode = 1; + } else { + console.log('✅ ESLint formatting applied'); + } + } +} + +/** + * Generates the content for the action types file. + * + * @param controller - The controller information object. + * @returns The content for the action types file. + */ +function generateActionTypesContent(controller: ControllerInfo): string { + const baseFileName = path.basename(controller.filePath, '.ts'); + const controllerImportPath = `./${baseFileName}`; + + let content = `/** + * This file is auto generated by \`scripts/generate-method-action-types.ts\`. + * Do not edit manually. + */ + +import type { ${controller.name} } from '${controllerImportPath}'; + +`; + + const actionTypeNames: string[] = []; + + // Generate action types for each exposed method + for (const method of controller.methods) { + const actionTypeName = `${controller.name}${capitalize(method.name)}Action`; + const actionString = `${controller.name}:${method.name}`; + + actionTypeNames.push(actionTypeName); + + // Add the JSDoc if available + if (method.jsDoc) { + content += `${method.jsDoc}\n`; + } + + content += `export type ${actionTypeName} = { + type: \`${actionString}\`; + handler: ${controller.name}['${method.name}']; +};\n\n`; + } + + // Generate union type of all action types + if (actionTypeNames.length > 0) { + const unionTypeName = `${controller.name}MethodActions`; + content += `/** + * Union of all ${controller.name} action types. + */ +export type ${unionTypeName} = ${actionTypeNames.join(' | ')};\n`; + } + + return `${content.trimEnd()}\n`; +} + +/** + * Capitalizes the first letter of a string. + * + * @param str - The string to capitalize. + * @returns The capitalized string. + */ +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +// Error handling wrapper +main().catch((error) => { + console.error('❌ Script failed:', error); + process.exitCode = 1; +}); diff --git a/yarn.lock b/yarn.lock index 99eb90ff4f..67a707b05f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4430,6 +4430,7 @@ __metadata: typescript: "npm:~5.3.3" typescript-eslint: "npm:^8.6.0" vite: "npm:^6.4.1" + yargs: "npm:^17.7.1" languageName: unknown linkType: soft From 4e04a91c9731e141de632e1b114090c16c9fa037 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Mon, 23 Mar 2026 14:48:39 +0100 Subject: [PATCH 02/17] refactor!: Standardise `CronjobController` action names and types (#3911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This renames all `CronjobController` action and event names and types to follow the `Controller...Action` pattern used in most other controllers. --- > [!NOTE] > **Medium Risk** > Primarily a type-level refactor but marked breaking because exported action type names and cronjob exports change; downstream TypeScript code and messenger typings may need updates. > > **Overview** > **Standardizes `CronjobController` messenger action typing.** The PR moves cronjob method action definitions into a new auto-generated `CronjobController-method-action-types.ts`, renaming the exported action types to the `CronjobController…Action` convention. > > `CronjobController` now wires its messenger via `registerMethodActionHandlers` with an explicit `MESSENGER_EXPOSED_METHODS` list, and narrows messenger generics by separating controller actions/events from externally *allowed* ones. Public exports from `cronjob/index.ts` are adjusted accordingly, and the changelog is updated to document the new breaking action type names (including `CronjobControllerScheduleAction`, `CronjobControllerCancelAction`, `CronjobControllerGetAction`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e5867a2364eb65066bdc694e2a4056fc646fdb82. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- packages/snaps-controllers/CHANGELOG.md | 65 +++++++++-------- .../CronjobController-method-action-types.ts | 60 ++++++++++++++++ .../src/cronjob/CronjobController.ts | 69 +++++-------------- .../snaps-controllers/src/cronjob/index.ts | 14 +++- .../src/snaps/SnapController.ts | 4 +- 5 files changed, 130 insertions(+), 82 deletions(-) create mode 100644 packages/snaps-controllers/src/cronjob/CronjobController-method-action-types.ts diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index 79c95c4809..ec8c8b8d95 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -9,36 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** All `SnapController` action types were renamed from `DoSomething` to `SnapControllerDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907)) - - `GetSnap` is now `SnapControllerGetSnapAction`. - - Note: The method is now called `getSnap` instead of `get`. - - `HandleSnapRequest` is now `SnapControllerHandleRequestAction`. - - `GetSnapState` is now `SnapControllerGetSnapStateAction`. - - `HasSnap` is now `SnapControllerHasSnapAction`. - - Note: The method is now called `hasSnap` instead of `has`. - - `UpdateSnapState` is now `SnapControllerUpdateSnapStateAction`. - - `ClearSnapState` is now `SnapControllerClearSnapStateAction`. - - `UpdateRegistry` is now `SnapControllerUpdateRegistryAction`. - - `EnableSnap` is now `SnapControllerEnableSnapAction`. - - Note: The method is now called `enableSnap` instead of `enable`. - - `DisableSnap` is now `SnapControllerDisableSnapAction`. - - Note: The method is now called `disableSnap` instead of `disable`. - - `RemoveSnap` is now `SnapControllerRemoveSnapAction`. - - Note: The method is now called `removeSnap` instead of `remove`. - - `GetPermittedSnaps` is now `SnapControllerGetPermittedSnapsAction`. - - Note: The method is now called `getPermittedSnaps` instead of `getPermitted`. - - `GetAllSnaps` is now `SnapControllerGetAllSnapsAction`. - - Note: The method is now called `getAllSnaps` instead of `getAll`. - - `GetRunnableSnaps` is now `SnapControllerGetRunnableSnapsAction`. - - `StopAllSnaps` is now `SnapControllerStopAllSnapsAction`. - - `InstallSnaps` is now `SnapControllerInstallSnapsAction`. - - Note: The method is now called `installSnaps` instead of `install`. - - `DisconnectOrigin` is now `SnapControllerDisconnectOriginAction`. - - Note: The method is now called `disconnectOrigin` instead of `removeSnapFromSubject`. - - `RevokeDynamicPermissions` is now `SnapControllerRevokeDynamicSnapPermissionsAction`. - - `GetSnapFile` is now `SnapControllerGetSnapFileAction`. - - `IsMinimumPlatformVersion` is now `SnapControllerIsMinimumPlatformVersionAction`. - - `SetClientActive` is now `SnapControllerSetClientActiveAction`. +- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911)) + - `SnapController` actions: + - `GetSnap` is now `SnapControllerGetSnapAction`. + - Note: The method is now called `getSnap` instead of `get`. + - `HandleSnapRequest` is now `SnapControllerHandleRequestAction`. + - `GetSnapState` is now `SnapControllerGetSnapStateAction`. + - `HasSnap` is now `SnapControllerHasSnapAction`. + - Note: The method is now called `hasSnap` instead of `has`. + - `UpdateSnapState` is now `SnapControllerUpdateSnapStateAction`. + - `ClearSnapState` is now `SnapControllerClearSnapStateAction`. + - `UpdateRegistry` is now `SnapControllerUpdateRegistryAction`. + - `EnableSnap` is now `SnapControllerEnableSnapAction`. + - Note: The method is now called `enableSnap` instead of `enable`. + - `DisableSnap` is now `SnapControllerDisableSnapAction`. + - Note: The method is now called `disableSnap` instead of `disable`. + - `RemoveSnap` is now `SnapControllerRemoveSnapAction`. + - Note: The method is now called `removeSnap` instead of `remove`. + - `GetPermittedSnaps` is now `SnapControllerGetPermittedSnapsAction`. + - Note: The method is now called `getPermittedSnaps` instead of `getPermitted`. + - `GetAllSnaps` is now `SnapControllerGetAllSnapsAction`. + - Note: The method is now called `getAllSnaps` instead of `getAll`. + - `GetRunnableSnaps` is now `SnapControllerGetRunnableSnapsAction`. + - `StopAllSnaps` is now `SnapControllerStopAllSnapsAction`. + - `InstallSnaps` is now `SnapControllerInstallSnapsAction`. + - Note: The method is now called `installSnaps` instead of `install`. + - `DisconnectOrigin` is now `SnapControllerDisconnectOriginAction`. + - Note: The method is now called `disconnectOrigin` instead of `removeSnapFromSubject`. + - `RevokeDynamicPermissions` is now `SnapControllerRevokeDynamicSnapPermissionsAction`. + - `GetSnapFile` is now `SnapControllerGetSnapFileAction`. + - `IsMinimumPlatformVersion` is now `SnapControllerIsMinimumPlatformVersionAction`. + - `SetClientActive` is now `SnapControllerSetClientActiveAction`. + - `CronjobController` actions: + - `Schedule` is now `CronjobControllerScheduleAction`. + - `Cancel` is now `CronjobControllerCancelAction`. + - `Get` is now `CronjobControllerGetAction`. - **BREAKING:** All `SnapController` event types were renamed from `OnSomething` to `SnapControllerOnSomethingEvent` ([#3907](https://github.com/MetaMask/snaps/pull/3907)) - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. - `SnapBlocked` is now `SnapControllerSnapBlockedEvent`. diff --git a/packages/snaps-controllers/src/cronjob/CronjobController-method-action-types.ts b/packages/snaps-controllers/src/cronjob/CronjobController-method-action-types.ts new file mode 100644 index 0000000000..162de24e67 --- /dev/null +++ b/packages/snaps-controllers/src/cronjob/CronjobController-method-action-types.ts @@ -0,0 +1,60 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { CronjobController } from './CronjobController'; + +/** + * Initialize the CronjobController. + * + * This starts the daily timer, clears out expired events + * and reschedules any remaining events. + */ +export type CronjobControllerInitAction = { + type: `CronjobController:init`; + handler: CronjobController['init']; +}; + +/** + * Schedule a non-recurring background event. + * + * @param event - The event to schedule. + * @returns The ID of the scheduled event. + */ +export type CronjobControllerScheduleAction = { + type: `CronjobController:schedule`; + handler: CronjobController['schedule']; +}; + +/** + * Cancel an event. + * + * @param origin - The origin making the cancel call. + * @param id - The id of the event to cancel. + * @throws If the event does not exist. + */ +export type CronjobControllerCancelAction = { + type: `CronjobController:cancel`; + handler: CronjobController['cancel']; +}; + +/** + * Get a list of a Snap's background events. + * + * @param snapId - The id of the Snap to fetch background events for. + * @returns An array of background events. + */ +export type CronjobControllerGetAction = { + type: `CronjobController:get`; + handler: CronjobController['get']; +}; + +/** + * Union of all CronjobController action types. + */ +export type CronjobControllerMethodActions = + | CronjobControllerInitAction + | CronjobControllerScheduleAction + | CronjobControllerCancelAction + | CronjobControllerGetAction; diff --git a/packages/snaps-controllers/src/cronjob/CronjobController.ts b/packages/snaps-controllers/src/cronjob/CronjobController.ts index c5914ca608..c59f4e2f7b 100644 --- a/packages/snaps-controllers/src/cronjob/CronjobController.ts +++ b/packages/snaps-controllers/src/cronjob/CronjobController.ts @@ -21,6 +21,7 @@ import { castDraft } from 'immer'; import { DateTime } from 'luxon'; import { nanoid } from 'nanoid'; +import type { CronjobControllerMethodActions } from './CronjobController-method-action-types'; import { getCronjobSpecificationSchedule, getExecutionDate } from './utils'; import type { SnapControllerHandleRequestAction, @@ -43,41 +44,15 @@ export type CronjobControllerStateChangeEvent = ControllerStateChangeEvent< CronjobControllerState >; -/** - * Initialise the CronjobController. This should be called after all controllers - * are created. - */ -export type CronjobControllerInitAction = { - type: `${typeof controllerName}:init`; - handler: CronjobController['init']; -}; - -export type Schedule = { - type: `${typeof controllerName}:schedule`; - handler: CronjobController['schedule']; -}; +export type CronjobControllerActions = + | CronjobControllerGetStateAction + | CronjobControllerMethodActions; -export type Cancel = { - type: `${typeof controllerName}:cancel`; - handler: CronjobController['cancel']; -}; +export type CronjobControllerEvents = CronjobControllerStateChangeEvent; -export type Get = { - type: `${typeof controllerName}:get`; - handler: CronjobController['get']; -}; +type AllowedActions = GetPermissions | SnapControllerHandleRequestAction; -export type CronjobControllerActions = - | CronjobControllerGetStateAction - | SnapControllerHandleRequestAction - | GetPermissions - | Schedule - | Cancel - | Get - | CronjobControllerInitAction; - -export type CronjobControllerEvents = - | CronjobControllerStateChangeEvent +type AllowedEvents = | SnapControllerSnapInstalledEvent | SnapControllerSnapUninstalledEvent | SnapControllerSnapUpdatedEvent @@ -86,8 +61,8 @@ export type CronjobControllerEvents = export type CronjobControllerMessenger = Messenger< typeof controllerName, - CronjobControllerActions, - CronjobControllerEvents + CronjobControllerActions | AllowedActions, + CronjobControllerEvents | AllowedEvents >; export const DAILY_TIMEOUT = inMilliseconds(24, Duration.Hour); @@ -157,6 +132,13 @@ export type CronjobControllerState = { const controllerName = 'CronjobController'; +const MESSENGER_EXPOSED_METHODS = [ + 'init', + 'schedule', + 'cancel', + 'get', +] as const; + /** * The cronjob controller is responsible for managing cronjobs and background * events for Snaps. It allows Snaps to schedule events that will be executed @@ -220,22 +202,9 @@ export class CronjobController extends BaseController< this.#handleSnapUpdatedEvent, ); - this.messenger.registerActionHandler(`${controllerName}:init`, (...args) => - this.init(...args), - ); - - this.messenger.registerActionHandler( - `${controllerName}:schedule`, - (...args) => this.schedule(...args), - ); - - this.messenger.registerActionHandler( - `${controllerName}:cancel`, - (...args) => this.cancel(...args), - ); - - this.messenger.registerActionHandler(`${controllerName}:get`, (...args) => - this.get(...args), + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } diff --git a/packages/snaps-controllers/src/cronjob/index.ts b/packages/snaps-controllers/src/cronjob/index.ts index ed316ed002..ad8d078a66 100644 --- a/packages/snaps-controllers/src/cronjob/index.ts +++ b/packages/snaps-controllers/src/cronjob/index.ts @@ -1 +1,13 @@ -export * from './CronjobController'; +export type { + CronjobControllerGetStateAction, + CronjobControllerState, + CronjobControllerStateChangeEvent, + CronjobControllerStateManager, +} from './CronjobController'; +export { CronjobController } from './CronjobController'; +export type { + CronjobControllerInitAction, + CronjobControllerScheduleAction, + CronjobControllerCancelAction, + CronjobControllerGetAction, +} from './CronjobController-method-action-types'; diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index af4aa5e5da..351df0cd2a 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -206,7 +206,9 @@ import { export const controllerName = 'SnapController'; -export const MESSENGER_EXPOSED_METHODS = [ +// This is used by the `generate-method-action-types` script. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const MESSENGER_EXPOSED_METHODS = [ 'init', 'updateRegistry', 'enableSnap', From 2799c4ce8daf555b37252a9284f6be3639e1b305 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Mon, 23 Mar 2026 15:38:39 +0100 Subject: [PATCH 03/17] refactor!: Standardise `SnapInterfaceController` action names and types (#3912) This renames all `SnapInterfaceController` action and event names and types to follow the `Controller...Action` pattern used in most other controllers. --- > [!NOTE] > **Medium Risk** > Breaking change that renames exported `SnapInterfaceController` action types and adjusts messenger handler registration; downstream packages relying on the old type names or exports will fail to compile. Runtime behavior should be unchanged but messaging wiring changes could affect integration if method exposure lists drift. > > **Overview** > **BREAKING:** Renames `SnapInterfaceController` action type aliases to the standardized `SnapInterfaceController...Action` naming scheme, updates `CHANGELOG`, and adjusts consumers (`SnapController`, `SnapInsightsController`, and `snaps-simulation`) to use the new types. > > Moves `SnapInterfaceController` method action type definitions into a new auto-generated `SnapInterfaceController-method-action-types.ts`, updates `interface/index.ts` exports, and replaces per-action `registerActionHandler` calls with `registerMethodActionHandlers` driven by a single `MESSENGER_EXPOSED_METHODS` list. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4cd791b7e1857e683954eb33dd7d1436464c56a1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- packages/snaps-controllers/CHANGELOG.md | 9 +- .../src/insights/SnapInsightsController.ts | 4 +- ...InterfaceController-method-action-types.ts | 116 ++++++++++++++++++ .../src/interface/SnapInterfaceController.ts | 112 +++-------------- .../snaps-controllers/src/interface/index.ts | 21 +++- .../src/snaps/SnapController.ts | 8 +- packages/snaps-simulation/src/controllers.ts | 2 +- 7 files changed, 168 insertions(+), 104 deletions(-) create mode 100644 packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index ec8c8b8d95..dd6302c997 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911)) +- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911), [#3912](https://github.com/MetaMask/snaps/pull/3912)) - `SnapController` actions: - `GetSnap` is now `SnapControllerGetSnapAction`. - Note: The method is now called `getSnap` instead of `get`. @@ -44,6 +44,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Schedule` is now `CronjobControllerScheduleAction`. - `Cancel` is now `CronjobControllerCancelAction`. - `Get` is now `CronjobControllerGetAction`. + - `SnapInterfaceController` actions: + - `CreateInterface` is now `SnapInterfaceControllerCreateInterfaceAction`. + - `GetInterface` is now `SnapInterfaceControllerGetInterfaceAction`. + - `UpdateInterface` is now `SnapInterfaceControllerUpdateInterfaceAction`. + - `DeleteInterface` is now `SnapInterfaceControllerDeleteInterfaceAction`. + - `UpdateInterfaceState` is now `SnapInterfaceControllerUpdateInterfaceStateAction`. + - `ResolveInterface` is now `SnapInterfaceControllerResolveInterfaceAction`. - **BREAKING:** All `SnapController` event types were renamed from `OnSomething` to `SnapControllerOnSomethingEvent` ([#3907](https://github.com/MetaMask/snaps/pull/3907)) - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. - `SnapBlocked` is now `SnapControllerSnapBlockedEvent`. diff --git a/packages/snaps-controllers/src/insights/SnapInsightsController.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.ts index 69dda216e8..057dd2860b 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.ts @@ -18,7 +18,7 @@ import type { Json, SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import { hasProperty, hexToBigInt } from '@metamask/utils'; -import type { DeleteInterface } from '../interface'; +import type { SnapInterfaceControllerDeleteInterfaceAction } from '../interface'; import type { SnapControllerGetAllSnapsAction, SnapControllerHandleRequestAction, @@ -39,7 +39,7 @@ export type SnapInsightsControllerAllowedActions = | SnapControllerHandleRequestAction | SnapControllerGetAllSnapsAction | GetPermissions - | DeleteInterface; + | SnapInterfaceControllerDeleteInterfaceAction; export type SnapInsightsControllerGetStateAction = ControllerGetStateAction< typeof controllerName, diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts new file mode 100644 index 0000000000..1641d798e4 --- /dev/null +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts @@ -0,0 +1,116 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { SnapInterfaceController } from './SnapInterfaceController'; + +/** + * Create an interface in the controller state with the associated data. + * + * @param snapId - The snap id that created the interface. + * @param content - The interface content. + * @param context - An optional interface context object. + * @param contentType - The type of content. + * @returns The newly interface id. + */ +export type SnapInterfaceControllerCreateInterfaceAction = { + type: `SnapInterfaceController:createInterface`; + handler: SnapInterfaceController['createInterface']; +}; + +/** + * Get the data of a given interface id. + * + * @param snapId - The snap id requesting the interface data. + * @param id - The interface id. + * @returns The interface state. + */ +export type SnapInterfaceControllerGetInterfaceAction = { + type: `SnapInterfaceController:getInterface`; + handler: SnapInterfaceController['getInterface']; +}; + +/** + * Get the state of a given interface ID, if the interface has been displayed + * at least once. + * + * @param snapId - The snap ID requesting the interface state. + * @param id - The interface ID. + * @returns The interface state. + */ +export type SnapInterfaceControllerGetInterfaceStateAction = { + type: `SnapInterfaceController:getInterfaceState`; + handler: SnapInterfaceController['getInterfaceState']; +}; + +/** + * Update the interface with the given content. + * + * @param snapId - The snap id requesting the update. + * @param id - The interface id. + * @param content - The new content. + * @param context - An optional interface context object. + */ +export type SnapInterfaceControllerUpdateInterfaceAction = { + type: `SnapInterfaceController:updateInterface`; + handler: SnapInterfaceController['updateInterface']; +}; + +/** + * Delete an interface from state. + * + * @param id - The interface id. + */ +export type SnapInterfaceControllerDeleteInterfaceAction = { + type: `SnapInterfaceController:deleteInterface`; + handler: SnapInterfaceController['deleteInterface']; +}; + +/** + * Update the interface state. + * + * @param id - The interface id. + * @param state - The new state. + */ +export type SnapInterfaceControllerUpdateInterfaceStateAction = { + type: `SnapInterfaceController:updateInterfaceState`; + handler: SnapInterfaceController['updateInterfaceState']; +}; + +/** + * Resolve the promise of a given interface approval request. + * The approval needs to have the same ID as the interface. + * + * @param snapId - The snap id. + * @param id - The interface id. + * @param value - The value to resolve the promise with. + */ +export type SnapInterfaceControllerResolveInterfaceAction = { + type: `SnapInterfaceController:resolveInterface`; + handler: SnapInterfaceController['resolveInterface']; +}; + +/** + * Set the interface as displayed. + * + * @param snapId - The snap ID requesting the update. + * @param id - The interface ID. + */ +export type SnapInterfaceControllerSetInterfaceDisplayedAction = { + type: `SnapInterfaceController:setInterfaceDisplayed`; + handler: SnapInterfaceController['setInterfaceDisplayed']; +}; + +/** + * Union of all SnapInterfaceController action types. + */ +export type SnapInterfaceControllerMethodActions = + | SnapInterfaceControllerCreateInterfaceAction + | SnapInterfaceControllerGetInterfaceAction + | SnapInterfaceControllerGetInterfaceStateAction + | SnapInterfaceControllerUpdateInterfaceAction + | SnapInterfaceControllerDeleteInterfaceAction + | SnapInterfaceControllerUpdateInterfaceStateAction + | SnapInterfaceControllerResolveInterfaceAction + | SnapInterfaceControllerSetInterfaceDisplayedAction; diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts index 574eccf619..d992660767 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts @@ -34,6 +34,7 @@ import { assert, hasProperty, parseCaipAccountId } from '@metamask/utils'; import { castDraft } from 'immer'; import { nanoid } from 'nanoid'; +import type { SnapInterfaceControllerMethodActions } from './SnapInterfaceController-method-action-types'; import { constructState, getJsxInterface, @@ -46,45 +47,16 @@ const MAX_UI_CONTENT_SIZE = 10_000_000; // 10 mb const controllerName = 'SnapInterfaceController'; -export type CreateInterface = { - type: `${typeof controllerName}:createInterface`; - handler: SnapInterfaceController['createInterface']; -}; - -export type GetInterface = { - type: `${typeof controllerName}:getInterface`; - handler: SnapInterfaceController['getInterface']; -}; - -export type UpdateInterface = { - type: `${typeof controllerName}:updateInterface`; - handler: SnapInterfaceController['updateInterface']; -}; - -export type DeleteInterface = { - type: `${typeof controllerName}:deleteInterface`; - handler: SnapInterfaceController['deleteInterface']; -}; - -export type UpdateInterfaceState = { - type: `${typeof controllerName}:updateInterfaceState`; - handler: SnapInterfaceController['updateInterfaceState']; -}; - -export type ResolveInterface = { - type: `${typeof controllerName}:resolveInterface`; - handler: SnapInterfaceController['resolveInterface']; -}; - -export type SnapInterfaceControllerGetInterfaceStateAction = { - type: `${typeof controllerName}:getInterfaceState`; - handler: SnapInterfaceController['getInterfaceState']; -}; - -export type SnapInterfaceControllerSetInterfaceDisplayedAction = { - type: `${typeof controllerName}:setInterfaceDisplayed`; - handler: SnapInterfaceController['setInterfaceDisplayed']; -}; +const MESSENGER_EXPOSED_METHODS = [ + 'createInterface', + 'getInterface', + 'getInterfaceState', + 'updateInterface', + 'deleteInterface', + 'updateInterfaceState', + 'resolveInterface', + 'setInterfaceDisplayed', +] as const; type AccountsControllerGetAccountByAddressAction = { type: `AccountsController:getAccountByAddress`; @@ -133,15 +105,8 @@ export type SnapInterfaceControllerAllowedActions = | HasPermission; export type SnapInterfaceControllerActions = - | CreateInterface - | GetInterface - | UpdateInterface - | DeleteInterface - | UpdateInterfaceState - | ResolveInterface - | SnapInterfaceControllerGetInterfaceStateAction - | SnapInterfaceControllerSetInterfaceDisplayedAction - | SnapInterfaceControllerGetStateAction; + | SnapInterfaceControllerGetStateAction + | SnapInterfaceControllerMethodActions; export type SnapInterfaceControllerStateChangeEvent = ControllerStateChangeEvent< @@ -151,7 +116,7 @@ export type SnapInterfaceControllerStateChangeEvent = type OtherNotification = { type: string; [key: string]: unknown }; -export type ExpandedView = { +type ExpandedView = { title: string; interfaceId: string; footerLink?: { href: string; text: string }; @@ -250,52 +215,9 @@ export class SnapInterfaceController extends BaseController< this.#onNotificationsListUpdated.bind(this), ); - this.#registerMessageHandlers(); - } - - /** - * Constructor helper for registering this controller's messaging system - * actions. - */ - #registerMessageHandlers() { - this.messenger.registerActionHandler( - `${controllerName}:createInterface`, - this.createInterface.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:getInterface`, - this.getInterface.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:getInterfaceState`, - this.getInterfaceState.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:updateInterface`, - this.updateInterface.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:deleteInterface`, - this.deleteInterface.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:updateInterfaceState`, - this.updateInterfaceState.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:resolveInterface`, - this.resolveInterface.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:setInterfaceDisplayed`, - this.setInterfaceDisplayed.bind(this), + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } diff --git a/packages/snaps-controllers/src/interface/index.ts b/packages/snaps-controllers/src/interface/index.ts index e3f329b66e..3abc18faf2 100644 --- a/packages/snaps-controllers/src/interface/index.ts +++ b/packages/snaps-controllers/src/interface/index.ts @@ -1 +1,20 @@ -export * from './SnapInterfaceController'; +export type { + SnapInterfaceControllerActions, + SnapInterfaceControllerAllowedActions, + SnapInterfaceControllerGetStateAction, + SnapInterfaceControllerState, + SnapInterfaceControllerStateChangeEvent, + StoredInterface, +} from './SnapInterfaceController'; +export { SnapInterfaceController } from './SnapInterfaceController'; +export type { + SnapInterfaceControllerCreateInterfaceAction, + SnapInterfaceControllerDeleteInterfaceAction, + SnapInterfaceControllerGetInterfaceAction, + SnapInterfaceControllerGetInterfaceStateAction, + SnapInterfaceControllerMethodActions, + SnapInterfaceControllerResolveInterfaceAction, + SnapInterfaceControllerSetInterfaceDisplayedAction, + SnapInterfaceControllerUpdateInterfaceAction, + SnapInterfaceControllerUpdateInterfaceStateAction, +} from './SnapInterfaceController-method-action-types'; diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 351df0cd2a..9a3b7e4aaf 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -175,8 +175,8 @@ import type { SnapControllerMethodActions } from './SnapController-method-action import { Timer } from './Timer'; import { forceStrict, validateMachine } from '../fsm'; import type { - CreateInterface, - GetInterface, + SnapInterfaceControllerCreateInterfaceAction, + SnapInterfaceControllerGetInterfaceAction, SnapInterfaceControllerSetInterfaceDisplayedAction, } from '../interface'; import { log } from '../logging'; @@ -539,8 +539,8 @@ export type AllowedActions = | GetMetadata | Update | ResolveVersion - | CreateInterface - | GetInterface + | SnapInterfaceControllerCreateInterfaceAction + | SnapInterfaceControllerGetInterfaceAction | SnapInterfaceControllerSetInterfaceDisplayedAction | StorageServiceSetItemAction | StorageServiceGetItemAction diff --git a/packages/snaps-simulation/src/controllers.ts b/packages/snaps-simulation/src/controllers.ts index 1f415bef17..4710aa642c 100644 --- a/packages/snaps-simulation/src/controllers.ts +++ b/packages/snaps-simulation/src/controllers.ts @@ -17,9 +17,9 @@ import { import { SnapInterfaceController } from '@metamask/snaps-controllers'; import type { ExecutionServiceActions, + SnapInterfaceControllerStateChangeEvent, SnapInterfaceControllerActions, SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerStateChangeEvent, } from '@metamask/snaps-controllers'; import { caveatSpecifications as snapsCaveatsSpecifications, From 254591e9abf90f06dd18c9376d0139aea1b6b159 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 24 Mar 2026 10:30:03 +0100 Subject: [PATCH 04/17] refactor!: Rename `MultichainRouter` to `MultichainRoutingService` and use messenger exposed methods pattern (#3913) This updates the `MultichainRouter` to use messenger exposed methods. Actions were already named properly. --- > [!NOTE] > **Medium Risk** > Breaking rename and action-type reshaping will require downstream updates, and the switch to `SnapController:getRunnableSnaps` changes the dependency contract for protocol snap routing. Runtime logic appears largely unchanged but touches request routing paths. > > **Overview** > Renames the multichain RPC router from `MultichainRouter` to `MultichainRoutingService` and updates exported types/namespace action strings accordingly (**breaking change**). > > Refactors messenger integration to use `registerMethodActionHandlers` with auto-generated method action types, and changes protocol-snap discovery to call `SnapController:getRunnableSnaps` directly (no longer requires `SnapController:getAllSnaps`). Tests and controller test-utils are updated to use the new messenger helpers and action names, and the changelog documents both breaking changes. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5b0d48282577a392a60cd769902db2e847bd606c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Frederik Bolding --- packages/snaps-controllers/CHANGELOG.md | 3 + ...chainRoutingService-method-action-types.ts | 70 ++++ ...st.ts => MultichainRoutingService.test.ts} | 301 +++++++++++------- ...nRouter.ts => MultichainRoutingService.ts} | 88 ++--- .../snaps-controllers/src/multichain/index.ts | 8 +- .../src/test-utils/controller.tsx | 22 +- 6 files changed, 305 insertions(+), 187 deletions(-) create mode 100644 packages/snaps-controllers/src/multichain/MultichainRoutingService-method-action-types.ts rename packages/snaps-controllers/src/multichain/{MultichainRouter.test.ts => MultichainRoutingService.test.ts} (66%) rename packages/snaps-controllers/src/multichain/{MultichainRouter.ts => MultichainRoutingService.ts} (84%) diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index dd6302c997..28ea6c8921 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -64,6 +64,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `SnapTerminated` is now `SnapControllerSnapTerminatedEvent`. - `SnapEnabled` is now `SnapControllerSnapEnabledEvent`. - `SnapDisabled` is now `SnapControllerSnapDisabledEvent`. +- **BREAKING:**: Rename `MultichainRouter` to `MultichainRoutingService` and update action types accordingly ([#3913](https://github.com/MetaMask/snaps/pull/3913)) + - This is consistent with the naming of other services. +- **BREAKING:** `MultichainRoutingService` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3913](https://github.com/MetaMask/snaps/pull/3913)) ### Removed diff --git a/packages/snaps-controllers/src/multichain/MultichainRoutingService-method-action-types.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingService-method-action-types.ts new file mode 100644 index 0000000000..e851e9c737 --- /dev/null +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingService-method-action-types.ts @@ -0,0 +1,70 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { MultichainRoutingService } from './MultichainRoutingService'; + +/** + * Handle an incoming JSON-RPC request tied to a specific scope by routing + * to either a protocol Snap or an account Snap. + * + * Note: Addresses are considered case-sensitive by the MultichainRoutingService as + * not all non-EVM chains are case-insensitive. + * + * @param options - An options bag. + * @param options.connectedAddresses - Addresses currently connected to the + * origin for the requested scope. + * @param options.origin - The origin of the RPC request. + * @param options.request - The JSON-RPC request. + * @param options.scope - The CAIP-2 scope for the request. + * @returns The response from the chosen Snap. + * @throws If no handler was found. + */ +export type MultichainRoutingServiceHandleRequestAction = { + type: `MultichainRoutingService:handleRequest`; + handler: MultichainRoutingService['handleRequest']; +}; + +/** + * Get a list of supported methods for a given scope. + * This combines both protocol and account Snaps supported methods. + * + * @param scope - The CAIP-2 scope. + * @returns A list of supported methods. + */ +export type MultichainRoutingServiceGetSupportedMethodsAction = { + type: `MultichainRoutingService:getSupportedMethods`; + handler: MultichainRoutingService['getSupportedMethods']; +}; + +/** + * Get a list of supported accounts for a given scope. + * + * @param scope - The CAIP-2 scope. + * @returns A list of CAIP-10 addresses. + */ +export type MultichainRoutingServiceGetSupportedAccountsAction = { + type: `MultichainRoutingService:getSupportedAccounts`; + handler: MultichainRoutingService['getSupportedAccounts']; +}; + +/** + * Determine whether a given CAIP-2 scope is supported by the router. + * + * @param scope - The CAIP-2 scope. + * @returns True if the router can service the scope, otherwise false. + */ +export type MultichainRoutingServiceIsSupportedScopeAction = { + type: `MultichainRoutingService:isSupportedScope`; + handler: MultichainRoutingService['isSupportedScope']; +}; + +/** + * Union of all MultichainRoutingService action types. + */ +export type MultichainRoutingServiceMethodActions = + | MultichainRoutingServiceHandleRequestAction + | MultichainRoutingServiceGetSupportedMethodsAction + | MultichainRoutingServiceGetSupportedAccountsAction + | MultichainRoutingServiceIsSupportedScopeAction; diff --git a/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingService.test.ts similarity index 66% rename from packages/snaps-controllers/src/multichain/MultichainRouter.test.ts rename to packages/snaps-controllers/src/multichain/MultichainRoutingService.test.ts index b0a0895edd..bbdb8d374f 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingService.test.ts @@ -4,11 +4,11 @@ import { MOCK_SNAP_ID, } from '@metamask/snaps-utils/test-utils'; -import { MultichainRouter } from './MultichainRouter'; +import { MultichainRoutingService } from './MultichainRoutingService'; import { METAMASK_ORIGIN } from '../snaps/constants'; import { - getMultichainRouterRootMessenger, - getRestrictedMultichainRouterMessenger, + getMultichainRoutingServiceRootMessenger, + getRestrictedMultichainRoutingServiceMessenger, BTC_CAIP2, BTC_CONNECTED_ACCOUNTS, MOCK_SOLANA_SNAP_PERMISSIONS, @@ -19,11 +19,12 @@ import { getMockWithSnapKeyring, } from '../test-utils'; -describe('MultichainRouter', () => { +describe('MultichainRoutingService', () => { describe('handleRequest', () => { it('can route signing requests to account Snaps without address resolution', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring({ submitRequest: jest.fn().mockResolvedValue({ txid: '53de51e2fa75c3cfa51132865f7d430138b1cd92a8f5267ec836ec565b422969', @@ -31,7 +32,7 @@ describe('MultichainRouter', () => { }); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -51,19 +52,22 @@ describe('MultichainRouter', () => { }, ); - const result = await messenger.call('MultichainRouter:handleRequest', { - origin: METAMASK_ORIGIN, - connectedAddresses: BTC_CONNECTED_ACCOUNTS, - scope: BTC_CAIP2, - request: { - jsonrpc: '2.0', - id: 1, - method: 'sendBitcoin', - params: { - message: 'foo', + const result = await messenger.call( + 'MultichainRoutingService:handleRequest', + { + origin: METAMASK_ORIGIN, + connectedAddresses: BTC_CONNECTED_ACCOUNTS, + scope: BTC_CAIP2, + request: { + jsonrpc: '2.0', + id: 1, + method: 'sendBitcoin', + params: { + message: 'foo', + }, }, }, - }); + ); expect(result).toStrictEqual({ txid: '53de51e2fa75c3cfa51132865f7d430138b1cd92a8f5267ec836ec565b422969', @@ -71,8 +75,9 @@ describe('MultichainRouter', () => { }); it('can route signing requests to account Snaps using address resolution', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring({ submitRequest: jest.fn().mockResolvedValue({ signature: '0x', @@ -80,7 +85,7 @@ describe('MultichainRouter', () => { }); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -105,30 +110,34 @@ describe('MultichainRouter', () => { }, ); - const result = await messenger.call('MultichainRouter:handleRequest', { - origin: METAMASK_ORIGIN, - connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, - scope: SOLANA_CAIP2, - request: { - jsonrpc: '2.0', - id: 1, - method: 'signAndSendTransaction', - params: { - message: 'foo', + const result = await messenger.call( + 'MultichainRoutingService:handleRequest', + { + origin: METAMASK_ORIGIN, + connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, + scope: SOLANA_CAIP2, + request: { + jsonrpc: '2.0', + id: 1, + method: 'signAndSendTransaction', + params: { + message: 'foo', + }, }, }, - }); + ); expect(result).toStrictEqual({ signature: '0x' }); }); it('disallows routing to unconnected accounts', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -149,7 +158,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: [], scope: SOLANA_CAIP2, @@ -166,12 +175,13 @@ describe('MultichainRouter', () => { }); it('can route protocol requests to protocol Snaps', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -181,9 +191,12 @@ describe('MultichainRouter', () => { () => [], ); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -198,16 +211,19 @@ describe('MultichainRouter', () => { }), ); - const result = await messenger.call('MultichainRouter:handleRequest', { - origin: METAMASK_ORIGIN, - connectedAddresses: [], - scope: SOLANA_CAIP2, - request: { - jsonrpc: '2.0', - id: 1, - method: 'getVersion', + const result = await messenger.call( + 'MultichainRoutingService:handleRequest', + { + origin: METAMASK_ORIGIN, + connectedAddresses: [], + scope: SOLANA_CAIP2, + request: { + jsonrpc: '2.0', + id: 1, + method: 'getVersion', + }, }, - }); + ); expect(result).toStrictEqual({ 'feature-set': 2891131721, @@ -237,12 +253,13 @@ describe('MultichainRouter', () => { }); it('throws if no suitable Snaps are found', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -252,12 +269,15 @@ describe('MultichainRouter', () => { () => [], ); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return []; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return []; + }, + ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: [], scope: SOLANA_CAIP2, @@ -271,12 +291,13 @@ describe('MultichainRouter', () => { }); it('throws if address resolution fails', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -303,7 +324,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, scope: SOLANA_CAIP2, @@ -320,12 +341,13 @@ describe('MultichainRouter', () => { }); it('throws if address resolution returns an address that isnt available', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -355,7 +377,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, scope: SOLANA_CAIP2, @@ -372,12 +394,13 @@ describe('MultichainRouter', () => { }); it(`throws if address resolution returns a lower case address that isn't available`, async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -406,7 +429,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, scope: SOLANA_CAIP2, @@ -425,19 +448,23 @@ describe('MultichainRouter', () => { describe('getSupportedMethods', () => { it('returns a set of both protocol and account Snap methods', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -450,24 +477,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedMethods', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedMethods', + SOLANA_CAIP2, + ), ).toStrictEqual(['signAndSendTransaction', 'getVersion']); }); it('handles lack of protocol Snaps', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -480,24 +514,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedMethods', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedMethods', + SOLANA_CAIP2, + ), ).toStrictEqual(['signAndSendTransaction']); }); it('handles lack of account Snaps', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -510,19 +551,23 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedMethods', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedMethods', + SOLANA_CAIP2, + ), ).toStrictEqual(['getVersion']); }); }); describe('getSupportedAccounts', () => { it('returns a set of accounts for the requested scope', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -533,7 +578,10 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedAccounts', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedAccounts', + SOLANA_CAIP2, + ), ).toStrictEqual([ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv', ]); @@ -542,19 +590,23 @@ describe('MultichainRouter', () => { describe('isSupportedScope', () => { it('returns true if an account Snap exists', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -567,24 +619,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:isSupportedScope', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:isSupportedScope', + SOLANA_CAIP2, + ), ).toBe(true); }); it('returns true if a protocol Snap exists', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -597,24 +656,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:isSupportedScope', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:isSupportedScope', + SOLANA_CAIP2, + ), ).toBe(true); }); it('returns false if no Snap is found', async () => { - const rootMessenger = getMultichainRouterRootMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return []; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return []; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -622,7 +688,10 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:isSupportedScope', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:isSupportedScope', + SOLANA_CAIP2, + ), ).toBe(false); }); }); diff --git a/packages/snaps-controllers/src/multichain/MultichainRouter.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts similarity index 84% rename from packages/snaps-controllers/src/multichain/MultichainRouter.ts rename to packages/snaps-controllers/src/multichain/MultichainRoutingService.ts index fefa0aa408..1f5fc7a983 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRouter.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts @@ -22,31 +22,11 @@ import { } from '@metamask/utils'; import { nanoid } from 'nanoid'; +import type { MultichainRoutingServiceMethodActions } from './MultichainRoutingService-method-action-types'; import type { - SnapControllerGetAllSnapsAction, + SnapControllerGetRunnableSnapsAction, SnapControllerHandleRequestAction, } from '../snaps'; -import { getRunnableSnaps } from '../snaps'; - -export type MultichainRouterHandleRequestAction = { - type: `${typeof name}:handleRequest`; - handler: MultichainRouter['handleRequest']; -}; - -export type MultichainRouterGetSupportedMethodsAction = { - type: `${typeof name}:getSupportedMethods`; - handler: MultichainRouter['getSupportedMethods']; -}; - -export type MultichainRouterGetSupportedAccountsAction = { - type: `${typeof name}:getSupportedAccounts`; - handler: MultichainRouter['getSupportedAccounts']; -}; - -export type MultichainRouterIsSupportedScopeAction = { - type: `${typeof name}:isSupportedScope`; - handler: MultichainRouter['isSupportedScope']; -}; type SnapKeyring = { submitRequest: (request: { @@ -68,27 +48,24 @@ export type AccountsControllerListMultichainAccountsAction = { handler: (chainId?: CaipChainId) => InternalAccount[]; }; -export type MultichainRouterActions = - | MultichainRouterHandleRequestAction - | MultichainRouterGetSupportedMethodsAction - | MultichainRouterGetSupportedAccountsAction - | MultichainRouterIsSupportedScopeAction; +export type MultichainRoutingServiceActions = + MultichainRoutingServiceMethodActions; -export type MultichainRouterAllowedActions = - | SnapControllerGetAllSnapsAction +export type MultichainRoutingServiceAllowedActions = + | SnapControllerGetRunnableSnapsAction | SnapControllerHandleRequestAction | GetPermissions | AccountsControllerListMultichainAccountsAction; -export type MultichainRouterEvents = never; +export type MultichainRoutingServiceEvents = never; -export type MultichainRouterMessenger = Messenger< +export type MultichainRoutingServiceMessenger = Messenger< typeof name, - MultichainRouterActions | MultichainRouterAllowedActions + MultichainRoutingServiceActions | MultichainRoutingServiceAllowedActions >; -export type MultichainRouterArgs = { - messenger: MultichainRouterMessenger; +export type MultichainRoutingServiceArgs = { + messenger: MultichainRoutingServiceMessenger; withSnapKeyring: WithSnapKeyringFunction; }; @@ -97,39 +74,31 @@ type ProtocolSnap = { methods: string[]; }; -const name = 'MultichainRouter'; +const name = 'MultichainRoutingService'; -export class MultichainRouter { +const MESSENGER_EXPOSED_METHODS = [ + 'handleRequest', + 'getSupportedMethods', + 'getSupportedAccounts', + 'isSupportedScope', +] as const; + +export class MultichainRoutingService { name: typeof name = name; state = null; - readonly #messenger: MultichainRouterMessenger; + readonly #messenger: MultichainRoutingServiceMessenger; readonly #withSnapKeyring: WithSnapKeyringFunction; - constructor({ messenger, withSnapKeyring }: MultichainRouterArgs) { + constructor({ messenger, withSnapKeyring }: MultichainRoutingServiceArgs) { this.#messenger = messenger; this.#withSnapKeyring = withSnapKeyring; - this.#messenger.registerActionHandler( - `${name}:handleRequest`, - async (...args) => this.handleRequest(...args), - ); - - this.#messenger.registerActionHandler( - `${name}:getSupportedMethods`, - (...args) => this.getSupportedMethods(...args), - ); - - this.#messenger.registerActionHandler( - `${name}:getSupportedAccounts`, - (...args) => this.getSupportedAccounts(...args), - ); - - this.#messenger.registerActionHandler( - `${name}:isSupportedScope`, - (...args) => this.isSupportedScope(...args), + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } @@ -263,8 +232,9 @@ export class MultichainRouter { * @returns A list of all the protocol Snaps available and their RPC methods. */ #getProtocolSnaps(scope: CaipChainId) { - const allSnaps = this.#messenger.call('SnapController:getAllSnaps'); - const filteredSnaps = getRunnableSnaps(allSnaps); + const filteredSnaps = this.#messenger.call( + 'SnapController:getRunnableSnaps', + ); return filteredSnaps.reduce((accumulator, snap) => { const permissions = this.#messenger.call( @@ -291,7 +261,7 @@ export class MultichainRouter { * Handle an incoming JSON-RPC request tied to a specific scope by routing * to either a protocol Snap or an account Snap. * - * Note: Addresses are considered case-sensitive by the MultichainRouter as + * Note: Addresses are considered case-sensitive by the MultichainRoutingService as * not all non-EVM chains are case-insensitive. * * @param options - An options bag. diff --git a/packages/snaps-controllers/src/multichain/index.ts b/packages/snaps-controllers/src/multichain/index.ts index 8c696c379e..7a1ce9328d 100644 --- a/packages/snaps-controllers/src/multichain/index.ts +++ b/packages/snaps-controllers/src/multichain/index.ts @@ -1 +1,7 @@ -export * from './MultichainRouter'; +export { MultichainRoutingService } from './MultichainRoutingService'; +export type { + MultichainRoutingServiceGetSupportedAccountsAction, + MultichainRoutingServiceGetSupportedMethodsAction, + MultichainRoutingServiceHandleRequestAction, + MultichainRoutingServiceIsSupportedScopeAction, +} from './MultichainRoutingService-method-action-types'; diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index ef26f2925d..42c735cf56 100644 --- a/packages/snaps-controllers/src/test-utils/controller.tsx +++ b/packages/snaps-controllers/src/test-utils/controller.tsx @@ -54,13 +54,13 @@ import type { Json } from '@metamask/utils'; import { MOCK_CRONJOB_PERMISSION } from './cronjob'; import { getNodeEES, getNodeEESMessenger } from './execution-environment'; import { MockSnapsRegistry } from './registry'; -import type { CronjobControllerMessenger } from '../cronjob'; +import type { CronjobControllerMessenger } from '../cronjob/CronjobController'; import type { SnapInsightsControllerMessenger } from '../insights'; import type { SnapInterfaceControllerMessenger, StoredInterface, } from '../interface/SnapInterfaceController'; -import type { MultichainRouterMessenger } from '../multichain'; +import type { MultichainRoutingServiceMessenger } from '../multichain/MultichainRoutingService'; import type { AbstractExecutionService, ExecutionServiceMessenger, @@ -986,14 +986,14 @@ export async function waitForStateChange( }); } -type MultichainRouterRootMessenger = Messenger< +type MultichainRoutingServiceRootMessenger = Messenger< MockAnyNamespace, - MessengerActions + MessengerActions >; // Mock controller messenger for Multichain Router -export const getMultichainRouterRootMessenger = () => { - const messenger: MultichainRouterRootMessenger = +export const getMultichainRoutingServiceRootMessenger = () => { + const messenger: MultichainRoutingServiceRootMessenger = new MockControllerMessenger(); jest.spyOn(messenger, 'call'); @@ -1001,18 +1001,18 @@ export const getMultichainRouterRootMessenger = () => { return messenger; }; -export const getRestrictedMultichainRouterMessenger = ( - messenger: MultichainRouterRootMessenger = getMultichainRouterRootMessenger(), +export const getRestrictedMultichainRoutingServiceMessenger = ( + messenger: MultichainRoutingServiceRootMessenger = getMultichainRoutingServiceRootMessenger(), ) => { - const controllerMessenger: MultichainRouterMessenger = new Messenger({ - namespace: 'MultichainRouter', + const controllerMessenger: MultichainRoutingServiceMessenger = new Messenger({ + namespace: 'MultichainRoutingService', parent: messenger, }); messenger.delegate({ actions: [ 'PermissionController:getPermissions', - 'SnapController:getAllSnaps', + 'SnapController:getRunnableSnaps', 'SnapController:handleRequest', 'AccountsController:listMultichainAccounts', ], From 2b927513a4f3fd74fa92412e1eb01186c21f7b89 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 24 Mar 2026 10:52:17 +0100 Subject: [PATCH 05/17] refactor!: Update `SnapInsightsController` to use `SnapController:getRunnableSnaps` (#3915) This updates `SnapInsightsController` to use `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps`. --- > [!NOTE] > **Medium Risk** > Introduces a breaking change to `SnapInsightsController`'s messenger dependencies by switching from `SnapController:getAllSnaps` to `SnapController:getRunnableSnaps`, which could affect clients/wiring and which snaps are queried for insights. > > **Overview** > `SnapInsightsController` now queries Snaps via `SnapController:getRunnableSnaps` instead of fetching all Snaps and filtering locally, updating its allowed action types accordingly. > > Tests and controller test-utils were updated to delegate/mock `getRunnableSnaps`, and the `SignatureController` state-change event type was renamed/propagated to `SignatureControllerStateChangeEvent`. The changelog documents the **breaking** dependency update, and `insights/index.ts` now uses explicit type/value exports instead of `export *`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6678cea8a88e3ad3ce437f1768ab3f057cf65d9e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- packages/snaps-controllers/CHANGELOG.md | 1 + .../insights/SnapInsightsController.test.ts | 72 ++++++++++++++----- .../src/insights/SnapInsightsController.ts | 16 ++--- .../snaps-controllers/src/insights/index.ts | 7 +- .../src/test-utils/controller.tsx | 2 +- .../src/types/controllers.ts | 2 +- 6 files changed, 71 insertions(+), 29 deletions(-) diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index 28ea6c8921..af67fef689 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:**: Rename `MultichainRouter` to `MultichainRoutingService` and update action types accordingly ([#3913](https://github.com/MetaMask/snaps/pull/3913)) - This is consistent with the naming of other services. - **BREAKING:** `MultichainRoutingService` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3913](https://github.com/MetaMask/snaps/pull/3913)) +- **BREAKING:** `SnapInsightsController` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3915](https://github.com/MetaMask/snaps/pull/3915)) ### Removed diff --git a/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts index 5dfee842aa..ee23a5fd30 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts @@ -31,9 +31,15 @@ describe('SnapInsightsController', () => { }, ); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -157,9 +163,15 @@ describe('SnapInsightsController', () => { }, ); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -285,9 +297,15 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -388,9 +406,15 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -456,9 +480,15 @@ describe('SnapInsightsController', () => { it('ignores insight if transaction has already been signed', async () => { const rootMessenger = getRootSnapInsightsControllerMessenger(); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); const { resolve, promise } = createDeferredPromise(); @@ -556,9 +586,15 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAllSnaps', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); const { resolve, promise } = createDeferredPromise(); diff --git a/packages/snaps-controllers/src/insights/SnapInsightsController.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.ts index 057dd2860b..9517252bda 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.ts @@ -20,14 +20,13 @@ import { hasProperty, hexToBigInt } from '@metamask/utils'; import type { SnapInterfaceControllerDeleteInterfaceAction } from '../interface'; import type { - SnapControllerGetAllSnapsAction, + SnapControllerGetRunnableSnapsAction, SnapControllerHandleRequestAction, } from '../snaps'; -import { getRunnableSnaps } from '../snaps'; import type { TransactionControllerUnapprovedTransactionAddedEvent, TransactionMeta, - SignatureStateChange, + SignatureControllerStateChangeEvent, SignatureControllerState, StateSignature, TransactionControllerTransactionStatusUpdatedEvent, @@ -36,9 +35,9 @@ import type { const controllerName = 'SnapInsightsController'; export type SnapInsightsControllerAllowedActions = - | SnapControllerHandleRequestAction - | SnapControllerGetAllSnapsAction | GetPermissions + | SnapControllerGetRunnableSnapsAction + | SnapControllerHandleRequestAction | SnapInterfaceControllerDeleteInterfaceAction; export type SnapInsightsControllerGetStateAction = ControllerGetStateAction< @@ -59,7 +58,7 @@ export type SnapInsightControllerEvents = SnapInsightControllerStateChangeEvent; export type SnapInsightsControllerAllowedEvents = | TransactionControllerUnapprovedTransactionAddedEvent | TransactionControllerTransactionStatusUpdatedEvent - | SignatureStateChange; + | SignatureControllerStateChangeEvent; export type SnapInsightsControllerMessenger = Messenger< typeof controllerName, @@ -146,8 +145,9 @@ export class SnapInsightsController extends BaseController< * @returns A list of objects containing Snap IDs and the permission object. */ #getSnapsWithPermission(permissionName: string) { - const allSnaps = this.messenger.call('SnapController:getAllSnaps'); - const filteredSnaps = getRunnableSnaps(allSnaps); + const filteredSnaps = this.messenger.call( + 'SnapController:getRunnableSnaps', + ); return filteredSnaps.reduce((accumulator, snap) => { const permissions = this.messenger.call( diff --git a/packages/snaps-controllers/src/insights/index.ts b/packages/snaps-controllers/src/insights/index.ts index 2cd0140cc6..d0d0fa10c5 100644 --- a/packages/snaps-controllers/src/insights/index.ts +++ b/packages/snaps-controllers/src/insights/index.ts @@ -1 +1,6 @@ -export * from './SnapInsightsController'; +export type { + SnapInsight, + SnapInsightsControllerMessenger, + SnapInsightsControllerState, +} from './SnapInsightsController'; +export { SnapInsightsController } from './SnapInsightsController'; diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index 42c735cf56..b166da106c 100644 --- a/packages/snaps-controllers/src/test-utils/controller.tsx +++ b/packages/snaps-controllers/src/test-utils/controller.tsx @@ -953,7 +953,7 @@ export const getRestrictedSnapInsightsControllerMessenger = ( messenger.delegate({ actions: [ 'PermissionController:getPermissions', - 'SnapController:getAllSnaps', + 'SnapController:getRunnableSnaps', 'SnapController:handleRequest', 'SnapInterfaceController:deleteInterface', ], diff --git a/packages/snaps-controllers/src/types/controllers.ts b/packages/snaps-controllers/src/types/controllers.ts index 1d24aab183..20a1085e92 100644 --- a/packages/snaps-controllers/src/types/controllers.ts +++ b/packages/snaps-controllers/src/types/controllers.ts @@ -155,7 +155,7 @@ export type SignatureControllerState = { unapprovedTypedMessagesCount: number; }; -export type SignatureStateChange = ControllerStateChangeEvent< +export type SignatureControllerStateChangeEvent = ControllerStateChangeEvent< 'SignatureController', SignatureControllerState >; From 95d127dd23647ff7f0e553173e285d7dde17cf9c Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 24 Mar 2026 15:31:42 +0100 Subject: [PATCH 06/17] refactor!: Standardise `ExecutionService` action names and types (#3916) This renames all `ExecutionService` action and event names and types to follow the `Service...Action` pattern used in most other services. --- > [!NOTE] > **Medium Risk** > Breaking API/type changes to `ExecutionService` and its messenger actions/events may require coordinated updates across consumers; runtime logic is largely moved/renamed but touches snap execution/termination pathways. > > **Overview** > Refactors `ExecutionService` to match the standard `ServiceName...Action` / `ServiceName...Event` naming pattern, including new auto-generated `ExecutionService-method-action-types.ts` and renamed event types (e.g. `ExecutionServiceUnhandledErrorEvent`, `ExecutionServiceOutboundRequestEvent`). > > Replaces the old `ExecutionService` interface + `AbstractExecutionService` base class by making `ExecutionService` the abstract base class, updating all concrete implementations (iframe/offscreen/proxy/webview/node) and public exports accordingly, and adjusting tests/simulation/jest helpers to use the new types and messenger handler registration. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit deff4712265c73acb5545978d65db23f8f290dcf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Frederik Bolding --- packages/snaps-controllers/CHANGELOG.md | 43 +- .../src/services/AbstractExecutionService.ts | 524 ---------------- .../ExecutionService-method-action-types.ts | 59 ++ ...rvice.test.ts => ExecutionService.test.ts} | 4 +- .../src/services/ExecutionService.ts | 571 ++++++++++++++++-- .../src/services/browser.test.ts | 2 +- .../snaps-controllers/src/services/browser.ts | 20 +- .../services/iframe/IframeExecutionService.ts | 6 +- .../snaps-controllers/src/services/index.ts | 19 +- .../node-js/NodeProcessExecutionService.ts | 6 +- .../node-js/NodeThreadExecutionService.ts | 6 +- .../offscreen/OffscreenExecutionService.ts | 2 +- .../services/proxy/ProxyExecutionService.ts | 6 +- .../webview/WebViewExecutionService.ts | 6 +- .../src/snaps/SnapController.test.tsx | 45 +- .../src/snaps/SnapController.ts | 12 +- .../src/test-utils/controller.tsx | 12 +- .../src/test-utils/execution-environment.ts | 5 +- .../src/test-utils/service.ts | 10 +- packages/snaps-jest/src/environment.ts | 4 +- packages/snaps-jest/src/helpers.ts | 18 +- packages/snaps-simulation/src/request.ts | 4 +- packages/snaps-simulation/src/simulation.ts | 18 +- 23 files changed, 712 insertions(+), 690 deletions(-) delete mode 100644 packages/snaps-controllers/src/services/AbstractExecutionService.ts create mode 100644 packages/snaps-controllers/src/services/ExecutionService-method-action-types.ts rename packages/snaps-controllers/src/services/{AbstractExecutionService.test.ts => ExecutionService.test.ts} (97%) diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index af67fef689..cc6ae41682 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911), [#3912](https://github.com/MetaMask/snaps/pull/3912)) +- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911), [#3912](https://github.com/MetaMask/snaps/pull/3912), [#3916](https://github.com/MetaMask/snaps/pull/3916)) - `SnapController` actions: - `GetSnap` is now `SnapControllerGetSnapAction`. - Note: The method is now called `getSnap` instead of `get`. @@ -51,27 +51,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `DeleteInterface` is now `SnapInterfaceControllerDeleteInterfaceAction`. - `UpdateInterfaceState` is now `SnapInterfaceControllerUpdateInterfaceStateAction`. - `ResolveInterface` is now `SnapInterfaceControllerResolveInterfaceAction`. -- **BREAKING:** All `SnapController` event types were renamed from `OnSomething` to `SnapControllerOnSomethingEvent` ([#3907](https://github.com/MetaMask/snaps/pull/3907)) - - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. - - `SnapBlocked` is now `SnapControllerSnapBlockedEvent`. - - `SnapInstallStarted` is now `SnapControllerSnapInstallStartedEvent`. - - `SnapInstallFailed` is now `SnapControllerSnapInstallFailedEvent`. - - `SnapInstalled` is now `SnapControllerSnapInstalledEvent`. - - `SnapUninstalled` is now `SnapControllerSnapUninstalledEvent`. - - `SnapUnblocked` is now `SnapControllerSnapUnblockedEvent. - - `SnapUpdated` is now `SnapControllerSnapUpdatedEvent`. - - `SnapRolledback` is now `SnapControllerSnapRolledbackEvent`. - - `SnapTerminated` is now `SnapControllerSnapTerminatedEvent`. - - `SnapEnabled` is now `SnapControllerSnapEnabledEvent`. - - `SnapDisabled` is now `SnapControllerSnapDisabledEvent`. + - `ExecutionService` actions: + - `ExecuteSnap` is now `ExecutionServiceExecuteSnapAction`. + - `HandleRequest` is now `ExecutionServiceHandleRequestAction`. + - `TerminateSnap` is now `ExecutionServiceTerminateSnapAction`. + - `GetExecutionStatus` is now `ExecutionServiceGetExecutionStatusAction`. +- **BREAKING:** All event types were renamed from `OnSomething` to `ControllerOnSomethingEvent` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3916](https://github.com/MetaMask/snaps/pull/3916)) + - `SnapController` events: + - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. + - `SnapBlocked` is now `SnapControllerSnapBlockedEvent`. + - `SnapInstallStarted` is now `SnapControllerSnapInstallStartedEvent`. + - `SnapInstallFailed` is now `SnapControllerSnapInstallFailedEvent`. + - `SnapInstalled` is now `SnapControllerSnapInstalledEvent`. + - `SnapUninstalled` is now `SnapControllerSnapUninstalledEvent`. + - `SnapUnblocked` is now `SnapControllerSnapUnblockedEvent`. + - `SnapUpdated` is now `SnapControllerSnapUpdatedEvent`. + - `SnapRolledback` is now `SnapControllerSnapRolledbackEvent`. + - `SnapTerminated` is now `SnapControllerSnapTerminatedEvent`. + - `SnapEnabled` is now `SnapControllerSnapEnabledEvent`. + - `SnapDisabled` is now `SnapControllerSnapDisabledEvent`. + - `ExecutionService` events: + - `ErrorMessageEvent` is now `ExecutionServiceUnhandledErrorEvent`. + - `OutboundRequest` is now `ExecutionServiceOutboundRequestEvent`. + - `OutboundResponse` is now `ExecutionServiceOutboundResponseEvent`. - **BREAKING:**: Rename `MultichainRouter` to `MultichainRoutingService` and update action types accordingly ([#3913](https://github.com/MetaMask/snaps/pull/3913)) - This is consistent with the naming of other services. - **BREAKING:** `MultichainRoutingService` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3913](https://github.com/MetaMask/snaps/pull/3913)) - **BREAKING:** `SnapInsightsController` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3915](https://github.com/MetaMask/snaps/pull/3915)) +- **RREAKING:** Replace `ExecutionService` interface with abstract class ([#3916](https://github.com/MetaMask/snaps/pull/3916)) + - The `ExecutionService` is now an abstract class and replaces the previous `AbstractExecutionService` class interface. ### Removed -- **BREAKING:** `incrementActiveReferences` and `decrementActiveReferences` actions were removed ([#3907](https://github.com/MetaMask/snaps/pull/3907)) +- **RREAKING:** Remove `AbstractExecutionService` class in favour of `ExecutionService` ([#3916](https://github.com/MetaMask/snaps/pull/3916)) +- **BREAKING:** Remove `incrementActiveReferences` and `decrementActiveReferences` actions ([#3907](https://github.com/MetaMask/snaps/pull/3907)) ## [18.0.4] diff --git a/packages/snaps-controllers/src/services/AbstractExecutionService.ts b/packages/snaps-controllers/src/services/AbstractExecutionService.ts deleted file mode 100644 index 4e8acf59f8..0000000000 --- a/packages/snaps-controllers/src/services/AbstractExecutionService.ts +++ /dev/null @@ -1,524 +0,0 @@ -import { asV2Middleware } from '@metamask/json-rpc-engine'; -import { JsonRpcEngineV2 as JsonRpcEngine } from '@metamask/json-rpc-engine/v2'; -import { createStreamMiddleware } from '@metamask/json-rpc-middleware-stream'; -import ObjectMultiplex from '@metamask/object-multiplex'; -import type { BasePostMessageStream } from '@metamask/post-message-stream'; -import type { SnapRpcHookArgs } from '@metamask/snaps-utils'; -import { SNAP_STREAM_NAMES, logError, logWarning } from '@metamask/snaps-utils'; -import type { - Json, - JsonRpcNotification, - JsonRpcRequest, -} from '@metamask/utils'; -import { - Duration, - assertIsJsonRpcRequest, - hasProperty, - inMilliseconds, -} from '@metamask/utils'; -import { nanoid } from 'nanoid'; -import { pipeline } from 'readable-stream'; -import type { Duplex } from 'readable-stream'; - -import type { - ExecutionService, - ExecutionServiceMessenger, - SnapErrorJson, - SnapExecutionData, -} from './ExecutionService'; -import { log } from '../logging'; -import { Timer } from '../snaps/Timer'; -import { hasTimedOut, withTimeout } from '../utils'; - -const controllerName = 'ExecutionService'; - -export type SetupSnapProvider = (snapId: string, stream: Duplex) => void; - -export type ExecutionServiceArgs = { - setupSnapProvider: SetupSnapProvider; - messenger: ExecutionServiceMessenger; - initTimeout?: number; - pingTimeout?: number; - terminationTimeout?: number; - usePing?: boolean; -}; - -export type JobStreams = { - command: Duplex; - rpc: Duplex; - connection: BasePostMessageStream; - mux: ObjectMultiplex; -}; - -export type Job = { - id: string; - streams: JobStreams; - rpcEngine: JsonRpcEngine; - worker: WorkerType; -}; - -export type TerminateJobArgs = Partial> & - Pick, 'id'>; - -/** - Statuses used for diagnostic purposes - - created: The initial state, no initialization has started - - initializing: Snap execution environment is initializing - - initialized: Snap execution environment has initialized - - executing: Snap source code is being executed - - running: Snap executed and ready for RPC requests - */ -type ExecutionStatus = - | 'created' - | 'initializing' - | 'initialized' - | 'executing' - | 'running'; - -export abstract class AbstractExecutionService - implements ExecutionService -{ - name: typeof controllerName = controllerName; - - state = null; - - readonly #jobs: Map>; - - readonly #status: Map; - - readonly #setupSnapProvider: SetupSnapProvider; - - readonly #messenger: ExecutionServiceMessenger; - - readonly #initTimeout: number; - - readonly #pingTimeout: number; - - readonly #terminationTimeout: number; - - readonly #usePing: boolean; - - constructor({ - setupSnapProvider, - messenger, - initTimeout = inMilliseconds(60, Duration.Second), - pingTimeout = inMilliseconds(10, Duration.Second), - terminationTimeout = inMilliseconds(1, Duration.Second), - usePing = true, - }: ExecutionServiceArgs) { - this.#jobs = new Map(); - this.#status = new Map(); - this.#setupSnapProvider = setupSnapProvider; - this.#messenger = messenger; - this.#initTimeout = initTimeout; - this.#pingTimeout = pingTimeout; - this.#terminationTimeout = terminationTimeout; - this.#usePing = usePing; - - this.#registerMessageHandlers(); - } - - /** - * Constructor helper for registering the controller's messaging system - * actions. - */ - #registerMessageHandlers(): void { - this.#messenger.registerActionHandler( - `${controllerName}:handleRpcRequest`, - async (snapId: string, options: SnapRpcHookArgs) => - this.handleRpcRequest(snapId, options), - ); - - this.#messenger.registerActionHandler( - `${controllerName}:executeSnap`, - async (data: SnapExecutionData) => this.executeSnap(data), - ); - - this.#messenger.registerActionHandler( - `${controllerName}:terminateSnap`, - async (snapId: string) => this.terminateSnap(snapId), - ); - - this.#messenger.registerActionHandler( - `${controllerName}:terminateAllSnaps`, - async () => this.terminateAllSnaps(), - ); - } - - /** - * Performs additional necessary work during job termination. **MUST** be - * implemented by concrete implementations. See - * {@link AbstractExecutionService.terminate} for details. - * - * @param job - The object corresponding to the job to be terminated. - */ - protected abstract terminateJob( - job: TerminateJobArgs, - ): Promise; - - /** - * Terminates the Snap with the specified ID and deletes all its associated - * data. Any subsequent messages targeting the Snap will fail with an error. - * Throws an error if termination fails unexpectedly. - * - * @param snapId - The id of the Snap to be terminated. - */ - public async terminateSnap(snapId: string): Promise { - const job = this.#jobs.get(snapId); - if (!job) { - return; - } - - try { - // Ping worker and tell it to run teardown, continue with termination if it takes too long - const result = await withTimeout( - this.#command(snapId, { - jsonrpc: '2.0', - method: 'terminate', - id: nanoid(), - }), - this.#terminationTimeout, - ); - - if (result === hasTimedOut || result !== 'OK') { - logWarning(`Snap "${snapId}" failed to terminate gracefully.`); - } - } catch { - // Ignore - } - - Object.values(job.streams).forEach((stream) => { - try { - if (!stream.destroyed) { - stream.destroy(); - } - } catch (error) { - logError('Error while destroying stream', error); - } - }); - - await this.terminateJob(job); - - this.#jobs.delete(snapId); - this.#status.delete(snapId); - log(`Snap "${snapId}" terminated.`); - } - - /** - * Initiates a job for a Snap. - * - * @param snapId - The ID of the Snap to initiate a job for. - * @param timer - The timer to use for timeouts. - * @returns Information regarding the created job. - * @throws If the execution service returns an error or execution times out. - */ - async #initJob(snapId: string, timer: Timer): Promise> { - const { streams, worker } = await this.#initStreams(snapId, timer); - - const jsonRpcConnection = createStreamMiddleware(); - - pipeline( - jsonRpcConnection.stream, - streams.command, - jsonRpcConnection.stream, - (error) => { - if (error && !error.message?.match('Premature close')) { - logError(`Command stream failure.`, error); - } - }, - ); - - const rpcEngine = JsonRpcEngine.create({ - middleware: [asV2Middleware(jsonRpcConnection.middleware)], - }); - - const envMetadata = { - id: snapId, - streams, - rpcEngine, - worker, - }; - this.#jobs.set(snapId, envMetadata); - - return envMetadata; - } - - /** - * Sets up the streams for an initiated job. - * - * @param snapId - The Snap ID. - * @param timer - The timer to use for timeouts. - * @returns The streams to communicate with the worker and the worker itself. - * @throws If the execution service returns an error or execution times out. - */ - async #initStreams( - snapId: string, - timer: Timer, - ): Promise<{ streams: JobStreams; worker: WorkerType }> { - const result = await withTimeout(this.initEnvStream(snapId), timer); - - if (result === hasTimedOut) { - // For certain environments, such as the iframe we may have already created the worker and wish to terminate it. - await this.terminateJob({ id: snapId }); - - const status = this.#status.get(snapId); - if (status === 'created') { - // Currently this error can only be thrown by OffscreenExecutionService. - throw new Error( - `The executor for "${snapId}" couldn't start initialization. The offscreen document may not exist.`, - ); - } - throw new Error( - `The executor for "${snapId}" failed to initialize. The iframe/webview/worker failed to load.`, - ); - } - - const { worker, stream: envStream } = result; - const mux = setupMultiplex(envStream, `Snap: "${snapId}"`); - const commandStream = mux.createStream(SNAP_STREAM_NAMES.COMMAND); - - // Handle out-of-band errors, i.e. errors thrown from the Snap outside of the req/res cycle. - // Also keep track of outbound request/responses - const notificationHandler = ( - message: - | JsonRpcRequest - | JsonRpcNotification>, - ) => { - if (hasProperty(message, 'id')) { - return; - } - - if (message.method === 'OutboundRequest') { - this.#messenger.publish('ExecutionService:outboundRequest', snapId); - } else if (message.method === 'OutboundResponse') { - this.#messenger.publish('ExecutionService:outboundResponse', snapId); - } else if (message.method === 'UnhandledError') { - this.#messenger.publish( - 'ExecutionService:unhandledError', - snapId, - (message.params as { error: SnapErrorJson }).error, - ); - commandStream.removeListener('data', notificationHandler); - } else { - logError( - new Error( - `Received unexpected command stream notification "${message.method}".`, - ), - ); - } - }; - - commandStream.on('data', notificationHandler); - - const rpcStream = mux - .createStream(SNAP_STREAM_NAMES.JSON_RPC) - .setMaxListeners(20); - - rpcStream.on('data', (chunk) => { - if (chunk?.data && hasProperty(chunk?.data, 'id')) { - this.#messenger.publish('ExecutionService:outboundRequest', snapId); - } - }); - - // An error handler is not attached to the RPC stream until `setupSnapProvider` is called. - // We must set it up here to prevent errors from bubbling up if the stream is destroyed before then. - rpcStream.on('error', (error) => { - if (error && !error.message?.match('Premature close')) { - logError(`Snap: "${snapId}" - RPC stream failure:`, error); - } - }); - - const originalWrite = rpcStream.write.bind(rpcStream); - - // @ts-expect-error Hack to inspect the messages being written to the stream. - rpcStream.write = (chunk, encoding, callback) => { - // Ignore chain switching notifications as it doesn't matter for the SnapProvider. - if (chunk?.data?.method === 'metamask_chainChanged') { - return true; - } - - if (chunk?.data && hasProperty(chunk?.data, 'id')) { - this.#messenger.publish('ExecutionService:outboundResponse', snapId); - } - - return originalWrite(chunk, encoding, callback); - }; - - return { - streams: { - command: commandStream, - rpc: rpcStream, - connection: envStream, - mux, - }, - worker, - }; - } - - /** - * Abstract function implemented by implementing class that spins up a new worker for a job. - * - * Depending on the execution environment, this may run forever if the Snap fails to start up properly, therefore any call to this function should be wrapped in a timeout. - */ - protected abstract initEnvStream(snapId: string): Promise<{ - worker: WorkerType; - stream: BasePostMessageStream; - }>; - - /** - * Set the execution status of the Snap. - * - * @param snapId - The Snap ID. - * @param status - The current execution status. - */ - protected setSnapStatus(snapId: string, status: ExecutionStatus) { - this.#status.set(snapId, status); - } - - async terminateAllSnaps() { - await Promise.all( - [...this.#jobs.keys()].map(async (snapId) => this.terminateSnap(snapId)), - ); - } - - /** - * Initializes and executes a Snap, setting up the communication channels to the Snap etc. - * - * @param snapData - Data needed for Snap execution. - * @param snapData.snapId - The ID of the Snap to execute. - * @param snapData.sourceCode - The source code of the Snap to execute. - * @param snapData.endowments - The endowments available to the executing Snap. - * @returns A string `OK` if execution succeeded. - * @throws If the execution service returns an error or execution times out. - */ - async executeSnap({ - snapId, - sourceCode, - endowments, - }: SnapExecutionData): Promise { - if (this.#jobs.has(snapId)) { - throw new Error(`"${snapId}" is already running.`); - } - - this.setSnapStatus(snapId, 'created'); - - const timer = new Timer(this.#initTimeout); - - // This may resolve even if the environment has failed to start up fully - const job = await this.#initJob(snapId, timer); - - // Certain environments use ping as part of their initialization and thus can skip it here - if (this.#usePing) { - // Ping the worker to ensure that it started up - const pingResult = await withTimeout( - this.#command(job.id, { - jsonrpc: '2.0', - method: 'ping', - id: nanoid(), - }), - this.#pingTimeout, - ); - - if (pingResult === hasTimedOut) { - throw new Error( - `The executor for "${snapId}" was unreachable. The executor did not respond in time.`, - ); - } - } - - const rpcStream = job.streams.rpc; - - this.#setupSnapProvider(snapId, rpcStream); - - // Use the remaining time as the timer, but ensure that the - // Snap gets at least half the init timeout. - const remainingTime = Math.max(timer.remaining, this.#initTimeout / 2); - - this.setSnapStatus(snapId, 'initialized'); - - const request = { - jsonrpc: '2.0', - method: 'executeSnap', - params: { snapId, sourceCode, endowments }, - id: nanoid(), - }; - - assertIsJsonRpcRequest(request); - - this.setSnapStatus(snapId, 'executing'); - - const result = await withTimeout( - this.#command(job.id, request), - remainingTime, - ); - - if (result === hasTimedOut) { - throw new Error(`${snapId} failed to start.`); - } - - if (result === 'OK') { - this.setSnapStatus(snapId, 'running'); - } - - return result as string; - } - - async #command( - snapId: string, - message: JsonRpcRequest, - ): Promise { - const job = this.#jobs.get(snapId); - if (!job) { - throw new Error(`"${snapId}" is not currently running.`); - } - - log('Parent: Sending Command', message); - return await job.rpcEngine.handle(message); - } - - /** - * Handle RPC request. - * - * @param snapId - The ID of the recipient Snap. - * @param options - Bag of options to pass to the RPC handler. - * @returns Promise that can handle the request. - */ - public async handleRpcRequest( - snapId: string, - options: SnapRpcHookArgs, - ): Promise { - const { handler, request, origin } = options; - - return await this.#command(snapId, { - id: nanoid(), - jsonrpc: '2.0', - method: 'snapRpc', - params: { - snapId, - origin, - handler, - request: request as JsonRpcRequest, - }, - }); - } -} - -/** - * Sets up stream multiplexing for the given stream. - * - * @param connectionStream - The stream to mux. - * @param streamName - The name of the stream, for identification in errors. - * @returns The multiplexed stream. - */ -export function setupMultiplex( - connectionStream: Duplex, - streamName: string, -): ObjectMultiplex { - const mux = new ObjectMultiplex(); - pipeline(connectionStream, mux, connectionStream, (error) => { - if (error && !error.message?.match('Premature close')) { - logError(`"${streamName}" stream failure.`, error); - } - }); - return mux; -} diff --git a/packages/snaps-controllers/src/services/ExecutionService-method-action-types.ts b/packages/snaps-controllers/src/services/ExecutionService-method-action-types.ts new file mode 100644 index 0000000000..0993572ac4 --- /dev/null +++ b/packages/snaps-controllers/src/services/ExecutionService-method-action-types.ts @@ -0,0 +1,59 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { ExecutionService } from './ExecutionService'; + +/** + * Terminates the Snap with the specified ID and deletes all its associated + * data. Any subsequent messages targeting the Snap will fail with an error. + * Throws an error if termination fails unexpectedly. + * + * @param snapId - The id of the Snap to be terminated. + */ +export type ExecutionServiceTerminateSnapAction = { + type: `ExecutionService:terminateSnap`; + handler: ExecutionService['terminateSnap']; +}; + +export type ExecutionServiceTerminateAllSnapsAction = { + type: `ExecutionService:terminateAllSnaps`; + handler: ExecutionService['terminateAllSnaps']; +}; + +/** + * Initializes and executes a Snap, setting up the communication channels to the Snap etc. + * + * @param snapData - Data needed for Snap execution. + * @param snapData.snapId - The ID of the Snap to execute. + * @param snapData.sourceCode - The source code of the Snap to execute. + * @param snapData.endowments - The endowments available to the executing Snap. + * @returns A string `OK` if execution succeeded. + * @throws If the execution service returns an error or execution times out. + */ +export type ExecutionServiceExecuteSnapAction = { + type: `ExecutionService:executeSnap`; + handler: ExecutionService['executeSnap']; +}; + +/** + * Handle RPC request. + * + * @param snapId - The ID of the recipient Snap. + * @param options - Bag of options to pass to the RPC handler. + * @returns Promise that can handle the request. + */ +export type ExecutionServiceHandleRpcRequestAction = { + type: `ExecutionService:handleRpcRequest`; + handler: ExecutionService['handleRpcRequest']; +}; + +/** + * Union of all ExecutionService action types. + */ +export type ExecutionServiceMethodActions = + | ExecutionServiceTerminateSnapAction + | ExecutionServiceTerminateAllSnapsAction + | ExecutionServiceExecuteSnapAction + | ExecutionServiceHandleRpcRequestAction; diff --git a/packages/snaps-controllers/src/services/AbstractExecutionService.test.ts b/packages/snaps-controllers/src/services/ExecutionService.test.ts similarity index 97% rename from packages/snaps-controllers/src/services/AbstractExecutionService.test.ts rename to packages/snaps-controllers/src/services/ExecutionService.test.ts index 342df5dd33..6bf7952ca8 100644 --- a/packages/snaps-controllers/src/services/AbstractExecutionService.test.ts +++ b/packages/snaps-controllers/src/services/ExecutionService.test.ts @@ -3,7 +3,7 @@ import { HandlerType } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; import { Duration, inMilliseconds } from '@metamask/utils'; -import type { ExecutionServiceArgs } from './AbstractExecutionService'; +import type { ExecutionServiceArgs } from './ExecutionService'; import { NodeThreadExecutionService } from './node'; import { createService } from '../test-utils'; @@ -18,7 +18,7 @@ class MockExecutionService extends NodeThreadExecutionService { } } -describe('AbstractExecutionService', () => { +describe('ExecutionService', () => { afterEach(() => { jest.restoreAllMocks(); }); diff --git a/packages/snaps-controllers/src/services/ExecutionService.ts b/packages/snaps-controllers/src/services/ExecutionService.ts index 3d6898ce21..36c2c0c176 100644 --- a/packages/snaps-controllers/src/services/ExecutionService.ts +++ b/packages/snaps-controllers/src/services/ExecutionService.ts @@ -1,28 +1,83 @@ +import { asV2Middleware } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 as JsonRpcEngine } from '@metamask/json-rpc-engine/v2'; +import { createStreamMiddleware } from '@metamask/json-rpc-middleware-stream'; import type { Messenger } from '@metamask/messenger'; +import ObjectMultiplex from '@metamask/object-multiplex'; +import type { BasePostMessageStream } from '@metamask/post-message-stream'; import type { SnapRpcHookArgs } from '@metamask/snaps-utils'; -import type { Json } from '@metamask/utils'; - -type TerminateSnap = (snapId: string) => Promise; -type TerminateAll = () => Promise; -type ExecuteSnap = (snapData: SnapExecutionData) => Promise; - -type HandleRpcRequest = ( - snapId: string, - options: SnapRpcHookArgs, -) => Promise; - -export type ExecutionService = { - // These fields are required for modular initialisation of the execution - // service in the MetaMask extension. - name: 'ExecutionService'; - state: null; - - terminateSnap: TerminateSnap; - terminateAllSnaps: TerminateAll; - executeSnap: ExecuteSnap; - handleRpcRequest: HandleRpcRequest; +import { SNAP_STREAM_NAMES, logError, logWarning } from '@metamask/snaps-utils'; +import type { + Json, + JsonRpcNotification, + JsonRpcRequest, +} from '@metamask/utils'; +import { + Duration, + assertIsJsonRpcRequest, + hasProperty, + inMilliseconds, +} from '@metamask/utils'; +import { nanoid } from 'nanoid'; +import { pipeline } from 'readable-stream'; +import type { Duplex } from 'readable-stream'; + +import type { ExecutionServiceMethodActions } from './ExecutionService-method-action-types'; +import { log } from '../logging'; +import { Timer } from '../snaps/Timer'; +import { hasTimedOut, withTimeout } from '../utils'; + +const serviceName = 'ExecutionService'; + +export type SetupSnapProvider = (snapId: string, stream: Duplex) => void; + +export type ExecutionServiceArgs = { + setupSnapProvider: SetupSnapProvider; + messenger: ExecutionServiceMessenger; + initTimeout?: number; + pingTimeout?: number; + terminationTimeout?: number; + usePing?: boolean; }; +type JobStreams = { + command: Duplex; + rpc: Duplex; + connection: BasePostMessageStream; + mux: ObjectMultiplex; +}; + +export type Job = { + id: string; + streams: JobStreams; + rpcEngine: JsonRpcEngine; + worker: WorkerType; +}; + +export type TerminateJobArgs = Partial> & + Pick, 'id'>; + +/** + Statuses used for diagnostic purposes + - created: The initial state, no initialization has started + - initializing: Snap execution environment is initializing + - initialized: Snap execution environment has initialized + - executing: Snap source code is being executed + - running: Snap executed and ready for RPC requests + */ +type ExecutionStatus = + | 'created' + | 'initializing' + | 'initialized' + | 'executing' + | 'running'; + +export const MESSENGER_EXPOSED_METHODS = [ + 'terminateSnap', + 'terminateAllSnaps', + 'executeSnap', + 'handleRpcRequest', +] as const; + export type SnapExecutionData = { snapId: string; sourceCode: string; @@ -35,68 +90,452 @@ export type SnapErrorJson = { data?: Json; }; -type ControllerName = 'ExecutionService'; - -export type ErrorMessageEvent = { +export type ExecutionServiceUnhandledErrorEvent = { type: 'ExecutionService:unhandledError'; payload: [string, SnapErrorJson]; }; -export type OutboundRequest = { +export type ExecutionServiceOutboundRequestEvent = { type: 'ExecutionService:outboundRequest'; payload: [string]; }; -export type OutboundResponse = { +export type ExecutionServiceOutboundResponseEvent = { type: 'ExecutionService:outboundResponse'; payload: [string]; }; export type ExecutionServiceEvents = - | ErrorMessageEvent - | OutboundRequest - | OutboundResponse; - -/** - * Handles RPC request. - */ -export type HandleRpcRequestAction = { - type: `${ControllerName}:handleRpcRequest`; - handler: ExecutionService['handleRpcRequest']; -}; - -/** - * Executes a given snap. - */ -export type ExecuteSnapAction = { - type: `${ControllerName}:executeSnap`; - handler: ExecutionService['executeSnap']; -}; + | ExecutionServiceUnhandledErrorEvent + | ExecutionServiceOutboundRequestEvent + | ExecutionServiceOutboundResponseEvent; -/** - * Terminates a given snap. - */ -export type TerminateSnapAction = { - type: `${ControllerName}:terminateSnap`; - handler: ExecutionService['terminateSnap']; -}; - -/** - * Terminates all snaps. - */ -export type TerminateAllSnapsAction = { - type: `${ControllerName}:terminateAllSnaps`; - handler: ExecutionService['terminateAllSnaps']; -}; - -export type ExecutionServiceActions = - | HandleRpcRequestAction - | ExecuteSnapAction - | TerminateSnapAction - | TerminateAllSnapsAction; +export type ExecutionServiceActions = ExecutionServiceMethodActions; export type ExecutionServiceMessenger = Messenger< 'ExecutionService', ExecutionServiceActions, ExecutionServiceEvents >; + +export abstract class ExecutionService { + name: typeof serviceName = serviceName; + + state = null; + + readonly #jobs: Map>; + + readonly #status: Map; + + readonly #setupSnapProvider: SetupSnapProvider; + + readonly #messenger: ExecutionServiceMessenger; + + readonly #initTimeout: number; + + readonly #pingTimeout: number; + + readonly #terminationTimeout: number; + + readonly #usePing: boolean; + + constructor({ + setupSnapProvider, + messenger, + initTimeout = inMilliseconds(60, Duration.Second), + pingTimeout = inMilliseconds(10, Duration.Second), + terminationTimeout = inMilliseconds(1, Duration.Second), + usePing = true, + }: ExecutionServiceArgs) { + this.#jobs = new Map(); + this.#status = new Map(); + this.#setupSnapProvider = setupSnapProvider; + this.#messenger = messenger; + this.#initTimeout = initTimeout; + this.#pingTimeout = pingTimeout; + this.#terminationTimeout = terminationTimeout; + this.#usePing = usePing; + + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); + } + + /** + * Performs additional necessary work during job termination. **MUST** be + * implemented by concrete implementations. See + * {@link AbstractExecutionService.terminate} for details. + * + * @param job - The object corresponding to the job to be terminated. + */ + protected abstract terminateJob( + job: TerminateJobArgs, + ): Promise; + + /** + * Terminates the Snap with the specified ID and deletes all its associated + * data. Any subsequent messages targeting the Snap will fail with an error. + * Throws an error if termination fails unexpectedly. + * + * @param snapId - The id of the Snap to be terminated. + */ + public async terminateSnap(snapId: string): Promise { + const job = this.#jobs.get(snapId); + if (!job) { + return; + } + + try { + // Ping worker and tell it to run teardown, continue with termination if it takes too long + const result = await withTimeout( + this.#command(snapId, { + jsonrpc: '2.0', + method: 'terminate', + id: nanoid(), + }), + this.#terminationTimeout, + ); + + if (result === hasTimedOut || result !== 'OK') { + logWarning(`Snap "${snapId}" failed to terminate gracefully.`); + } + } catch { + // Ignore + } + + Object.values(job.streams).forEach((stream) => { + try { + if (!stream.destroyed) { + stream.destroy(); + } + } catch (error) { + logError('Error while destroying stream', error); + } + }); + + await this.terminateJob(job); + + this.#jobs.delete(snapId); + this.#status.delete(snapId); + log(`Snap "${snapId}" terminated.`); + } + + /** + * Initiates a job for a Snap. + * + * @param snapId - The ID of the Snap to initiate a job for. + * @param timer - The timer to use for timeouts. + * @returns Information regarding the created job. + * @throws If the execution service returns an error or execution times out. + */ + async #initJob(snapId: string, timer: Timer): Promise> { + const { streams, worker } = await this.#initStreams(snapId, timer); + + const jsonRpcConnection = createStreamMiddleware(); + + pipeline( + jsonRpcConnection.stream, + streams.command, + jsonRpcConnection.stream, + (error) => { + if (error && !error.message?.match('Premature close')) { + logError(`Command stream failure.`, error); + } + }, + ); + + const rpcEngine = JsonRpcEngine.create({ + middleware: [asV2Middleware(jsonRpcConnection.middleware)], + }); + + const envMetadata = { + id: snapId, + streams, + rpcEngine, + worker, + }; + this.#jobs.set(snapId, envMetadata); + + return envMetadata; + } + + /** + * Sets up the streams for an initiated job. + * + * @param snapId - The Snap ID. + * @param timer - The timer to use for timeouts. + * @returns The streams to communicate with the worker and the worker itself. + * @throws If the execution service returns an error or execution times out. + */ + async #initStreams( + snapId: string, + timer: Timer, + ): Promise<{ streams: JobStreams; worker: WorkerType }> { + const result = await withTimeout(this.initEnvStream(snapId), timer); + + if (result === hasTimedOut) { + // For certain environments, such as the iframe we may have already created the worker and wish to terminate it. + await this.terminateJob({ id: snapId }); + + const status = this.#status.get(snapId); + if (status === 'created') { + // Currently this error can only be thrown by OffscreenExecutionService. + throw new Error( + `The executor for "${snapId}" couldn't start initialization. The offscreen document may not exist.`, + ); + } + throw new Error( + `The executor for "${snapId}" failed to initialize. The iframe/webview/worker failed to load.`, + ); + } + + const { worker, stream: envStream } = result; + const mux = setupMultiplex(envStream, `Snap: "${snapId}"`); + const commandStream = mux.createStream(SNAP_STREAM_NAMES.COMMAND); + + // Handle out-of-band errors, i.e. errors thrown from the Snap outside of the req/res cycle. + // Also keep track of outbound request/responses + const notificationHandler = ( + message: + | JsonRpcRequest + | JsonRpcNotification>, + ) => { + if (hasProperty(message, 'id')) { + return; + } + + if (message.method === 'OutboundRequest') { + this.#messenger.publish('ExecutionService:outboundRequest', snapId); + } else if (message.method === 'OutboundResponse') { + this.#messenger.publish('ExecutionService:outboundResponse', snapId); + } else if (message.method === 'UnhandledError') { + this.#messenger.publish( + 'ExecutionService:unhandledError', + snapId, + (message.params as { error: SnapErrorJson }).error, + ); + commandStream.removeListener('data', notificationHandler); + } else { + logError( + new Error( + `Received unexpected command stream notification "${message.method}".`, + ), + ); + } + }; + + commandStream.on('data', notificationHandler); + + const rpcStream = mux + .createStream(SNAP_STREAM_NAMES.JSON_RPC) + .setMaxListeners(20); + + rpcStream.on('data', (chunk) => { + if (chunk?.data && hasProperty(chunk?.data, 'id')) { + this.#messenger.publish('ExecutionService:outboundRequest', snapId); + } + }); + + // An error handler is not attached to the RPC stream until `setupSnapProvider` is called. + // We must set it up here to prevent errors from bubbling up if the stream is destroyed before then. + rpcStream.on('error', (error) => { + if (error && !error.message?.match('Premature close')) { + logError(`Snap: "${snapId}" - RPC stream failure:`, error); + } + }); + + const originalWrite = rpcStream.write.bind(rpcStream); + + // @ts-expect-error Hack to inspect the messages being written to the stream. + rpcStream.write = (chunk, encoding, callback) => { + // Ignore chain switching notifications as it doesn't matter for the SnapProvider. + if (chunk?.data?.method === 'metamask_chainChanged') { + return true; + } + + if (chunk?.data && hasProperty(chunk?.data, 'id')) { + this.#messenger.publish('ExecutionService:outboundResponse', snapId); + } + + return originalWrite(chunk, encoding, callback); + }; + + return { + streams: { + command: commandStream, + rpc: rpcStream, + connection: envStream, + mux, + }, + worker, + }; + } + + /** + * Abstract function implemented by implementing class that spins up a new worker for a job. + * + * Depending on the execution environment, this may run forever if the Snap fails to start up properly, therefore any call to this function should be wrapped in a timeout. + */ + protected abstract initEnvStream(snapId: string): Promise<{ + worker: WorkerType; + stream: BasePostMessageStream; + }>; + + /** + * Set the execution status of the Snap. + * + * @param snapId - The Snap ID. + * @param status - The current execution status. + */ + protected setSnapStatus(snapId: string, status: ExecutionStatus) { + this.#status.set(snapId, status); + } + + async terminateAllSnaps() { + await Promise.all( + [...this.#jobs.keys()].map(async (snapId) => this.terminateSnap(snapId)), + ); + } + + /** + * Initializes and executes a Snap, setting up the communication channels to the Snap etc. + * + * @param snapData - Data needed for Snap execution. + * @param snapData.snapId - The ID of the Snap to execute. + * @param snapData.sourceCode - The source code of the Snap to execute. + * @param snapData.endowments - The endowments available to the executing Snap. + * @returns A string `OK` if execution succeeded. + * @throws If the execution service returns an error or execution times out. + */ + async executeSnap({ + snapId, + sourceCode, + endowments, + }: SnapExecutionData): Promise { + if (this.#jobs.has(snapId)) { + throw new Error(`"${snapId}" is already running.`); + } + + this.setSnapStatus(snapId, 'created'); + + const timer = new Timer(this.#initTimeout); + + // This may resolve even if the environment has failed to start up fully + const job = await this.#initJob(snapId, timer); + + // Certain environments use ping as part of their initialization and thus can skip it here + if (this.#usePing) { + // Ping the worker to ensure that it started up + const pingResult = await withTimeout( + this.#command(job.id, { + jsonrpc: '2.0', + method: 'ping', + id: nanoid(), + }), + this.#pingTimeout, + ); + + if (pingResult === hasTimedOut) { + throw new Error( + `The executor for "${snapId}" was unreachable. The executor did not respond in time.`, + ); + } + } + + const rpcStream = job.streams.rpc; + + this.#setupSnapProvider(snapId, rpcStream); + + // Use the remaining time as the timer, but ensure that the + // Snap gets at least half the init timeout. + const remainingTime = Math.max(timer.remaining, this.#initTimeout / 2); + + this.setSnapStatus(snapId, 'initialized'); + + const request = { + jsonrpc: '2.0', + method: 'executeSnap', + params: { snapId, sourceCode, endowments }, + id: nanoid(), + }; + + assertIsJsonRpcRequest(request); + + this.setSnapStatus(snapId, 'executing'); + + const result = await withTimeout( + this.#command(job.id, request), + remainingTime, + ); + + if (result === hasTimedOut) { + throw new Error(`${snapId} failed to start.`); + } + + if (result === 'OK') { + this.setSnapStatus(snapId, 'running'); + } + + return result as string; + } + + async #command( + snapId: string, + message: JsonRpcRequest, + ): Promise { + const job = this.#jobs.get(snapId); + if (!job) { + throw new Error(`"${snapId}" is not currently running.`); + } + + log('Parent: Sending Command', message); + return await job.rpcEngine.handle(message); + } + + /** + * Handle RPC request. + * + * @param snapId - The ID of the recipient Snap. + * @param options - Bag of options to pass to the RPC handler. + * @returns Promise that can handle the request. + */ + public async handleRpcRequest( + snapId: string, + options: SnapRpcHookArgs, + ): Promise { + const { handler, request, origin } = options; + + return await this.#command(snapId, { + id: nanoid(), + jsonrpc: '2.0', + method: 'snapRpc', + params: { + snapId, + origin, + handler, + request: request as JsonRpcRequest, + }, + }); + } +} + +/** + * Sets up stream multiplexing for the given stream. + * + * @param connectionStream - The stream to mux. + * @param streamName - The name of the stream, for identification in errors. + * @returns The multiplexed stream. + */ +export function setupMultiplex( + connectionStream: Duplex, + streamName: string, +): ObjectMultiplex { + const mux = new ObjectMultiplex(); + pipeline(connectionStream, mux, connectionStream, (error) => { + if (error && !error.message?.match('Premature close')) { + logError(`"${streamName}" stream failure.`, error); + } + }); + return mux; +} diff --git a/packages/snaps-controllers/src/services/browser.test.ts b/packages/snaps-controllers/src/services/browser.test.ts index b4cf203e56..dfe475be40 100644 --- a/packages/snaps-controllers/src/services/browser.test.ts +++ b/packages/snaps-controllers/src/services/browser.test.ts @@ -2,7 +2,7 @@ import * as BrowserExport from './browser'; describe('browser entrypoint', () => { const expectedExports = [ - 'AbstractExecutionService', + 'ExecutionService', 'setupMultiplex', 'IframeExecutionService', 'OffscreenExecutionService', diff --git a/packages/snaps-controllers/src/services/browser.ts b/packages/snaps-controllers/src/services/browser.ts index 871061032c..f5d3151ba5 100644 --- a/packages/snaps-controllers/src/services/browser.ts +++ b/packages/snaps-controllers/src/services/browser.ts @@ -1,6 +1,22 @@ // Subset of exports meant for browser environments, omits Node.js services -export * from './AbstractExecutionService'; -export type * from './ExecutionService'; +export type { + ExecutionServiceActions, + ExecutionServiceEvents, + ExecutionServiceMessenger, + ExecutionServiceOutboundRequestEvent, + ExecutionServiceOutboundResponseEvent, + ExecutionServiceUnhandledErrorEvent, + SetupSnapProvider, + SnapErrorJson, + SnapExecutionData, +} from './ExecutionService'; +export { ExecutionService, setupMultiplex } from './ExecutionService'; +export type { + ExecutionServiceTerminateSnapAction, + ExecutionServiceTerminateAllSnapsAction, + ExecutionServiceExecuteSnapAction, + ExecutionServiceHandleRpcRequestAction, +} from './ExecutionService-method-action-types'; export * from './ProxyPostMessageStream'; export * from './iframe'; export * from './offscreen'; diff --git a/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts b/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts index cb49d91f89..70daadda29 100644 --- a/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts +++ b/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts @@ -6,14 +6,14 @@ import { withTimeout } from '../../utils'; import type { ExecutionServiceArgs, TerminateJobArgs, -} from '../AbstractExecutionService'; -import { AbstractExecutionService } from '../AbstractExecutionService'; +} from '../ExecutionService'; +import { ExecutionService } from '../ExecutionService'; type IframeExecutionEnvironmentServiceArgs = { iframeUrl: URL; } & ExecutionServiceArgs; -export class IframeExecutionService extends AbstractExecutionService { +export class IframeExecutionService extends ExecutionService { public iframeUrl: URL; constructor({ diff --git a/packages/snaps-controllers/src/services/index.ts b/packages/snaps-controllers/src/services/index.ts index 2e0ea1263b..5447d7e102 100644 --- a/packages/snaps-controllers/src/services/index.ts +++ b/packages/snaps-controllers/src/services/index.ts @@ -1,5 +1,20 @@ -export * from './AbstractExecutionService'; -export type * from './ExecutionService'; +export type { + ExecutionServiceActions, + ExecutionServiceEvents, + ExecutionServiceMessenger, + ExecutionServiceOutboundRequestEvent, + ExecutionServiceOutboundResponseEvent, + ExecutionServiceUnhandledErrorEvent, + SnapErrorJson, + SnapExecutionData, +} from './ExecutionService'; +export { ExecutionService, setupMultiplex } from './ExecutionService'; +export type { + ExecutionServiceTerminateSnapAction, + ExecutionServiceTerminateAllSnapsAction, + ExecutionServiceExecuteSnapAction, + ExecutionServiceHandleRpcRequestAction, +} from './ExecutionService-method-action-types'; export * from './ProxyPostMessageStream'; export * from './iframe'; export * from './offscreen'; diff --git a/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts b/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts index 26f38f0d82..24bd916642 100644 --- a/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts +++ b/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts @@ -3,10 +3,10 @@ import { ProcessParentMessageStream } from '@metamask/post-message-stream/node'; import type { ChildProcess } from 'child_process'; import { fork } from 'child_process'; -import type { TerminateJobArgs } from '..'; -import { AbstractExecutionService } from '..'; +import type { TerminateJobArgs } from '../ExecutionService'; +import { ExecutionService } from '../ExecutionService'; -export class NodeProcessExecutionService extends AbstractExecutionService { +export class NodeProcessExecutionService extends ExecutionService { protected async initEnvStream(snapId: string): Promise<{ worker: ChildProcess; stream: BasePostMessageStream; diff --git a/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts b/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts index 9165986784..dfcdc7d083 100644 --- a/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts +++ b/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts @@ -2,10 +2,10 @@ import type { BasePostMessageStream } from '@metamask/post-message-stream'; import { ThreadParentMessageStream } from '@metamask/post-message-stream/node'; import { Worker } from 'worker_threads'; -import type { TerminateJobArgs } from '..'; -import { AbstractExecutionService } from '..'; +import { ExecutionService } from '../ExecutionService'; +import type { TerminateJobArgs } from '../ExecutionService'; -export class NodeThreadExecutionService extends AbstractExecutionService { +export class NodeThreadExecutionService extends ExecutionService { protected async initEnvStream(snapId: string): Promise<{ worker: Worker; stream: BasePostMessageStream; diff --git a/packages/snaps-controllers/src/services/offscreen/OffscreenExecutionService.ts b/packages/snaps-controllers/src/services/offscreen/OffscreenExecutionService.ts index 85b2ff231f..231445e8b0 100644 --- a/packages/snaps-controllers/src/services/offscreen/OffscreenExecutionService.ts +++ b/packages/snaps-controllers/src/services/offscreen/OffscreenExecutionService.ts @@ -1,6 +1,6 @@ import { BrowserRuntimePostMessageStream } from '@metamask/post-message-stream'; -import type { ExecutionServiceArgs } from '../AbstractExecutionService'; +import type { ExecutionServiceArgs } from '../ExecutionService'; import { ProxyExecutionService } from '../proxy/ProxyExecutionService'; type OffscreenExecutionEnvironmentServiceArgs = { diff --git a/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts b/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts index 03d4fe531d..a452c57299 100644 --- a/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts +++ b/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts @@ -4,15 +4,15 @@ import { nanoid } from 'nanoid'; import type { ExecutionServiceArgs, TerminateJobArgs, -} from '../AbstractExecutionService'; -import { AbstractExecutionService } from '../AbstractExecutionService'; +} from '../ExecutionService'; +import { ExecutionService } from '../ExecutionService'; import { ProxyPostMessageStream } from '../ProxyPostMessageStream'; type ProxyExecutionEnvironmentServiceArgs = { stream: BasePostMessageStream; } & ExecutionServiceArgs; -export class ProxyExecutionService extends AbstractExecutionService { +export class ProxyExecutionService extends ExecutionService { readonly #stream: BasePostMessageStream; /** diff --git a/packages/snaps-controllers/src/services/webview/WebViewExecutionService.ts b/packages/snaps-controllers/src/services/webview/WebViewExecutionService.ts index 9a2a97982e..c03abb7f24 100644 --- a/packages/snaps-controllers/src/services/webview/WebViewExecutionService.ts +++ b/packages/snaps-controllers/src/services/webview/WebViewExecutionService.ts @@ -1,17 +1,17 @@ import type { WebViewInterface } from './WebViewMessageStream'; import { WebViewMessageStream } from './WebViewMessageStream'; -import { AbstractExecutionService } from '../AbstractExecutionService'; +import { ExecutionService } from '../ExecutionService'; import type { ExecutionServiceArgs, TerminateJobArgs, -} from '../AbstractExecutionService'; +} from '../ExecutionService'; export type WebViewExecutionServiceArgs = ExecutionServiceArgs & { createWebView: (jobId: string) => Promise; removeWebView: (jobId: string) => void; }; -export class WebViewExecutionService extends AbstractExecutionService { +export class WebViewExecutionService extends ExecutionService { readonly #createWebView; readonly #removeWebView; diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index 94ec443b68..e81c630ae1 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -95,11 +95,11 @@ import { SNAP_APPROVAL_RESULT, SNAP_APPROVAL_UPDATE, } from './SnapController'; -import { AbstractExecutionService, setupMultiplex } from '../services'; +import { ExecutionService, setupMultiplex } from '../services'; +import type { TerminateJobArgs } from '../services/ExecutionService'; import type { ExecutionServiceMessenger, NodeThreadExecutionService, - TerminateJobArgs, } from '../services/node'; import type { SnapControllerStateWithStorageService } from '../test-utils'; import { @@ -1870,7 +1870,7 @@ describe('SnapController', () => { state: { snaps: getPersistedSnapsState() }, }); - class BrickedExecutionService extends AbstractExecutionService { + class BrickedExecutionService extends ExecutionService { constructor(messenger: ExecutionServiceMessenger) { super({ messenger, setupSnapProvider: jest.fn(), pingTimeout: 1 }); } @@ -1982,18 +1982,16 @@ describe('SnapController', () => { }); const rootMessenger = getRootMessenger(); - const [snapController, service] = await getSnapControllerWithEES( - getSnapControllerOptions({ - maxRequestTime: 50, - rootMessenger, - detectSnapLocation: loopbackDetect({ - manifest, - files: [sourceCode, svgIcon as VirtualFile], - }), + const options = getSnapControllerOptions({ + maxRequestTime: 50, + rootMessenger, + detectSnapLocation: loopbackDetect({ + manifest, + files: [sourceCode, svgIcon as VirtualFile], }), - ); + }); - const spy = jest.spyOn(service, 'executeSnap'); + const [snapController, service] = await getSnapControllerWithEES(options); await snapController.installSnaps(MOCK_ORIGIN, { [MOCK_SNAP_ID]: {}, @@ -2043,7 +2041,21 @@ describe('SnapController', () => { expect(await promise).toBe('foo'); - expect(spy).toHaveBeenCalledTimes(2); + expect(options.messenger.call).toHaveBeenNthCalledWith( + 8, + 'ExecutionService:executeSnap', + expect.objectContaining({ + snapId: MOCK_SNAP_ID, + }), + ); + + expect(options.messenger.call).toHaveBeenNthCalledWith( + 19, + 'ExecutionService:executeSnap', + expect.objectContaining({ + snapId: MOCK_SNAP_ID, + }), + ); // @ts-expect-error Accessing protected value. service.terminateJob = originalTerminateFunction; @@ -2127,7 +2139,10 @@ describe('SnapController', () => { rootMessenger.registerActionHandler( 'ExecutionService:executeSnap', - async () => await sleep(300), + async () => { + await sleep(300); + return 'OK'; + }, ); const snap = snapController.getSnapExpect(MOCK_SNAP_ID); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 9a3b7e4aaf..2e47e79aad 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -181,11 +181,11 @@ import type { } from '../interface'; import { log } from '../logging'; import type { - ExecuteSnapAction, ExecutionServiceEvents, - HandleRpcRequestAction, + ExecutionServiceExecuteSnapAction, + ExecutionServiceHandleRpcRequestAction, + ExecutionServiceTerminateSnapAction, SnapErrorJson, - TerminateSnapAction, } from '../services'; import type { EncryptionResult, @@ -530,9 +530,9 @@ export type AllowedActions = | RevokePermissionForAllSubjects | GrantPermissions | ApprovalControllerAddRequestAction - | HandleRpcRequestAction - | ExecuteSnapAction - | TerminateSnapAction + | ExecutionServiceHandleRpcRequestAction + | ExecutionServiceExecuteSnapAction + | ExecutionServiceTerminateSnapAction | UpdateCaveat | ApprovalControllerUpdateRequestStateAction | GetResult diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index b166da106c..7211352855 100644 --- a/packages/snaps-controllers/src/test-utils/controller.tsx +++ b/packages/snaps-controllers/src/test-utils/controller.tsx @@ -61,10 +61,7 @@ import type { StoredInterface, } from '../interface/SnapInterfaceController'; import type { MultichainRoutingServiceMessenger } from '../multichain/MultichainRoutingService'; -import type { - AbstractExecutionService, - ExecutionServiceMessenger, -} from '../services'; +import type { ExecutionService, ExecutionServiceMessenger } from '../services'; import type { SnapsRegistryActions, SnapsRegistryEvents, @@ -426,7 +423,10 @@ export const getRootMessenger = () => { () => undefined, ); - messenger.registerActionHandler('ExecutionService:executeSnap', asyncNoOp); + messenger.registerActionHandler( + 'ExecutionService:executeSnap', + async () => 'OK', + ); messenger.registerActionHandler( 'ExecutionService:handleRpcRequest', asyncNoOp, @@ -693,7 +693,7 @@ export const getSnapController = async ( export const getSnapControllerWithEES = async ( options = getSnapControllerOptions(), - service?: AbstractExecutionService, + service?: ExecutionService, init = true, ) => { const _service = diff --git a/packages/snaps-controllers/src/test-utils/execution-environment.ts b/packages/snaps-controllers/src/test-utils/execution-environment.ts index 575ebce491..0bd738d2c6 100644 --- a/packages/snaps-controllers/src/test-utils/execution-environment.ts +++ b/packages/snaps-controllers/src/test-utils/execution-environment.ts @@ -7,12 +7,11 @@ import { pipeline } from 'readable-stream'; import { MOCK_BLOCK_NUMBER } from './constants'; import type { RootMessenger } from './controller'; import type { - ExecutionService, ExecutionServiceActions, ExecutionServiceEvents, - SetupSnapProvider, SnapExecutionData, } from '../services'; +import type { SetupSnapProvider } from '../services/ExecutionService'; import { NodeThreadExecutionService, setupMultiplex } from '../services/node'; export const getNodeEESMessenger = (messenger: RootMessenger) => { @@ -59,7 +58,7 @@ export const getNodeEES = ( }), }); -export class ExecutionEnvironmentStub implements ExecutionService { +export class ExecutionEnvironmentStub { name: 'ExecutionService' = 'ExecutionService' as const; state = null; diff --git a/packages/snaps-controllers/src/test-utils/service.ts b/packages/snaps-controllers/src/test-utils/service.ts index f324a2257e..2c3f0f129e 100644 --- a/packages/snaps-controllers/src/test-utils/service.ts +++ b/packages/snaps-controllers/src/test-utils/service.ts @@ -6,7 +6,7 @@ import { pipeline } from 'readable-stream'; import type { Duplex } from 'readable-stream'; import { MOCK_BLOCK_NUMBER } from './constants'; -import type { ErrorMessageEvent } from '../services'; +import type { ExecutionServiceUnhandledErrorEvent } from '../services'; import { setupMultiplex } from '../services'; export const createService = < @@ -18,9 +18,11 @@ export const createService = < 'messenger' | 'setupSnapProvider' >, ) => { - const messenger = new Messenger<'ExecutionService', never, ErrorMessageEvent>( - { namespace: 'ExecutionService' }, - ); + const messenger = new Messenger< + 'ExecutionService', + never, + ExecutionServiceUnhandledErrorEvent + >({ namespace: 'ExecutionService' }); const service = new ServiceClass({ messenger, diff --git a/packages/snaps-jest/src/environment.ts b/packages/snaps-jest/src/environment.ts index 130a433c16..000b4fa984 100644 --- a/packages/snaps-jest/src/environment.ts +++ b/packages/snaps-jest/src/environment.ts @@ -2,7 +2,7 @@ import type { EnvironmentContext, JestEnvironmentConfig, } from '@jest/environment'; -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { installSnap } from '@metamask/snaps-simulation'; import type { @@ -85,7 +85,7 @@ export class SnapsEnvironment extends NodeEnvironment { async installSnap< Service extends new ( ...args: any[] - ) => InstanceType, + ) => InstanceType, >( snapId: string = this.snapId, options: Partial> = {}, diff --git a/packages/snaps-jest/src/helpers.ts b/packages/snaps-jest/src/helpers.ts index 1508cfa2c0..4602a1a2b3 100644 --- a/packages/snaps-jest/src/helpers.ts +++ b/packages/snaps-jest/src/helpers.ts @@ -1,4 +1,4 @@ -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import type { AccountSelectorState, AssetSelectorState, @@ -35,9 +35,7 @@ const log = createModuleLogger(rootLogger, 'helpers'); * @returns The options. */ function getOptions< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId: SnapId | Partial> | undefined, options: Partial>, @@ -102,9 +100,7 @@ export async function installSnap(): Promise; * @throws If the built-in server is not running, and no snap ID is provided. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >(options: Partial>): Promise; /** @@ -140,9 +136,7 @@ export async function installSnap< * @throws If the built-in server is not running, and no snap ID is provided. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId: SnapId, options?: Partial>, @@ -181,9 +175,7 @@ export async function installSnap< * @throws If the built-in server is not running, and no snap ID is provided. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId?: SnapId | Partial>, options: Partial> = {}, diff --git a/packages/snaps-simulation/src/request.ts b/packages/snaps-simulation/src/request.ts index 4ff5608f42..8267ae1c70 100644 --- a/packages/snaps-simulation/src/request.ts +++ b/packages/snaps-simulation/src/request.ts @@ -1,4 +1,4 @@ -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import { type ComponentOrElement, ComponentOrElementStruct, @@ -38,7 +38,7 @@ import type { export type HandleRequestOptions = { snapId: SnapId; store: Store; - executionService: AbstractExecutionService; + executionService: ExecutionService; handler: HandlerType; controllerMessenger: RootControllerMessenger; simulationOptions: SimulationOptions; diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 9c2e097119..23d955d171 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -12,7 +12,7 @@ import { type Caveat, type RequestedPermissions, } from '@metamask/permission-controller'; -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import { detectSnapLocation, fetchSnap, @@ -88,7 +88,7 @@ export type ExecutionServiceOptions< Service extends new (...args: any[]) => any, > = Omit< ConstructorParameters[0], - keyof ConstructorParameters>[0] + keyof ConstructorParameters[0] >; /** @@ -102,9 +102,7 @@ export type ExecutionServiceOptions< * @template Service - The type of the execution service. */ export type InstallSnapOptions< - Service extends new ( - ...args: any[] - ) => InstanceType>, + Service extends new (...args: any[]) => InstanceType, > = ExecutionServiceOptions extends Record ? { @@ -121,7 +119,7 @@ export type InstallSnapOptions< export type InstalledSnap = { snapId: SnapId; store: Store; - executionService: InstanceType; + executionService: InstanceType; controllerMessenger: Messenger< NamespacedName, ActionConstraint, @@ -406,9 +404,7 @@ export type MultichainMiddlewareHooks = { * @template Service - The type of the execution service. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId: SnapId, { @@ -481,8 +477,8 @@ export async function installSnap< }); // Create execution service. - const ExecutionService = executionService ?? NodeThreadExecutionService; - const service = new ExecutionService({ + const ActualService = executionService ?? NodeThreadExecutionService; + const service = new ActualService({ ...executionServiceOptions, messenger: new Messenger({ namespace: 'ExecutionService', From 75f65f442cb0c72cd4f02a4d973f7c153c850f0c Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Mar 2026 10:23:36 +0100 Subject: [PATCH 07/17] refactor: Refactor `WebSocketService` to use messenger exposed methods (#3917) This refactors the `WebSocketService` to use the messenger exposed methods. --- > [!NOTE] > **Medium Risk** > Moderate risk because it changes how `WebSocketService` actions are registered/exposed on the messenger and makes previously private methods public, which could break callers relying on old action types or handler names. > > **Overview** > Refactors `WebSocketService` to register messenger actions via `registerMethodActionHandlers` using an explicit `MESSENGER_EXPOSED_METHODS` list, replacing per-action `registerActionHandler` wiring. > > Adds an auto-generated `WebSocketService-method-action-types.ts` file for strongly-typed method action definitions, updates `WebSocketService` to use these types, and adjusts exports in `websocket/index.ts` to export the service class plus selected types (instead of `export *`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 44b0f399df1ea86bf6b4bd229005ed5d67db2c4a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../WebSocketService-method-action-types.ts | 63 ++++++++++++++ .../src/websocket/WebSocketService.ts | 87 +++++-------------- .../snaps-controllers/src/websocket/index.ts | 12 ++- 3 files changed, 97 insertions(+), 65 deletions(-) create mode 100644 packages/snaps-controllers/src/websocket/WebSocketService-method-action-types.ts diff --git a/packages/snaps-controllers/src/websocket/WebSocketService-method-action-types.ts b/packages/snaps-controllers/src/websocket/WebSocketService-method-action-types.ts new file mode 100644 index 0000000000..1dff4bae87 --- /dev/null +++ b/packages/snaps-controllers/src/websocket/WebSocketService-method-action-types.ts @@ -0,0 +1,63 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { WebSocketService } from './WebSocketService'; + +/** + * Open a WebSocket connection. + * + * @param snapId - The Snap ID. + * @param url - The URL for the WebSocket connection. + * @param protocols - An optional parameter for protocols. + * @returns The identifier for the opened connection. + * @throws If the connection fails. + */ +export type WebSocketServiceOpenAction = { + type: `WebSocketService:open`; + handler: WebSocketService['open']; +}; + +/** + * Close a given WebSocket connection. + * + * @param snapId - The Snap ID. + * @param id - The identifier for the WebSocket connection. + */ +export type WebSocketServiceCloseAction = { + type: `WebSocketService:close`; + handler: WebSocketService['close']; +}; + +/** + * Send a message from a given Snap ID to a WebSocket connection. + * + * @param snapId - The Snap ID. + * @param id - The identifier for the WebSocket connection. + * @param data - The message to send. + */ +export type WebSocketServiceSendMessageAction = { + type: `WebSocketService:sendMessage`; + handler: WebSocketService['sendMessage']; +}; + +/** + * Get a list of all open WebSocket connections for a Snap ID. + * + * @param snapId - The Snap ID. + * @returns A list of WebSocket connections. + */ +export type WebSocketServiceGetAllAction = { + type: `WebSocketService:getAll`; + handler: WebSocketService['getAll']; +}; + +/** + * Union of all WebSocketService action types. + */ +export type WebSocketServiceMethodActions = + | WebSocketServiceOpenAction + | WebSocketServiceCloseAction + | WebSocketServiceSendMessageAction + | WebSocketServiceGetAllAction; diff --git a/packages/snaps-controllers/src/websocket/WebSocketService.ts b/packages/snaps-controllers/src/websocket/WebSocketService.ts index 0b077e9ca0..9c83195526 100644 --- a/packages/snaps-controllers/src/websocket/WebSocketService.ts +++ b/packages/snaps-controllers/src/websocket/WebSocketService.ts @@ -1,14 +1,11 @@ import type { Messenger } from '@metamask/messenger'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { - GetWebSocketsResult, - SnapId, - WebSocketEvent, -} from '@metamask/snaps-sdk'; +import type { SnapId, WebSocketEvent } from '@metamask/snaps-sdk'; import { HandlerType, isEqual, logError } from '@metamask/snaps-utils'; import { assert, createDeferredPromise } from '@metamask/utils'; import { nanoid } from 'nanoid'; +import type { WebSocketServiceMethodActions } from './WebSocketService-method-action-types'; import type { SnapControllerHandleRequestAction, SnapControllerSnapInstalledEvent, @@ -19,51 +16,26 @@ import { METAMASK_ORIGIN } from '../snaps'; const serviceName = 'WebSocketService'; -export type WebSocketServiceOpenAction = { - type: `${typeof serviceName}:open`; - handler: ( - snapId: SnapId, - url: string, - protocols?: string[], - ) => Promise; -}; - -export type WebSocketServiceCloseAction = { - type: `${typeof serviceName}:close`; - handler: (snapId: SnapId, id: string) => void; -}; - -export type WebSocketServiceSendMessageAction = { - type: `${typeof serviceName}:sendMessage`; - handler: ( - snapId: SnapId, - id: string, - data: string | number[], - ) => Promise; -}; - -export type WebSocketServiceGetAllAction = { - type: `${typeof serviceName}:getAll`; - handler: (snapId: SnapId) => GetWebSocketsResult; -}; +const MESSENGER_EXPOSED_METHODS = [ + 'open', + 'close', + 'sendMessage', + 'getAll', +] as const; -export type WebSocketServiceActions = - | WebSocketServiceOpenAction - | WebSocketServiceCloseAction - | WebSocketServiceSendMessageAction - | WebSocketServiceGetAllAction; +export type WebSocketServiceActions = WebSocketServiceMethodActions; -export type WebSocketServiceAllowedActions = SnapControllerHandleRequestAction; +type AllowedActions = SnapControllerHandleRequestAction; -export type WebSocketServiceEvents = +type AllowedEvents = | SnapControllerSnapUninstalledEvent | SnapControllerSnapUpdatedEvent | SnapControllerSnapInstalledEvent; export type WebSocketServiceMessenger = Messenger< 'WebSocketService', - WebSocketServiceActions | WebSocketServiceAllowedActions, - WebSocketServiceEvents + WebSocketServiceActions | AllowedActions, + AllowedEvents >; type WebSocketServiceArgs = { @@ -93,22 +65,9 @@ export class WebSocketService { this.#messenger = messenger; this.#sockets = new Map(); - this.#messenger.registerActionHandler( - `${serviceName}:open`, - async (...args) => this.#open(...args), - ); - - this.#messenger.registerActionHandler(`${serviceName}:close`, (...args) => - this.#close(...args), - ); - - this.#messenger.registerActionHandler( - `${serviceName}:sendMessage`, - async (...args) => this.#sendMessage(...args), - ); - - this.#messenger.registerActionHandler(`${serviceName}:getAll`, (...args) => - this.#getAll(...args), + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); this.#messenger.subscribe('SnapController:snapUpdated', (snap) => { @@ -154,7 +113,7 @@ export class WebSocketService { * @returns True if a matching connection already exists, otherwise false. */ #exists(snapId: SnapId, url: string, protocols: string[]) { - return this.#getAll(snapId).some( + return this.getAll(snapId).some( (socket) => socket.url === url && isEqual(socket.protocols, protocols), ); } @@ -190,7 +149,7 @@ export class WebSocketService { * @returns The identifier for the opened connection. * @throws If the connection fails. */ - async #open(snapId: SnapId, url: string, protocols: string[] = []) { + async open(snapId: SnapId, url: string, protocols: string[] = []) { assert( !this.#exists(snapId, url, protocols), `An open WebSocket connection to ${url} already exists.`, @@ -280,7 +239,7 @@ export class WebSocketService { * @param snapId - The Snap ID. * @param id - The identifier for the WebSocket connection. */ - #close(snapId: SnapId, id: string) { + close(snapId: SnapId, id: string) { const { socket } = this.#get(snapId, id); socket.close(); @@ -292,8 +251,8 @@ export class WebSocketService { * @param snapId - The Snap ID. */ #closeAll(snapId: SnapId) { - for (const socket of this.#getAll(snapId)) { - this.#close(snapId, socket.id); + for (const socket of this.getAll(snapId)) { + this.close(snapId, socket.id); } } @@ -304,7 +263,7 @@ export class WebSocketService { * @param id - The identifier for the WebSocket connection. * @param data - The message to send. */ - async #sendMessage(snapId: SnapId, id: string, data: string | number[]) { + async sendMessage(snapId: SnapId, id: string, data: string | number[]) { const { socket, openPromise } = this.#get(snapId, id); await openPromise; @@ -320,7 +279,7 @@ export class WebSocketService { * @param snapId - The Snap ID. * @returns A list of WebSocket connections. */ - #getAll(snapId: SnapId) { + getAll(snapId: SnapId) { return [...this.#sockets.values()] .filter((socket) => socket.snapId === snapId) .map((socket) => ({ diff --git a/packages/snaps-controllers/src/websocket/index.ts b/packages/snaps-controllers/src/websocket/index.ts index f0a947eb67..1d535dc632 100644 --- a/packages/snaps-controllers/src/websocket/index.ts +++ b/packages/snaps-controllers/src/websocket/index.ts @@ -1 +1,11 @@ -export * from './WebSocketService'; +export type { + WebSocketServiceActions, + WebSocketServiceMessenger, +} from './WebSocketService'; +export { WebSocketService } from './WebSocketService'; +export type { + WebSocketServiceCloseAction, + WebSocketServiceGetAllAction, + WebSocketServiceOpenAction, + WebSocketServiceSendMessageAction, +} from './WebSocketService-method-action-types'; From ba6c56a3e1184b6c460932189b50ce6e7eedda77 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Mar 2026 12:53:51 +0100 Subject: [PATCH 08/17] chore!: Remove active references from `SnapController` (#3920) This removes `activeReferences` from the Snap runtime data, including the methods to increment and decrement it. This was never used in production. --- > [!NOTE] > **Medium Risk** > Removes public `SnapController` APIs (`incrementActiveReferences`/`decrementActiveReferences`) and drops the `activeReferences` runtime guard, which could change idle-time termination behavior for any downstream code that relied on keeping snaps alive. > > **Overview** > Removes `activeReferences` tracking from `SnapController` runtime data, including deleting the `incrementActiveReferences`/`decrementActiveReferences` methods and the associated idle-timeout exclusion. > > Updates tests by removing the scenario that asserted snaps with open sessions are not terminated, and documents the breaking removal in the `snaps-controllers` changelog. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f4474b4284a899be751f71ca655f081db914546b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- packages/snaps-controllers/CHANGELOG.md | 3 +- .../src/snaps/SnapController.test.tsx | 61 ------------------- .../src/snaps/SnapController.ts | 31 ---------- 3 files changed, 2 insertions(+), 93 deletions(-) diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index cc6ae41682..e225996250 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -84,7 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - **RREAKING:** Remove `AbstractExecutionService` class in favour of `ExecutionService` ([#3916](https://github.com/MetaMask/snaps/pull/3916)) -- **BREAKING:** Remove `incrementActiveReferences` and `decrementActiveReferences` actions ([#3907](https://github.com/MetaMask/snaps/pull/3907)) +- **BREAKING:** Remove `incrementActiveReferences` and `decrementActiveReferences` actions and methods ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3920](https://github.com/MetaMask/snaps/pull/3920)) + - This was never used in production. ## [18.0.4] diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index e81c630ae1..6899f3e29f 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -2063,67 +2063,6 @@ describe('SnapController', () => { await service.terminateAllSnaps(); }); - it('does not kill snaps with open sessions', async () => { - const sourceCode = ` - module.exports.onRpcRequest = () => 'foo bar'; - `; - - const rootMessenger = getRootMessenger(); - - const options = getSnapControllerOptions({ - rootMessenger, - idleTimeCheckInterval: 10, - maxIdleTime: 50, - state: { - snaps: getPersistedSnapsState( - getPersistedSnapObject({ - manifest: getSnapManifest({ - shasum: await getSnapChecksum(getMockSnapFiles({ sourceCode })), - }), - sourceCode, - }), - ), - }, - }); - const [snapController, service] = await getSnapControllerWithEES(options); - - const snap = snapController.getSnapExpect(MOCK_SNAP_ID); - - await snapController.startSnap(snap.id); - expect(snapController.state.snaps[snap.id].status).toBe('running'); - - snapController.incrementActiveReferences(snap.id); - - expect( - await snapController.handleRequest({ - snapId: snap.id, - origin: MOCK_ORIGIN, - handler: HandlerType.OnRpcRequest, - request: { - jsonrpc: '2.0', - method: 'test', - params: {}, - id: 1, - }, - }), - ).toBe('foo bar'); - - await sleep(100); - - // Should still be running after idle timeout - expect(snapController.state.snaps[snap.id].status).toBe('running'); - - snapController.decrementActiveReferences(snap.id); - - await sleep(100); - - // Should be terminated by idle timeout now - expect(snapController.state.snaps[snap.id].status).toBe('stopped'); - - snapController.destroy(); - await service.terminateAllSnaps(); - }); - it(`shouldn't time out a long running snap on start up`, async () => { const rootMessenger = getRootMessenger(); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 2e47e79aad..f67a8cd7f6 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -293,11 +293,6 @@ export type SnapRuntimeData = { */ lastRequest: null | number; - /** - * The current number of active references where this Snap is being used - */ - activeReferences: number; - /** * The current pending inbound requests, meaning requests that are processed by snaps. */ @@ -1675,7 +1670,6 @@ export class SnapController extends BaseController< entries .filter( ([_snapId, runtime]) => - runtime.activeReferences === 0 && runtime.pendingInboundRequests.length === 0 && runtime.lastRequest && this.#maxIdleTime && @@ -2569,30 +2563,6 @@ export class SnapController extends BaseController< } } - /** - * Handles incrementing the activeReferences counter. - * - * @param snapId - The snap id of the snap that was referenced. - */ - incrementActiveReferences(snapId: SnapId) { - const runtime = this.#getRuntimeExpect(snapId); - runtime.activeReferences += 1; - } - - /** - * Handles decrement the activeReferences counter. - * - * @param snapId - The snap id of the snap that was referenced.. - */ - decrementActiveReferences(snapId: SnapId) { - const runtime = this.#getRuntimeExpect(snapId); - assert( - runtime.activeReferences > 0, - 'SnapController reference management is in an invalid state.', - ); - runtime.activeReferences -= 1; - } - /** * Gets all snaps in their truncated format. * @@ -4324,7 +4294,6 @@ export class SnapController extends BaseController< installPromise: null, encryptionKey: null, encryptionSalt: null, - activeReferences: 0, pendingInboundRequests: [], pendingOutboundRequests: 0, interpreter, From 89b167514e79fde086fadbaa36647b33a15300be Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Mar 2026 13:13:39 +0100 Subject: [PATCH 09/17] refactor!: Standardise `SnapsRegistry` action names and types (#3918) This renames the `JsonSnapsRegistry` class to `SnapsRegistryController` and updates actions and events to standardise them. --- > [!NOTE] > **Medium Risk** > Medium risk because it is a breaking rename of registry controller class/namespace and messenger action/event names, which can silently break any callers or tests that still use the old `SnapsRegistry:*` API. > > **Overview** > **BREAKING refactor of the snaps registry controller API.** Renames `JsonSnapsRegistry`/`SnapsRegistry` to `SnapRegistryController`, moves registry types into `registry/types.ts`, and switches messenger wiring to `registerMethodActionHandlers` with new method-based action types (including `requestUpdate` replacing `update`). > > Updates `SnapController`, tests, and test utilities/mocks to use the new `SnapRegistryController:*` action/event names and `SnapRegistryStatus` enum, and refreshes the changelog to document the rename. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7a64b389a7d67cf38f81a173cc1b4942a145341b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- packages/snaps-controllers/CHANGELOG.md | 11 +- packages/snaps-controllers/coverage.json | 2 +- .../src/snaps/SnapController.test.tsx | 52 +++---- .../src/snaps/SnapController.ts | 50 +++--- ...pRegistryController-method-action-types.ts | 55 +++++++ ...test.ts => SnapRegistryController.test.ts} | 126 ++++++++------- .../{json.ts => SnapRegistryController.ts} | 144 +++++++----------- .../src/snaps/registry/index.ts | 25 ++- .../src/snaps/registry/registry.ts | 53 ------- .../src/snaps/registry/types.ts | 23 +++ .../src/test-utils/controller.tsx | 92 +++++------ .../src/test-utils/registry.ts | 21 ++- 12 files changed, 348 insertions(+), 306 deletions(-) create mode 100644 packages/snaps-controllers/src/snaps/registry/SnapRegistryController-method-action-types.ts rename packages/snaps-controllers/src/snaps/registry/{json.test.ts => SnapRegistryController.test.ts} (83%) rename packages/snaps-controllers/src/snaps/registry/{json.ts => SnapRegistryController.ts} (78%) delete mode 100644 packages/snaps-controllers/src/snaps/registry/registry.ts create mode 100644 packages/snaps-controllers/src/snaps/registry/types.ts diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index e225996250..d24f3defbc 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911), [#3912](https://github.com/MetaMask/snaps/pull/3912), [#3916](https://github.com/MetaMask/snaps/pull/3916)) +- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911), [#3912](https://github.com/MetaMask/snaps/pull/3912), [#3916](https://github.com/MetaMask/snaps/pull/3916), [#3918](https://github.com/MetaMask/snaps/pull/3918)) - `SnapController` actions: - `GetSnap` is now `SnapControllerGetSnapAction`. - Note: The method is now called `getSnap` instead of `get`. @@ -56,6 +56,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `HandleRequest` is now `ExecutionServiceHandleRequestAction`. - `TerminateSnap` is now `ExecutionServiceTerminateSnapAction`. - `GetExecutionStatus` is now `ExecutionServiceGetExecutionStatusAction`. + - `SnapRegistryController` actions: + - `GetResult` is now `SnapRegistryControllerGetAction`. + - `GetMetadata` is now `SnapRegistryControllerGetMetadataAction`. + - `ResolveVersion` is now `SnapRegistryControllerResolveVersionAction`. + - `Update` is now `SnapRegistryControllerRequestUpdateAction`. + - Note: The method is now called `requestUpdate` instead of `update`. - **BREAKING:** All event types were renamed from `OnSomething` to `ControllerOnSomethingEvent` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3916](https://github.com/MetaMask/snaps/pull/3916)) - `SnapController` events: - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. @@ -76,6 +82,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OutboundResponse` is now `ExecutionServiceOutboundResponseEvent`. - **BREAKING:**: Rename `MultichainRouter` to `MultichainRoutingService` and update action types accordingly ([#3913](https://github.com/MetaMask/snaps/pull/3913)) - This is consistent with the naming of other services. +- **BREAKING:** Rename `JsonSnapsRegistry` to `SnapRegistryController` and update action types accordingly ([#3918](https://github.com/MetaMask/snaps/pull/3918)) + - This is consistent with the naming of other controllers. + - The controller name is now `SnapRegistryController` instead of `SnapsRegistry` as well. - **BREAKING:** `MultichainRoutingService` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3913](https://github.com/MetaMask/snaps/pull/3913)) - **BREAKING:** `SnapInsightsController` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3915](https://github.com/MetaMask/snaps/pull/3915)) - **RREAKING:** Replace `ExecutionService` interface with abstract class ([#3916](https://github.com/MetaMask/snaps/pull/3916)) diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index b63ccf012e..428f6713f9 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -2,5 +2,5 @@ "branches": 94.97, "functions": 98.78, "lines": 98.63, - "statements": 98.43 + "statements": 98.32 } diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index 6899f3e29f..f8891a56dc 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -87,7 +87,7 @@ import { METAMASK_ORIGIN, STATE_DEBOUNCE_TIMEOUT, } from './constants'; -import { SnapsRegistryStatus } from './registry'; +import { SnapRegistryStatus } from './registry'; import type { SnapControllerState } from './SnapController'; import { controllerName, @@ -127,7 +127,7 @@ import { MOCK_SNAP_PERMISSIONS, MOCK_SNAP_SUBJECT_METADATA, MOCK_WALLET_SNAP_PERMISSION, - MockSnapsRegistry, + MockSnapRegistryController, sleep, waitForStateChange, } from '../test-utils'; @@ -817,7 +817,7 @@ describe('SnapController', () => { expect(options.messenger.call).toHaveBeenNthCalledWith( 2, - 'SnapsRegistry:get', + 'SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0', @@ -1048,7 +1048,7 @@ describe('SnapController', () => { it('throws an error if snap is not on allowlist and allowlisting is required but resolve succeeds', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const controller = await getSnapController( getSnapControllerOptions({ @@ -1077,7 +1077,7 @@ describe('SnapController', () => { it('throws an error if the registry is unavailable and allowlisting is required but resolve succeeds', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const controller = await getSnapController( getSnapControllerOptions({ @@ -1091,7 +1091,7 @@ describe('SnapController', () => { // Mock resolve to succeed, but registry.get() will fail later registry.resolveVersion.mockReturnValue('1.0.0'); registry.get.mockReturnValue({ - [MOCK_SNAP_ID]: { status: SnapsRegistryStatus.Unavailable }, + [MOCK_SNAP_ID]: { status: SnapRegistryStatus.Unavailable }, }); await expect( @@ -1141,7 +1141,7 @@ describe('SnapController', () => { it('resolves to allowlisted version when allowlisting is required', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const { manifest, sourceCode, svgIcon } = await getMockSnapFilesWithUpdatedChecksum({ @@ -1151,7 +1151,7 @@ describe('SnapController', () => { }); registry.get.mockResolvedValueOnce({ - [MOCK_SNAP_ID]: { status: SnapsRegistryStatus.Verified }, + [MOCK_SNAP_ID]: { status: SnapRegistryStatus.Verified }, }); registry.resolveVersion.mockReturnValue('1.1.0'); @@ -1181,7 +1181,7 @@ describe('SnapController', () => { it('does not use registry resolving when allowlist is not required', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const controller = await getSnapController( getSnapControllerOptions({ @@ -8857,7 +8857,7 @@ describe('SnapController', () => { it('throws an error if the new version of the snap is blocked', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ manifest: getSnapManifest({ @@ -8879,7 +8879,7 @@ describe('SnapController', () => { ); registry.get.mockResolvedValueOnce({ - [MOCK_SNAP_ID]: { status: SnapsRegistryStatus.Blocked }, + [MOCK_SNAP_ID]: { status: SnapRegistryStatus.Blocked }, }); await expect( @@ -10458,7 +10458,7 @@ describe('SnapController', () => { describe('updateRegistry', () => { it('updates the registry database', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const snapController = await getSnapController( getSnapControllerOptions({ @@ -10470,14 +10470,14 @@ describe('SnapController', () => { ); await snapController.updateRegistry(); - expect(registry.update).toHaveBeenCalled(); + expect(registry.requestUpdate).toHaveBeenCalled(); snapController.destroy(); }); it('blocks snaps as expected', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnapA = getMockSnapData({ id: 'npm:exampleA' as SnapId, @@ -10508,7 +10508,7 @@ describe('SnapController', () => { // Block snap A, ignore B. registry.get.mockResolvedValueOnce({ [mockSnapA.id]: { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: { explanation, infoUrl }, }, }); @@ -10548,7 +10548,7 @@ describe('SnapController', () => { it('stops running snaps when they are blocked', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnap = getMockSnapData({ id: 'npm:example' as SnapId, @@ -10568,7 +10568,7 @@ describe('SnapController', () => { // Block the snap registry.get.mockResolvedValueOnce({ - [mockSnap.id]: { status: SnapsRegistryStatus.Blocked }, + [mockSnap.id]: { status: SnapRegistryStatus.Blocked }, }); await snapController.updateRegistry(); await waitForStateChange(options.messenger); @@ -10583,7 +10583,7 @@ describe('SnapController', () => { it('unblocks snaps as expected', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnapA = getMockSnapData({ id: 'npm:exampleA' as SnapId, @@ -10622,8 +10622,8 @@ describe('SnapController', () => { // Indicate that both snaps A and B are unblocked, and update blocked // states. registry.get.mockResolvedValueOnce({ - [mockSnapA.id]: { status: SnapsRegistryStatus.Unverified }, - [mockSnapB.id]: { status: SnapsRegistryStatus.Unverified }, + [mockSnapA.id]: { status: SnapRegistryStatus.Unverified }, + [mockSnapB.id]: { status: SnapRegistryStatus.Unverified }, }); await snapController.updateRegistry(); @@ -10646,7 +10646,7 @@ describe('SnapController', () => { it('updating blocked snaps does not throw if a snap is removed while fetching the blocklist', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnap = getMockSnapData({ id: 'npm:example' as SnapId, @@ -10675,7 +10675,7 @@ describe('SnapController', () => { // Resolve the blocklist and wait for the call to complete resolveBlockListPromise({ - [mockSnap.id]: { status: SnapsRegistryStatus.Blocked }, + [mockSnap.id]: { status: SnapRegistryStatus.Blocked }, }); await updateBlockList; @@ -10689,7 +10689,7 @@ describe('SnapController', () => { it('logs but does not throw unexpected errors while blocking', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnap = getMockSnapData({ id: 'npm:example' as SnapId, @@ -10713,7 +10713,7 @@ describe('SnapController', () => { // Block the snap registry.get.mockResolvedValueOnce({ - [mockSnap.id]: { status: SnapsRegistryStatus.Blocked }, + [mockSnap.id]: { status: SnapRegistryStatus.Blocked }, }); await snapController.updateRegistry(); @@ -10732,7 +10732,7 @@ describe('SnapController', () => { it('updates preinstalled Snaps', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); // Simulate previous permissions, some of which will be removed rootMessenger.registerActionHandler( @@ -10828,7 +10828,7 @@ describe('SnapController', () => { it('does not update preinstalled Snaps when the feature flag is off', async () => { const rootMessenger = getRootMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const registry = new MockSnapRegistryController(rootMessenger); const snapId = 'npm:@metamask/jsx-example-snap' as SnapId; diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index f67a8cd7f6..7baf94c8e4 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -161,15 +161,15 @@ import { import type { SnapLocation } from './location'; import { detectSnapLocation } from './location'; import type { - GetMetadata, - GetResult, - ResolveVersion, - SnapsRegistryInfo, - SnapsRegistryRequest, - SnapsRegistryStateChangeEvent, - Update, + SnapRegistryControllerGetAction, + SnapRegistryControllerGetMetadataAction, + SnapRegistryControllerResolveVersionAction, + SnapRegistryControllerRequestUpdateAction, + SnapRegistryInfo, + SnapRegistryRequest, + SnapRegistryControllerStateChangeEvent, } from './registry'; -import { SnapsRegistryStatus } from './registry'; +import { SnapRegistryStatus } from './registry'; import { getRunnableSnaps } from './selectors'; import type { SnapControllerMethodActions } from './SnapController-method-action-types'; import { Timer } from './Timer'; @@ -530,10 +530,10 @@ export type AllowedActions = | ExecutionServiceTerminateSnapAction | UpdateCaveat | ApprovalControllerUpdateRequestStateAction - | GetResult - | GetMetadata - | Update - | ResolveVersion + | SnapRegistryControllerGetAction + | SnapRegistryControllerGetMetadataAction + | SnapRegistryControllerResolveVersionAction + | SnapRegistryControllerRequestUpdateAction | SnapInterfaceControllerCreateInterfaceAction | SnapInterfaceControllerGetInterfaceAction | SnapInterfaceControllerSetInterfaceDisplayedAction @@ -547,7 +547,7 @@ export type AllowedEvents = | SnapControllerSnapInstalledEvent | SnapControllerSnapUpdatedEvent | KeyringControllerLockEvent - | SnapsRegistryStateChangeEvent; + | SnapRegistryControllerStateChangeEvent; export type SnapControllerMessenger = Messenger< typeof controllerName, @@ -975,7 +975,7 @@ export class SnapController extends BaseController< ); this.messenger.subscribe( - 'SnapsRegistry:stateChange', + 'SnapRegistryController:stateChange', () => { this.#handleRegistryUpdate().catch((error) => { logError( @@ -998,7 +998,7 @@ export class SnapController extends BaseController< this.#trackSnapExport = throttleTracking( (snapId: SnapId, handler: string, success: boolean, origin: string) => { const snapMetadata = this.messenger.call( - 'SnapsRegistry:getMetadata', + 'SnapRegistryController:getMetadata', snapId, ); this.#trackEvent({ @@ -1452,7 +1452,7 @@ export class SnapController extends BaseController< */ async updateRegistry(): Promise { await this.#ensureCanUsePlatform(); - await this.messenger.call('SnapsRegistry:update'); + await this.messenger.call('SnapRegistryController:requestUpdate'); } /** @@ -1464,8 +1464,8 @@ export class SnapController extends BaseController< */ async #handleRegistryUpdate() { const blockedSnaps = await this.messenger.call( - 'SnapsRegistry:get', - Object.values(this.state.snaps).reduce( + 'SnapRegistryController:get', + Object.values(this.state.snaps).reduce( (blockListArg, snap) => { blockListArg[snap.id] = { version: snap.version, @@ -1479,7 +1479,7 @@ export class SnapController extends BaseController< await Promise.all( Object.entries(blockedSnaps).map(async ([snapId, { status, reason }]) => { - if (status === SnapsRegistryStatus.Blocked) { + if (status === SnapRegistryStatus.Blocked) { return this.#blockSnap(snapId as SnapId, reason); } @@ -1586,17 +1586,17 @@ export class SnapController extends BaseController< { platformVersion, ...snapInfo - }: SnapsRegistryInfo & { + }: SnapRegistryInfo & { permissions: SnapPermissions; platformVersion: string | undefined; }, ) { - const results = await this.messenger.call('SnapsRegistry:get', { + const results = await this.messenger.call('SnapRegistryController:get', { [snapId]: snapInfo, }); const result = results[snapId]; - if (result.status === SnapsRegistryStatus.Blocked) { + if (result.status === SnapRegistryStatus.Blocked) { throw new Error( `Cannot install version "${ snapInfo.version @@ -1613,11 +1613,11 @@ export class SnapController extends BaseController< if ( this.#featureFlags.requireAllowlist && isAllowlistingRequired && - result.status !== SnapsRegistryStatus.Verified + result.status !== SnapRegistryStatus.Verified ) { throw new Error( `Cannot install version "${snapInfo.version}" of snap "${snapId}": ${ - result.status === SnapsRegistryStatus.Unavailable + result.status === SnapRegistryStatus.Unavailable ? 'The registry is temporarily unavailable.' : 'The snap is not on the allowlist.' }`, @@ -3107,7 +3107,7 @@ export class SnapController extends BaseController< versionRange: SemVerRange, ): Promise { return await this.messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', snapId, versionRange, ); diff --git a/packages/snaps-controllers/src/snaps/registry/SnapRegistryController-method-action-types.ts b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController-method-action-types.ts new file mode 100644 index 0000000000..5fcac09d6e --- /dev/null +++ b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController-method-action-types.ts @@ -0,0 +1,55 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { SnapRegistryController } from './SnapRegistryController'; + +/** + * Triggers an update of the registry database. + * + * If an existing update is in progress this function will await that update. + */ +export type SnapRegistryControllerRequestUpdateAction = { + type: `SnapRegistryController:requestUpdate`; + handler: SnapRegistryController['requestUpdate']; +}; + +export type SnapRegistryControllerGetAction = { + type: `SnapRegistryController:get`; + handler: SnapRegistryController['get']; +}; + +/** + * Find an allowlisted version within a specified version range. Otherwise return the version range itself. + * + * @param snapId - The ID of the snap we are trying to resolve a version for. + * @param versionRange - The version range. + * @param refetch - An optional flag used to determine if we are refetching the registry. + * @returns An allowlisted version within the specified version range if available otherwise returns the input version range. + */ +export type SnapRegistryControllerResolveVersionAction = { + type: `SnapRegistryController:resolveVersion`; + handler: SnapRegistryController['resolveVersion']; +}; + +/** + * Get metadata for the given snap ID, if available, without updating registry. + * + * @param snapId - The ID of the snap to get metadata for. + * @returns The metadata for the given snap ID, or `null` if the snap is not + * verified. + */ +export type SnapRegistryControllerGetMetadataAction = { + type: `SnapRegistryController:getMetadata`; + handler: SnapRegistryController['getMetadata']; +}; + +/** + * Union of all SnapRegistryController action types. + */ +export type SnapRegistryControllerMethodActions = + | SnapRegistryControllerRequestUpdateAction + | SnapRegistryControllerGetAction + | SnapRegistryControllerResolveVersionAction + | SnapRegistryControllerGetMetadataAction; diff --git a/packages/snaps-controllers/src/snaps/registry/json.test.ts b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.test.ts similarity index 83% rename from packages/snaps-controllers/src/snaps/registry/json.test.ts rename to packages/snaps-controllers/src/snaps/registry/SnapRegistryController.test.ts index 3aaada161b..fb77965395 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.test.ts +++ b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.test.ts @@ -7,20 +7,20 @@ import { import type { SemVerRange, SemVerVersion } from '@metamask/utils'; import fetchMock from 'jest-fetch-mock'; -import type { JsonSnapsRegistryArgs } from './json'; -import { JsonSnapsRegistry } from './json'; -import { SnapsRegistryStatus } from './registry'; -import { getRestrictedSnapsRegistryControllerMessenger } from '../../test-utils'; +import type { SnapRegistryControllerArgs } from './SnapRegistryController'; +import { SnapRegistryController } from './SnapRegistryController'; +import { SnapRegistryStatus } from './types'; +import { getRestrictedSnapRegistryControllerMessenger } from '../../test-utils'; // Public key for the private key: // `0x541c6759fd86c69eceb8d792d7174623db139d81a5b560aa026afcb2dd1bb21c`. const MOCK_PUBLIC_KEY = '0x03a885324b8520fba54a173999629952cfa1f97930c20902ec389f9c32c6ffbc40'; -const getRegistry = (args?: Partial) => { - const messenger = getRestrictedSnapsRegistryControllerMessenger(); +const getRegistry = (args?: Partial) => { + const messenger = getRestrictedSnapRegistryControllerMessenger(); return { - registry: new JsonSnapsRegistry({ + registry: new SnapRegistryController({ messenger, publicKey: MOCK_PUBLIC_KEY, clientConfig: { @@ -125,7 +125,7 @@ const MOCK_COMPATIBILITY_SIGNATURE_FILE = { format: 'DER', }; -describe('JsonSnapsRegistry', () => { +describe('SnapRegistryController', () => { fetchMock.enableMocks(); afterEach(() => { @@ -138,7 +138,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -147,7 +147,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -159,7 +159,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_EMPTY_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -168,7 +168,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -179,7 +179,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.1' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -188,7 +188,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -199,7 +199,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: 'bar', @@ -208,7 +208,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -219,7 +219,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: 'foo', @@ -228,7 +228,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: { explanation: 'malicious' }, }, }); @@ -240,7 +240,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { 'npm:@consensys/starknet-snap': { version: '0.1.10' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -249,7 +249,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ 'npm:@consensys/starknet-snap': { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: { explanation: 'vuln' }, }, }); @@ -267,7 +267,7 @@ describe('JsonSnapsRegistry', () => { database: { verifiedSnaps: {}, blockedSnaps: [] }, }, }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -276,7 +276,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -287,7 +287,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -296,7 +296,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -307,7 +307,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.1.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -316,7 +316,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -332,7 +332,7 @@ describe('JsonSnapsRegistry', () => { }, }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -341,7 +341,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -357,7 +357,7 @@ describe('JsonSnapsRegistry', () => { }, }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.1' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -366,7 +366,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -376,7 +376,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -385,7 +385,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -399,7 +399,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -408,7 +408,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -423,7 +423,7 @@ describe('JsonSnapsRegistry', () => { '0x034ca27b046507d1a9997bddc991b56d96b93d4adac3a96dfe01ce450bfb661455', }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -432,7 +432,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -445,7 +445,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -460,7 +460,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -477,7 +477,7 @@ describe('JsonSnapsRegistry', () => { clientConfig: { type: 'extension', version: '15.0.0' as SemVerVersion }, }); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -496,7 +496,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); expect( await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, range, ), @@ -512,7 +512,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); expect( await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, range, ), @@ -532,7 +532,7 @@ describe('JsonSnapsRegistry', () => { }, }); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -568,7 +568,7 @@ describe('JsonSnapsRegistry', () => { }, }); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -584,8 +584,11 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); - const result = messenger.call('SnapsRegistry:getMetadata', MOCK_SNAP_ID); + await messenger.call('SnapRegistryController:requestUpdate'); + const result = messenger.call( + 'SnapRegistryController:getMetadata', + MOCK_SNAP_ID, + ); expect(result).toStrictEqual({ name: 'Mock Snap', @@ -598,8 +601,11 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); - const result = messenger.call('SnapsRegistry:getMetadata', 'foo'); + await messenger.call('SnapRegistryController:requestUpdate'); + const result = messenger.call( + 'SnapRegistryController:getMetadata', + 'foo', + ); expect(result).toBeNull(); }); @@ -612,7 +618,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); + await messenger.call('SnapRegistryController:requestUpdate'); expect(fetchMock).toHaveBeenCalledTimes(2); }); @@ -632,7 +638,7 @@ describe('JsonSnapsRegistry', () => { databaseUnavailable: false, }, }); - await messenger.call('SnapsRegistry:update'); + await messenger.call('SnapRegistryController:requestUpdate'); expect(fetchMock).toHaveBeenCalledTimes(2); expect(spy).not.toHaveBeenCalled(); @@ -644,8 +650,8 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); - await messenger.call('SnapsRegistry:update'); + await messenger.call('SnapRegistryController:requestUpdate'); + await messenger.call('SnapRegistryController:requestUpdate'); expect(fetchMock).toHaveBeenCalledTimes(2); }); @@ -657,8 +663,8 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); await Promise.all([ - messenger.call('SnapsRegistry:update'), - messenger.call('SnapsRegistry:update'), + messenger.call('SnapRegistryController:requestUpdate'), + messenger.call('SnapRegistryController:requestUpdate'), ]); expect(fetchMock).toHaveBeenCalledTimes(2); @@ -670,8 +676,18 @@ describe('JsonSnapsRegistry', () => { const { registry } = getRegistry(); expect( - deriveStateFromMetadata(registry.state, registry.metadata, 'anonymous'), - ).toMatchInlineSnapshot(`{}`); + deriveStateFromMetadata( + registry.state, + registry.metadata, + 'includeInDebugSnapshot', + ), + ).toMatchInlineSnapshot(` + { + "databaseUnavailable": false, + "lastUpdated": null, + "signature": null, + } + `); }); it('includes expected state in state logs', () => { diff --git a/packages/snaps-controllers/src/snaps/registry/json.ts b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.ts similarity index 78% rename from packages/snaps-controllers/src/snaps/registry/json.ts rename to packages/snaps-controllers/src/snaps/registry/SnapRegistryController.ts index 3c5310f5a5..4944f3789b 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.ts +++ b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.ts @@ -20,14 +20,14 @@ import { satisfiesVersionRange, } from '@metamask/utils'; +import type { SnapRegistryControllerMethodActions } from './SnapRegistryController-method-action-types'; import type { - SnapsRegistry, - SnapsRegistryInfo, - SnapsRegistryMetadata, - SnapsRegistryRequest, - SnapsRegistryResult, -} from './registry'; -import { SnapsRegistryStatus } from './registry'; + SnapRegistryInfo, + SnapRegistryMetadata, + SnapRegistryRequest, + SnapRegistryResult, +} from './types'; +import { SnapRegistryStatus } from './types'; const SNAP_REGISTRY_URL = 'https://acl.execution.metamask.io/latest/registry.json'; @@ -48,9 +48,9 @@ export type ClientConfig = { version: SemVerVersion; }; -export type JsonSnapsRegistryArgs = { - messenger: SnapsRegistryMessenger; - state?: SnapsRegistryState; +export type SnapRegistryControllerArgs = { + messenger: SnapRegistryControllerMessenger; + state?: SnapRegistryControllerState; fetchFunction?: typeof fetch; url?: JsonSnapsRegistryUrl; recentFetchThreshold?: number; @@ -59,59 +59,44 @@ export type JsonSnapsRegistryArgs = { clientConfig: ClientConfig; }; -export type GetResult = { - type: `${typeof controllerName}:get`; - handler: SnapsRegistry['get']; -}; - -export type ResolveVersion = { - type: `${typeof controllerName}:resolveVersion`; - handler: SnapsRegistry['resolveVersion']; -}; - -export type GetMetadata = { - type: `${typeof controllerName}:getMetadata`; - handler: SnapsRegistry['getMetadata']; -}; - -export type Update = { - type: `${typeof controllerName}:update`; - handler: SnapsRegistry['update']; -}; - -export type SnapsRegistryGetStateAction = ControllerGetStateAction< +export type SnapRegistryControllerGetStateAction = ControllerGetStateAction< typeof controllerName, - SnapsRegistryState + SnapRegistryControllerState >; -export type SnapsRegistryActions = - | SnapsRegistryGetStateAction - | GetResult - | GetMetadata - | Update - | ResolveVersion; +export type SnapRegistryControllerActions = + | SnapRegistryControllerGetStateAction + | SnapRegistryControllerMethodActions; -export type SnapsRegistryStateChangeEvent = ControllerStateChangeEvent< +export type SnapRegistryControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, - SnapsRegistryState + SnapRegistryControllerState >; -export type SnapsRegistryEvents = SnapsRegistryStateChangeEvent; +export type SnapRegistryControllerEvents = + SnapRegistryControllerStateChangeEvent; -export type SnapsRegistryMessenger = Messenger< - 'SnapsRegistry', - SnapsRegistryActions, - SnapsRegistryEvents +export type SnapRegistryControllerMessenger = Messenger< + typeof controllerName, + SnapRegistryControllerActions, + SnapRegistryControllerEvents >; -export type SnapsRegistryState = { +export type SnapRegistryControllerState = { database: SnapsRegistryDatabase | null; signature: string | null; lastUpdated: number | null; databaseUnavailable: boolean; }; -const controllerName = 'SnapsRegistry'; +const controllerName = 'SnapRegistryController'; + +const MESSENGER_EXPOSED_METHODS = [ + 'get', + 'getMetadata', + 'resolveVersion', + 'requestUpdate', +] as const; const defaultState = { database: null, @@ -120,10 +105,10 @@ const defaultState = { databaseUnavailable: false, }; -export class JsonSnapsRegistry extends BaseController< +export class SnapRegistryController extends BaseController< typeof controllerName, - SnapsRegistryState, - SnapsRegistryMessenger + SnapRegistryControllerState, + SnapRegistryControllerMessenger > { readonly #url: JsonSnapsRegistryUrl; @@ -151,7 +136,7 @@ export class JsonSnapsRegistry extends BaseController< fetchFunction = globalThis.fetch.bind(undefined), recentFetchThreshold = inMilliseconds(5, Duration.Minute), refetchOnAllowlistMiss = true, - }: JsonSnapsRegistryArgs) { + }: SnapRegistryControllerArgs) { super({ messenger, metadata: { @@ -194,22 +179,9 @@ export class JsonSnapsRegistry extends BaseController< this.#refetchOnAllowlistMiss = refetchOnAllowlistMiss; this.#currentUpdate = null; - this.messenger.registerActionHandler('SnapsRegistry:get', async (...args) => - this.#get(...args), - ); - - this.messenger.registerActionHandler( - 'SnapsRegistry:getMetadata', - (...args) => this.#getMetadata(...args), - ); - - this.messenger.registerActionHandler( - 'SnapsRegistry:resolveVersion', - async (...args) => this.#resolveVersion(...args), - ); - - this.messenger.registerActionHandler('SnapsRegistry:update', async () => - this.#triggerUpdate(), + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } @@ -225,7 +197,7 @@ export class JsonSnapsRegistry extends BaseController< * * If an existing update is in progress this function will await that update. */ - async #triggerUpdate() { + async requestUpdate() { // If an update is ongoing, wait for that. if (this.#currentUpdate) { await this.#currentUpdate; @@ -285,7 +257,7 @@ export class JsonSnapsRegistry extends BaseController< async #getDatabase(): Promise { if (this.state.database === null) { - await this.#triggerUpdate(); + await this.requestUpdate(); } return this.state.database; @@ -293,9 +265,9 @@ export class JsonSnapsRegistry extends BaseController< async #getSingle( snapId: string, - snapInfo: SnapsRegistryInfo, + snapInfo: SnapRegistryInfo, refetch = false, - ): Promise { + ): Promise { const database = await this.#getDatabase(); const blockedEntry = database?.blockedSnaps.find((blocked) => { @@ -311,7 +283,7 @@ export class JsonSnapsRegistry extends BaseController< if (blockedEntry) { return { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: blockedEntry.reason, }; } @@ -323,25 +295,25 @@ export class JsonSnapsRegistry extends BaseController< !clientRange || satisfiesVersionRange(this.#clientConfig.version, clientRange); if (version && version.checksum === snapInfo.checksum && isCompatible) { - return { status: SnapsRegistryStatus.Verified }; + return { status: SnapRegistryStatus.Verified }; } // For now, if we have an allowlist miss, we can refetch once and try again. if (this.#refetchOnAllowlistMiss && !refetch) { - await this.#triggerUpdate(); + await this.requestUpdate(); return this.#getSingle(snapId, snapInfo, true); } return { status: this.state.databaseUnavailable - ? SnapsRegistryStatus.Unavailable - : SnapsRegistryStatus.Unverified, + ? SnapRegistryStatus.Unavailable + : SnapRegistryStatus.Unverified, }; } - async #get( - snaps: SnapsRegistryRequest, - ): Promise> { + async get( + snaps: SnapRegistryRequest, + ): Promise> { return Object.entries(snaps).reduce< - Promise> + Promise> >(async (previousPromise, [snapId, snapInfo]) => { const result = await this.#getSingle(snapId, snapInfo); const acc = await previousPromise; @@ -358,7 +330,7 @@ export class JsonSnapsRegistry extends BaseController< * @param refetch - An optional flag used to determine if we are refetching the registry. * @returns An allowlisted version within the specified version range if available otherwise returns the input version range. */ - async #resolveVersion( + async resolveVersion( snapId: string, versionRange: SemVerRange, refetch = false, @@ -367,8 +339,8 @@ export class JsonSnapsRegistry extends BaseController< const versions = database?.verifiedSnaps[snapId]?.versions ?? null; if (!versions && this.#refetchOnAllowlistMiss && !refetch) { - await this.#triggerUpdate(); - return this.#resolveVersion(snapId, versionRange, true); + await this.requestUpdate(); + return this.resolveVersion(snapId, versionRange, true); } // If we cannot narrow down the version range we return the unaltered version range. @@ -394,8 +366,8 @@ export class JsonSnapsRegistry extends BaseController< const targetVersion = getTargetVersion(compatibleVersions, versionRange); if (!targetVersion && this.#refetchOnAllowlistMiss && !refetch) { - await this.#triggerUpdate(); - return this.#resolveVersion(snapId, versionRange, true); + await this.requestUpdate(); + return this.resolveVersion(snapId, versionRange, true); } // If we cannot narrow down the version range we return the unaltered version range. @@ -415,7 +387,7 @@ export class JsonSnapsRegistry extends BaseController< * @returns The metadata for the given snap ID, or `null` if the snap is not * verified. */ - #getMetadata(snapId: string): SnapsRegistryMetadata | null { + getMetadata(snapId: string): SnapRegistryMetadata | null { return this.state?.database?.verifiedSnaps[snapId]?.metadata ?? null; } diff --git a/packages/snaps-controllers/src/snaps/registry/index.ts b/packages/snaps-controllers/src/snaps/registry/index.ts index e1865b60d5..a4c0875fa5 100644 --- a/packages/snaps-controllers/src/snaps/registry/index.ts +++ b/packages/snaps-controllers/src/snaps/registry/index.ts @@ -1,2 +1,23 @@ -export * from './registry'; -export * from './json'; +export type { + SnapRegistryControllerActions, + SnapRegistryControllerEvents, + SnapRegistryControllerArgs, + SnapRegistryControllerGetStateAction, + SnapRegistryControllerMessenger, + SnapRegistryControllerState, + SnapRegistryControllerStateChangeEvent, +} from './SnapRegistryController'; +export { SnapRegistryController } from './SnapRegistryController'; +export type { + SnapRegistryControllerGetAction, + SnapRegistryControllerGetMetadataAction, + SnapRegistryControllerRequestUpdateAction, + SnapRegistryControllerResolveVersionAction, +} from './SnapRegistryController-method-action-types'; +export type { + SnapRegistryInfo, + SnapRegistryMetadata, + SnapRegistryRequest, + SnapRegistryResult, +} from './types'; +export { SnapRegistryStatus } from './types'; diff --git a/packages/snaps-controllers/src/snaps/registry/registry.ts b/packages/snaps-controllers/src/snaps/registry/registry.ts deleted file mode 100644 index 07c5009677..0000000000 --- a/packages/snaps-controllers/src/snaps/registry/registry.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { - BlockReason, - SnapsRegistryDatabase, -} from '@metamask/snaps-registry'; -import type { SnapId } from '@metamask/snaps-sdk'; -import type { SemVerRange, SemVerVersion } from '@metamask/utils'; - -export type SnapsRegistryInfo = { version: SemVerVersion; checksum: string }; -export type SnapsRegistryRequest = Record; -export type SnapsRegistryMetadata = - SnapsRegistryDatabase['verifiedSnaps'][SnapId]['metadata']; - -export enum SnapsRegistryStatus { - Unverified = 0, - Blocked = 1, - Verified = 2, - Unavailable = 3, -} - -export type SnapsRegistryResult = { - status: SnapsRegistryStatus; - reason?: BlockReason; -}; - -export type SnapsRegistry = { - get( - snaps: SnapsRegistryRequest, - ): Promise>; - - update(): Promise; - - /** - * Find an allowlisted version within a specified version range. - * - * @param snapId - The ID of the snap we are trying to resolve a version for. - * @param versionRange - The version range. - * @returns An allowlisted version within the specified version range. - * @throws If an allowlisted version does not exist within the version range. - */ - resolveVersion( - snapId: SnapId, - versionRange: SemVerRange, - ): Promise; - - /** - * Get metadata for the given snap ID. - * - * @param snapId - The ID of the snap to get metadata for. - * @returns The metadata for the given snap ID, or `null` if the snap is not - * verified. - */ - getMetadata(snapId: SnapId): SnapsRegistryMetadata | null; -}; diff --git a/packages/snaps-controllers/src/snaps/registry/types.ts b/packages/snaps-controllers/src/snaps/registry/types.ts new file mode 100644 index 0000000000..bbdcb82163 --- /dev/null +++ b/packages/snaps-controllers/src/snaps/registry/types.ts @@ -0,0 +1,23 @@ +import type { + BlockReason, + SnapsRegistryDatabase, +} from '@metamask/snaps-registry'; +import type { SnapId } from '@metamask/snaps-sdk'; +import type { SemVerVersion } from '@metamask/utils'; + +export type SnapRegistryInfo = { version: SemVerVersion; checksum: string }; +export type SnapRegistryRequest = Record; +export type SnapRegistryMetadata = + SnapsRegistryDatabase['verifiedSnaps'][SnapId]['metadata']; + +export enum SnapRegistryStatus { + Unverified = 0, + Blocked = 1, + Verified = 2, + Unavailable = 3, +} + +export type SnapRegistryResult = { + status: SnapRegistryStatus; + reason?: BlockReason; +}; diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index 7211352855..0fd143a10f 100644 --- a/packages/snaps-controllers/src/test-utils/controller.tsx +++ b/packages/snaps-controllers/src/test-utils/controller.tsx @@ -53,7 +53,7 @@ import type { Json } from '@metamask/utils'; import { MOCK_CRONJOB_PERMISSION } from './cronjob'; import { getNodeEES, getNodeEESMessenger } from './execution-environment'; -import { MockSnapsRegistry } from './registry'; +import { MockSnapRegistryController } from './registry'; import type { CronjobControllerMessenger } from '../cronjob/CronjobController'; import type { SnapInsightsControllerMessenger } from '../insights'; import type { @@ -62,11 +62,7 @@ import type { } from '../interface/SnapInterfaceController'; import type { MultichainRoutingServiceMessenger } from '../multichain/MultichainRoutingService'; import type { ExecutionService, ExecutionServiceMessenger } from '../services'; -import type { - SnapsRegistryActions, - SnapsRegistryEvents, - SnapsRegistryMessenger, -} from '../snaps'; +import type { SnapRegistryControllerMessenger } from '../snaps'; import { SnapController } from '../snaps'; import type { PersistedSnapControllerState, @@ -74,11 +70,7 @@ import type { SnapControllerStateChangeEvent, } from '../snaps/SnapController'; import type { KeyDerivationOptions } from '../types'; -import type { - WebSocketServiceActions, - WebSocketServiceAllowedActions, - WebSocketServiceEvents, -} from '../websocket'; +import type { WebSocketServiceMessenger } from '../websocket'; const asyncNoOp = async () => Promise.resolve(); @@ -329,10 +321,14 @@ export const MOCK_INSIGHTS_PERMISSIONS_NO_ORIGINS: Record< export type RootMessenger = Messenger< MockAnyNamespace, MessengerActions< - SnapControllerMessenger | SnapsRegistryMessenger | ExecutionServiceMessenger + | SnapControllerMessenger + | SnapRegistryControllerMessenger + | ExecutionServiceMessenger >, MessengerEvents< - SnapControllerMessenger | SnapsRegistryMessenger | ExecutionServiceMessenger + | SnapControllerMessenger + | SnapRegistryControllerMessenger + | ExecutionServiceMessenger > >; @@ -434,7 +430,7 @@ export const getRootMessenger = () => { messenger.registerActionHandler('ExecutionService:terminateSnap', asyncNoOp); // eslint-disable-next-line no-new - new MockSnapsRegistry(messenger); + new MockSnapRegistryController(messenger); messenger.registerActionHandler( 'SnapInterfaceController:createInterface', @@ -496,10 +492,10 @@ export const getSnapControllerMessenger = ( 'PermissionController:getSubjectNames', 'SubjectMetadataController:getSubjectMetadata', 'SubjectMetadataController:addSubjectMetadata', - 'SnapsRegistry:get', - 'SnapsRegistry:getMetadata', - 'SnapsRegistry:update', - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:get', + 'SnapRegistryController:getMetadata', + 'SnapRegistryController:requestUpdate', + 'SnapRegistryController:resolveVersion', 'SnapInterfaceController:createInterface', 'SnapInterfaceController:setInterfaceDisplayed', 'SnapInterfaceController:getInterface', @@ -513,7 +509,7 @@ export const getSnapControllerMessenger = ( 'ExecutionService:outboundRequest', 'ExecutionService:outboundResponse', 'KeyringController:lock', - 'SnapsRegistry:stateChange', + 'SnapRegistryController:stateChange', ], messenger: snapControllerMessenger, }); @@ -789,29 +785,32 @@ export const getRestrictedCronjobControllerMessenger = ( return cronjobControllerMessenger; }; -// Mock controller messenger for registry -export const getRootSnapsRegistryControllerMessenger = () => { - const messenger = new MockControllerMessenger< - SnapsRegistryActions, - SnapsRegistryEvents - >(); +type SnapRegistryControllerRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + +export const getRootSnapRegistryControllerMessenger = () => { + const messenger: SnapRegistryControllerRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); return messenger; }; -export const getRestrictedSnapsRegistryControllerMessenger = ( - messenger: ReturnType< - typeof getRootSnapsRegistryControllerMessenger - > = getRootSnapsRegistryControllerMessenger(), +export const getRestrictedSnapRegistryControllerMessenger = ( + rootMessenger: ReturnType< + typeof getRootSnapRegistryControllerMessenger + > = getRootSnapRegistryControllerMessenger(), ) => { - return new Messenger< - 'SnapsRegistry', - SnapsRegistryActions, - SnapsRegistryEvents, - any - >({ namespace: 'SnapsRegistry', parent: messenger }); + const messenger: SnapRegistryControllerMessenger = new Messenger({ + namespace: 'SnapRegistryController', + parent: rootMessenger, + }); + + return messenger; }; /** @@ -1024,12 +1023,15 @@ export const getRestrictedMultichainRoutingServiceMessenger = ( return controllerMessenger; }; -// Mock controller messenger for WebSocketService +type WebSocketServiceRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + export const getRootWebSocketServiceMessenger = () => { - const messenger = new MockControllerMessenger< - WebSocketServiceActions | WebSocketServiceAllowedActions, - WebSocketServiceEvents - >(); + const messenger: WebSocketServiceRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); @@ -1041,12 +1043,10 @@ export const getRestrictedWebSocketServiceMessenger = ( typeof getRootWebSocketServiceMessenger > = getRootWebSocketServiceMessenger(), ) => { - const controllerMessenger = new Messenger< - 'WebSocketService', - WebSocketServiceActions | WebSocketServiceAllowedActions, - WebSocketServiceEvents, - any - >({ namespace: 'WebSocketService', parent: messenger }); + const controllerMessenger: WebSocketServiceMessenger = new Messenger({ + namespace: 'WebSocketService', + parent: messenger, + }); messenger.delegate({ actions: ['SnapController:handleRequest'], diff --git a/packages/snaps-controllers/src/test-utils/registry.ts b/packages/snaps-controllers/src/test-utils/registry.ts index ef44f797ff..34bacae94a 100644 --- a/packages/snaps-controllers/src/test-utils/registry.ts +++ b/packages/snaps-controllers/src/test-utils/registry.ts @@ -1,31 +1,30 @@ import type { RootMessenger } from './controller'; -import type { SnapsRegistry } from '../snaps'; -import { SnapsRegistryStatus } from '../snaps'; +import { SnapRegistryStatus } from '../snaps'; -export class MockSnapsRegistry implements SnapsRegistry { +export class MockSnapRegistryController { readonly #messenger; constructor(messenger: RootMessenger) { this.#messenger = messenger; this.#messenger.registerActionHandler( - 'SnapsRegistry:get', + 'SnapRegistryController:get', this.get.bind(this), ); this.#messenger.registerActionHandler( - 'SnapsRegistry:getMetadata', + 'SnapRegistryController:getMetadata', this.getMetadata.bind(this), ); this.#messenger.registerActionHandler( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', this.resolveVersion.bind(this), ); this.#messenger.registerActionHandler( - 'SnapsRegistry:update', - this.update.bind(this), + 'SnapRegistryController:requestUpdate', + this.requestUpdate.bind(this), ); } @@ -34,7 +33,7 @@ export class MockSnapsRegistry implements SnapsRegistry { Object.keys(snaps).reduce( (acc, snapId) => ({ ...acc, - [snapId]: { status: SnapsRegistryStatus.Unverified }, + [snapId]: { status: SnapRegistryStatus.Unverified }, }), {}, ), @@ -47,9 +46,9 @@ export class MockSnapsRegistry implements SnapsRegistry { getMetadata = jest.fn().mockReturnValue(null); - update = jest.fn().mockImplementation(() => { + requestUpdate = jest.fn().mockImplementation(() => { this.#messenger.publish( - 'SnapsRegistry:stateChange', + 'SnapRegistryController:stateChange', { database: { verifiedSnaps: {}, blockedSnaps: [] }, lastUpdated: Date.now(), From c038fb4c1004576cbafcc203a941192601cc6c6c Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Mar 2026 14:21:46 +0100 Subject: [PATCH 10/17] chore: Standardise controller/service exports (#3921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > **Low Risk** > Primarily TypeScript type/export refactors; runtime behavior should be unchanged, but downstream packages may require import/type updates due to renamed or newly exported messenger/action/event types. > > **Overview** > Standardizes public exports across `snaps-controllers` by re-exporting each controller/service’s `*Args`, `*Actions`, `*Events`, and `*Messenger` types (e.g., `CronjobController`, `SnapInsightsController`, `SnapInterfaceController`, `MultichainRoutingService`, `ExecutionService`, `SnapController`). > > Tightens internal messenger typing by making previously exported `AllowedActions`/`AllowedEvents` helper unions internal (renamed to local `Allowed*` types) and updating `snaps-simulation` to use the concrete `*Messenger` types and a strongly-typed `RootControllerMessenger` based on `@metamask/messenger`’s `MessengerActions`/`MessengerEvents`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e1c676d5cd53702ae26932df271772949cf39809. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../snaps-controllers/src/cronjob/index.ts | 4 ++ .../src/insights/SnapInsightsController.ts | 8 +-- .../snaps-controllers/src/insights/index.ts | 3 + .../src/interface/SnapInterfaceController.ts | 11 ++-- .../snaps-controllers/src/interface/index.ts | 4 +- .../multichain/MultichainRoutingService.ts | 4 +- .../snaps-controllers/src/multichain/index.ts | 5 ++ .../snaps-controllers/src/services/index.ts | 1 + .../src/snaps/SnapController.ts | 6 +- packages/snaps-controllers/src/snaps/index.ts | 4 ++ packages/snaps-simulation/src/controllers.ts | 55 +++++++++++-------- .../src/test-utils/controller.ts | 16 ++---- 12 files changed, 70 insertions(+), 51 deletions(-) diff --git a/packages/snaps-controllers/src/cronjob/index.ts b/packages/snaps-controllers/src/cronjob/index.ts index ad8d078a66..c87ea0c970 100644 --- a/packages/snaps-controllers/src/cronjob/index.ts +++ b/packages/snaps-controllers/src/cronjob/index.ts @@ -1,5 +1,9 @@ export type { + CronjobControllerActions, + CronjobControllerArgs, + CronjobControllerEvents, CronjobControllerGetStateAction, + CronjobControllerMessenger, CronjobControllerState, CronjobControllerStateChangeEvent, CronjobControllerStateManager, diff --git a/packages/snaps-controllers/src/insights/SnapInsightsController.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.ts index 9517252bda..1c19431e6d 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.ts @@ -34,7 +34,7 @@ import type { const controllerName = 'SnapInsightsController'; -export type SnapInsightsControllerAllowedActions = +type AllowedActions = | GetPermissions | SnapControllerGetRunnableSnapsAction | SnapControllerHandleRequestAction @@ -55,15 +55,15 @@ export type SnapInsightControllerStateChangeEvent = ControllerStateChangeEvent< export type SnapInsightControllerEvents = SnapInsightControllerStateChangeEvent; -export type SnapInsightsControllerAllowedEvents = +type AllowedEvents = | TransactionControllerUnapprovedTransactionAddedEvent | TransactionControllerTransactionStatusUpdatedEvent | SignatureControllerStateChangeEvent; export type SnapInsightsControllerMessenger = Messenger< typeof controllerName, - SnapInsightsControllerActions | SnapInsightsControllerAllowedActions, - SnapInsightControllerEvents | SnapInsightsControllerAllowedEvents + SnapInsightsControllerActions | AllowedActions, + SnapInsightControllerEvents | AllowedEvents >; export type SnapInsight = { diff --git a/packages/snaps-controllers/src/insights/index.ts b/packages/snaps-controllers/src/insights/index.ts index d0d0fa10c5..50f42ec0aa 100644 --- a/packages/snaps-controllers/src/insights/index.ts +++ b/packages/snaps-controllers/src/insights/index.ts @@ -1,5 +1,8 @@ export type { SnapInsight, + SnapInsightsControllerActions, + SnapInsightsControllerArgs, + SnapInsightControllerEvents, SnapInsightsControllerMessenger, SnapInsightsControllerState, } from './SnapInsightsController'; diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts index d992660767..5b47cfe547 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts @@ -93,7 +93,7 @@ type PhishingControllerTestOrigin = { handler: (origin: string) => { result: boolean; type: string }; }; -export type SnapInterfaceControllerAllowedActions = +type AllowedActions = | PhishingControllerTestOrigin | ApprovalControllerHasRequestAction | ApprovalControllerAcceptRequestAction @@ -143,14 +143,15 @@ type NotificationListUpdatedEvent = { payload: [Notification[]]; }; +type AllowedEvents = NotificationListUpdatedEvent; + export type SnapInterfaceControllerEvents = - | SnapInterfaceControllerStateChangeEvent - | NotificationListUpdatedEvent; + SnapInterfaceControllerStateChangeEvent; export type SnapInterfaceControllerMessenger = Messenger< typeof controllerName, - SnapInterfaceControllerActions | SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerEvents + SnapInterfaceControllerActions | AllowedActions, + SnapInterfaceControllerEvents | AllowedEvents >; export type StoredInterface = { diff --git a/packages/snaps-controllers/src/interface/index.ts b/packages/snaps-controllers/src/interface/index.ts index 3abc18faf2..6a41e11678 100644 --- a/packages/snaps-controllers/src/interface/index.ts +++ b/packages/snaps-controllers/src/interface/index.ts @@ -1,7 +1,9 @@ export type { SnapInterfaceControllerActions, - SnapInterfaceControllerAllowedActions, + SnapInterfaceControllerArgs, + SnapInterfaceControllerEvents, SnapInterfaceControllerGetStateAction, + SnapInterfaceControllerMessenger, SnapInterfaceControllerState, SnapInterfaceControllerStateChangeEvent, StoredInterface, diff --git a/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts index 1f5fc7a983..2e8cd7e77d 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts @@ -51,7 +51,7 @@ export type AccountsControllerListMultichainAccountsAction = { export type MultichainRoutingServiceActions = MultichainRoutingServiceMethodActions; -export type MultichainRoutingServiceAllowedActions = +type AllowedActions = | SnapControllerGetRunnableSnapsAction | SnapControllerHandleRequestAction | GetPermissions @@ -61,7 +61,7 @@ export type MultichainRoutingServiceEvents = never; export type MultichainRoutingServiceMessenger = Messenger< typeof name, - MultichainRoutingServiceActions | MultichainRoutingServiceAllowedActions + MultichainRoutingServiceActions | AllowedActions >; export type MultichainRoutingServiceArgs = { diff --git a/packages/snaps-controllers/src/multichain/index.ts b/packages/snaps-controllers/src/multichain/index.ts index 7a1ce9328d..42ebffe200 100644 --- a/packages/snaps-controllers/src/multichain/index.ts +++ b/packages/snaps-controllers/src/multichain/index.ts @@ -1,3 +1,8 @@ +export type { + MultichainRoutingServiceActions, + MultichainRoutingServiceEvents, + MultichainRoutingServiceMessenger, +} from './MultichainRoutingService'; export { MultichainRoutingService } from './MultichainRoutingService'; export type { MultichainRoutingServiceGetSupportedAccountsAction, diff --git a/packages/snaps-controllers/src/services/index.ts b/packages/snaps-controllers/src/services/index.ts index 5447d7e102..34e9a13543 100644 --- a/packages/snaps-controllers/src/services/index.ts +++ b/packages/snaps-controllers/src/services/index.ts @@ -1,5 +1,6 @@ export type { ExecutionServiceActions, + ExecutionServiceArgs, ExecutionServiceEvents, ExecutionServiceMessenger, ExecutionServiceOutboundRequestEvent, diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 7baf94c8e4..cbfbc9d3bb 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -512,7 +512,7 @@ export type SnapControllerEvents = | SnapControllerSnapDisabledEvent | SnapControllerStateChangeEvent; -export type AllowedActions = +type AllowedActions = | GetEndowments | GetPermissions | GetSubjects @@ -542,7 +542,7 @@ export type AllowedActions = | StorageServiceRemoveItemAction | StorageServiceClearAction; -export type AllowedEvents = +type AllowedEvents = | ExecutionServiceEvents | SnapControllerSnapInstalledEvent | SnapControllerSnapUpdatedEvent @@ -580,7 +580,7 @@ type DynamicFeatureFlags = { disableSnaps?: boolean; }; -type SnapControllerArgs = { +export type SnapControllerArgs = { /** * A list of permissions that are allowed to be dynamic, meaning they can be revoked from the snap whenever. */ diff --git a/packages/snaps-controllers/src/snaps/index.ts b/packages/snaps-controllers/src/snaps/index.ts index d744095d96..d8fe12b1dd 100644 --- a/packages/snaps-controllers/src/snaps/index.ts +++ b/packages/snaps-controllers/src/snaps/index.ts @@ -1,7 +1,11 @@ export * from './constants'; export * from './location'; export type { + SnapControllerActions, + SnapControllerArgs, + SnapControllerEvents, SnapControllerGetStateAction, + SnapControllerMessenger, SnapControllerSnapBlockedEvent, SnapControllerSnapDisabledEvent, SnapControllerSnapEnabledEvent, diff --git a/packages/snaps-simulation/src/controllers.ts b/packages/snaps-simulation/src/controllers.ts index 4710aa642c..1b9fd8eb4f 100644 --- a/packages/snaps-simulation/src/controllers.ts +++ b/packages/snaps-simulation/src/controllers.ts @@ -2,25 +2,29 @@ import { caip25CaveatBuilder, Caip25CaveatType, } from '@metamask/chain-agnostic-permission'; +import type { + MessengerActions, + MessengerEvents, + MockAnyNamespace, +} from '@metamask/messenger'; import { Messenger } from '@metamask/messenger'; import type { CaveatSpecificationConstraint, + PermissionControllerMessenger, PermissionSpecificationConstraint, - PermissionControllerActions, - SubjectMetadataControllerActions, } from '@metamask/permission-controller'; import { PermissionController, SubjectMetadataController, SubjectType, } from '@metamask/permission-controller'; -import { SnapInterfaceController } from '@metamask/snaps-controllers'; import type { - ExecutionServiceActions, - SnapInterfaceControllerStateChangeEvent, - SnapInterfaceControllerActions, - SnapInterfaceControllerAllowedActions, + ExecutionServiceMessenger, + SnapControllerMessenger, + SnapInterfaceControllerMessenger, + SnapRegistryControllerMessenger, } from '@metamask/snaps-controllers'; +import { SnapInterfaceController } from '@metamask/snaps-controllers'; import { caveatSpecifications as snapsCaveatsSpecifications, endowmentCaveatSpecifications as snapsEndowmentCaveatSpecifications, @@ -37,20 +41,22 @@ import type { SimulationOptions } from './options'; import type { RestrictedMiddlewareHooks } from './simulation'; import type { RunSagaFunction } from './store'; -export type RootControllerAllowedActions = - | SnapInterfaceControllerActions - | SnapInterfaceControllerAllowedActions - | PermissionControllerActions - | ExecutionServiceActions - | SubjectMetadataControllerActions; - -export type RootControllerAllowedEvents = - SnapInterfaceControllerStateChangeEvent; - export type RootControllerMessenger = Messenger< - any, - RootControllerAllowedActions, - RootControllerAllowedEvents + MockAnyNamespace, + MessengerActions< + | ExecutionServiceMessenger + | PermissionControllerMessenger + | SnapControllerMessenger + | SnapInterfaceControllerMessenger + | SnapRegistryControllerMessenger + >, + MessengerEvents< + | ExecutionServiceMessenger + | PermissionControllerMessenger + | SnapControllerMessenger + | SnapInterfaceControllerMessenger + | SnapRegistryControllerMessenger + > >; export type GetControllersOptions = { @@ -85,10 +91,11 @@ export function getControllers(options: GetControllersOptions): Controllers { subjectCacheLimit: 100, }); - const interfaceControllerMessenger = new Messenger({ - namespace: 'SnapInterfaceController', - parent: controllerMessenger, - }); + const interfaceControllerMessenger: SnapInterfaceControllerMessenger = + new Messenger({ + namespace: 'SnapInterfaceController', + parent: controllerMessenger, + }); controllerMessenger.delegate({ messenger: interfaceControllerMessenger, diff --git a/packages/snaps-simulation/src/test-utils/controller.ts b/packages/snaps-simulation/src/test-utils/controller.ts index b926709522..73b3a52fc2 100644 --- a/packages/snaps-simulation/src/test-utils/controller.ts +++ b/packages/snaps-simulation/src/test-utils/controller.ts @@ -1,14 +1,11 @@ import { Messenger } from '@metamask/messenger'; -import type { SnapInterfaceControllerAllowedActions } from '@metamask/snaps-controllers'; +import type { SnapInterfaceControllerMessenger } from '@metamask/snaps-controllers'; import { MockControllerMessenger } from '@metamask/snaps-utils/test-utils'; -import type { RootControllerAllowedActions } from '../controllers'; +import type { RootControllerMessenger } from '../controllers'; export const getRootControllerMessenger = (mocked = true) => { - const messenger = new MockControllerMessenger< - RootControllerAllowedActions, - any - >(); + const messenger: RootControllerMessenger = new MockControllerMessenger(); if (mocked) { messenger.registerActionHandler('PhishingController:testOrigin', () => ({ @@ -42,12 +39,7 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( typeof getRootControllerMessenger > = getRootControllerMessenger(), ) => { - const controllerMessenger = new Messenger< - 'SnapInterfaceController', - SnapInterfaceControllerAllowedActions, - never, - any - >({ + const controllerMessenger: SnapInterfaceControllerMessenger = new Messenger({ namespace: 'SnapInterfaceController', parent: messenger, }); From a1b0a842090310344aab59fa6c5ddec97279d3ba Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Mar 2026 16:31:48 +0100 Subject: [PATCH 11/17] chore!: Make `getTruncatedSnap` and `getTruncatedSnapExpect` private (#3923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes the `getTruncatedSnap` and `getTruncatedSnapExpect` methods on `SnapController` to be private. --- > [!NOTE] > **Medium Risk** > Medium risk because it is a breaking API surface change: external consumers can no longer call `SnapController.getTruncatedSnap*` and must switch to other helpers, though runtime behavior is otherwise unchanged. > > **Overview** > Makes `SnapController`’s `getTruncatedSnap`/`getTruncatedSnapExpect` methods private (converted to `#getTruncatedSnap`/`#getTruncatedSnapExpect`) and updates all internal event payloads and flows (`snapInstalled`, `snapUpdated`, `snapEnabled/disabled`, `snapTerminated`, rollback, etc.) to use the new private methods. > > Updates tests to stop calling the controller methods directly and instead assert against `getTruncatedSnap(...)`, and documents the breaking removal in the `CHANGELOG`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f72851444ccbf72c40fb5a40876a7666cb236a26. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- packages/snaps-controllers/CHANGELOG.md | 3 +- .../src/snaps/SnapController.test.tsx | 14 ++++----- .../src/snaps/SnapController.ts | 30 +++++++++---------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index d24f3defbc..7db3d2a965 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -93,8 +93,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - **RREAKING:** Remove `AbstractExecutionService` class in favour of `ExecutionService` ([#3916](https://github.com/MetaMask/snaps/pull/3916)) -- **BREAKING:** Remove `incrementActiveReferences` and `decrementActiveReferences` actions and methods ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3920](https://github.com/MetaMask/snaps/pull/3920)) +- **BREAKING:** Remove `incrementActiveReferences` and `decrementActiveReferences` actions and methods from `SnapController` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3920](https://github.com/MetaMask/snaps/pull/3920)) - This was never used in production. +- **BREAKING:** Remove public `getTruncatedSnap` and `getTruncatedSnapExpect` methods from `SnapController` ([#3923](https://github.com/MetaMask/snaps/pull/3923)) ## [18.0.4] diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index f8891a56dc..bef7b93321 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -8979,11 +8979,11 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.1.0' }, }); - const newSnapTruncated = controller.getTruncatedSnap(MOCK_SNAP_ID); - const newSnap = controller.getSnap(MOCK_SNAP_ID); - expect(result).toStrictEqual({ [MOCK_SNAP_ID]: newSnapTruncated }); + expect(result).toStrictEqual({ + [MOCK_SNAP_ID]: getTruncatedSnap(newSnap), + }); expect(newSnap?.version).toBe('1.1.0'); expect(newSnap?.versionHistory).toStrictEqual([ { @@ -9090,7 +9090,7 @@ describe('SnapController', () => { ); expect(onSnapUpdated).toHaveBeenCalledTimes(1); expect(onSnapUpdated).toHaveBeenCalledWith( - newSnapTruncated, + getTruncatedSnap(newSnap), '1.0.0', MOCK_ORIGIN, false, @@ -9362,11 +9362,11 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.1.0' }, }); - const newSnapTruncated = controller.getTruncatedSnap(MOCK_SNAP_ID); - const newSnap = controller.getSnap(MOCK_SNAP_ID); - expect(result).toStrictEqual({ [MOCK_SNAP_ID]: newSnapTruncated }); + expect(result).toStrictEqual({ + [MOCK_SNAP_ID]: getTruncatedSnap(newSnap), + }); expect(newSnap?.version).toBe('1.1.0'); expect(newSnap?.versionHistory).toStrictEqual([ { diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index cbfbc9d3bb..a1a17920fd 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -1348,7 +1348,7 @@ export class SnapController extends BaseController< if (isUpdate) { this.messenger.publish( 'SnapController:snapUpdated', - this.getTruncatedSnapExpect(snapId), + this.#getTruncatedSnapExpect(snapId), existingSnap.version, METAMASK_ORIGIN, true, @@ -1356,7 +1356,7 @@ export class SnapController extends BaseController< } else if (!isMissingSource) { this.messenger.publish( 'SnapController:snapInstalled', - this.getTruncatedSnapExpect(snapId), + this.#getTruncatedSnapExpect(snapId), METAMASK_ORIGIN, true, ); @@ -1772,7 +1772,7 @@ export class SnapController extends BaseController< this.messenger.publish( 'SnapController:snapEnabled', - this.getTruncatedSnapExpect(snapId), + this.#getTruncatedSnapExpect(snapId), ); } @@ -1797,7 +1797,7 @@ export class SnapController extends BaseController< this.messenger.publish( 'SnapController:snapDisabled', - this.getTruncatedSnapExpect(snapId), + this.#getTruncatedSnapExpect(snapId), ); } @@ -1891,7 +1891,7 @@ export class SnapController extends BaseController< this.messenger.publish( 'SnapController:snapTerminated', - this.getTruncatedSnapExpect(snapId), + this.#getTruncatedSnapExpect(snapId), ); } @@ -1952,7 +1952,7 @@ export class SnapController extends BaseController< * @returns A truncated version of the snap state, that is less expensive to serialize. */ // TODO(ritave): this.get returns undefined, this.getTruncated returns null - getTruncatedSnap(snapId: SnapId): TruncatedSnap | null { + #getTruncatedSnap(snapId: SnapId): TruncatedSnap | null { const snap = this.getSnap(snapId); return snap ? truncateSnap(snap) : null; @@ -1965,7 +1965,7 @@ export class SnapController extends BaseController< * @param snapId - The id of the snap to get. * @returns A truncated version of the snap state, that is less expensive to serialize. */ - getTruncatedSnapExpect(snapId: SnapId): TruncatedSnap { + #getTruncatedSnapExpect(snapId: SnapId): TruncatedSnap { return truncateSnap(this.getSnapExpect(snapId)); } @@ -2371,7 +2371,7 @@ export class SnapController extends BaseController< await Promise.all( snapIds.map(async (snapId) => { const snap = this.getSnapExpect(snapId); - const truncated = this.getTruncatedSnapExpect(snapId); + const truncated = this.#getTruncatedSnapExpect(snapId); // Disable the snap and revoke all of its permissions before deleting // it. This ensures that the snap will not be restarted or otherwise // affect the host environment while we are deleting it. @@ -2597,7 +2597,7 @@ export class SnapController extends BaseController< return Object.keys(snaps).reduce( (permittedSnaps, snapId) => { const snap = this.getSnap(snapId); - const truncatedSnap = this.getTruncatedSnap(snapId as SnapId); + const truncatedSnap = this.#getTruncatedSnap(snapId as SnapId); if (truncatedSnap && snap?.status !== SnapStatus.Installing) { permittedSnaps[snapId] = truncatedSnap; @@ -2687,7 +2687,7 @@ export class SnapController extends BaseController< pendingInstalls.forEach((snapId) => this.messenger.publish( `SnapController:snapInstalled`, - this.getTruncatedSnapExpect(snapId), + this.#getTruncatedSnapExpect(snapId), origin, false, ), @@ -2696,7 +2696,7 @@ export class SnapController extends BaseController< pendingUpdates.forEach(({ snapId, oldVersion }) => this.messenger.publish( `SnapController:snapUpdated`, - this.getTruncatedSnapExpect(snapId), + this.#getTruncatedSnapExpect(snapId), oldVersion, origin, false, @@ -2737,7 +2737,7 @@ export class SnapController extends BaseController< location: SnapLocation, versionRange: SemVerRange, ): Promise { - const existingSnap = this.getTruncatedSnap(snapId); + const existingSnap = this.#getTruncatedSnap(snapId); // For devX we always re-install local snaps. if (existingSnap && !location.shouldAlwaysReload) { @@ -2799,7 +2799,7 @@ export class SnapController extends BaseController< sourceCode, }); - const truncated = this.getTruncatedSnapExpect(snapId); + const truncated = this.#getTruncatedSnapExpect(snapId); this.#updateApproval(pendingApproval.id, { loading: false, @@ -3066,7 +3066,7 @@ export class SnapController extends BaseController< throw new Error(`Snap ${snapId} crashed with updated source code.`); } - const truncatedSnap = this.getTruncatedSnapExpect(snapId); + const truncatedSnap = this.#getTruncatedSnapExpect(snapId); if (pendingApproval) { this.#updateApproval(pendingApproval.id, { @@ -4238,7 +4238,7 @@ export class SnapController extends BaseController< previousInitialConnections ?? {}, ); - const truncatedSnap = this.getTruncatedSnapExpect(snapId); + const truncatedSnap = this.#getTruncatedSnapExpect(snapId); this.messenger.publish( 'SnapController:snapRolledback', From 3f037b53cf17b4111eb8c0ecafa22ef81bdf44f8 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Mar 2026 10:57:59 +0100 Subject: [PATCH 12/17] Fix import --- packages/snaps-controllers/src/test-utils/multichain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-controllers/src/test-utils/multichain.ts b/packages/snaps-controllers/src/test-utils/multichain.ts index 56eab1914f..8bac8bf167 100644 --- a/packages/snaps-controllers/src/test-utils/multichain.ts +++ b/packages/snaps-controllers/src/test-utils/multichain.ts @@ -4,7 +4,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; import type { CaipAccountId, CaipChainId, Json } from '@metamask/utils'; -import type { WithSnapKeyringFunction } from '../multichain'; +import type { WithSnapKeyringFunction } from '../multichain/MultichainRoutingService'; export const BTC_CAIP2 = 'bip122:000000000019d6689c085ae165831e93' as CaipChainId; From 5957a9f09a2d849b5203bcc68994ef223671fa54 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Mar 2026 11:09:37 +0100 Subject: [PATCH 13/17] Fix more imports --- .../src/insights/SnapInsightsController.ts | 7 ++++--- packages/snaps-controllers/src/insights/index.ts | 4 +++- packages/snaps-controllers/src/services/browser.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/snaps-controllers/src/insights/SnapInsightsController.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.ts index 1c19431e6d..07a3ce7472 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.ts @@ -48,12 +48,13 @@ export type SnapInsightsControllerGetStateAction = ControllerGetStateAction< export type SnapInsightsControllerActions = SnapInsightsControllerGetStateAction; -export type SnapInsightControllerStateChangeEvent = ControllerStateChangeEvent< +export type SnapInsightsControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, SnapInsightsControllerState >; -export type SnapInsightControllerEvents = SnapInsightControllerStateChangeEvent; +export type SnapInsightsControllerEvents = + SnapInsightsControllerStateChangeEvent; type AllowedEvents = | TransactionControllerUnapprovedTransactionAddedEvent @@ -63,7 +64,7 @@ type AllowedEvents = export type SnapInsightsControllerMessenger = Messenger< typeof controllerName, SnapInsightsControllerActions | AllowedActions, - SnapInsightControllerEvents | AllowedEvents + SnapInsightsControllerEvents | AllowedEvents >; export type SnapInsight = { diff --git a/packages/snaps-controllers/src/insights/index.ts b/packages/snaps-controllers/src/insights/index.ts index 50f42ec0aa..d5d822efdd 100644 --- a/packages/snaps-controllers/src/insights/index.ts +++ b/packages/snaps-controllers/src/insights/index.ts @@ -2,8 +2,10 @@ export type { SnapInsight, SnapInsightsControllerActions, SnapInsightsControllerArgs, - SnapInsightControllerEvents, + SnapInsightsControllerEvents, + SnapInsightsControllerGetStateAction, SnapInsightsControllerMessenger, SnapInsightsControllerState, + SnapInsightsControllerStateChangeEvent, } from './SnapInsightsController'; export { SnapInsightsController } from './SnapInsightsController'; diff --git a/packages/snaps-controllers/src/services/browser.ts b/packages/snaps-controllers/src/services/browser.ts index f5d3151ba5..f237113bb5 100644 --- a/packages/snaps-controllers/src/services/browser.ts +++ b/packages/snaps-controllers/src/services/browser.ts @@ -1,12 +1,12 @@ // Subset of exports meant for browser environments, omits Node.js services export type { ExecutionServiceActions, + ExecutionServiceArgs, ExecutionServiceEvents, ExecutionServiceMessenger, ExecutionServiceOutboundRequestEvent, ExecutionServiceOutboundResponseEvent, ExecutionServiceUnhandledErrorEvent, - SetupSnapProvider, SnapErrorJson, SnapExecutionData, } from './ExecutionService'; From f91a74255b7ec99271c9adf177dc978559174685 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Mar 2026 13:23:38 +0100 Subject: [PATCH 14/17] Move `setupMultiplex` to separate file --- .../src/services/ExecutionService.ts | 23 ++---------------- .../snaps-controllers/src/services/browser.ts | 3 ++- .../snaps-controllers/src/services/index.ts | 3 ++- .../src/services/multiplex.ts | 24 +++++++++++++++++++ 4 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 packages/snaps-controllers/src/services/multiplex.ts diff --git a/packages/snaps-controllers/src/services/ExecutionService.ts b/packages/snaps-controllers/src/services/ExecutionService.ts index 36c2c0c176..48a72a9cac 100644 --- a/packages/snaps-controllers/src/services/ExecutionService.ts +++ b/packages/snaps-controllers/src/services/ExecutionService.ts @@ -2,7 +2,7 @@ import { asV2Middleware } from '@metamask/json-rpc-engine'; import { JsonRpcEngineV2 as JsonRpcEngine } from '@metamask/json-rpc-engine/v2'; import { createStreamMiddleware } from '@metamask/json-rpc-middleware-stream'; import type { Messenger } from '@metamask/messenger'; -import ObjectMultiplex from '@metamask/object-multiplex'; +import type ObjectMultiplex from '@metamask/object-multiplex'; import type { BasePostMessageStream } from '@metamask/post-message-stream'; import type { SnapRpcHookArgs } from '@metamask/snaps-utils'; import { SNAP_STREAM_NAMES, logError, logWarning } from '@metamask/snaps-utils'; @@ -22,6 +22,7 @@ import { pipeline } from 'readable-stream'; import type { Duplex } from 'readable-stream'; import type { ExecutionServiceMethodActions } from './ExecutionService-method-action-types'; +import { setupMultiplex } from './multiplex'; import { log } from '../logging'; import { Timer } from '../snaps/Timer'; import { hasTimedOut, withTimeout } from '../utils'; @@ -519,23 +520,3 @@ export abstract class ExecutionService { }); } } - -/** - * Sets up stream multiplexing for the given stream. - * - * @param connectionStream - The stream to mux. - * @param streamName - The name of the stream, for identification in errors. - * @returns The multiplexed stream. - */ -export function setupMultiplex( - connectionStream: Duplex, - streamName: string, -): ObjectMultiplex { - const mux = new ObjectMultiplex(); - pipeline(connectionStream, mux, connectionStream, (error) => { - if (error && !error.message?.match('Premature close')) { - logError(`"${streamName}" stream failure.`, error); - } - }); - return mux; -} diff --git a/packages/snaps-controllers/src/services/browser.ts b/packages/snaps-controllers/src/services/browser.ts index f237113bb5..6ee15db764 100644 --- a/packages/snaps-controllers/src/services/browser.ts +++ b/packages/snaps-controllers/src/services/browser.ts @@ -10,13 +10,14 @@ export type { SnapErrorJson, SnapExecutionData, } from './ExecutionService'; -export { ExecutionService, setupMultiplex } from './ExecutionService'; +export { ExecutionService } from './ExecutionService'; export type { ExecutionServiceTerminateSnapAction, ExecutionServiceTerminateAllSnapsAction, ExecutionServiceExecuteSnapAction, ExecutionServiceHandleRpcRequestAction, } from './ExecutionService-method-action-types'; +export { setupMultiplex } from './multiplex'; export * from './ProxyPostMessageStream'; export * from './iframe'; export * from './offscreen'; diff --git a/packages/snaps-controllers/src/services/index.ts b/packages/snaps-controllers/src/services/index.ts index 34e9a13543..341861f457 100644 --- a/packages/snaps-controllers/src/services/index.ts +++ b/packages/snaps-controllers/src/services/index.ts @@ -9,13 +9,14 @@ export type { SnapErrorJson, SnapExecutionData, } from './ExecutionService'; -export { ExecutionService, setupMultiplex } from './ExecutionService'; +export { ExecutionService } from './ExecutionService'; export type { ExecutionServiceTerminateSnapAction, ExecutionServiceTerminateAllSnapsAction, ExecutionServiceExecuteSnapAction, ExecutionServiceHandleRpcRequestAction, } from './ExecutionService-method-action-types'; +export { setupMultiplex } from './multiplex'; export * from './ProxyPostMessageStream'; export * from './iframe'; export * from './offscreen'; diff --git a/packages/snaps-controllers/src/services/multiplex.ts b/packages/snaps-controllers/src/services/multiplex.ts new file mode 100644 index 0000000000..34417ffe69 --- /dev/null +++ b/packages/snaps-controllers/src/services/multiplex.ts @@ -0,0 +1,24 @@ +import ObjectMultiplex from '@metamask/object-multiplex'; +import { logError } from '@metamask/snaps-utils'; +import type { Duplex } from 'readable-stream'; +import { pipeline } from 'readable-stream'; + +/** + * Sets up stream multiplexing for the given stream. + * + * @param connectionStream - The stream to mux. + * @param streamName - The name of the stream, for identification in errors. + * @returns The multiplexed stream. + */ +export function setupMultiplex( + connectionStream: Duplex, + streamName: string, +): ObjectMultiplex { + const mux = new ObjectMultiplex(); + pipeline(connectionStream, mux, connectionStream, (error) => { + if (error && !error.message?.match('Premature close')) { + logError(`"${streamName}" stream failure.`, error); + } + }); + return mux; +} From 12990dea4d7ad50c447b51a2fdad927d2d9983f7 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Mar 2026 13:30:32 +0100 Subject: [PATCH 15/17] Return `null` from `getSnap` if Snap is not found --- packages/snaps-controllers/CHANGELOG.md | 2 ++ .../src/snaps/SnapController.test.tsx | 6 +++--- .../src/snaps/SnapController.ts | 9 ++++----- packages/snaps-utils/src/test-utils/snap.ts | 18 +++++++++++------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/snaps-controllers/CHANGELOG.md b/packages/snaps-controllers/CHANGELOG.md index 7db3d2a965..e7bbb35dd6 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `SnapController` actions: - `GetSnap` is now `SnapControllerGetSnapAction`. - Note: The method is now called `getSnap` instead of `get`. + - Note: It now returns `null` if a Snap is not found, instead of + `undefined`. - `HandleSnapRequest` is now `SnapControllerHandleRequestAction`. - `GetSnapState` is now `SnapControllerGetSnapStateAction`. - `HasSnap` is now `SnapControllerHasSnapAction`. diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index bef7b93321..2e13d6de0b 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -1299,7 +1299,7 @@ describe('SnapController', () => { MOCK_SNAP_ID, ); - expect(controller.getSnap(MOCK_SNAP_ID)).toBeUndefined(); + expect(controller.getSnap(MOCK_SNAP_ID)).toBeNull(); expect(options.messenger.publish).not.toHaveBeenCalledWith( 'SnapController:snapUninstalled', @@ -8445,7 +8445,7 @@ describe('SnapController', () => { expect(detect).toHaveBeenCalledTimes(5); - expect(controller.getSnap(snapId3)).toBeUndefined(); + expect(controller.getSnap(snapId3)).toBeNull(); expect(controller.getSnap(snapId1)?.manifest.version).toBe(oldVersion); expect(controller.getSnap(snapId2)?.manifest.version).toBe(oldVersion); expect(controller.getSnap(snapId1)?.status).toBe('stopped'); @@ -8597,7 +8597,7 @@ describe('SnapController', () => { expect(detect).toHaveBeenCalledTimes(4); - expect(controller.getSnap(snapId3)).toBeUndefined(); + expect(controller.getSnap(snapId3)).toBeNull(); expect(controller.getSnap(snapId1)?.manifest.version).toBe(oldVersion); expect(controller.getSnap(snapId2)?.manifest.version).toBe(oldVersion); expect(listener).toHaveBeenCalledTimes(0); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index a1a17920fd..13fc7750d7 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -1237,7 +1237,7 @@ export class SnapController extends BaseController< hideSnapBranding, } of preinstalledSnaps) { const existingSnap = this.getSnap(snapId); - const isAlreadyInstalled = existingSnap !== undefined; + const isAlreadyInstalled = existingSnap !== null; const isUpdate = isAlreadyInstalled && gtVersion(manifest.version, existingSnap.version); const isPreinstalled = existingSnap?.preinstalled === true; @@ -1924,8 +1924,8 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to get. * @returns The entire snap object from the controller state. */ - getSnap(snapId: string): Snap | undefined { - return this.state.snaps[snapId as SnapId]; + getSnap(snapId: string): Snap | null { + return this.state.snaps[snapId as SnapId] ?? null; } /** @@ -1940,7 +1940,7 @@ export class SnapController extends BaseController< */ getSnapExpect(snapId: SnapId): Snap { const snap = this.getSnap(snapId); - assert(snap !== undefined, `Snap "${snapId}" not found.`); + assert(snap !== null, `Snap "${snapId}" not found.`); return snap; } @@ -1951,7 +1951,6 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to get. * @returns A truncated version of the snap state, that is less expensive to serialize. */ - // TODO(ritave): this.get returns undefined, this.getTruncated returns null #getTruncatedSnap(snapId: SnapId): TruncatedSnap | null { const snap = this.getSnap(snapId); diff --git a/packages/snaps-utils/src/test-utils/snap.ts b/packages/snaps-utils/src/test-utils/snap.ts index f46f1ef56d..5294f6b011 100644 --- a/packages/snaps-utils/src/test-utils/snap.ts +++ b/packages/snaps-utils/src/test-utils/snap.ts @@ -86,13 +86,17 @@ export const getSnapObject = ({ } as const; }; -export const getTruncatedSnap = ({ - initialPermissions = getSnapManifest().initialPermissions, - id = MOCK_SNAP_ID, - version = getSnapManifest().version, - enabled = true, - blocked = false, -}: GetTruncatedSnapOptions = {}): TruncatedSnap => { +export const getTruncatedSnap = ( + options: GetTruncatedSnapOptions | null = null, +): TruncatedSnap => { + const { + initialPermissions = getSnapManifest().initialPermissions, + id = MOCK_SNAP_ID, + version = getSnapManifest().version, + enabled = true, + blocked = false, + } = options ?? {}; + return { initialPermissions, id, From 0c94ace53941aee5d95bb7bdf539d9a95305e9a9 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Mar 2026 13:39:59 +0100 Subject: [PATCH 16/17] Fix type error --- packages/snaps-rpc-methods/src/restricted/notify.ts | 2 +- packages/snaps-utils/src/ui.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/snaps-rpc-methods/src/restricted/notify.ts b/packages/snaps-rpc-methods/src/restricted/notify.ts index 703b0d09f5..bc6575ddac 100644 --- a/packages/snaps-rpc-methods/src/restricted/notify.ts +++ b/packages/snaps-rpc-methods/src/restricted/notify.ts @@ -96,7 +96,7 @@ export type NotifyMethodHooks = { context?: InterfaceContext, contentType?: ContentType, ) => Promise; - getSnap: (snapId: string) => Snap | undefined; + getSnap: (snapId: string) => Snap | null; }; type SpecificationBuilderOptions = { diff --git a/packages/snaps-utils/src/ui.tsx b/packages/snaps-utils/src/ui.tsx index 7e8982e953..ea910767f5 100644 --- a/packages/snaps-utils/src/ui.tsx +++ b/packages/snaps-utils/src/ui.tsx @@ -346,7 +346,7 @@ function getMarkdownLinks(text: string) { export function validateLink( link: string, isOnPhishingList: (url: string) => boolean, - getSnap: (id: string) => Snap | undefined, + getSnap: (id: string) => Snap | null, ) { try { const url = new URL(link); @@ -398,7 +398,7 @@ export function validateLink( export function validateTextLinks( text: string, isOnPhishingList: (url: string) => boolean, - getSnap: (id: string) => Snap | undefined, + getSnap: (id: string) => Snap | null, ) { const links = getMarkdownLinks(text); @@ -445,7 +445,7 @@ export function validateJsxElements( hasPermission, }: { isOnPhishingList: (url: string) => boolean; - getSnap: (id: string) => Snap | undefined; + getSnap: (id: string) => Snap | null; getAccountByAddress: ( address: CaipAccountId, ) => InternalAccount | undefined; From 3f024e3083eabb2c10e2bd9c0a150268972e8da6 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Mar 2026 13:44:02 +0100 Subject: [PATCH 17/17] Don't export `MESSENGER_EXPOSED_METHODS` --- packages/snaps-controllers/src/services/ExecutionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-controllers/src/services/ExecutionService.ts b/packages/snaps-controllers/src/services/ExecutionService.ts index 48a72a9cac..2ec7593b1d 100644 --- a/packages/snaps-controllers/src/services/ExecutionService.ts +++ b/packages/snaps-controllers/src/services/ExecutionService.ts @@ -72,7 +72,7 @@ type ExecutionStatus = | 'executing' | 'running'; -export const MESSENGER_EXPOSED_METHODS = [ +const MESSENGER_EXPOSED_METHODS = [ 'terminateSnap', 'terminateAllSnaps', 'executeSnap',