From b6174c16801fad13d14ac497c3788685c3583af6 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Mon, 23 Mar 2026 14:08:42 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 cc88ab1ed0d7652998787bacee2805bd5c3ee235 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Mon, 23 Mar 2026 23:31:20 +0100 Subject: [PATCH 4/4] feat: use @metamask/messenger/generate-action-types CLI Replace local copy-pasted script with the CLI from @metamask/messenger. Uses preview build via resolution until release. --- package.json | 1 + packages/snaps-controllers/package.json | 2 +- .../CronjobController-method-action-types.ts | 2 +- ...InterfaceController-method-action-types.ts | 2 +- .../SnapController-method-action-types.ts | 2 +- scripts/generate-method-action-types.mts | 773 ------------------ yarn.lock | 24 +- 7 files changed, 25 insertions(+), 781 deletions(-) delete mode 100644 scripts/generate-method-action-types.mts diff --git a/package.json b/package.json index a7cd8198e8..ef227d66e0 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ ] }, "resolutions": { + "@metamask/messenger": "npm:@metamask-previews/messenger@0.3.0-preview-a462582", "@esbuild-plugins/node-modules-polyfill@^0.2.2": "patch:@esbuild-plugins/node-modules-polyfill@npm%3A0.2.2#./.yarn/patches/@esbuild-plugins-node-modules-polyfill-npm-0.2.2-f612681798.patch", "@puppeteer/browsers@1.4.6": "patch:@puppeteer/browsers@npm%3A1.7.0#./.yarn/patches/@puppeteer-browsers-npm-1.7.0-203cb4f44b.patch", "@puppeteer/browsers@^1.6.0": "patch:@puppeteer/browsers@npm%3A1.7.0#./.yarn/patches/@puppeteer-browsers-npm-1.7.0-203cb4f44b.patch", diff --git a/packages/snaps-controllers/package.json b/packages/snaps-controllers/package.json index 73b90f4bc3..4214bd0b85 100644 --- a/packages/snaps-controllers/package.json +++ b/packages/snaps-controllers/package.json @@ -62,7 +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", + "generate-method-action-types": "messenger-generate-action-types", "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-method-action-types.ts b/packages/snaps-controllers/src/cronjob/CronjobController-method-action-types.ts index 162de24e67..f11a9f0fe2 100644 --- a/packages/snaps-controllers/src/cronjob/CronjobController-method-action-types.ts +++ b/packages/snaps-controllers/src/cronjob/CronjobController-method-action-types.ts @@ -1,5 +1,5 @@ /** - * This file is auto generated by `scripts/generate-method-action-types.ts`. + * This file is auto generated by `@metamask/messenger/generate-action-types`. * Do not edit manually. */ diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts index 1641d798e4..f0d0fc6945 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController-method-action-types.ts @@ -1,5 +1,5 @@ /** - * This file is auto generated by `scripts/generate-method-action-types.ts`. + * This file is auto generated by `@metamask/messenger/generate-action-types`. * Do not edit manually. */ diff --git a/packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts b/packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts index 8c2bcdfb26..6abc6c22ec 100644 --- a/packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts +++ b/packages/snaps-controllers/src/snaps/SnapController-method-action-types.ts @@ -1,5 +1,5 @@ /** - * This file is auto generated by `scripts/generate-method-action-types.ts`. + * This file is auto generated by `@metamask/messenger/generate-action-types`. * Do not edit manually. */ diff --git a/scripts/generate-method-action-types.mts b/scripts/generate-method-action-types.mts deleted file mode 100644 index 27f3a5a436..0000000000 --- a/scripts/generate-method-action-types.mts +++ /dev/null @@ -1,773 +0,0 @@ -#!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 67a707b05f..3484b1fecb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3770,10 +3770,26 @@ __metadata: languageName: unknown linkType: soft -"@metamask/messenger@npm:^0.3.0": - version: 0.3.0 - resolution: "@metamask/messenger@npm:0.3.0" - checksum: 10/84e9f4193646d749c7260a4958b13974b3c8738cc2e414116279ed31734e1edba687ff56ddbfdb75033bce30aaa9eeb7c391bccb87a66dbc99a902882271f673 +"@metamask/messenger@npm:@metamask-previews/messenger@0.3.0-preview-a462582": + version: 0.3.0-preview-a462582 + resolution: "@metamask-previews/messenger@npm:0.3.0-preview-a462582" + peerDependencies: + "@metamask/utils": ^11.9.0 + eslint: ">=8" + typescript: ~5.3.3 + yargs: ^17.7.2 + peerDependenciesMeta: + "@metamask/utils": + optional: true + eslint: + optional: true + typescript: + optional: true + yargs: + optional: true + bin: + messenger-generate-action-types: ./dist/generate-action-types/cli.mjs + checksum: 10/210e6f1e05178b8778ed2799ca0ad32936cceb7c191b86bfb105e312f78187b48129bad229e841e6c1851babefed5c7b81d7377bdcf5461c8fbd8ad0890399dd languageName: node linkType: hard