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..e7bbb35dd6 100644 --- a/packages/snaps-controllers/CHANGELOG.md +++ b/packages/snaps-controllers/CHANGELOG.md @@ -7,6 +7,98 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** All action types were renamed from `DoSomething` to `ControllerNameDoSomethingAction` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3911](https://github.com/MetaMask/snaps/pull/3911), [#3912](https://github.com/MetaMask/snaps/pull/3912), [#3916](https://github.com/MetaMask/snaps/pull/3916), [#3918](https://github.com/MetaMask/snaps/pull/3918)) + - `SnapController` actions: + - `GetSnap` is now `SnapControllerGetSnapAction`. + - Note: The method is now called `getSnap` instead of `get`. + - Note: It now returns `null` if a Snap is not found, instead of + `undefined`. + - `HandleSnapRequest` is now `SnapControllerHandleRequestAction`. + - `GetSnapState` is now `SnapControllerGetSnapStateAction`. + - `HasSnap` is now `SnapControllerHasSnapAction`. + - 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`. + - `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`. + - `ExecutionService` actions: + - `ExecuteSnap` is now `ExecutionServiceExecuteSnapAction`. + - `HandleRequest` is now `ExecutionServiceHandleRequestAction`. + - `TerminateSnap` is now `ExecutionServiceTerminateSnapAction`. + - `GetExecutionStatus` is now `ExecutionServiceGetExecutionStatusAction`. + - `SnapRegistryController` actions: + - `GetResult` is now `SnapRegistryControllerGetAction`. + - `GetMetadata` is now `SnapRegistryControllerGetMetadataAction`. + - `ResolveVersion` is now `SnapRegistryControllerResolveVersionAction`. + - `Update` is now `SnapRegistryControllerRequestUpdateAction`. + - Note: The method is now called `requestUpdate` instead of `update`. +- **BREAKING:** All event types were renamed from `OnSomething` to `ControllerOnSomethingEvent` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3916](https://github.com/MetaMask/snaps/pull/3916)) + - `SnapController` events: + - `SnapStateChange` was removed in favour of `SnapControllerStateChangeEvent`. + - `SnapBlocked` is now `SnapControllerSnapBlockedEvent`. + - `SnapInstallStarted` is now `SnapControllerSnapInstallStartedEvent`. + - `SnapInstallFailed` is now `SnapControllerSnapInstallFailedEvent`. + - `SnapInstalled` is now `SnapControllerSnapInstalledEvent`. + - `SnapUninstalled` is now `SnapControllerSnapUninstalledEvent`. + - `SnapUnblocked` is now `SnapControllerSnapUnblockedEvent`. + - `SnapUpdated` is now `SnapControllerSnapUpdatedEvent`. + - `SnapRolledback` is now `SnapControllerSnapRolledbackEvent`. + - `SnapTerminated` is now `SnapControllerSnapTerminatedEvent`. + - `SnapEnabled` is now `SnapControllerSnapEnabledEvent`. + - `SnapDisabled` is now `SnapControllerSnapDisabledEvent`. + - `ExecutionService` events: + - `ErrorMessageEvent` is now `ExecutionServiceUnhandledErrorEvent`. + - `OutboundRequest` is now `ExecutionServiceOutboundRequestEvent`. + - `OutboundResponse` is now `ExecutionServiceOutboundResponseEvent`. +- **BREAKING:**: Rename `MultichainRouter` to `MultichainRoutingService` and update action types accordingly ([#3913](https://github.com/MetaMask/snaps/pull/3913)) + - This is consistent with the naming of other services. +- **BREAKING:** Rename `JsonSnapsRegistry` to `SnapRegistryController` and update action types accordingly ([#3918](https://github.com/MetaMask/snaps/pull/3918)) + - This is consistent with the naming of other controllers. + - The controller name is now `SnapRegistryController` instead of `SnapsRegistry` as well. +- **BREAKING:** `MultichainRoutingService` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3913](https://github.com/MetaMask/snaps/pull/3913)) +- **BREAKING:** `SnapInsightsController` now requires `SnapController:getRunnableSnaps` instead of `SnapController:getAllSnaps` ([#3915](https://github.com/MetaMask/snaps/pull/3915)) +- **RREAKING:** Replace `ExecutionService` interface with abstract class ([#3916](https://github.com/MetaMask/snaps/pull/3916)) + - The `ExecutionService` is now an abstract class and replaces the previous `AbstractExecutionService` class interface. + +### Removed + +- **RREAKING:** Remove `AbstractExecutionService` class in favour of `ExecutionService` ([#3916](https://github.com/MetaMask/snaps/pull/3916)) +- **BREAKING:** Remove `incrementActiveReferences` and `decrementActiveReferences` actions and methods from `SnapController` ([#3907](https://github.com/MetaMask/snaps/pull/3907), [#3920](https://github.com/MetaMask/snaps/pull/3920)) + - This was never used in production. +- **BREAKING:** Remove public `getTruncatedSnap` and `getTruncatedSnapExpect` methods from `SnapController` ([#3923](https://github.com/MetaMask/snaps/pull/3923)) + ## [18.0.4] ### Fixed diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index b63ccf012e..428f6713f9 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -2,5 +2,5 @@ "branches": 94.97, "functions": 98.78, "lines": 98.63, - "statements": 98.43 + "statements": 98.32 } diff --git a/packages/snaps-controllers/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-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.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..c59f4e2f7b 100644 --- a/packages/snaps-controllers/src/cronjob/CronjobController.ts +++ b/packages/snaps-controllers/src/cronjob/CronjobController.ts @@ -21,15 +21,16 @@ 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 { - 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,56 +38,31 @@ export type CronjobControllerGetStateAction = ControllerGetStateAction< typeof controllerName, CronjobControllerState >; + export type CronjobControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, 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 CronjobControllerActions = + | CronjobControllerGetStateAction + | CronjobControllerMethodActions; -export type Schedule = { - type: `${typeof controllerName}:schedule`; - handler: CronjobController['schedule']; -}; +export type CronjobControllerEvents = CronjobControllerStateChangeEvent; -export type Cancel = { - type: `${typeof controllerName}:cancel`; - handler: CronjobController['cancel']; -}; +type AllowedActions = GetPermissions | SnapControllerHandleRequestAction; -export type Get = { - type: `${typeof controllerName}:get`; - handler: CronjobController['get']; -}; - -export type CronjobControllerActions = - | CronjobControllerGetStateAction - | HandleSnapRequest - | GetPermissions - | Schedule - | Cancel - | Get - | CronjobControllerInitAction; - -export type CronjobControllerEvents = - | CronjobControllerStateChangeEvent - | SnapInstalled - | SnapUninstalled - | SnapUpdated - | SnapEnabled - | SnapDisabled; +type AllowedEvents = + | SnapControllerSnapInstalledEvent + | SnapControllerSnapUninstalledEvent + | SnapControllerSnapUpdatedEvent + | SnapControllerSnapEnabledEvent + | SnapControllerSnapDisabledEvent; export type CronjobControllerMessenger = Messenger< typeof controllerName, - CronjobControllerActions, - CronjobControllerEvents + CronjobControllerActions | AllowedActions, + CronjobControllerEvents | AllowedEvents >; export const DAILY_TIMEOUT = inMilliseconds(24, Duration.Hour); @@ -156,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 @@ -219,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..c87ea0c970 100644 --- a/packages/snaps-controllers/src/cronjob/index.ts +++ b/packages/snaps-controllers/src/cronjob/index.ts @@ -1 +1,17 @@ -export * from './CronjobController'; +export type { + CronjobControllerActions, + CronjobControllerArgs, + CronjobControllerEvents, + CronjobControllerGetStateAction, + CronjobControllerMessenger, + 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/insights/SnapInsightsController.test.ts b/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts index cf934e930f..ee23a5fd30 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.test.ts @@ -31,9 +31,15 @@ describe('SnapInsightsController', () => { }, ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -157,9 +163,15 @@ describe('SnapInsightsController', () => { }, ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -285,9 +297,15 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -388,9 +406,15 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); rootMessenger.registerActionHandler( 'SnapController:handleRequest', @@ -456,9 +480,15 @@ describe('SnapInsightsController', () => { it('ignores insight if transaction has already been signed', async () => { const rootMessenger = getRootSnapInsightsControllerMessenger(); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); const { resolve, promise } = createDeferredPromise(); @@ -556,9 +586,15 @@ describe('SnapInsightsController', () => { messenger: controllerMessenger, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap(), getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID })]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [ + getTruncatedSnap(), + getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + ]; + }, + ); const { resolve, promise } = createDeferredPromise(); @@ -661,7 +697,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..07a3ce7472 100644 --- a/packages/snaps-controllers/src/insights/SnapInsightsController.ts +++ b/packages/snaps-controllers/src/insights/SnapInsightsController.ts @@ -18,13 +18,15 @@ 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 { GetAllSnaps, HandleSnapRequest } from '../snaps'; -import { getRunnableSnaps } from '../snaps'; +import type { SnapInterfaceControllerDeleteInterfaceAction } from '../interface'; +import type { + SnapControllerGetRunnableSnapsAction, + SnapControllerHandleRequestAction, +} from '../snaps'; import type { TransactionControllerUnapprovedTransactionAddedEvent, TransactionMeta, - SignatureStateChange, + SignatureControllerStateChangeEvent, SignatureControllerState, StateSignature, TransactionControllerTransactionStatusUpdatedEvent, @@ -32,11 +34,11 @@ import type { const controllerName = 'SnapInsightsController'; -export type SnapInsightsControllerAllowedActions = - | HandleSnapRequest - | GetAllSnaps +type AllowedActions = | GetPermissions - | DeleteInterface; + | SnapControllerGetRunnableSnapsAction + | SnapControllerHandleRequestAction + | SnapInterfaceControllerDeleteInterfaceAction; export type SnapInsightsControllerGetStateAction = ControllerGetStateAction< typeof controllerName, @@ -46,22 +48,23 @@ export type SnapInsightsControllerGetStateAction = ControllerGetStateAction< export type SnapInsightsControllerActions = SnapInsightsControllerGetStateAction; -export type SnapInsightControllerStateChangeEvent = ControllerStateChangeEvent< +export type SnapInsightsControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, SnapInsightsControllerState >; -export type SnapInsightControllerEvents = SnapInsightControllerStateChangeEvent; +export type SnapInsightsControllerEvents = + SnapInsightsControllerStateChangeEvent; -export type SnapInsightsControllerAllowedEvents = +type AllowedEvents = | TransactionControllerUnapprovedTransactionAddedEvent | TransactionControllerTransactionStatusUpdatedEvent - | SignatureStateChange; + | SignatureControllerStateChangeEvent; export type SnapInsightsControllerMessenger = Messenger< typeof controllerName, - SnapInsightsControllerActions | SnapInsightsControllerAllowedActions, - SnapInsightControllerEvents | SnapInsightsControllerAllowedEvents + SnapInsightsControllerActions | AllowedActions, + SnapInsightsControllerEvents | AllowedEvents >; export type SnapInsight = { @@ -143,8 +146,9 @@ export class SnapInsightsController extends BaseController< * @returns A list of objects containing Snap IDs and the permission object. */ #getSnapsWithPermission(permissionName: string) { - const allSnaps = this.messenger.call('SnapController:getAll'); - const filteredSnaps = getRunnableSnaps(allSnaps); + const filteredSnaps = this.messenger.call( + 'SnapController:getRunnableSnaps', + ); return filteredSnaps.reduce((accumulator, snap) => { const permissions = this.messenger.call( diff --git a/packages/snaps-controllers/src/insights/index.ts b/packages/snaps-controllers/src/insights/index.ts index 2cd0140cc6..d5d822efdd 100644 --- a/packages/snaps-controllers/src/insights/index.ts +++ b/packages/snaps-controllers/src/insights/index.ts @@ -1 +1,11 @@ -export * from './SnapInsightsController'; +export type { + SnapInsight, + SnapInsightsControllerActions, + SnapInsightsControllerArgs, + SnapInsightsControllerEvents, + SnapInsightsControllerGetStateAction, + SnapInsightsControllerMessenger, + SnapInsightsControllerState, + SnapInsightsControllerStateChangeEvent, +} from './SnapInsightsController'; +export { SnapInsightsController } from './SnapInsightsController'; 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.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..5b47cfe547 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts @@ -34,57 +34,29 @@ 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, isMatchingChainId, validateInterfaceContext, } from './utils'; -import type { GetSnap } from '../snaps'; +import type { SnapControllerGetSnapAction } from '../snaps'; 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`; @@ -121,11 +93,11 @@ type PhishingControllerTestOrigin = { handler: (origin: string) => { result: boolean; type: string }; }; -export type SnapInterfaceControllerAllowedActions = +type AllowedActions = | PhishingControllerTestOrigin | ApprovalControllerHasRequestAction | ApprovalControllerAcceptRequestAction - | GetSnap + | SnapControllerGetSnapAction | MultichainAssetsControllerGetStateAction | AccountsControllerGetSelectedMultichainAccountAction | AccountsControllerGetAccountByAddressAction @@ -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 }; @@ -178,14 +143,15 @@ type NotificationListUpdatedEvent = { payload: [Notification[]]; }; +type AllowedEvents = NotificationListUpdatedEvent; + export type SnapInterfaceControllerEvents = - | SnapInterfaceControllerStateChangeEvent - | NotificationListUpdatedEvent; + SnapInterfaceControllerStateChangeEvent; export type SnapInterfaceControllerMessenger = Messenger< typeof controllerName, - SnapInterfaceControllerActions | SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerEvents + SnapInterfaceControllerActions | AllowedActions, + SnapInterfaceControllerEvents | AllowedEvents >; export type StoredInterface = { @@ -250,52 +216,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, ); } @@ -598,7 +521,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/interface/index.ts b/packages/snaps-controllers/src/interface/index.ts index e3f329b66e..6a41e11678 100644 --- a/packages/snaps-controllers/src/interface/index.ts +++ b/packages/snaps-controllers/src/interface/index.ts @@ -1 +1,22 @@ -export * from './SnapInterfaceController'; +export type { + SnapInterfaceControllerActions, + SnapInterfaceControllerArgs, + SnapInterfaceControllerEvents, + SnapInterfaceControllerGetStateAction, + SnapInterfaceControllerMessenger, + 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/multichain/MultichainRoutingService-method-action-types.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingService-method-action-types.ts new file mode 100644 index 0000000000..e851e9c737 --- /dev/null +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingService-method-action-types.ts @@ -0,0 +1,70 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { MultichainRoutingService } from './MultichainRoutingService'; + +/** + * Handle an incoming JSON-RPC request tied to a specific scope by routing + * to either a protocol Snap or an account Snap. + * + * Note: Addresses are considered case-sensitive by the MultichainRoutingService as + * not all non-EVM chains are case-insensitive. + * + * @param options - An options bag. + * @param options.connectedAddresses - Addresses currently connected to the + * origin for the requested scope. + * @param options.origin - The origin of the RPC request. + * @param options.request - The JSON-RPC request. + * @param options.scope - The CAIP-2 scope for the request. + * @returns The response from the chosen Snap. + * @throws If no handler was found. + */ +export type MultichainRoutingServiceHandleRequestAction = { + type: `MultichainRoutingService:handleRequest`; + handler: MultichainRoutingService['handleRequest']; +}; + +/** + * Get a list of supported methods for a given scope. + * This combines both protocol and account Snaps supported methods. + * + * @param scope - The CAIP-2 scope. + * @returns A list of supported methods. + */ +export type MultichainRoutingServiceGetSupportedMethodsAction = { + type: `MultichainRoutingService:getSupportedMethods`; + handler: MultichainRoutingService['getSupportedMethods']; +}; + +/** + * Get a list of supported accounts for a given scope. + * + * @param scope - The CAIP-2 scope. + * @returns A list of CAIP-10 addresses. + */ +export type MultichainRoutingServiceGetSupportedAccountsAction = { + type: `MultichainRoutingService:getSupportedAccounts`; + handler: MultichainRoutingService['getSupportedAccounts']; +}; + +/** + * Determine whether a given CAIP-2 scope is supported by the router. + * + * @param scope - The CAIP-2 scope. + * @returns True if the router can service the scope, otherwise false. + */ +export type MultichainRoutingServiceIsSupportedScopeAction = { + type: `MultichainRoutingService:isSupportedScope`; + handler: MultichainRoutingService['isSupportedScope']; +}; + +/** + * Union of all MultichainRoutingService action types. + */ +export type MultichainRoutingServiceMethodActions = + | MultichainRoutingServiceHandleRequestAction + | MultichainRoutingServiceGetSupportedMethodsAction + | MultichainRoutingServiceGetSupportedAccountsAction + | MultichainRoutingServiceIsSupportedScopeAction; diff --git a/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingService.test.ts similarity index 66% rename from packages/snaps-controllers/src/multichain/MultichainRouter.test.ts rename to packages/snaps-controllers/src/multichain/MultichainRoutingService.test.ts index 8988c4dee3..bbdb8d374f 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRouter.test.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingService.test.ts @@ -4,11 +4,11 @@ import { MOCK_SNAP_ID, } from '@metamask/snaps-utils/test-utils'; -import { MultichainRouter } from './MultichainRouter'; +import { MultichainRoutingService } from './MultichainRoutingService'; import { METAMASK_ORIGIN } from '../snaps/constants'; import { - getRootMultichainRouterMessenger, - getRestrictedMultichainRouterMessenger, + getMultichainRoutingServiceRootMessenger, + getRestrictedMultichainRoutingServiceMessenger, BTC_CAIP2, BTC_CONNECTED_ACCOUNTS, MOCK_SOLANA_SNAP_PERMISSIONS, @@ -19,11 +19,12 @@ import { getMockWithSnapKeyring, } from '../test-utils'; -describe('MultichainRouter', () => { +describe('MultichainRoutingService', () => { describe('handleRequest', () => { it('can route signing requests to account Snaps without address resolution', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring({ submitRequest: jest.fn().mockResolvedValue({ txid: '53de51e2fa75c3cfa51132865f7d430138b1cd92a8f5267ec836ec565b422969', @@ -31,7 +32,7 @@ describe('MultichainRouter', () => { }); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -51,19 +52,22 @@ describe('MultichainRouter', () => { }, ); - const result = await messenger.call('MultichainRouter:handleRequest', { - origin: METAMASK_ORIGIN, - connectedAddresses: BTC_CONNECTED_ACCOUNTS, - scope: BTC_CAIP2, - request: { - jsonrpc: '2.0', - id: 1, - method: 'sendBitcoin', - params: { - message: 'foo', + const result = await messenger.call( + 'MultichainRoutingService:handleRequest', + { + origin: METAMASK_ORIGIN, + connectedAddresses: BTC_CONNECTED_ACCOUNTS, + scope: BTC_CAIP2, + request: { + jsonrpc: '2.0', + id: 1, + method: 'sendBitcoin', + params: { + message: 'foo', + }, }, }, - }); + ); expect(result).toStrictEqual({ txid: '53de51e2fa75c3cfa51132865f7d430138b1cd92a8f5267ec836ec565b422969', @@ -71,8 +75,9 @@ describe('MultichainRouter', () => { }); it('can route signing requests to account Snaps using address resolution', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring({ submitRequest: jest.fn().mockResolvedValue({ signature: '0x', @@ -80,7 +85,7 @@ describe('MultichainRouter', () => { }); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -105,30 +110,34 @@ describe('MultichainRouter', () => { }, ); - const result = await messenger.call('MultichainRouter:handleRequest', { - origin: METAMASK_ORIGIN, - connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, - scope: SOLANA_CAIP2, - request: { - jsonrpc: '2.0', - id: 1, - method: 'signAndSendTransaction', - params: { - message: 'foo', + const result = await messenger.call( + 'MultichainRoutingService:handleRequest', + { + origin: METAMASK_ORIGIN, + connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, + scope: SOLANA_CAIP2, + request: { + jsonrpc: '2.0', + id: 1, + method: 'signAndSendTransaction', + params: { + message: 'foo', + }, }, }, - }); + ); expect(result).toStrictEqual({ signature: '0x' }); }); it('disallows routing to unconnected accounts', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -149,7 +158,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: [], scope: SOLANA_CAIP2, @@ -166,12 +175,13 @@ describe('MultichainRouter', () => { }); it('can route protocol requests to protocol Snaps', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -181,9 +191,12 @@ describe('MultichainRouter', () => { () => [], ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -198,16 +211,19 @@ describe('MultichainRouter', () => { }), ); - const result = await messenger.call('MultichainRouter:handleRequest', { - origin: METAMASK_ORIGIN, - connectedAddresses: [], - scope: SOLANA_CAIP2, - request: { - jsonrpc: '2.0', - id: 1, - method: 'getVersion', + const result = await messenger.call( + 'MultichainRoutingService:handleRequest', + { + origin: METAMASK_ORIGIN, + connectedAddresses: [], + scope: SOLANA_CAIP2, + request: { + jsonrpc: '2.0', + id: 1, + method: 'getVersion', + }, }, - }); + ); expect(result).toStrictEqual({ 'feature-set': 2891131721, @@ -237,12 +253,13 @@ describe('MultichainRouter', () => { }); it('throws if no suitable Snaps are found', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -252,12 +269,15 @@ describe('MultichainRouter', () => { () => [], ); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return []; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return []; + }, + ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: [], scope: SOLANA_CAIP2, @@ -271,12 +291,13 @@ describe('MultichainRouter', () => { }); it('throws if address resolution fails', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -303,7 +324,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, scope: SOLANA_CAIP2, @@ -320,12 +341,13 @@ describe('MultichainRouter', () => { }); it('throws if address resolution returns an address that isnt available', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -355,7 +377,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, scope: SOLANA_CAIP2, @@ -372,12 +394,13 @@ describe('MultichainRouter', () => { }); it(`throws if address resolution returns a lower case address that isn't available`, async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -406,7 +429,7 @@ describe('MultichainRouter', () => { ); await expect( - messenger.call('MultichainRouter:handleRequest', { + messenger.call('MultichainRoutingService:handleRequest', { origin: METAMASK_ORIGIN, connectedAddresses: SOLANA_CONNECTED_ACCOUNTS, scope: SOLANA_CAIP2, @@ -425,19 +448,23 @@ describe('MultichainRouter', () => { describe('getSupportedMethods', () => { it('returns a set of both protocol and account Snap methods', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -450,24 +477,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedMethods', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedMethods', + SOLANA_CAIP2, + ), ).toStrictEqual(['signAndSendTransaction', 'getVersion']); }); it('handles lack of protocol Snaps', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -480,24 +514,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedMethods', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedMethods', + SOLANA_CAIP2, + ), ).toStrictEqual(['signAndSendTransaction']); }); it('handles lack of account Snaps', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -510,19 +551,23 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedMethods', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedMethods', + SOLANA_CAIP2, + ), ).toStrictEqual(['getVersion']); }); }); describe('getSupportedAccounts', () => { it('returns a set of accounts for the requested scope', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); @@ -533,7 +578,10 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:getSupportedAccounts', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:getSupportedAccounts', + SOLANA_CAIP2, + ), ).toStrictEqual([ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv', ]); @@ -542,19 +590,23 @@ describe('MultichainRouter', () => { describe('isSupportedScope', () => { it('returns true if an account Snap exists', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -567,24 +619,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:isSupportedScope', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:isSupportedScope', + SOLANA_CAIP2, + ), ).toBe(true); }); it('returns true if a protocol Snap exists', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return [getTruncatedSnap()]; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return [getTruncatedSnap()]; + }, + ); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -597,24 +656,31 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:isSupportedScope', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:isSupportedScope', + SOLANA_CAIP2, + ), ).toBe(true); }); it('returns false if no Snap is found', async () => { - const rootMessenger = getRootMultichainRouterMessenger(); - const messenger = getRestrictedMultichainRouterMessenger(rootMessenger); + const rootMessenger = getMultichainRoutingServiceRootMessenger(); + const messenger = + getRestrictedMultichainRoutingServiceMessenger(rootMessenger); const withSnapKeyring = getMockWithSnapKeyring(); /* eslint-disable-next-line no-new */ - new MultichainRouter({ + new MultichainRoutingService({ messenger, withSnapKeyring, }); - rootMessenger.registerActionHandler('SnapController:getAll', () => { - return []; - }); + rootMessenger.registerActionHandler( + 'SnapController:getRunnableSnaps', + () => { + return []; + }, + ); rootMessenger.registerActionHandler( 'AccountsController:listMultichainAccounts', @@ -622,7 +688,10 @@ describe('MultichainRouter', () => { ); expect( - messenger.call('MultichainRouter:isSupportedScope', SOLANA_CAIP2), + messenger.call( + 'MultichainRoutingService:isSupportedScope', + SOLANA_CAIP2, + ), ).toBe(false); }); }); diff --git a/packages/snaps-controllers/src/multichain/MultichainRouter.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts similarity index 84% rename from packages/snaps-controllers/src/multichain/MultichainRouter.ts rename to packages/snaps-controllers/src/multichain/MultichainRoutingService.ts index 87df9a634e..2e8cd7e77d 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRouter.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingService.ts @@ -22,28 +22,11 @@ import { } from '@metamask/utils'; import { nanoid } from 'nanoid'; -import { getRunnableSnaps } from '../snaps'; -import type { GetAllSnaps, HandleSnapRequest } from '../snaps'; - -export type MultichainRouterHandleRequestAction = { - type: `${typeof name}:handleRequest`; - handler: MultichainRouter['handleRequest']; -}; - -export type MultichainRouterGetSupportedMethodsAction = { - type: `${typeof name}:getSupportedMethods`; - handler: MultichainRouter['getSupportedMethods']; -}; - -export type MultichainRouterGetSupportedAccountsAction = { - type: `${typeof name}:getSupportedAccounts`; - handler: MultichainRouter['getSupportedAccounts']; -}; - -export type MultichainRouterIsSupportedScopeAction = { - type: `${typeof name}:isSupportedScope`; - handler: MultichainRouter['isSupportedScope']; -}; +import type { MultichainRoutingServiceMethodActions } from './MultichainRoutingService-method-action-types'; +import type { + SnapControllerGetRunnableSnapsAction, + SnapControllerHandleRequestAction, +} from '../snaps'; type SnapKeyring = { submitRequest: (request: { @@ -65,27 +48,24 @@ export type AccountsControllerListMultichainAccountsAction = { handler: (chainId?: CaipChainId) => InternalAccount[]; }; -export type MultichainRouterActions = - | MultichainRouterHandleRequestAction - | MultichainRouterGetSupportedMethodsAction - | MultichainRouterGetSupportedAccountsAction - | MultichainRouterIsSupportedScopeAction; +export type MultichainRoutingServiceActions = + MultichainRoutingServiceMethodActions; -export type MultichainRouterAllowedActions = - | GetAllSnaps - | HandleSnapRequest +type AllowedActions = + | SnapControllerGetRunnableSnapsAction + | SnapControllerHandleRequestAction | GetPermissions | AccountsControllerListMultichainAccountsAction; -export type MultichainRouterEvents = never; +export type MultichainRoutingServiceEvents = never; -export type MultichainRouterMessenger = Messenger< +export type MultichainRoutingServiceMessenger = Messenger< typeof name, - MultichainRouterActions | MultichainRouterAllowedActions + MultichainRoutingServiceActions | AllowedActions >; -export type MultichainRouterArgs = { - messenger: MultichainRouterMessenger; +export type MultichainRoutingServiceArgs = { + messenger: MultichainRoutingServiceMessenger; withSnapKeyring: WithSnapKeyringFunction; }; @@ -94,39 +74,31 @@ type ProtocolSnap = { methods: string[]; }; -const name = 'MultichainRouter'; +const name = 'MultichainRoutingService'; -export class MultichainRouter { +const MESSENGER_EXPOSED_METHODS = [ + 'handleRequest', + 'getSupportedMethods', + 'getSupportedAccounts', + 'isSupportedScope', +] as const; + +export class MultichainRoutingService { name: typeof name = name; state = null; - readonly #messenger: MultichainRouterMessenger; + readonly #messenger: MultichainRoutingServiceMessenger; readonly #withSnapKeyring: WithSnapKeyringFunction; - constructor({ messenger, withSnapKeyring }: MultichainRouterArgs) { + constructor({ messenger, withSnapKeyring }: MultichainRoutingServiceArgs) { this.#messenger = messenger; this.#withSnapKeyring = withSnapKeyring; - this.#messenger.registerActionHandler( - `${name}:handleRequest`, - async (...args) => this.handleRequest(...args), - ); - - this.#messenger.registerActionHandler( - `${name}:getSupportedMethods`, - (...args) => this.getSupportedMethods(...args), - ); - - this.#messenger.registerActionHandler( - `${name}:getSupportedAccounts`, - (...args) => this.getSupportedAccounts(...args), - ); - - this.#messenger.registerActionHandler( - `${name}:isSupportedScope`, - (...args) => this.isSupportedScope(...args), + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } @@ -260,8 +232,9 @@ export class MultichainRouter { * @returns A list of all the protocol Snaps available and their RPC methods. */ #getProtocolSnaps(scope: CaipChainId) { - const allSnaps = this.#messenger.call('SnapController:getAll'); - const filteredSnaps = getRunnableSnaps(allSnaps); + const filteredSnaps = this.#messenger.call( + 'SnapController:getRunnableSnaps', + ); return filteredSnaps.reduce((accumulator, snap) => { const permissions = this.#messenger.call( @@ -288,7 +261,7 @@ export class MultichainRouter { * Handle an incoming JSON-RPC request tied to a specific scope by routing * to either a protocol Snap or an account Snap. * - * Note: Addresses are considered case-sensitive by the MultichainRouter as + * Note: Addresses are considered case-sensitive by the MultichainRoutingService as * not all non-EVM chains are case-insensitive. * * @param options - An options bag. diff --git a/packages/snaps-controllers/src/multichain/index.ts b/packages/snaps-controllers/src/multichain/index.ts index 8c696c379e..42ebffe200 100644 --- a/packages/snaps-controllers/src/multichain/index.ts +++ b/packages/snaps-controllers/src/multichain/index.ts @@ -1 +1,12 @@ -export * from './MultichainRouter'; +export type { + MultichainRoutingServiceActions, + MultichainRoutingServiceEvents, + MultichainRoutingServiceMessenger, +} from './MultichainRoutingService'; +export { MultichainRoutingService } from './MultichainRoutingService'; +export type { + MultichainRoutingServiceGetSupportedAccountsAction, + MultichainRoutingServiceGetSupportedMethodsAction, + MultichainRoutingServiceHandleRequestAction, + MultichainRoutingServiceIsSupportedScopeAction, +} from './MultichainRoutingService-method-action-types'; diff --git a/packages/snaps-controllers/src/services/AbstractExecutionService.ts b/packages/snaps-controllers/src/services/AbstractExecutionService.ts deleted file mode 100644 index 4e8acf59f8..0000000000 --- a/packages/snaps-controllers/src/services/AbstractExecutionService.ts +++ /dev/null @@ -1,524 +0,0 @@ -import { asV2Middleware } from '@metamask/json-rpc-engine'; -import { JsonRpcEngineV2 as JsonRpcEngine } from '@metamask/json-rpc-engine/v2'; -import { createStreamMiddleware } from '@metamask/json-rpc-middleware-stream'; -import ObjectMultiplex from '@metamask/object-multiplex'; -import type { BasePostMessageStream } from '@metamask/post-message-stream'; -import type { SnapRpcHookArgs } from '@metamask/snaps-utils'; -import { SNAP_STREAM_NAMES, logError, logWarning } from '@metamask/snaps-utils'; -import type { - Json, - JsonRpcNotification, - JsonRpcRequest, -} from '@metamask/utils'; -import { - Duration, - assertIsJsonRpcRequest, - hasProperty, - inMilliseconds, -} from '@metamask/utils'; -import { nanoid } from 'nanoid'; -import { pipeline } from 'readable-stream'; -import type { Duplex } from 'readable-stream'; - -import type { - ExecutionService, - ExecutionServiceMessenger, - SnapErrorJson, - SnapExecutionData, -} from './ExecutionService'; -import { log } from '../logging'; -import { Timer } from '../snaps/Timer'; -import { hasTimedOut, withTimeout } from '../utils'; - -const controllerName = 'ExecutionService'; - -export type SetupSnapProvider = (snapId: string, stream: Duplex) => void; - -export type ExecutionServiceArgs = { - setupSnapProvider: SetupSnapProvider; - messenger: ExecutionServiceMessenger; - initTimeout?: number; - pingTimeout?: number; - terminationTimeout?: number; - usePing?: boolean; -}; - -export type JobStreams = { - command: Duplex; - rpc: Duplex; - connection: BasePostMessageStream; - mux: ObjectMultiplex; -}; - -export type Job = { - id: string; - streams: JobStreams; - rpcEngine: JsonRpcEngine; - worker: WorkerType; -}; - -export type TerminateJobArgs = Partial> & - Pick, 'id'>; - -/** - Statuses used for diagnostic purposes - - created: The initial state, no initialization has started - - initializing: Snap execution environment is initializing - - initialized: Snap execution environment has initialized - - executing: Snap source code is being executed - - running: Snap executed and ready for RPC requests - */ -type ExecutionStatus = - | 'created' - | 'initializing' - | 'initialized' - | 'executing' - | 'running'; - -export abstract class AbstractExecutionService - implements ExecutionService -{ - name: typeof controllerName = controllerName; - - state = null; - - readonly #jobs: Map>; - - readonly #status: Map; - - readonly #setupSnapProvider: SetupSnapProvider; - - readonly #messenger: ExecutionServiceMessenger; - - readonly #initTimeout: number; - - readonly #pingTimeout: number; - - readonly #terminationTimeout: number; - - readonly #usePing: boolean; - - constructor({ - setupSnapProvider, - messenger, - initTimeout = inMilliseconds(60, Duration.Second), - pingTimeout = inMilliseconds(10, Duration.Second), - terminationTimeout = inMilliseconds(1, Duration.Second), - usePing = true, - }: ExecutionServiceArgs) { - this.#jobs = new Map(); - this.#status = new Map(); - this.#setupSnapProvider = setupSnapProvider; - this.#messenger = messenger; - this.#initTimeout = initTimeout; - this.#pingTimeout = pingTimeout; - this.#terminationTimeout = terminationTimeout; - this.#usePing = usePing; - - this.#registerMessageHandlers(); - } - - /** - * Constructor helper for registering the controller's messaging system - * actions. - */ - #registerMessageHandlers(): void { - this.#messenger.registerActionHandler( - `${controllerName}:handleRpcRequest`, - async (snapId: string, options: SnapRpcHookArgs) => - this.handleRpcRequest(snapId, options), - ); - - this.#messenger.registerActionHandler( - `${controllerName}:executeSnap`, - async (data: SnapExecutionData) => this.executeSnap(data), - ); - - this.#messenger.registerActionHandler( - `${controllerName}:terminateSnap`, - async (snapId: string) => this.terminateSnap(snapId), - ); - - this.#messenger.registerActionHandler( - `${controllerName}:terminateAllSnaps`, - async () => this.terminateAllSnaps(), - ); - } - - /** - * Performs additional necessary work during job termination. **MUST** be - * implemented by concrete implementations. See - * {@link AbstractExecutionService.terminate} for details. - * - * @param job - The object corresponding to the job to be terminated. - */ - protected abstract terminateJob( - job: TerminateJobArgs, - ): Promise; - - /** - * Terminates the Snap with the specified ID and deletes all its associated - * data. Any subsequent messages targeting the Snap will fail with an error. - * Throws an error if termination fails unexpectedly. - * - * @param snapId - The id of the Snap to be terminated. - */ - public async terminateSnap(snapId: string): Promise { - const job = this.#jobs.get(snapId); - if (!job) { - return; - } - - try { - // Ping worker and tell it to run teardown, continue with termination if it takes too long - const result = await withTimeout( - this.#command(snapId, { - jsonrpc: '2.0', - method: 'terminate', - id: nanoid(), - }), - this.#terminationTimeout, - ); - - if (result === hasTimedOut || result !== 'OK') { - logWarning(`Snap "${snapId}" failed to terminate gracefully.`); - } - } catch { - // Ignore - } - - Object.values(job.streams).forEach((stream) => { - try { - if (!stream.destroyed) { - stream.destroy(); - } - } catch (error) { - logError('Error while destroying stream', error); - } - }); - - await this.terminateJob(job); - - this.#jobs.delete(snapId); - this.#status.delete(snapId); - log(`Snap "${snapId}" terminated.`); - } - - /** - * Initiates a job for a Snap. - * - * @param snapId - The ID of the Snap to initiate a job for. - * @param timer - The timer to use for timeouts. - * @returns Information regarding the created job. - * @throws If the execution service returns an error or execution times out. - */ - async #initJob(snapId: string, timer: Timer): Promise> { - const { streams, worker } = await this.#initStreams(snapId, timer); - - const jsonRpcConnection = createStreamMiddleware(); - - pipeline( - jsonRpcConnection.stream, - streams.command, - jsonRpcConnection.stream, - (error) => { - if (error && !error.message?.match('Premature close')) { - logError(`Command stream failure.`, error); - } - }, - ); - - const rpcEngine = JsonRpcEngine.create({ - middleware: [asV2Middleware(jsonRpcConnection.middleware)], - }); - - const envMetadata = { - id: snapId, - streams, - rpcEngine, - worker, - }; - this.#jobs.set(snapId, envMetadata); - - return envMetadata; - } - - /** - * Sets up the streams for an initiated job. - * - * @param snapId - The Snap ID. - * @param timer - The timer to use for timeouts. - * @returns The streams to communicate with the worker and the worker itself. - * @throws If the execution service returns an error or execution times out. - */ - async #initStreams( - snapId: string, - timer: Timer, - ): Promise<{ streams: JobStreams; worker: WorkerType }> { - const result = await withTimeout(this.initEnvStream(snapId), timer); - - if (result === hasTimedOut) { - // For certain environments, such as the iframe we may have already created the worker and wish to terminate it. - await this.terminateJob({ id: snapId }); - - const status = this.#status.get(snapId); - if (status === 'created') { - // Currently this error can only be thrown by OffscreenExecutionService. - throw new Error( - `The executor for "${snapId}" couldn't start initialization. The offscreen document may not exist.`, - ); - } - throw new Error( - `The executor for "${snapId}" failed to initialize. The iframe/webview/worker failed to load.`, - ); - } - - const { worker, stream: envStream } = result; - const mux = setupMultiplex(envStream, `Snap: "${snapId}"`); - const commandStream = mux.createStream(SNAP_STREAM_NAMES.COMMAND); - - // Handle out-of-band errors, i.e. errors thrown from the Snap outside of the req/res cycle. - // Also keep track of outbound request/responses - const notificationHandler = ( - message: - | JsonRpcRequest - | JsonRpcNotification>, - ) => { - if (hasProperty(message, 'id')) { - return; - } - - if (message.method === 'OutboundRequest') { - this.#messenger.publish('ExecutionService:outboundRequest', snapId); - } else if (message.method === 'OutboundResponse') { - this.#messenger.publish('ExecutionService:outboundResponse', snapId); - } else if (message.method === 'UnhandledError') { - this.#messenger.publish( - 'ExecutionService:unhandledError', - snapId, - (message.params as { error: SnapErrorJson }).error, - ); - commandStream.removeListener('data', notificationHandler); - } else { - logError( - new Error( - `Received unexpected command stream notification "${message.method}".`, - ), - ); - } - }; - - commandStream.on('data', notificationHandler); - - const rpcStream = mux - .createStream(SNAP_STREAM_NAMES.JSON_RPC) - .setMaxListeners(20); - - rpcStream.on('data', (chunk) => { - if (chunk?.data && hasProperty(chunk?.data, 'id')) { - this.#messenger.publish('ExecutionService:outboundRequest', snapId); - } - }); - - // An error handler is not attached to the RPC stream until `setupSnapProvider` is called. - // We must set it up here to prevent errors from bubbling up if the stream is destroyed before then. - rpcStream.on('error', (error) => { - if (error && !error.message?.match('Premature close')) { - logError(`Snap: "${snapId}" - RPC stream failure:`, error); - } - }); - - const originalWrite = rpcStream.write.bind(rpcStream); - - // @ts-expect-error Hack to inspect the messages being written to the stream. - rpcStream.write = (chunk, encoding, callback) => { - // Ignore chain switching notifications as it doesn't matter for the SnapProvider. - if (chunk?.data?.method === 'metamask_chainChanged') { - return true; - } - - if (chunk?.data && hasProperty(chunk?.data, 'id')) { - this.#messenger.publish('ExecutionService:outboundResponse', snapId); - } - - return originalWrite(chunk, encoding, callback); - }; - - return { - streams: { - command: commandStream, - rpc: rpcStream, - connection: envStream, - mux, - }, - worker, - }; - } - - /** - * Abstract function implemented by implementing class that spins up a new worker for a job. - * - * Depending on the execution environment, this may run forever if the Snap fails to start up properly, therefore any call to this function should be wrapped in a timeout. - */ - protected abstract initEnvStream(snapId: string): Promise<{ - worker: WorkerType; - stream: BasePostMessageStream; - }>; - - /** - * Set the execution status of the Snap. - * - * @param snapId - The Snap ID. - * @param status - The current execution status. - */ - protected setSnapStatus(snapId: string, status: ExecutionStatus) { - this.#status.set(snapId, status); - } - - async terminateAllSnaps() { - await Promise.all( - [...this.#jobs.keys()].map(async (snapId) => this.terminateSnap(snapId)), - ); - } - - /** - * Initializes and executes a Snap, setting up the communication channels to the Snap etc. - * - * @param snapData - Data needed for Snap execution. - * @param snapData.snapId - The ID of the Snap to execute. - * @param snapData.sourceCode - The source code of the Snap to execute. - * @param snapData.endowments - The endowments available to the executing Snap. - * @returns A string `OK` if execution succeeded. - * @throws If the execution service returns an error or execution times out. - */ - async executeSnap({ - snapId, - sourceCode, - endowments, - }: SnapExecutionData): Promise { - if (this.#jobs.has(snapId)) { - throw new Error(`"${snapId}" is already running.`); - } - - this.setSnapStatus(snapId, 'created'); - - const timer = new Timer(this.#initTimeout); - - // This may resolve even if the environment has failed to start up fully - const job = await this.#initJob(snapId, timer); - - // Certain environments use ping as part of their initialization and thus can skip it here - if (this.#usePing) { - // Ping the worker to ensure that it started up - const pingResult = await withTimeout( - this.#command(job.id, { - jsonrpc: '2.0', - method: 'ping', - id: nanoid(), - }), - this.#pingTimeout, - ); - - if (pingResult === hasTimedOut) { - throw new Error( - `The executor for "${snapId}" was unreachable. The executor did not respond in time.`, - ); - } - } - - const rpcStream = job.streams.rpc; - - this.#setupSnapProvider(snapId, rpcStream); - - // Use the remaining time as the timer, but ensure that the - // Snap gets at least half the init timeout. - const remainingTime = Math.max(timer.remaining, this.#initTimeout / 2); - - this.setSnapStatus(snapId, 'initialized'); - - const request = { - jsonrpc: '2.0', - method: 'executeSnap', - params: { snapId, sourceCode, endowments }, - id: nanoid(), - }; - - assertIsJsonRpcRequest(request); - - this.setSnapStatus(snapId, 'executing'); - - const result = await withTimeout( - this.#command(job.id, request), - remainingTime, - ); - - if (result === hasTimedOut) { - throw new Error(`${snapId} failed to start.`); - } - - if (result === 'OK') { - this.setSnapStatus(snapId, 'running'); - } - - return result as string; - } - - async #command( - snapId: string, - message: JsonRpcRequest, - ): Promise { - const job = this.#jobs.get(snapId); - if (!job) { - throw new Error(`"${snapId}" is not currently running.`); - } - - log('Parent: Sending Command', message); - return await job.rpcEngine.handle(message); - } - - /** - * Handle RPC request. - * - * @param snapId - The ID of the recipient Snap. - * @param options - Bag of options to pass to the RPC handler. - * @returns Promise that can handle the request. - */ - public async handleRpcRequest( - snapId: string, - options: SnapRpcHookArgs, - ): Promise { - const { handler, request, origin } = options; - - return await this.#command(snapId, { - id: nanoid(), - jsonrpc: '2.0', - method: 'snapRpc', - params: { - snapId, - origin, - handler, - request: request as JsonRpcRequest, - }, - }); - } -} - -/** - * Sets up stream multiplexing for the given stream. - * - * @param connectionStream - The stream to mux. - * @param streamName - The name of the stream, for identification in errors. - * @returns The multiplexed stream. - */ -export function setupMultiplex( - connectionStream: Duplex, - streamName: string, -): ObjectMultiplex { - const mux = new ObjectMultiplex(); - pipeline(connectionStream, mux, connectionStream, (error) => { - if (error && !error.message?.match('Premature close')) { - logError(`"${streamName}" stream failure.`, error); - } - }); - return mux; -} diff --git a/packages/snaps-controllers/src/services/ExecutionService-method-action-types.ts b/packages/snaps-controllers/src/services/ExecutionService-method-action-types.ts new file mode 100644 index 0000000000..0993572ac4 --- /dev/null +++ b/packages/snaps-controllers/src/services/ExecutionService-method-action-types.ts @@ -0,0 +1,59 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { ExecutionService } from './ExecutionService'; + +/** + * Terminates the Snap with the specified ID and deletes all its associated + * data. Any subsequent messages targeting the Snap will fail with an error. + * Throws an error if termination fails unexpectedly. + * + * @param snapId - The id of the Snap to be terminated. + */ +export type ExecutionServiceTerminateSnapAction = { + type: `ExecutionService:terminateSnap`; + handler: ExecutionService['terminateSnap']; +}; + +export type ExecutionServiceTerminateAllSnapsAction = { + type: `ExecutionService:terminateAllSnaps`; + handler: ExecutionService['terminateAllSnaps']; +}; + +/** + * Initializes and executes a Snap, setting up the communication channels to the Snap etc. + * + * @param snapData - Data needed for Snap execution. + * @param snapData.snapId - The ID of the Snap to execute. + * @param snapData.sourceCode - The source code of the Snap to execute. + * @param snapData.endowments - The endowments available to the executing Snap. + * @returns A string `OK` if execution succeeded. + * @throws If the execution service returns an error or execution times out. + */ +export type ExecutionServiceExecuteSnapAction = { + type: `ExecutionService:executeSnap`; + handler: ExecutionService['executeSnap']; +}; + +/** + * Handle RPC request. + * + * @param snapId - The ID of the recipient Snap. + * @param options - Bag of options to pass to the RPC handler. + * @returns Promise that can handle the request. + */ +export type ExecutionServiceHandleRpcRequestAction = { + type: `ExecutionService:handleRpcRequest`; + handler: ExecutionService['handleRpcRequest']; +}; + +/** + * Union of all ExecutionService action types. + */ +export type ExecutionServiceMethodActions = + | ExecutionServiceTerminateSnapAction + | ExecutionServiceTerminateAllSnapsAction + | ExecutionServiceExecuteSnapAction + | ExecutionServiceHandleRpcRequestAction; diff --git a/packages/snaps-controllers/src/services/AbstractExecutionService.test.ts b/packages/snaps-controllers/src/services/ExecutionService.test.ts similarity index 97% rename from packages/snaps-controllers/src/services/AbstractExecutionService.test.ts rename to packages/snaps-controllers/src/services/ExecutionService.test.ts index 342df5dd33..6bf7952ca8 100644 --- a/packages/snaps-controllers/src/services/AbstractExecutionService.test.ts +++ b/packages/snaps-controllers/src/services/ExecutionService.test.ts @@ -3,7 +3,7 @@ import { HandlerType } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; import { Duration, inMilliseconds } from '@metamask/utils'; -import type { ExecutionServiceArgs } from './AbstractExecutionService'; +import type { ExecutionServiceArgs } from './ExecutionService'; import { NodeThreadExecutionService } from './node'; import { createService } from '../test-utils'; @@ -18,7 +18,7 @@ class MockExecutionService extends NodeThreadExecutionService { } } -describe('AbstractExecutionService', () => { +describe('ExecutionService', () => { afterEach(() => { jest.restoreAllMocks(); }); diff --git a/packages/snaps-controllers/src/services/ExecutionService.ts b/packages/snaps-controllers/src/services/ExecutionService.ts index 3d6898ce21..2ec7593b1d 100644 --- a/packages/snaps-controllers/src/services/ExecutionService.ts +++ b/packages/snaps-controllers/src/services/ExecutionService.ts @@ -1,28 +1,84 @@ +import { asV2Middleware } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 as JsonRpcEngine } from '@metamask/json-rpc-engine/v2'; +import { createStreamMiddleware } from '@metamask/json-rpc-middleware-stream'; import type { Messenger } from '@metamask/messenger'; +import type ObjectMultiplex from '@metamask/object-multiplex'; +import type { BasePostMessageStream } from '@metamask/post-message-stream'; import type { SnapRpcHookArgs } from '@metamask/snaps-utils'; -import type { Json } from '@metamask/utils'; - -type TerminateSnap = (snapId: string) => Promise; -type TerminateAll = () => Promise; -type ExecuteSnap = (snapData: SnapExecutionData) => Promise; - -type HandleRpcRequest = ( - snapId: string, - options: SnapRpcHookArgs, -) => Promise; - -export type ExecutionService = { - // These fields are required for modular initialisation of the execution - // service in the MetaMask extension. - name: 'ExecutionService'; - state: null; - - terminateSnap: TerminateSnap; - terminateAllSnaps: TerminateAll; - executeSnap: ExecuteSnap; - handleRpcRequest: HandleRpcRequest; +import { SNAP_STREAM_NAMES, logError, logWarning } from '@metamask/snaps-utils'; +import type { + Json, + JsonRpcNotification, + JsonRpcRequest, +} from '@metamask/utils'; +import { + Duration, + assertIsJsonRpcRequest, + hasProperty, + inMilliseconds, +} from '@metamask/utils'; +import { nanoid } from 'nanoid'; +import { pipeline } from 'readable-stream'; +import type { Duplex } from 'readable-stream'; + +import type { ExecutionServiceMethodActions } from './ExecutionService-method-action-types'; +import { setupMultiplex } from './multiplex'; +import { log } from '../logging'; +import { Timer } from '../snaps/Timer'; +import { hasTimedOut, withTimeout } from '../utils'; + +const serviceName = 'ExecutionService'; + +export type SetupSnapProvider = (snapId: string, stream: Duplex) => void; + +export type ExecutionServiceArgs = { + setupSnapProvider: SetupSnapProvider; + messenger: ExecutionServiceMessenger; + initTimeout?: number; + pingTimeout?: number; + terminationTimeout?: number; + usePing?: boolean; +}; + +type JobStreams = { + command: Duplex; + rpc: Duplex; + connection: BasePostMessageStream; + mux: ObjectMultiplex; }; +export type Job = { + id: string; + streams: JobStreams; + rpcEngine: JsonRpcEngine; + worker: WorkerType; +}; + +export type TerminateJobArgs = Partial> & + Pick, 'id'>; + +/** + Statuses used for diagnostic purposes + - created: The initial state, no initialization has started + - initializing: Snap execution environment is initializing + - initialized: Snap execution environment has initialized + - executing: Snap source code is being executed + - running: Snap executed and ready for RPC requests + */ +type ExecutionStatus = + | 'created' + | 'initializing' + | 'initialized' + | 'executing' + | 'running'; + +const MESSENGER_EXPOSED_METHODS = [ + 'terminateSnap', + 'terminateAllSnaps', + 'executeSnap', + 'handleRpcRequest', +] as const; + export type SnapExecutionData = { snapId: string; sourceCode: string; @@ -35,68 +91,432 @@ export type SnapErrorJson = { data?: Json; }; -type ControllerName = 'ExecutionService'; - -export type ErrorMessageEvent = { +export type ExecutionServiceUnhandledErrorEvent = { type: 'ExecutionService:unhandledError'; payload: [string, SnapErrorJson]; }; -export type OutboundRequest = { +export type ExecutionServiceOutboundRequestEvent = { type: 'ExecutionService:outboundRequest'; payload: [string]; }; -export type OutboundResponse = { +export type ExecutionServiceOutboundResponseEvent = { type: 'ExecutionService:outboundResponse'; payload: [string]; }; export type ExecutionServiceEvents = - | ErrorMessageEvent - | OutboundRequest - | OutboundResponse; - -/** - * Handles RPC request. - */ -export type HandleRpcRequestAction = { - type: `${ControllerName}:handleRpcRequest`; - handler: ExecutionService['handleRpcRequest']; -}; + | ExecutionServiceUnhandledErrorEvent + | ExecutionServiceOutboundRequestEvent + | ExecutionServiceOutboundResponseEvent; -/** - * Executes a given snap. - */ -export type ExecuteSnapAction = { - type: `${ControllerName}:executeSnap`; - handler: ExecutionService['executeSnap']; -}; - -/** - * Terminates a given snap. - */ -export type TerminateSnapAction = { - type: `${ControllerName}:terminateSnap`; - handler: ExecutionService['terminateSnap']; -}; - -/** - * Terminates all snaps. - */ -export type TerminateAllSnapsAction = { - type: `${ControllerName}:terminateAllSnaps`; - handler: ExecutionService['terminateAllSnaps']; -}; - -export type ExecutionServiceActions = - | HandleRpcRequestAction - | ExecuteSnapAction - | TerminateSnapAction - | TerminateAllSnapsAction; +export type ExecutionServiceActions = ExecutionServiceMethodActions; export type ExecutionServiceMessenger = Messenger< 'ExecutionService', ExecutionServiceActions, ExecutionServiceEvents >; + +export abstract class ExecutionService { + name: typeof serviceName = serviceName; + + state = null; + + readonly #jobs: Map>; + + readonly #status: Map; + + readonly #setupSnapProvider: SetupSnapProvider; + + readonly #messenger: ExecutionServiceMessenger; + + readonly #initTimeout: number; + + readonly #pingTimeout: number; + + readonly #terminationTimeout: number; + + readonly #usePing: boolean; + + constructor({ + setupSnapProvider, + messenger, + initTimeout = inMilliseconds(60, Duration.Second), + pingTimeout = inMilliseconds(10, Duration.Second), + terminationTimeout = inMilliseconds(1, Duration.Second), + usePing = true, + }: ExecutionServiceArgs) { + this.#jobs = new Map(); + this.#status = new Map(); + this.#setupSnapProvider = setupSnapProvider; + this.#messenger = messenger; + this.#initTimeout = initTimeout; + this.#pingTimeout = pingTimeout; + this.#terminationTimeout = terminationTimeout; + this.#usePing = usePing; + + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); + } + + /** + * Performs additional necessary work during job termination. **MUST** be + * implemented by concrete implementations. See + * {@link AbstractExecutionService.terminate} for details. + * + * @param job - The object corresponding to the job to be terminated. + */ + protected abstract terminateJob( + job: TerminateJobArgs, + ): Promise; + + /** + * Terminates the Snap with the specified ID and deletes all its associated + * data. Any subsequent messages targeting the Snap will fail with an error. + * Throws an error if termination fails unexpectedly. + * + * @param snapId - The id of the Snap to be terminated. + */ + public async terminateSnap(snapId: string): Promise { + const job = this.#jobs.get(snapId); + if (!job) { + return; + } + + try { + // Ping worker and tell it to run teardown, continue with termination if it takes too long + const result = await withTimeout( + this.#command(snapId, { + jsonrpc: '2.0', + method: 'terminate', + id: nanoid(), + }), + this.#terminationTimeout, + ); + + if (result === hasTimedOut || result !== 'OK') { + logWarning(`Snap "${snapId}" failed to terminate gracefully.`); + } + } catch { + // Ignore + } + + Object.values(job.streams).forEach((stream) => { + try { + if (!stream.destroyed) { + stream.destroy(); + } + } catch (error) { + logError('Error while destroying stream', error); + } + }); + + await this.terminateJob(job); + + this.#jobs.delete(snapId); + this.#status.delete(snapId); + log(`Snap "${snapId}" terminated.`); + } + + /** + * Initiates a job for a Snap. + * + * @param snapId - The ID of the Snap to initiate a job for. + * @param timer - The timer to use for timeouts. + * @returns Information regarding the created job. + * @throws If the execution service returns an error or execution times out. + */ + async #initJob(snapId: string, timer: Timer): Promise> { + const { streams, worker } = await this.#initStreams(snapId, timer); + + const jsonRpcConnection = createStreamMiddleware(); + + pipeline( + jsonRpcConnection.stream, + streams.command, + jsonRpcConnection.stream, + (error) => { + if (error && !error.message?.match('Premature close')) { + logError(`Command stream failure.`, error); + } + }, + ); + + const rpcEngine = JsonRpcEngine.create({ + middleware: [asV2Middleware(jsonRpcConnection.middleware)], + }); + + const envMetadata = { + id: snapId, + streams, + rpcEngine, + worker, + }; + this.#jobs.set(snapId, envMetadata); + + return envMetadata; + } + + /** + * Sets up the streams for an initiated job. + * + * @param snapId - The Snap ID. + * @param timer - The timer to use for timeouts. + * @returns The streams to communicate with the worker and the worker itself. + * @throws If the execution service returns an error or execution times out. + */ + async #initStreams( + snapId: string, + timer: Timer, + ): Promise<{ streams: JobStreams; worker: WorkerType }> { + const result = await withTimeout(this.initEnvStream(snapId), timer); + + if (result === hasTimedOut) { + // For certain environments, such as the iframe we may have already created the worker and wish to terminate it. + await this.terminateJob({ id: snapId }); + + const status = this.#status.get(snapId); + if (status === 'created') { + // Currently this error can only be thrown by OffscreenExecutionService. + throw new Error( + `The executor for "${snapId}" couldn't start initialization. The offscreen document may not exist.`, + ); + } + throw new Error( + `The executor for "${snapId}" failed to initialize. The iframe/webview/worker failed to load.`, + ); + } + + const { worker, stream: envStream } = result; + const mux = setupMultiplex(envStream, `Snap: "${snapId}"`); + const commandStream = mux.createStream(SNAP_STREAM_NAMES.COMMAND); + + // Handle out-of-band errors, i.e. errors thrown from the Snap outside of the req/res cycle. + // Also keep track of outbound request/responses + const notificationHandler = ( + message: + | JsonRpcRequest + | JsonRpcNotification>, + ) => { + if (hasProperty(message, 'id')) { + return; + } + + if (message.method === 'OutboundRequest') { + this.#messenger.publish('ExecutionService:outboundRequest', snapId); + } else if (message.method === 'OutboundResponse') { + this.#messenger.publish('ExecutionService:outboundResponse', snapId); + } else if (message.method === 'UnhandledError') { + this.#messenger.publish( + 'ExecutionService:unhandledError', + snapId, + (message.params as { error: SnapErrorJson }).error, + ); + commandStream.removeListener('data', notificationHandler); + } else { + logError( + new Error( + `Received unexpected command stream notification "${message.method}".`, + ), + ); + } + }; + + commandStream.on('data', notificationHandler); + + const rpcStream = mux + .createStream(SNAP_STREAM_NAMES.JSON_RPC) + .setMaxListeners(20); + + rpcStream.on('data', (chunk) => { + if (chunk?.data && hasProperty(chunk?.data, 'id')) { + this.#messenger.publish('ExecutionService:outboundRequest', snapId); + } + }); + + // An error handler is not attached to the RPC stream until `setupSnapProvider` is called. + // We must set it up here to prevent errors from bubbling up if the stream is destroyed before then. + rpcStream.on('error', (error) => { + if (error && !error.message?.match('Premature close')) { + logError(`Snap: "${snapId}" - RPC stream failure:`, error); + } + }); + + const originalWrite = rpcStream.write.bind(rpcStream); + + // @ts-expect-error Hack to inspect the messages being written to the stream. + rpcStream.write = (chunk, encoding, callback) => { + // Ignore chain switching notifications as it doesn't matter for the SnapProvider. + if (chunk?.data?.method === 'metamask_chainChanged') { + return true; + } + + if (chunk?.data && hasProperty(chunk?.data, 'id')) { + this.#messenger.publish('ExecutionService:outboundResponse', snapId); + } + + return originalWrite(chunk, encoding, callback); + }; + + return { + streams: { + command: commandStream, + rpc: rpcStream, + connection: envStream, + mux, + }, + worker, + }; + } + + /** + * Abstract function implemented by implementing class that spins up a new worker for a job. + * + * Depending on the execution environment, this may run forever if the Snap fails to start up properly, therefore any call to this function should be wrapped in a timeout. + */ + protected abstract initEnvStream(snapId: string): Promise<{ + worker: WorkerType; + stream: BasePostMessageStream; + }>; + + /** + * Set the execution status of the Snap. + * + * @param snapId - The Snap ID. + * @param status - The current execution status. + */ + protected setSnapStatus(snapId: string, status: ExecutionStatus) { + this.#status.set(snapId, status); + } + + async terminateAllSnaps() { + await Promise.all( + [...this.#jobs.keys()].map(async (snapId) => this.terminateSnap(snapId)), + ); + } + + /** + * Initializes and executes a Snap, setting up the communication channels to the Snap etc. + * + * @param snapData - Data needed for Snap execution. + * @param snapData.snapId - The ID of the Snap to execute. + * @param snapData.sourceCode - The source code of the Snap to execute. + * @param snapData.endowments - The endowments available to the executing Snap. + * @returns A string `OK` if execution succeeded. + * @throws If the execution service returns an error or execution times out. + */ + async executeSnap({ + snapId, + sourceCode, + endowments, + }: SnapExecutionData): Promise { + if (this.#jobs.has(snapId)) { + throw new Error(`"${snapId}" is already running.`); + } + + this.setSnapStatus(snapId, 'created'); + + const timer = new Timer(this.#initTimeout); + + // This may resolve even if the environment has failed to start up fully + const job = await this.#initJob(snapId, timer); + + // Certain environments use ping as part of their initialization and thus can skip it here + if (this.#usePing) { + // Ping the worker to ensure that it started up + const pingResult = await withTimeout( + this.#command(job.id, { + jsonrpc: '2.0', + method: 'ping', + id: nanoid(), + }), + this.#pingTimeout, + ); + + if (pingResult === hasTimedOut) { + throw new Error( + `The executor for "${snapId}" was unreachable. The executor did not respond in time.`, + ); + } + } + + const rpcStream = job.streams.rpc; + + this.#setupSnapProvider(snapId, rpcStream); + + // Use the remaining time as the timer, but ensure that the + // Snap gets at least half the init timeout. + const remainingTime = Math.max(timer.remaining, this.#initTimeout / 2); + + this.setSnapStatus(snapId, 'initialized'); + + const request = { + jsonrpc: '2.0', + method: 'executeSnap', + params: { snapId, sourceCode, endowments }, + id: nanoid(), + }; + + assertIsJsonRpcRequest(request); + + this.setSnapStatus(snapId, 'executing'); + + const result = await withTimeout( + this.#command(job.id, request), + remainingTime, + ); + + if (result === hasTimedOut) { + throw new Error(`${snapId} failed to start.`); + } + + if (result === 'OK') { + this.setSnapStatus(snapId, 'running'); + } + + return result as string; + } + + async #command( + snapId: string, + message: JsonRpcRequest, + ): Promise { + const job = this.#jobs.get(snapId); + if (!job) { + throw new Error(`"${snapId}" is not currently running.`); + } + + log('Parent: Sending Command', message); + return await job.rpcEngine.handle(message); + } + + /** + * Handle RPC request. + * + * @param snapId - The ID of the recipient Snap. + * @param options - Bag of options to pass to the RPC handler. + * @returns Promise that can handle the request. + */ + public async handleRpcRequest( + snapId: string, + options: SnapRpcHookArgs, + ): Promise { + const { handler, request, origin } = options; + + return await this.#command(snapId, { + id: nanoid(), + jsonrpc: '2.0', + method: 'snapRpc', + params: { + snapId, + origin, + handler, + request: request as JsonRpcRequest, + }, + }); + } +} diff --git a/packages/snaps-controllers/src/services/browser.test.ts b/packages/snaps-controllers/src/services/browser.test.ts index b4cf203e56..dfe475be40 100644 --- a/packages/snaps-controllers/src/services/browser.test.ts +++ b/packages/snaps-controllers/src/services/browser.test.ts @@ -2,7 +2,7 @@ import * as BrowserExport from './browser'; describe('browser entrypoint', () => { const expectedExports = [ - 'AbstractExecutionService', + 'ExecutionService', 'setupMultiplex', 'IframeExecutionService', 'OffscreenExecutionService', diff --git a/packages/snaps-controllers/src/services/browser.ts b/packages/snaps-controllers/src/services/browser.ts index 871061032c..6ee15db764 100644 --- a/packages/snaps-controllers/src/services/browser.ts +++ b/packages/snaps-controllers/src/services/browser.ts @@ -1,6 +1,23 @@ // Subset of exports meant for browser environments, omits Node.js services -export * from './AbstractExecutionService'; -export type * from './ExecutionService'; +export type { + ExecutionServiceActions, + ExecutionServiceArgs, + ExecutionServiceEvents, + ExecutionServiceMessenger, + ExecutionServiceOutboundRequestEvent, + ExecutionServiceOutboundResponseEvent, + ExecutionServiceUnhandledErrorEvent, + SnapErrorJson, + SnapExecutionData, +} from './ExecutionService'; +export { ExecutionService } from './ExecutionService'; +export type { + ExecutionServiceTerminateSnapAction, + ExecutionServiceTerminateAllSnapsAction, + ExecutionServiceExecuteSnapAction, + ExecutionServiceHandleRpcRequestAction, +} from './ExecutionService-method-action-types'; +export { setupMultiplex } from './multiplex'; export * from './ProxyPostMessageStream'; export * from './iframe'; export * from './offscreen'; diff --git a/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts b/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts index cb49d91f89..70daadda29 100644 --- a/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts +++ b/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts @@ -6,14 +6,14 @@ import { withTimeout } from '../../utils'; import type { ExecutionServiceArgs, TerminateJobArgs, -} from '../AbstractExecutionService'; -import { AbstractExecutionService } from '../AbstractExecutionService'; +} from '../ExecutionService'; +import { ExecutionService } from '../ExecutionService'; type IframeExecutionEnvironmentServiceArgs = { iframeUrl: URL; } & ExecutionServiceArgs; -export class IframeExecutionService extends AbstractExecutionService { +export class IframeExecutionService extends ExecutionService { public iframeUrl: URL; constructor({ diff --git a/packages/snaps-controllers/src/services/index.ts b/packages/snaps-controllers/src/services/index.ts index 2e0ea1263b..341861f457 100644 --- a/packages/snaps-controllers/src/services/index.ts +++ b/packages/snaps-controllers/src/services/index.ts @@ -1,5 +1,22 @@ -export * from './AbstractExecutionService'; -export type * from './ExecutionService'; +export type { + ExecutionServiceActions, + ExecutionServiceArgs, + ExecutionServiceEvents, + ExecutionServiceMessenger, + ExecutionServiceOutboundRequestEvent, + ExecutionServiceOutboundResponseEvent, + ExecutionServiceUnhandledErrorEvent, + SnapErrorJson, + SnapExecutionData, +} from './ExecutionService'; +export { ExecutionService } from './ExecutionService'; +export type { + ExecutionServiceTerminateSnapAction, + ExecutionServiceTerminateAllSnapsAction, + ExecutionServiceExecuteSnapAction, + ExecutionServiceHandleRpcRequestAction, +} from './ExecutionService-method-action-types'; +export { setupMultiplex } from './multiplex'; export * from './ProxyPostMessageStream'; export * from './iframe'; export * from './offscreen'; diff --git a/packages/snaps-controllers/src/services/multiplex.ts b/packages/snaps-controllers/src/services/multiplex.ts new file mode 100644 index 0000000000..34417ffe69 --- /dev/null +++ b/packages/snaps-controllers/src/services/multiplex.ts @@ -0,0 +1,24 @@ +import ObjectMultiplex from '@metamask/object-multiplex'; +import { logError } from '@metamask/snaps-utils'; +import type { Duplex } from 'readable-stream'; +import { pipeline } from 'readable-stream'; + +/** + * Sets up stream multiplexing for the given stream. + * + * @param connectionStream - The stream to mux. + * @param streamName - The name of the stream, for identification in errors. + * @returns The multiplexed stream. + */ +export function setupMultiplex( + connectionStream: Duplex, + streamName: string, +): ObjectMultiplex { + const mux = new ObjectMultiplex(); + pipeline(connectionStream, mux, connectionStream, (error) => { + if (error && !error.message?.match('Premature close')) { + logError(`"${streamName}" stream failure.`, error); + } + }); + return mux; +} diff --git a/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts b/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts index 26f38f0d82..24bd916642 100644 --- a/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts +++ b/packages/snaps-controllers/src/services/node-js/NodeProcessExecutionService.ts @@ -3,10 +3,10 @@ import { ProcessParentMessageStream } from '@metamask/post-message-stream/node'; import type { ChildProcess } from 'child_process'; import { fork } from 'child_process'; -import type { TerminateJobArgs } from '..'; -import { AbstractExecutionService } from '..'; +import type { TerminateJobArgs } from '../ExecutionService'; +import { ExecutionService } from '../ExecutionService'; -export class NodeProcessExecutionService extends AbstractExecutionService { +export class NodeProcessExecutionService extends ExecutionService { protected async initEnvStream(snapId: string): Promise<{ worker: ChildProcess; stream: BasePostMessageStream; diff --git a/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts b/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts index 223506acf5..dfcdc7d083 100644 --- a/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts +++ b/packages/snaps-controllers/src/services/node-js/NodeThreadExecutionService.ts @@ -2,10 +2,10 @@ import type { BasePostMessageStream } from '@metamask/post-message-stream'; import { ThreadParentMessageStream } from '@metamask/post-message-stream/node'; import { Worker } from 'worker_threads'; -import type { TerminateJobArgs } from '..'; -import { AbstractExecutionService } from '..'; +import { ExecutionService } from '../ExecutionService'; +import type { TerminateJobArgs } from '../ExecutionService'; -export class NodeThreadExecutionService extends AbstractExecutionService { +export class NodeThreadExecutionService extends ExecutionService { protected async initEnvStream(snapId: string): Promise<{ worker: Worker; stream: BasePostMessageStream; @@ -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/offscreen/OffscreenExecutionService.ts b/packages/snaps-controllers/src/services/offscreen/OffscreenExecutionService.ts index 85b2ff231f..231445e8b0 100644 --- a/packages/snaps-controllers/src/services/offscreen/OffscreenExecutionService.ts +++ b/packages/snaps-controllers/src/services/offscreen/OffscreenExecutionService.ts @@ -1,6 +1,6 @@ import { BrowserRuntimePostMessageStream } from '@metamask/post-message-stream'; -import type { ExecutionServiceArgs } from '../AbstractExecutionService'; +import type { ExecutionServiceArgs } from '../ExecutionService'; import { ProxyExecutionService } from '../proxy/ProxyExecutionService'; type OffscreenExecutionEnvironmentServiceArgs = { diff --git a/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts b/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts index 251f7131b9..a452c57299 100644 --- a/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts +++ b/packages/snaps-controllers/src/services/proxy/ProxyExecutionService.ts @@ -4,15 +4,15 @@ import { nanoid } from 'nanoid'; import type { ExecutionServiceArgs, TerminateJobArgs, -} from '../AbstractExecutionService'; -import { AbstractExecutionService } from '../AbstractExecutionService'; +} from '../ExecutionService'; +import { ExecutionService } from '../ExecutionService'; import { ProxyPostMessageStream } from '../ProxyPostMessageStream'; type ProxyExecutionEnvironmentServiceArgs = { stream: BasePostMessageStream; } & ExecutionServiceArgs; -export class ProxyExecutionService extends AbstractExecutionService { +export class ProxyExecutionService extends ExecutionService { readonly #stream: BasePostMessageStream; /** @@ -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/services/webview/WebViewExecutionService.ts b/packages/snaps-controllers/src/services/webview/WebViewExecutionService.ts index 9a2a97982e..c03abb7f24 100644 --- a/packages/snaps-controllers/src/services/webview/WebViewExecutionService.ts +++ b/packages/snaps-controllers/src/services/webview/WebViewExecutionService.ts @@ -1,17 +1,17 @@ import type { WebViewInterface } from './WebViewMessageStream'; import { WebViewMessageStream } from './WebViewMessageStream'; -import { AbstractExecutionService } from '../AbstractExecutionService'; +import { ExecutionService } from '../ExecutionService'; import type { ExecutionServiceArgs, TerminateJobArgs, -} from '../AbstractExecutionService'; +} from '../ExecutionService'; export type WebViewExecutionServiceArgs = ExecutionServiceArgs & { createWebView: (jobId: string) => Promise; removeWebView: (jobId: string) => void; }; -export class WebViewExecutionService extends AbstractExecutionService { +export class WebViewExecutionService extends ExecutionService { readonly #createWebView; readonly #removeWebView; diff --git a/packages/snaps-controllers/src/snaps/SnapController-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..2e13d6de0b 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -87,7 +87,7 @@ import { METAMASK_ORIGIN, STATE_DEBOUNCE_TIMEOUT, } from './constants'; -import { SnapsRegistryStatus } from './registry'; +import { SnapRegistryStatus } from './registry'; import type { SnapControllerState } from './SnapController'; import { controllerName, @@ -95,18 +95,18 @@ import { SNAP_APPROVAL_RESULT, SNAP_APPROVAL_UPDATE, } from './SnapController'; -import { AbstractExecutionService, setupMultiplex } from '../services'; +import { ExecutionService, setupMultiplex } from '../services'; +import type { TerminateJobArgs } from '../services/ExecutionService'; import type { ExecutionServiceMessenger, NodeThreadExecutionService, - TerminateJobArgs, } from '../services/node'; import type { SnapControllerStateWithStorageService } from '../test-utils'; import { approvalControllerMock, DEFAULT_ENCRYPTION_KEY_DERIVATION_OPTIONS, ExecutionEnvironmentStub, - getControllerMessenger, + getRootMessenger, getNodeEES, getNodeEESMessenger, getPersistedSnapsState, @@ -127,7 +127,7 @@ import { MOCK_SNAP_PERMISSIONS, MOCK_SNAP_SUBJECT_METADATA, MOCK_WALLET_SNAP_PERMISSION, - MockSnapsRegistry, + MockSnapRegistryController, sleep, waitForStateChange, } from '../test-utils'; @@ -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': {}, @@ -815,7 +817,7 @@ describe('SnapController', () => { expect(options.messenger.call).toHaveBeenNthCalledWith( 2, - 'SnapsRegistry:get', + 'SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0', @@ -1045,8 +1047,8 @@ describe('SnapController', () => { }); it('throws an error if snap is not on allowlist and allowlisting is required but resolve succeeds', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const controller = await getSnapController( getSnapControllerOptions({ @@ -1074,8 +1076,8 @@ describe('SnapController', () => { }); it('throws an error if the registry is unavailable and allowlisting is required but resolve succeeds', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const controller = await getSnapController( getSnapControllerOptions({ @@ -1089,7 +1091,7 @@ describe('SnapController', () => { // Mock resolve to succeed, but registry.get() will fail later registry.resolveVersion.mockReturnValue('1.0.0'); registry.get.mockReturnValue({ - [MOCK_SNAP_ID]: { status: SnapsRegistryStatus.Unavailable }, + [MOCK_SNAP_ID]: { status: SnapRegistryStatus.Unavailable }, }); await expect( @@ -1138,8 +1140,8 @@ describe('SnapController', () => { }); it('resolves to allowlisted version when allowlisting is required', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const { manifest, sourceCode, svgIcon } = await getMockSnapFilesWithUpdatedChecksum({ @@ -1149,7 +1151,7 @@ describe('SnapController', () => { }); registry.get.mockResolvedValueOnce({ - [MOCK_SNAP_ID]: { status: SnapsRegistryStatus.Verified }, + [MOCK_SNAP_ID]: { status: SnapRegistryStatus.Verified }, }); registry.resolveVersion.mockReturnValue('1.1.0'); @@ -1171,15 +1173,15 @@ 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 registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const controller = await getSnapController( getSnapControllerOptions({ @@ -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)).toBeNull(); 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,13 +1864,13 @@ 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() }, }); - class BrickedExecutionService extends AbstractExecutionService { + class BrickedExecutionService extends ExecutionService { constructor(messenger: ExecutionServiceMessenger) { super({ messenger, setupSnapProvider: jest.fn(), pingTimeout: 1 }); } @@ -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,32 +1981,30 @@ describe('SnapController', () => { `, }); - const rootMessenger = getControllerMessenger(); - const [snapController, service] = await getSnapControllerWithEES( - getSnapControllerOptions({ - maxRequestTime: 50, - rootMessenger, - detectSnapLocation: loopbackDetect({ - manifest, - files: [sourceCode, svgIcon as VirtualFile], - }), + const rootMessenger = getRootMessenger(); + const options = getSnapControllerOptions({ + maxRequestTime: 50, + rootMessenger, + detectSnapLocation: loopbackDetect({ + manifest, + files: [sourceCode, svgIcon as VirtualFile], }), - ); + }); - const spy = jest.spyOn(service, 'executeSnap'); + const [snapController, service] = await getSnapControllerWithEES(options); await snapController.installSnaps(MOCK_ORIGIN, { [MOCK_SNAP_ID]: {}, }); - 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. @@ -2041,83 +2041,30 @@ describe('SnapController', () => { expect(await promise).toBe('foo'); - expect(spy).toHaveBeenCalledTimes(2); - - // @ts-expect-error Accessing protected value. - service.terminateJob = originalTerminateFunction; - snapController.destroy(); - await service.terminateAllSnaps(); - }); - - it('does not kill snaps with open sessions', async () => { - const sourceCode = ` - module.exports.onRpcRequest = () => 'foo bar'; - `; - - const rootMessenger = getControllerMessenger(); - - const options = getSnapControllerOptions({ - rootMessenger, - idleTimeCheckInterval: 10, - maxIdleTime: 50, - state: { - snaps: getPersistedSnapsState( - getPersistedSnapObject({ - manifest: getSnapManifest({ - shasum: await getSnapChecksum(getMockSnapFiles({ sourceCode })), - }), - sourceCode, - }), - ), - }, - }); - const [snapController, service] = await getSnapControllerWithEES(options); - - const snap = snapController.getExpect(MOCK_SNAP_ID); - - await snapController.startSnap(snap.id); - expect(snapController.state.snaps[snap.id].status).toBe('running'); - - rootMessenger.call( - 'SnapController:incrementActiveReferences', - MOCK_SNAP_ID, + expect(options.messenger.call).toHaveBeenNthCalledWith( + 8, + 'ExecutionService:executeSnap', + expect.objectContaining({ + snapId: MOCK_SNAP_ID, + }), ); - expect( - await snapController.handleRequest({ - snapId: snap.id, - origin: MOCK_ORIGIN, - handler: HandlerType.OnRpcRequest, - request: { - jsonrpc: '2.0', - method: 'test', - params: {}, - id: 1, - }, + expect(options.messenger.call).toHaveBeenNthCalledWith( + 19, + 'ExecutionService:executeSnap', + expect.objectContaining({ + snapId: MOCK_SNAP_ID, }), - ).toBe('foo bar'); - - await sleep(100); - - // Should still be running after idle timeout - expect(snapController.state.snaps[snap.id].status).toBe('running'); - - options.rootMessenger.call( - 'SnapController:decrementActiveReferences', - MOCK_SNAP_ID, ); - await sleep(100); - - // Should be terminated by idle timeout now - expect(snapController.state.snaps[snap.id].status).toBe('stopped'); - + // @ts-expect-error Accessing protected value. + service.terminateJob = originalTerminateFunction; snapController.destroy(); await service.terminateAllSnaps(); }); it(`shouldn't time out a long running snap on start up`, async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2131,10 +2078,13 @@ describe('SnapController', () => { rootMessenger.registerActionHandler( 'ExecutionService:executeSnap', - async () => await sleep(300), + async () => { + await sleep(300); + return 'OK'; + }, ); - 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 +2098,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 +2116,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 +2168,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 +2256,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 +2272,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 +2291,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 +2307,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 +2325,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 +2358,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 +2372,7 @@ describe('SnapController', () => { }); it('allows MetaMask to send a keyring request', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -2455,7 +2405,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 +2419,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 +2452,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 +2466,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 +2499,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 +2513,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 +2547,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 +2561,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 +2594,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 +2608,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 +2642,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 +2669,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 +2700,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 +2717,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 +2736,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 +2770,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 +2794,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 +2810,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 +2834,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 +2850,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 +2883,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 +2899,7 @@ describe('SnapController', () => { }); it('injects context into onUserInput', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -3021,7 +2971,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 +3028,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 +3093,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 +3154,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 +3213,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 +3267,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 +3305,7 @@ describe('SnapController', () => { rootMessenger.registerActionHandler( 'SnapInterfaceController:getInterface', + // @ts-expect-error: Partial mock. () => ({ snapId: MOCK_SNAP_ID, content: foo, state: {} }), ); @@ -3376,7 +3327,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 +3392,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 +3453,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 +3507,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 +3567,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 +3619,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 +3671,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 +3736,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 +3790,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 +3849,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 +3914,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 +3968,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 +4027,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 +4084,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 +4146,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 +4199,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 +4261,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 +4337,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 +4429,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 +4551,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 +4613,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 +4687,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 +4772,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 +4834,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 +4907,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 +4992,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 +5116,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 +5178,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 +5255,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 +5298,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 +5342,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 +5591,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 +5731,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 +5773,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 +5973,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 +6084,7 @@ describe('SnapController', () => { }); it('grants connection permission to initialConnections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -6192,7 +6143,7 @@ describe('SnapController', () => { }); it('updates existing caveats to satisfy initialConnections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const initialConnections = { 'npm:filsnap': {}, @@ -6241,7 +6192,7 @@ describe('SnapController', () => { }); it('supports preinstalled snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); jest.spyOn(rootMessenger, 'publish'); @@ -6333,11 +6284,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 +6375,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 +6459,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 +6548,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 +6665,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 +6698,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 +6806,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 +6867,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 +6915,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 +6969,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 +7066,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 +7156,7 @@ describe('SnapController', () => { }); it('supports onInstall for preinstalled Snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); rootMessenger.registerActionHandler( @@ -7270,7 +7222,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 +7289,7 @@ describe('SnapController', () => { }); it('supports onUpdate for preinstalled Snaps', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); jest.spyOn(rootMessenger, 'call'); rootMessenger.registerActionHandler( @@ -7410,7 +7362,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 +7871,7 @@ describe('SnapController', () => { manifest: manifest.result, }); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -7999,7 +7951,7 @@ describe('SnapController', () => { manifest: manifest.result, }); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); const options = getSnapControllerOptions({ rootMessenger, @@ -8073,7 +8025,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 +8377,7 @@ describe('SnapController', () => { }), ); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); let revokedConnection = false; @@ -8474,8 +8426,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 +8445,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)).toBeNull(); + 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 +8582,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 +8597,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)).toBeNull(); + expect(controller.getSnap(snapId1)?.manifest.version).toBe(oldVersion); + expect(controller.getSnap(snapId2)?.manifest.version).toBe(oldVersion); expect(listener).toHaveBeenCalledTimes(0); controller.destroy(); @@ -8780,12 +8732,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 +8771,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 +8780,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 +8836,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,8 +8856,8 @@ describe('SnapController', () => { }); it('throws an error if the new version of the snap is blocked', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ manifest: getSnapManifest({ @@ -8927,7 +8879,7 @@ describe('SnapController', () => { ); registry.get.mockResolvedValueOnce({ - [MOCK_SNAP_ID]: { status: SnapsRegistryStatus.Blocked }, + [MOCK_SNAP_ID]: { status: SnapRegistryStatus.Blocked }, }); await expect( @@ -8959,13 +8911,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,11 +8979,11 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.1.0' }, }); - const newSnapTruncated = controller.getTruncated(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(result).toStrictEqual({ + [MOCK_SNAP_ID]: getTruncatedSnap(newSnap), + }); expect(newSnap?.version).toBe('1.1.0'); expect(newSnap?.versionHistory).toStrictEqual([ { @@ -9138,7 +9090,7 @@ describe('SnapController', () => { ); expect(onSnapUpdated).toHaveBeenCalledTimes(1); expect(onSnapUpdated).toHaveBeenCalledWith( - newSnapTruncated, + getTruncatedSnap(newSnap), '1.0.0', MOCK_ORIGIN, false, @@ -9168,7 +9120,7 @@ describe('SnapController', () => { }), ); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -9283,7 +9235,7 @@ describe('SnapController', () => { }), ); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -9410,11 +9362,11 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: { version: '1.1.0' }, }); - const newSnapTruncated = controller.getTruncated(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(result).toStrictEqual({ + [MOCK_SNAP_ID]: getTruncatedSnap(newSnap), + }); expect(newSnap?.version).toBe('1.1.0'); expect(newSnap?.versionHistory).toStrictEqual([ { @@ -9460,7 +9412,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 +9514,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 +9574,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 +9630,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 +9873,7 @@ describe('SnapController', () => { }); it('supports initialConnections', async () => { - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:getPermissions', @@ -10037,7 +9989,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 +10143,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 +10362,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 +10412,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 +10433,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,8 +10457,8 @@ describe('SnapController', () => { describe('updateRegistry', () => { it('updates the registry database', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const snapController = await getSnapController( getSnapControllerOptions({ @@ -10518,14 +10470,14 @@ describe('SnapController', () => { ); await snapController.updateRegistry(); - expect(registry.update).toHaveBeenCalled(); + expect(registry.requestUpdate).toHaveBeenCalled(); snapController.destroy(); }); it('blocks snaps as expected', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnapA = getMockSnapData({ id: 'npm:exampleA' as SnapId, @@ -10556,7 +10508,7 @@ describe('SnapController', () => { // Block snap A, ignore B. registry.get.mockResolvedValueOnce({ [mockSnapA.id]: { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: { explanation, infoUrl }, }, }); @@ -10575,12 +10527,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,8 +10547,8 @@ describe('SnapController', () => { }); it('stops running snaps when they are blocked', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnap = getMockSnapData({ id: 'npm:example' as SnapId, @@ -10616,22 +10568,22 @@ describe('SnapController', () => { // Block the snap registry.get.mockResolvedValueOnce({ - [mockSnap.id]: { status: SnapsRegistryStatus.Blocked }, + [mockSnap.id]: { status: SnapRegistryStatus.Blocked }, }); await snapController.updateRegistry(); await waitForStateChange(options.messenger); // 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 registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnapA = getMockSnapData({ id: 'npm:exampleA' as SnapId, @@ -10660,28 +10612,28 @@ 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. registry.get.mockResolvedValueOnce({ - [mockSnapA.id]: { status: SnapsRegistryStatus.Unverified }, - [mockSnapB.id]: { status: SnapsRegistryStatus.Unverified }, + [mockSnapA.id]: { status: SnapRegistryStatus.Unverified }, + [mockSnapB.id]: { status: SnapRegistryStatus.Unverified }, }); await snapController.updateRegistry(); // 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,8 +10645,8 @@ 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 registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnap = getMockSnapData({ id: 'npm:example' as SnapId, @@ -10723,12 +10675,12 @@ describe('SnapController', () => { // Resolve the blocklist and wait for the call to complete resolveBlockListPromise({ - [mockSnap.id]: { status: SnapsRegistryStatus.Blocked }, + [mockSnap.id]: { status: SnapRegistryStatus.Blocked }, }); await updateBlockList; // 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,8 +10688,8 @@ describe('SnapController', () => { it('logs but does not throw unexpected errors while blocking', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const mockSnap = getMockSnapData({ id: 'npm:example' as SnapId, @@ -10761,13 +10713,13 @@ describe('SnapController', () => { // Block the snap registry.get.mockResolvedValueOnce({ - [mockSnap.id]: { status: SnapsRegistryStatus.Blocked }, + [mockSnap.id]: { status: SnapRegistryStatus.Blocked }, }); 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,8 +10731,8 @@ describe('SnapController', () => { }); it('updates preinstalled Snaps', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); // Simulate previous permissions, some of which will be removed rootMessenger.registerActionHandler( @@ -10834,7 +10786,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,8 +10827,8 @@ describe('SnapController', () => { }); it('does not update preinstalled Snaps when the feature flag is off', async () => { - const rootMessenger = getControllerMessenger(); - const registry = new MockSnapsRegistry(rootMessenger); + const rootMessenger = getRootMessenger(); + const registry = new MockSnapRegistryController(rootMessenger); const snapId = 'npm:@metamask/jsx-example-snap' as SnapId; @@ -10903,7 +10855,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 +10892,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 +10905,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 +10962,7 @@ describe('SnapController', () => { }, ]; - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:revokeAllPermissions', @@ -11050,16 +11002,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 +11056,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 +11074,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 +11155,7 @@ describe('SnapController', () => { .spyOn(console, 'error') .mockImplementation(); - const rootMessenger = getControllerMessenger(); + const rootMessenger = getRootMessenger(); rootMessenger.registerActionHandler( 'PermissionController:hasPermission', @@ -11261,9 +11211,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 +11253,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 +11269,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 +11303,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 +11336,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 +11361,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 +11377,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 +11403,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 +11421,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 +11838,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 +12217,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 +12240,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 +12263,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 +12272,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 +12288,7 @@ describe('SnapController', () => { const snapController = await getSnapController(options); const result = options.messenger.call( - 'SnapController:getPermitted', + 'SnapController:getPermittedSnaps', mockSnap.origin, ); expect(result).toStrictEqual({ @@ -12364,7 +12314,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 +12362,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 +12371,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 +12395,7 @@ describe('SnapController', () => { const removeSnapFromSubjectSpy = jest.spyOn( snapController, - 'removeSnapFromSubject', + 'disconnectOrigin', ); const callActionSpy = jest.spyOn(options.messenger, 'call'); @@ -12487,7 +12437,7 @@ describe('SnapController', () => { const callActionSpy = jest.spyOn(options.messenger, 'call'); options.messenger.call( - 'SnapController:revokeDynamicPermissions', + 'SnapController:revokeDynamicSnapPermissions', MOCK_SNAP_ID, ['endowment:caip25'], ); @@ -12507,7 +12457,7 @@ describe('SnapController', () => { expect(() => options.messenger.call( - 'SnapController:revokeDynamicPermissions', + 'SnapController:revokeDynamicSnapPermissions', MOCK_SNAP_ID, ['snap_notify'], ), @@ -12545,7 +12495,7 @@ describe('SnapController', () => { expect( await options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './src/foo.json', ), @@ -12595,7 +12545,7 @@ describe('SnapController', () => { expect( await options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './src/foo.json', AuxiliaryFileEncoding.Hex, @@ -12621,7 +12571,7 @@ describe('SnapController', () => { expect( await options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './foo.json', ), @@ -12668,7 +12618,7 @@ describe('SnapController', () => { await expect( options.messenger.call( - 'SnapController:getFile', + 'SnapController:getSnapFile', MOCK_SNAP_ID, './src/foo.json', AuxiliaryFileEncoding.Hex, @@ -12683,7 +12633,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 +12664,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12750,7 +12701,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 +12721,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12795,7 +12747,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 +12770,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12832,7 +12785,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 +12817,7 @@ describe('SnapController', () => { getTruncatedSnap(), '0.9.0', MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12900,7 +12854,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 +12874,7 @@ describe('SnapController', () => { 'SnapController:snapInstalled', getTruncatedSnap(), MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -12945,7 +12900,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 +12924,7 @@ describe('SnapController', () => { getTruncatedSnap(), '0.9.0', MOCK_ORIGIN, + false, ); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -13112,7 +13068,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 +13155,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 +13401,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..13fc7750d7 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -161,30 +161,31 @@ import { import type { SnapLocation } from './location'; import { detectSnapLocation } from './location'; import type { - GetMetadata, - GetResult, - ResolveVersion, - SnapsRegistryInfo, - SnapsRegistryRequest, - SnapsRegistryStateChangeEvent, - Update, + SnapRegistryControllerGetAction, + SnapRegistryControllerGetMetadataAction, + SnapRegistryControllerResolveVersionAction, + SnapRegistryControllerRequestUpdateAction, + SnapRegistryInfo, + SnapRegistryRequest, + SnapRegistryControllerStateChangeEvent, } from './registry'; -import { SnapsRegistryStatus } from './registry'; +import { SnapRegistryStatus } from './registry'; import { getRunnableSnaps } from './selectors'; +import type { SnapControllerMethodActions } from './SnapController-method-action-types'; import { Timer } from './Timer'; import { forceStrict, validateMachine } from '../fsm'; import type { - CreateInterface, - GetInterface, + SnapInterfaceControllerCreateInterfaceAction, + SnapInterfaceControllerGetInterfaceAction, SnapInterfaceControllerSetInterfaceDisplayedAction, } from '../interface'; import { log } from '../logging'; import type { - ExecuteSnapAction, ExecutionServiceEvents, - HandleRpcRequestAction, + ExecutionServiceExecuteSnapAction, + ExecutionServiceHandleRpcRequestAction, + ExecutionServiceTerminateSnapAction, SnapErrorJson, - TerminateSnapAction, } from '../services'; import type { EncryptionResult, @@ -205,7 +206,36 @@ import { export const controllerName = 'SnapController'; -// TODO: Figure out how to name these +// 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', + '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'; @@ -263,11 +293,6 @@ export type SnapRuntimeData = { */ lastRequest: null | number; - /** - * The current number of active references where this Snap is being used - */ - activeReferences: number; - /** * The current pending inbound requests, meaning requests that are processed by snaps. */ @@ -318,12 +343,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 +378,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 +400,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 +408,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 +417,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 +425,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 +433,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 +441,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 +454,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 +463,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 +472,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 +480,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,27 +493,26 @@ 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 = +type AllowedActions = | GetEndowments | GetPermissions | GetSubjects @@ -674,31 +525,31 @@ export type AllowedActions = | RevokePermissionForAllSubjects | GrantPermissions | ApprovalControllerAddRequestAction - | HandleRpcRequestAction - | ExecuteSnapAction - | TerminateSnapAction + | ExecutionServiceHandleRpcRequestAction + | ExecutionServiceExecuteSnapAction + | ExecutionServiceTerminateSnapAction | UpdateCaveat | ApprovalControllerUpdateRequestStateAction - | GetResult - | GetMetadata - | Update - | ResolveVersion - | CreateInterface - | GetInterface + | SnapRegistryControllerGetAction + | SnapRegistryControllerGetMetadataAction + | SnapRegistryControllerResolveVersionAction + | SnapRegistryControllerRequestUpdateAction + | SnapInterfaceControllerCreateInterfaceAction + | SnapInterfaceControllerGetInterfaceAction | SnapInterfaceControllerSetInterfaceDisplayedAction | StorageServiceSetItemAction | StorageServiceGetItemAction | StorageServiceRemoveItemAction | StorageServiceClearAction; -export type AllowedEvents = +type AllowedEvents = | ExecutionServiceEvents - | SnapInstalled - | SnapUpdated - | KeyringControllerLock - | SnapsRegistryStateChangeEvent; + | SnapControllerSnapInstalledEvent + | SnapControllerSnapUpdatedEvent + | KeyringControllerLockEvent + | SnapRegistryControllerStateChangeEvent; -type SnapControllerMessenger = Messenger< +export type SnapControllerMessenger = Messenger< typeof controllerName, SnapControllerActions | AllowedActions, SnapControllerEvents | AllowedEvents @@ -729,7 +580,7 @@ type DynamicFeatureFlags = { disableSnaps?: boolean; }; -type SnapControllerArgs = { +export type SnapControllerArgs = { /** * A list of permissions that are allowed to be dynamic, meaning they can be revoked from the snap whenever. */ @@ -1124,7 +975,7 @@ export class SnapController extends BaseController< ); this.messenger.subscribe( - 'SnapsRegistry:stateChange', + 'SnapRegistryController:stateChange', () => { this.#handleRegistryUpdate().catch((error) => { logError( @@ -1147,7 +998,7 @@ export class SnapController extends BaseController< this.#trackSnapExport = throttleTracking( (snapId: SnapId, handler: string, success: boolean, origin: string) => { const snapMetadata = this.messenger.call( - 'SnapsRegistry:getMetadata', + 'SnapRegistryController:getMetadata', snapId, ); this.#trackEvent({ @@ -1178,7 +1029,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 +1101,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 +1116,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 +1132,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 +1166,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,8 +1236,8 @@ export class SnapController extends BaseController< hidden, hideSnapBranding, } of preinstalledSnaps) { - const existingSnap = this.get(snapId); - const isAlreadyInstalled = existingSnap !== undefined; + const existingSnap = this.getSnap(snapId); + const isAlreadyInstalled = existingSnap !== null; const isUpdate = isAlreadyInstalled && gtVersion(manifest.version, existingSnap.version); const isPreinstalled = existingSnap?.preinstalled === true; @@ -1505,7 +1348,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 +1356,7 @@ export class SnapController extends BaseController< } else if (!isMissingSource) { this.messenger.publish( 'SnapController:snapInstalled', - this.getTruncatedExpect(snapId), + this.#getTruncatedSnapExpect(snapId), METAMASK_ORIGIN, true, ); @@ -1609,7 +1452,7 @@ export class SnapController extends BaseController< */ async updateRegistry(): Promise { await this.#ensureCanUsePlatform(); - await this.messenger.call('SnapsRegistry:update'); + await this.messenger.call('SnapRegistryController:requestUpdate'); } /** @@ -1621,8 +1464,8 @@ export class SnapController extends BaseController< */ async #handleRegistryUpdate() { const blockedSnaps = await this.messenger.call( - 'SnapsRegistry:get', - Object.values(this.state.snaps).reduce( + 'SnapRegistryController:get', + Object.values(this.state.snaps).reduce( (blockListArg, snap) => { blockListArg[snap.id] = { version: snap.version, @@ -1636,7 +1479,7 @@ export class SnapController extends BaseController< await Promise.all( Object.entries(blockedSnaps).map(async ([snapId, { status, reason }]) => { - if (status === SnapsRegistryStatus.Blocked) { + if (status === SnapRegistryStatus.Blocked) { return this.#blockSnap(snapId as SnapId, reason); } @@ -1684,7 +1527,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 +1536,7 @@ export class SnapController extends BaseController< snapId: SnapId, blockedSnapInfo?: BlockReason, ): Promise { - if (!this.has(snapId)) { + if (!this.hasSnap(snapId)) { return; } @@ -1720,13 +1563,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; } @@ -1743,17 +1586,17 @@ export class SnapController extends BaseController< { platformVersion, ...snapInfo - }: SnapsRegistryInfo & { + }: SnapRegistryInfo & { permissions: SnapPermissions; platformVersion: string | undefined; }, ) { - const results = await this.messenger.call('SnapsRegistry:get', { + const results = await this.messenger.call('SnapRegistryController:get', { [snapId]: snapInfo, }); const result = results[snapId]; - if (result.status === SnapsRegistryStatus.Blocked) { + if (result.status === SnapRegistryStatus.Blocked) { throw new Error( `Cannot install version "${ snapInfo.version @@ -1770,11 +1613,11 @@ export class SnapController extends BaseController< if ( this.#featureFlags.requireAllowlist && isAllowlistingRequired && - result.status !== SnapsRegistryStatus.Verified + result.status !== SnapRegistryStatus.Verified ) { throw new Error( `Cannot install version "${snapInfo.version}" of snap "${snapId}": ${ - result.status === SnapsRegistryStatus.Unavailable + result.status === SnapRegistryStatus.Unavailable ? 'The registry is temporarily unavailable.' : 'The snap is not on the allowlist.' }`, @@ -1827,7 +1670,6 @@ export class SnapController extends BaseController< entries .filter( ([_snapId, runtime]) => - runtime.activeReferences === 0 && runtime.pendingInboundRequests.length === 0 && runtime.lastRequest && this.#maxIdleTime && @@ -1918,7 +1760,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 +1772,7 @@ export class SnapController extends BaseController< this.messenger.publish( 'SnapController:snapEnabled', - this.getTruncatedExpect(snapId), + this.#getTruncatedSnapExpect(snapId), ); } @@ -1941,7 +1783,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 +1791,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 +1832,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 +1841,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 +1861,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 +1891,7 @@ export class SnapController extends BaseController< this.messenger.publish( 'SnapController:snapTerminated', - this.getTruncatedExpect(snapId), + this.#getTruncatedSnapExpect(snapId), ); } @@ -2060,8 +1902,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 +1912,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,8 +1924,8 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to get. * @returns The entire snap object from the controller state. */ - get(snapId: string): Snap | undefined { - return this.state.snaps[snapId as SnapId]; + getSnap(snapId: string): Snap | null { + return this.state.snaps[snapId as SnapId] ?? null; } /** @@ -2091,14 +1933,14 @@ 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); - assert(snap !== undefined, `Snap "${snapId}" not found.`); + getSnapExpect(snapId: SnapId): Snap { + const snap = this.getSnap(snapId); + assert(snap !== null, `Snap "${snapId}" not found.`); return snap; } @@ -2109,9 +1951,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. */ - // 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 +1964,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 +2271,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 +2300,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 +2363,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 +2409,7 @@ export class SnapController extends BaseController< ); for (const origin of Object.keys(revokedInitialConnections)) { - this.removeSnapFromSubject(origin, snapId); + this.disconnectOrigin(origin, snapId); } } @@ -2628,12 +2469,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 +2547,7 @@ export class SnapController extends BaseController< 'PermissionController:getSubjectNames', ); for (const subject of subjects) { - this.removeSnapFromSubject(subject, snapId); + this.disconnectOrigin(subject, snapId); } } @@ -2720,30 +2562,6 @@ export class SnapController extends BaseController< } } - /** - * Handles incrementing the activeReferences counter. - * - * @param snapId - The snap id of the snap that was referenced. - */ - incrementActiveReferences(snapId: SnapId) { - const runtime = this.#getRuntimeExpect(snapId); - runtime.activeReferences += 1; - } - - /** - * Handles decrement the activeReferences counter. - * - * @param snapId - The snap id of the snap that was referenced.. - */ - decrementActiveReferences(snapId: SnapId) { - const runtime = this.#getRuntimeExpect(snapId); - assert( - runtime.activeReferences > 0, - 'SnapController reference management is in an invalid state.', - ); - runtime.activeReferences -= 1; - } - /** * Gets all snaps in their truncated format. * @@ -2777,8 +2595,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 +2658,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 +2686,7 @@ export class SnapController extends BaseController< pendingInstalls.forEach((snapId) => this.messenger.publish( `SnapController:snapInstalled`, - this.getTruncatedExpect(snapId), + this.#getTruncatedSnapExpect(snapId), origin, false, ), @@ -2877,7 +2695,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 +2704,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 +2736,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 +2768,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 +2798,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 +2911,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 +3010,7 @@ export class SnapController extends BaseController< approvedNewPermissions = newPermissions; } - if (this.isRunning(snapId)) { + if (this.isSnapRunning(snapId)) { await this.stopSnap(snapId, SnapStatusEvents.Stop); } @@ -3245,7 +3065,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, { @@ -3286,7 +3106,7 @@ export class SnapController extends BaseController< versionRange: SemVerRange, ): Promise { return await this.messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', snapId, versionRange, ); @@ -3357,7 +3177,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 +3553,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 +3656,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 +3689,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 +3745,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 +4120,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 +4193,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 +4217,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 +4237,7 @@ export class SnapController extends BaseController< previousInitialConnections ?? {}, ); - const truncatedSnap = this.getTruncatedExpect(snapId); + const truncatedSnap = this.#getTruncatedSnapExpect(snapId); this.messenger.publish( 'SnapController:snapRolledback', @@ -4454,7 +4275,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 }, @@ -4472,7 +4293,6 @@ export class SnapController extends BaseController< installPromise: null, encryptionKey: null, encryptionSalt: null, - activeReferences: 0, pendingInboundRequests: [], pendingOutboundRequests: 0, interpreter, @@ -4678,7 +4498,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..d8fe12b1dd 100644 --- a/packages/snaps-controllers/src/snaps/index.ts +++ b/packages/snaps-controllers/src/snaps/index.ts @@ -1,5 +1,55 @@ export * from './constants'; export * from './location'; -export * from './SnapController'; +export type { + SnapControllerActions, + SnapControllerArgs, + SnapControllerEvents, + SnapControllerGetStateAction, + SnapControllerMessenger, + 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/snaps/registry/SnapRegistryController-method-action-types.ts b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController-method-action-types.ts new file mode 100644 index 0000000000..5fcac09d6e --- /dev/null +++ b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController-method-action-types.ts @@ -0,0 +1,55 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { SnapRegistryController } from './SnapRegistryController'; + +/** + * Triggers an update of the registry database. + * + * If an existing update is in progress this function will await that update. + */ +export type SnapRegistryControllerRequestUpdateAction = { + type: `SnapRegistryController:requestUpdate`; + handler: SnapRegistryController['requestUpdate']; +}; + +export type SnapRegistryControllerGetAction = { + type: `SnapRegistryController:get`; + handler: SnapRegistryController['get']; +}; + +/** + * Find an allowlisted version within a specified version range. Otherwise return the version range itself. + * + * @param snapId - The ID of the snap we are trying to resolve a version for. + * @param versionRange - The version range. + * @param refetch - An optional flag used to determine if we are refetching the registry. + * @returns An allowlisted version within the specified version range if available otherwise returns the input version range. + */ +export type SnapRegistryControllerResolveVersionAction = { + type: `SnapRegistryController:resolveVersion`; + handler: SnapRegistryController['resolveVersion']; +}; + +/** + * Get metadata for the given snap ID, if available, without updating registry. + * + * @param snapId - The ID of the snap to get metadata for. + * @returns The metadata for the given snap ID, or `null` if the snap is not + * verified. + */ +export type SnapRegistryControllerGetMetadataAction = { + type: `SnapRegistryController:getMetadata`; + handler: SnapRegistryController['getMetadata']; +}; + +/** + * Union of all SnapRegistryController action types. + */ +export type SnapRegistryControllerMethodActions = + | SnapRegistryControllerRequestUpdateAction + | SnapRegistryControllerGetAction + | SnapRegistryControllerResolveVersionAction + | SnapRegistryControllerGetMetadataAction; diff --git a/packages/snaps-controllers/src/snaps/registry/json.test.ts b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.test.ts similarity index 83% rename from packages/snaps-controllers/src/snaps/registry/json.test.ts rename to packages/snaps-controllers/src/snaps/registry/SnapRegistryController.test.ts index 3aaada161b..fb77965395 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.test.ts +++ b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.test.ts @@ -7,20 +7,20 @@ import { import type { SemVerRange, SemVerVersion } from '@metamask/utils'; import fetchMock from 'jest-fetch-mock'; -import type { JsonSnapsRegistryArgs } from './json'; -import { JsonSnapsRegistry } from './json'; -import { SnapsRegistryStatus } from './registry'; -import { getRestrictedSnapsRegistryControllerMessenger } from '../../test-utils'; +import type { SnapRegistryControllerArgs } from './SnapRegistryController'; +import { SnapRegistryController } from './SnapRegistryController'; +import { SnapRegistryStatus } from './types'; +import { getRestrictedSnapRegistryControllerMessenger } from '../../test-utils'; // Public key for the private key: // `0x541c6759fd86c69eceb8d792d7174623db139d81a5b560aa026afcb2dd1bb21c`. const MOCK_PUBLIC_KEY = '0x03a885324b8520fba54a173999629952cfa1f97930c20902ec389f9c32c6ffbc40'; -const getRegistry = (args?: Partial) => { - const messenger = getRestrictedSnapsRegistryControllerMessenger(); +const getRegistry = (args?: Partial) => { + const messenger = getRestrictedSnapRegistryControllerMessenger(); return { - registry: new JsonSnapsRegistry({ + registry: new SnapRegistryController({ messenger, publicKey: MOCK_PUBLIC_KEY, clientConfig: { @@ -125,7 +125,7 @@ const MOCK_COMPATIBILITY_SIGNATURE_FILE = { format: 'DER', }; -describe('JsonSnapsRegistry', () => { +describe('SnapRegistryController', () => { fetchMock.enableMocks(); afterEach(() => { @@ -138,7 +138,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -147,7 +147,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -159,7 +159,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_EMPTY_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -168,7 +168,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -179,7 +179,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.1' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -188,7 +188,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -199,7 +199,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: 'bar', @@ -208,7 +208,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -219,7 +219,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: 'foo', @@ -228,7 +228,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: { explanation: 'malicious' }, }, }); @@ -240,7 +240,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { 'npm:@consensys/starknet-snap': { version: '0.1.10' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -249,7 +249,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ 'npm:@consensys/starknet-snap': { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: { explanation: 'vuln' }, }, }); @@ -267,7 +267,7 @@ describe('JsonSnapsRegistry', () => { database: { verifiedSnaps: {}, blockedSnaps: [] }, }, }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -276,7 +276,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -287,7 +287,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -296,7 +296,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -307,7 +307,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.1.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -316,7 +316,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unverified, + status: SnapRegistryStatus.Unverified, }, }); }); @@ -332,7 +332,7 @@ describe('JsonSnapsRegistry', () => { }, }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -341,7 +341,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Verified, + status: SnapRegistryStatus.Verified, }, }); }); @@ -357,7 +357,7 @@ describe('JsonSnapsRegistry', () => { }, }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.1' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -366,7 +366,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -376,7 +376,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -385,7 +385,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -399,7 +399,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -408,7 +408,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -423,7 +423,7 @@ describe('JsonSnapsRegistry', () => { '0x034ca27b046507d1a9997bddc991b56d96b93d4adac3a96dfe01ce450bfb661455', }); - const result = await messenger.call('SnapsRegistry:get', { + const result = await messenger.call('SnapRegistryController:get', { [MOCK_SNAP_ID]: { version: '1.0.0' as SemVerVersion, checksum: DEFAULT_SNAP_SHASUM, @@ -432,7 +432,7 @@ describe('JsonSnapsRegistry', () => { expect(result).toStrictEqual({ [MOCK_SNAP_ID]: { - status: SnapsRegistryStatus.Unavailable, + status: SnapRegistryStatus.Unavailable, }, }); }); @@ -445,7 +445,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -460,7 +460,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -477,7 +477,7 @@ describe('JsonSnapsRegistry', () => { clientConfig: { type: 'extension', version: '15.0.0' as SemVerVersion }, }); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -496,7 +496,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); expect( await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, range, ), @@ -512,7 +512,7 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); expect( await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, range, ), @@ -532,7 +532,7 @@ describe('JsonSnapsRegistry', () => { }, }); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -568,7 +568,7 @@ describe('JsonSnapsRegistry', () => { }, }); const result = await messenger.call( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', MOCK_SNAP_ID, '^1.0.0' as SemVerRange, ); @@ -584,8 +584,11 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); - const result = messenger.call('SnapsRegistry:getMetadata', MOCK_SNAP_ID); + await messenger.call('SnapRegistryController:requestUpdate'); + const result = messenger.call( + 'SnapRegistryController:getMetadata', + MOCK_SNAP_ID, + ); expect(result).toStrictEqual({ name: 'Mock Snap', @@ -598,8 +601,11 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); - const result = messenger.call('SnapsRegistry:getMetadata', 'foo'); + await messenger.call('SnapRegistryController:requestUpdate'); + const result = messenger.call( + 'SnapRegistryController:getMetadata', + 'foo', + ); expect(result).toBeNull(); }); @@ -612,7 +618,7 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); + await messenger.call('SnapRegistryController:requestUpdate'); expect(fetchMock).toHaveBeenCalledTimes(2); }); @@ -632,7 +638,7 @@ describe('JsonSnapsRegistry', () => { databaseUnavailable: false, }, }); - await messenger.call('SnapsRegistry:update'); + await messenger.call('SnapRegistryController:requestUpdate'); expect(fetchMock).toHaveBeenCalledTimes(2); expect(spy).not.toHaveBeenCalled(); @@ -644,8 +650,8 @@ describe('JsonSnapsRegistry', () => { .mockResponseOnce(JSON.stringify(MOCK_SIGNATURE_FILE)); const { messenger } = getRegistry(); - await messenger.call('SnapsRegistry:update'); - await messenger.call('SnapsRegistry:update'); + await messenger.call('SnapRegistryController:requestUpdate'); + await messenger.call('SnapRegistryController:requestUpdate'); expect(fetchMock).toHaveBeenCalledTimes(2); }); @@ -657,8 +663,8 @@ describe('JsonSnapsRegistry', () => { const { messenger } = getRegistry(); await Promise.all([ - messenger.call('SnapsRegistry:update'), - messenger.call('SnapsRegistry:update'), + messenger.call('SnapRegistryController:requestUpdate'), + messenger.call('SnapRegistryController:requestUpdate'), ]); expect(fetchMock).toHaveBeenCalledTimes(2); @@ -670,8 +676,18 @@ describe('JsonSnapsRegistry', () => { const { registry } = getRegistry(); expect( - deriveStateFromMetadata(registry.state, registry.metadata, 'anonymous'), - ).toMatchInlineSnapshot(`{}`); + deriveStateFromMetadata( + registry.state, + registry.metadata, + 'includeInDebugSnapshot', + ), + ).toMatchInlineSnapshot(` + { + "databaseUnavailable": false, + "lastUpdated": null, + "signature": null, + } + `); }); it('includes expected state in state logs', () => { diff --git a/packages/snaps-controllers/src/snaps/registry/json.ts b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.ts similarity index 78% rename from packages/snaps-controllers/src/snaps/registry/json.ts rename to packages/snaps-controllers/src/snaps/registry/SnapRegistryController.ts index 3c5310f5a5..4944f3789b 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.ts +++ b/packages/snaps-controllers/src/snaps/registry/SnapRegistryController.ts @@ -20,14 +20,14 @@ import { satisfiesVersionRange, } from '@metamask/utils'; +import type { SnapRegistryControllerMethodActions } from './SnapRegistryController-method-action-types'; import type { - SnapsRegistry, - SnapsRegistryInfo, - SnapsRegistryMetadata, - SnapsRegistryRequest, - SnapsRegistryResult, -} from './registry'; -import { SnapsRegistryStatus } from './registry'; + SnapRegistryInfo, + SnapRegistryMetadata, + SnapRegistryRequest, + SnapRegistryResult, +} from './types'; +import { SnapRegistryStatus } from './types'; const SNAP_REGISTRY_URL = 'https://acl.execution.metamask.io/latest/registry.json'; @@ -48,9 +48,9 @@ export type ClientConfig = { version: SemVerVersion; }; -export type JsonSnapsRegistryArgs = { - messenger: SnapsRegistryMessenger; - state?: SnapsRegistryState; +export type SnapRegistryControllerArgs = { + messenger: SnapRegistryControllerMessenger; + state?: SnapRegistryControllerState; fetchFunction?: typeof fetch; url?: JsonSnapsRegistryUrl; recentFetchThreshold?: number; @@ -59,59 +59,44 @@ export type JsonSnapsRegistryArgs = { clientConfig: ClientConfig; }; -export type GetResult = { - type: `${typeof controllerName}:get`; - handler: SnapsRegistry['get']; -}; - -export type ResolveVersion = { - type: `${typeof controllerName}:resolveVersion`; - handler: SnapsRegistry['resolveVersion']; -}; - -export type GetMetadata = { - type: `${typeof controllerName}:getMetadata`; - handler: SnapsRegistry['getMetadata']; -}; - -export type Update = { - type: `${typeof controllerName}:update`; - handler: SnapsRegistry['update']; -}; - -export type SnapsRegistryGetStateAction = ControllerGetStateAction< +export type SnapRegistryControllerGetStateAction = ControllerGetStateAction< typeof controllerName, - SnapsRegistryState + SnapRegistryControllerState >; -export type SnapsRegistryActions = - | SnapsRegistryGetStateAction - | GetResult - | GetMetadata - | Update - | ResolveVersion; +export type SnapRegistryControllerActions = + | SnapRegistryControllerGetStateAction + | SnapRegistryControllerMethodActions; -export type SnapsRegistryStateChangeEvent = ControllerStateChangeEvent< +export type SnapRegistryControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, - SnapsRegistryState + SnapRegistryControllerState >; -export type SnapsRegistryEvents = SnapsRegistryStateChangeEvent; +export type SnapRegistryControllerEvents = + SnapRegistryControllerStateChangeEvent; -export type SnapsRegistryMessenger = Messenger< - 'SnapsRegistry', - SnapsRegistryActions, - SnapsRegistryEvents +export type SnapRegistryControllerMessenger = Messenger< + typeof controllerName, + SnapRegistryControllerActions, + SnapRegistryControllerEvents >; -export type SnapsRegistryState = { +export type SnapRegistryControllerState = { database: SnapsRegistryDatabase | null; signature: string | null; lastUpdated: number | null; databaseUnavailable: boolean; }; -const controllerName = 'SnapsRegistry'; +const controllerName = 'SnapRegistryController'; + +const MESSENGER_EXPOSED_METHODS = [ + 'get', + 'getMetadata', + 'resolveVersion', + 'requestUpdate', +] as const; const defaultState = { database: null, @@ -120,10 +105,10 @@ const defaultState = { databaseUnavailable: false, }; -export class JsonSnapsRegistry extends BaseController< +export class SnapRegistryController extends BaseController< typeof controllerName, - SnapsRegistryState, - SnapsRegistryMessenger + SnapRegistryControllerState, + SnapRegistryControllerMessenger > { readonly #url: JsonSnapsRegistryUrl; @@ -151,7 +136,7 @@ export class JsonSnapsRegistry extends BaseController< fetchFunction = globalThis.fetch.bind(undefined), recentFetchThreshold = inMilliseconds(5, Duration.Minute), refetchOnAllowlistMiss = true, - }: JsonSnapsRegistryArgs) { + }: SnapRegistryControllerArgs) { super({ messenger, metadata: { @@ -194,22 +179,9 @@ export class JsonSnapsRegistry extends BaseController< this.#refetchOnAllowlistMiss = refetchOnAllowlistMiss; this.#currentUpdate = null; - this.messenger.registerActionHandler('SnapsRegistry:get', async (...args) => - this.#get(...args), - ); - - this.messenger.registerActionHandler( - 'SnapsRegistry:getMetadata', - (...args) => this.#getMetadata(...args), - ); - - this.messenger.registerActionHandler( - 'SnapsRegistry:resolveVersion', - async (...args) => this.#resolveVersion(...args), - ); - - this.messenger.registerActionHandler('SnapsRegistry:update', async () => - this.#triggerUpdate(), + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } @@ -225,7 +197,7 @@ export class JsonSnapsRegistry extends BaseController< * * If an existing update is in progress this function will await that update. */ - async #triggerUpdate() { + async requestUpdate() { // If an update is ongoing, wait for that. if (this.#currentUpdate) { await this.#currentUpdate; @@ -285,7 +257,7 @@ export class JsonSnapsRegistry extends BaseController< async #getDatabase(): Promise { if (this.state.database === null) { - await this.#triggerUpdate(); + await this.requestUpdate(); } return this.state.database; @@ -293,9 +265,9 @@ export class JsonSnapsRegistry extends BaseController< async #getSingle( snapId: string, - snapInfo: SnapsRegistryInfo, + snapInfo: SnapRegistryInfo, refetch = false, - ): Promise { + ): Promise { const database = await this.#getDatabase(); const blockedEntry = database?.blockedSnaps.find((blocked) => { @@ -311,7 +283,7 @@ export class JsonSnapsRegistry extends BaseController< if (blockedEntry) { return { - status: SnapsRegistryStatus.Blocked, + status: SnapRegistryStatus.Blocked, reason: blockedEntry.reason, }; } @@ -323,25 +295,25 @@ export class JsonSnapsRegistry extends BaseController< !clientRange || satisfiesVersionRange(this.#clientConfig.version, clientRange); if (version && version.checksum === snapInfo.checksum && isCompatible) { - return { status: SnapsRegistryStatus.Verified }; + return { status: SnapRegistryStatus.Verified }; } // For now, if we have an allowlist miss, we can refetch once and try again. if (this.#refetchOnAllowlistMiss && !refetch) { - await this.#triggerUpdate(); + await this.requestUpdate(); return this.#getSingle(snapId, snapInfo, true); } return { status: this.state.databaseUnavailable - ? SnapsRegistryStatus.Unavailable - : SnapsRegistryStatus.Unverified, + ? SnapRegistryStatus.Unavailable + : SnapRegistryStatus.Unverified, }; } - async #get( - snaps: SnapsRegistryRequest, - ): Promise> { + async get( + snaps: SnapRegistryRequest, + ): Promise> { return Object.entries(snaps).reduce< - Promise> + Promise> >(async (previousPromise, [snapId, snapInfo]) => { const result = await this.#getSingle(snapId, snapInfo); const acc = await previousPromise; @@ -358,7 +330,7 @@ export class JsonSnapsRegistry extends BaseController< * @param refetch - An optional flag used to determine if we are refetching the registry. * @returns An allowlisted version within the specified version range if available otherwise returns the input version range. */ - async #resolveVersion( + async resolveVersion( snapId: string, versionRange: SemVerRange, refetch = false, @@ -367,8 +339,8 @@ export class JsonSnapsRegistry extends BaseController< const versions = database?.verifiedSnaps[snapId]?.versions ?? null; if (!versions && this.#refetchOnAllowlistMiss && !refetch) { - await this.#triggerUpdate(); - return this.#resolveVersion(snapId, versionRange, true); + await this.requestUpdate(); + return this.resolveVersion(snapId, versionRange, true); } // If we cannot narrow down the version range we return the unaltered version range. @@ -394,8 +366,8 @@ export class JsonSnapsRegistry extends BaseController< const targetVersion = getTargetVersion(compatibleVersions, versionRange); if (!targetVersion && this.#refetchOnAllowlistMiss && !refetch) { - await this.#triggerUpdate(); - return this.#resolveVersion(snapId, versionRange, true); + await this.requestUpdate(); + return this.resolveVersion(snapId, versionRange, true); } // If we cannot narrow down the version range we return the unaltered version range. @@ -415,7 +387,7 @@ export class JsonSnapsRegistry extends BaseController< * @returns The metadata for the given snap ID, or `null` if the snap is not * verified. */ - #getMetadata(snapId: string): SnapsRegistryMetadata | null { + getMetadata(snapId: string): SnapRegistryMetadata | null { return this.state?.database?.verifiedSnaps[snapId]?.metadata ?? null; } diff --git a/packages/snaps-controllers/src/snaps/registry/index.ts b/packages/snaps-controllers/src/snaps/registry/index.ts index e1865b60d5..a4c0875fa5 100644 --- a/packages/snaps-controllers/src/snaps/registry/index.ts +++ b/packages/snaps-controllers/src/snaps/registry/index.ts @@ -1,2 +1,23 @@ -export * from './registry'; -export * from './json'; +export type { + SnapRegistryControllerActions, + SnapRegistryControllerEvents, + SnapRegistryControllerArgs, + SnapRegistryControllerGetStateAction, + SnapRegistryControllerMessenger, + SnapRegistryControllerState, + SnapRegistryControllerStateChangeEvent, +} from './SnapRegistryController'; +export { SnapRegistryController } from './SnapRegistryController'; +export type { + SnapRegistryControllerGetAction, + SnapRegistryControllerGetMetadataAction, + SnapRegistryControllerRequestUpdateAction, + SnapRegistryControllerResolveVersionAction, +} from './SnapRegistryController-method-action-types'; +export type { + SnapRegistryInfo, + SnapRegistryMetadata, + SnapRegistryRequest, + SnapRegistryResult, +} from './types'; +export { SnapRegistryStatus } from './types'; diff --git a/packages/snaps-controllers/src/snaps/registry/registry.ts b/packages/snaps-controllers/src/snaps/registry/registry.ts deleted file mode 100644 index 07c5009677..0000000000 --- a/packages/snaps-controllers/src/snaps/registry/registry.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { - BlockReason, - SnapsRegistryDatabase, -} from '@metamask/snaps-registry'; -import type { SnapId } from '@metamask/snaps-sdk'; -import type { SemVerRange, SemVerVersion } from '@metamask/utils'; - -export type SnapsRegistryInfo = { version: SemVerVersion; checksum: string }; -export type SnapsRegistryRequest = Record; -export type SnapsRegistryMetadata = - SnapsRegistryDatabase['verifiedSnaps'][SnapId]['metadata']; - -export enum SnapsRegistryStatus { - Unverified = 0, - Blocked = 1, - Verified = 2, - Unavailable = 3, -} - -export type SnapsRegistryResult = { - status: SnapsRegistryStatus; - reason?: BlockReason; -}; - -export type SnapsRegistry = { - get( - snaps: SnapsRegistryRequest, - ): Promise>; - - update(): Promise; - - /** - * Find an allowlisted version within a specified version range. - * - * @param snapId - The ID of the snap we are trying to resolve a version for. - * @param versionRange - The version range. - * @returns An allowlisted version within the specified version range. - * @throws If an allowlisted version does not exist within the version range. - */ - resolveVersion( - snapId: SnapId, - versionRange: SemVerRange, - ): Promise; - - /** - * Get metadata for the given snap ID. - * - * @param snapId - The ID of the snap to get metadata for. - * @returns The metadata for the given snap ID, or `null` if the snap is not - * verified. - */ - getMetadata(snapId: SnapId): SnapsRegistryMetadata | null; -}; diff --git a/packages/snaps-controllers/src/snaps/registry/types.ts b/packages/snaps-controllers/src/snaps/registry/types.ts new file mode 100644 index 0000000000..bbdcb82163 --- /dev/null +++ b/packages/snaps-controllers/src/snaps/registry/types.ts @@ -0,0 +1,23 @@ +import type { + BlockReason, + SnapsRegistryDatabase, +} from '@metamask/snaps-registry'; +import type { SnapId } from '@metamask/snaps-sdk'; +import type { SemVerVersion } from '@metamask/utils'; + +export type SnapRegistryInfo = { version: SemVerVersion; checksum: string }; +export type SnapRegistryRequest = Record; +export type SnapRegistryMetadata = + SnapsRegistryDatabase['verifiedSnaps'][SnapId]['metadata']; + +export enum SnapRegistryStatus { + Unverified = 0, + Blocked = 1, + Verified = 2, + Unavailable = 3, +} + +export type SnapRegistryResult = { + status: SnapRegistryStatus; + reason?: BlockReason; +}; diff --git a/packages/snaps-controllers/src/test-utils/controller.tsx b/packages/snaps-controllers/src/test-utils/controller.tsx index d8423cc939..0fd143a10f 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, @@ -48,44 +53,24 @@ import type { Json } from '@metamask/utils'; import { MOCK_CRONJOB_PERMISSION } from './cronjob'; import { getNodeEES, getNodeEESMessenger } from './execution-environment'; -import { MockSnapsRegistry } from './registry'; -import type { - CronjobControllerActions, - CronjobControllerEvents, -} from '../cronjob'; +import { MockSnapRegistryController } from './registry'; +import type { CronjobControllerMessenger } from '../cronjob/CronjobController'; +import type { SnapInsightsControllerMessenger } from '../insights'; import type { - SnapInsightsControllerAllowedActions, - SnapInsightsControllerAllowedEvents, -} from '../insights'; -import type { - SnapInterfaceControllerActions, - SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerEvents, + SnapInterfaceControllerMessenger, StoredInterface, } from '../interface/SnapInterfaceController'; +import type { MultichainRoutingServiceMessenger } from '../multichain/MultichainRoutingService'; +import type { ExecutionService, ExecutionServiceMessenger } from '../services'; +import type { SnapRegistryControllerMessenger } from '../snaps'; +import { SnapController } from '../snaps'; import type { - MultichainRouterActions, - MultichainRouterAllowedActions, - MultichainRouterEvents, -} from '../multichain'; -import type { AbstractExecutionService } from '../services'; -import type { - AllowedActions, - AllowedEvents, PersistedSnapControllerState, - SnapControllerActions, - SnapControllerEvents, + SnapControllerMessenger, SnapControllerStateChangeEvent, - SnapsRegistryActions, - SnapsRegistryEvents, -} from '../snaps'; -import { SnapController } from '../snaps'; +} from '../snaps/SnapController'; import type { KeyDerivationOptions } from '../types'; -import type { - WebSocketServiceActions, - WebSocketServiceAllowedActions, - WebSocketServiceEvents, -} from '../websocket'; +import type { WebSocketServiceMessenger } from '../websocket'; const asyncNoOp = async () => Promise.resolve(); @@ -329,11 +314,26 @@ 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 + | SnapRegistryControllerMessenger + | ExecutionServiceMessenger + >, + MessengerEvents< + | SnapControllerMessenger + | SnapRegistryControllerMessenger + | ExecutionServiceMessenger + > +>; + +export const getRootMessenger = () => { + const messenger: RootMessenger = new MockControllerMessenger(); messenger.registerActionHandler( 'PermissionController:hasPermission', @@ -419,7 +419,10 @@ export const getControllerMessenger = () => { () => undefined, ); - messenger.registerActionHandler('ExecutionService:executeSnap', asyncNoOp); + messenger.registerActionHandler( + 'ExecutionService:executeSnap', + async () => 'OK', + ); messenger.registerActionHandler( 'ExecutionService:handleRpcRequest', asyncNoOp, @@ -427,7 +430,7 @@ export const getControllerMessenger = () => { messenger.registerActionHandler('ExecutionService:terminateSnap', asyncNoOp); // eslint-disable-next-line no-new - new MockSnapsRegistry(messenger); + new MockSnapRegistryController(messenger); messenger.registerActionHandler( 'SnapInterfaceController:createInterface', @@ -463,16 +466,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, }); @@ -496,10 +492,10 @@ export const getSnapControllerMessenger = ( 'PermissionController:getSubjectNames', 'SubjectMetadataController:getSubjectMetadata', 'SubjectMetadataController:addSubjectMetadata', - 'SnapsRegistry:get', - 'SnapsRegistry:getMetadata', - 'SnapsRegistry:update', - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:get', + 'SnapRegistryController:getMetadata', + 'SnapRegistryController:requestUpdate', + 'SnapRegistryController:resolveVersion', 'SnapInterfaceController:createInterface', 'SnapInterfaceController:setInterfaceDisplayed', 'SnapInterfaceController:getInterface', @@ -513,7 +509,7 @@ export const getSnapControllerMessenger = ( 'ExecutionService:outboundRequest', 'ExecutionService:outboundResponse', 'KeyringController:lock', - 'SnapsRegistry:stateChange', + 'SnapRegistryController:stateChange', ], messenger: snapControllerMessenger, }); @@ -582,10 +578,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 +604,7 @@ export const getSnapControllerOptions = ({ ensureOnboardingComplete: jest.fn().mockResolvedValue(undefined), ...opts, } as SnapControllerConstructorParamsWithStorage & { - rootMessenger: ReturnType; + rootMessenger: ReturnType; }; options.state = { @@ -643,7 +639,7 @@ export const extractSourceCodeFromSnapsState = ( }; export const getStorageService = ( - messenger: ReturnType, + messenger: ReturnType, initialData?: InitialStorageData, ) => { const storageServiceMessenger = new Messenger< @@ -687,16 +683,16 @@ export const getSnapController = async ( if (init) { await controller.init(); } + return controller; }; export const getSnapControllerWithEES = async ( options = getSnapControllerOptions(), - service?: AbstractExecutionService, + service?: ExecutionService, init = true, ) => { const _service = - // @ts-expect-error: TODO: Investigate type mismatch. service ?? getNodeEES(getNodeEESMessenger(options.rootMessenger)); const { snaps, snapsData } = extractSourceCodeFromSnapsState( @@ -733,12 +729,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 +746,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 +770,6 @@ export const getRestrictedCronjobControllerMessenger = ( }); if (mocked) { - messenger.registerActionHandler( - 'PermissionController:hasPermission', - () => { - return true; - }, - ); - messenger.registerActionHandler( 'PermissionController:getPermissions', () => { @@ -800,37 +785,48 @@ export const getRestrictedCronjobControllerMessenger = ( return cronjobControllerMessenger; }; -// Mock controller messenger for registry -export const getRootSnapsRegistryControllerMessenger = () => { - const messenger = new MockControllerMessenger< - SnapsRegistryActions, - SnapsRegistryEvents - >(); +type SnapRegistryControllerRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + +export const getRootSnapRegistryControllerMessenger = () => { + const messenger: SnapRegistryControllerRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); return messenger; }; -export const getRestrictedSnapsRegistryControllerMessenger = ( - messenger: ReturnType< - typeof getRootSnapsRegistryControllerMessenger - > = getRootSnapsRegistryControllerMessenger(), +export const getRestrictedSnapRegistryControllerMessenger = ( + rootMessenger: ReturnType< + typeof getRootSnapRegistryControllerMessenger + > = getRootSnapRegistryControllerMessenger(), ) => { - return new Messenger< - 'SnapsRegistry', - SnapsRegistryActions, - SnapsRegistryEvents, - any - >({ namespace: 'SnapsRegistry', parent: messenger }); + const messenger: SnapRegistryControllerMessenger = new Messenger({ + namespace: 'SnapRegistryController', + parent: rootMessenger, + }); + + return messenger; }; +/** + * 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 +839,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 +849,7 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( 'ApprovalController:acceptRequest', 'MultichainAssetsController:getState', 'AccountsController:getAccountByAddress', - 'SnapController:get', + 'SnapController:getSnap', 'AccountsController:getSelectedMultichainAccount', 'AccountsController:listMultichainAccounts', 'PermissionController:hasPermission', @@ -920,9 +912,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 +925,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 +942,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 +952,7 @@ export const getRestrictedSnapInsightsControllerMessenger = ( messenger.delegate({ actions: [ 'PermissionController:getPermissions', - 'SnapController:getAll', + 'SnapController:getRunnableSnaps', 'SnapController:handleRequest', 'SnapInterfaceController:deleteInterface', ], @@ -993,34 +985,33 @@ export async function waitForStateChange( }); } +type MultichainRoutingServiceRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions +>; + // Mock controller messenger for Multichain Router -export const getRootMultichainRouterMessenger = () => { - const messenger = new MockControllerMessenger< - MultichainRouterActions | MultichainRouterAllowedActions, - MultichainRouterEvents - >(); +export const getMultichainRoutingServiceRootMessenger = () => { + const messenger: MultichainRoutingServiceRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); return messenger; }; -export const getRestrictedMultichainRouterMessenger = ( - messenger: ReturnType< - typeof getRootMultichainRouterMessenger - > = getRootMultichainRouterMessenger(), +export const getRestrictedMultichainRoutingServiceMessenger = ( + messenger: MultichainRoutingServiceRootMessenger = getMultichainRoutingServiceRootMessenger(), ) => { - const controllerMessenger = new Messenger< - 'MultichainRouter', - MultichainRouterActions | MultichainRouterAllowedActions, - never, - any - >({ namespace: 'MultichainRouter', parent: messenger }); + const controllerMessenger: MultichainRoutingServiceMessenger = new Messenger({ + namespace: 'MultichainRoutingService', + parent: messenger, + }); messenger.delegate({ actions: [ 'PermissionController:getPermissions', - 'SnapController:getAll', + 'SnapController:getRunnableSnaps', 'SnapController:handleRequest', 'AccountsController:listMultichainAccounts', ], @@ -1032,12 +1023,15 @@ export const getRestrictedMultichainRouterMessenger = ( return controllerMessenger; }; -// Mock controller messenger for WebSocketService +type WebSocketServiceRootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + export const getRootWebSocketServiceMessenger = () => { - const messenger = new MockControllerMessenger< - WebSocketServiceActions | WebSocketServiceAllowedActions, - WebSocketServiceEvents - >(); + const messenger: WebSocketServiceRootMessenger = + new MockControllerMessenger(); jest.spyOn(messenger, 'call'); @@ -1049,12 +1043,10 @@ export const getRestrictedWebSocketServiceMessenger = ( typeof getRootWebSocketServiceMessenger > = getRootWebSocketServiceMessenger(), ) => { - const controllerMessenger = new Messenger< - 'WebSocketService', - WebSocketServiceActions | WebSocketServiceAllowedActions, - WebSocketServiceEvents, - any - >({ namespace: 'WebSocketService', parent: messenger }); + const controllerMessenger: WebSocketServiceMessenger = new Messenger({ + namespace: 'WebSocketService', + parent: messenger, + }); messenger.delegate({ actions: ['SnapController:handleRequest'], diff --git a/packages/snaps-controllers/src/test-utils/execution-environment.ts b/packages/snaps-controllers/src/test-utils/execution-environment.ts index f0126f179c..0bd738d2c6 100644 --- a/packages/snaps-controllers/src/test-utils/execution-environment.ts +++ b/packages/snaps-controllers/src/test-utils/execution-environment.ts @@ -2,25 +2,19 @@ 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, ExecutionServiceEvents, - SetupSnapProvider, SnapExecutionData, } from '../services'; +import type { SetupSnapProvider } from '../services/ExecutionService'; import { NodeThreadExecutionService, setupMultiplex } from '../services/node'; -export const getNodeEESMessenger = ( - messenger: MockControllerMessenger< - ExecutionServiceActions, - ExecutionServiceEvents - >, -) => { +export const getNodeEESMessenger = (messenger: RootMessenger) => { const executionServiceMessenger = new Messenger< 'ExecutionService', ExecutionServiceActions, @@ -64,7 +58,7 @@ export const getNodeEES = ( }), }); -export class ExecutionEnvironmentStub implements ExecutionService { +export class ExecutionEnvironmentStub { name: 'ExecutionService' = 'ExecutionService' as const; state = null; diff --git a/packages/snaps-controllers/src/test-utils/multichain.ts b/packages/snaps-controllers/src/test-utils/multichain.ts index 56eab1914f..8bac8bf167 100644 --- a/packages/snaps-controllers/src/test-utils/multichain.ts +++ b/packages/snaps-controllers/src/test-utils/multichain.ts @@ -4,7 +4,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; import type { CaipAccountId, CaipChainId, Json } from '@metamask/utils'; -import type { WithSnapKeyringFunction } from '../multichain'; +import type { WithSnapKeyringFunction } from '../multichain/MultichainRoutingService'; export const BTC_CAIP2 = 'bip122:000000000019d6689c085ae165831e93' as CaipChainId; diff --git a/packages/snaps-controllers/src/test-utils/registry.ts b/packages/snaps-controllers/src/test-utils/registry.ts index 086c3ffade..34bacae94a 100644 --- a/packages/snaps-controllers/src/test-utils/registry.ts +++ b/packages/snaps-controllers/src/test-utils/registry.ts @@ -1,32 +1,30 @@ -import type { MockControllerMessenger } from '@metamask/snaps-utils/test-utils'; +import type { RootMessenger } from './controller'; +import { SnapRegistryStatus } from '../snaps'; -import type { SnapsRegistry } from '../snaps'; -import { SnapsRegistryStatus } from '../snaps'; - -export class MockSnapsRegistry implements SnapsRegistry { +export class MockSnapRegistryController { readonly #messenger; - constructor(messenger: MockControllerMessenger) { + constructor(messenger: RootMessenger) { this.#messenger = messenger; this.#messenger.registerActionHandler( - 'SnapsRegistry:get', + 'SnapRegistryController:get', this.get.bind(this), ); this.#messenger.registerActionHandler( - 'SnapsRegistry:getMetadata', + 'SnapRegistryController:getMetadata', this.getMetadata.bind(this), ); this.#messenger.registerActionHandler( - 'SnapsRegistry:resolveVersion', + 'SnapRegistryController:resolveVersion', this.resolveVersion.bind(this), ); this.#messenger.registerActionHandler( - 'SnapsRegistry:update', - this.update.bind(this), + 'SnapRegistryController:requestUpdate', + this.requestUpdate.bind(this), ); } @@ -35,7 +33,7 @@ export class MockSnapsRegistry implements SnapsRegistry { Object.keys(snaps).reduce( (acc, snapId) => ({ ...acc, - [snapId]: { status: SnapsRegistryStatus.Unverified }, + [snapId]: { status: SnapRegistryStatus.Unverified }, }), {}, ), @@ -48,9 +46,9 @@ export class MockSnapsRegistry implements SnapsRegistry { getMetadata = jest.fn().mockReturnValue(null); - update = jest.fn().mockImplementation(() => { + requestUpdate = jest.fn().mockImplementation(() => { this.#messenger.publish( - 'SnapsRegistry:stateChange', + 'SnapRegistryController:stateChange', { database: { verifiedSnaps: {}, blockedSnaps: [] }, lastUpdated: Date.now(), diff --git a/packages/snaps-controllers/src/test-utils/service.ts b/packages/snaps-controllers/src/test-utils/service.ts index f324a2257e..2c3f0f129e 100644 --- a/packages/snaps-controllers/src/test-utils/service.ts +++ b/packages/snaps-controllers/src/test-utils/service.ts @@ -6,7 +6,7 @@ import { pipeline } from 'readable-stream'; import type { Duplex } from 'readable-stream'; import { MOCK_BLOCK_NUMBER } from './constants'; -import type { ErrorMessageEvent } from '../services'; +import type { ExecutionServiceUnhandledErrorEvent } from '../services'; import { setupMultiplex } from '../services'; export const createService = < @@ -18,9 +18,11 @@ export const createService = < 'messenger' | 'setupSnapProvider' >, ) => { - const messenger = new Messenger<'ExecutionService', never, ErrorMessageEvent>( - { namespace: 'ExecutionService' }, - ); + const messenger = new Messenger< + 'ExecutionService', + never, + ExecutionServiceUnhandledErrorEvent + >({ namespace: 'ExecutionService' }); const service = new ServiceClass({ messenger, diff --git a/packages/snaps-controllers/src/types/controllers.ts b/packages/snaps-controllers/src/types/controllers.ts index 1d24aab183..20a1085e92 100644 --- a/packages/snaps-controllers/src/types/controllers.ts +++ b/packages/snaps-controllers/src/types/controllers.ts @@ -155,7 +155,7 @@ export type SignatureControllerState = { unapprovedTypedMessagesCount: number; }; -export type SignatureStateChange = ControllerStateChangeEvent< +export type SignatureControllerStateChangeEvent = ControllerStateChangeEvent< 'SignatureController', SignatureControllerState >; diff --git a/packages/snaps-controllers/src/websocket/WebSocketService-method-action-types.ts b/packages/snaps-controllers/src/websocket/WebSocketService-method-action-types.ts new file mode 100644 index 0000000000..1dff4bae87 --- /dev/null +++ b/packages/snaps-controllers/src/websocket/WebSocketService-method-action-types.ts @@ -0,0 +1,63 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { WebSocketService } from './WebSocketService'; + +/** + * Open a WebSocket connection. + * + * @param snapId - The Snap ID. + * @param url - The URL for the WebSocket connection. + * @param protocols - An optional parameter for protocols. + * @returns The identifier for the opened connection. + * @throws If the connection fails. + */ +export type WebSocketServiceOpenAction = { + type: `WebSocketService:open`; + handler: WebSocketService['open']; +}; + +/** + * Close a given WebSocket connection. + * + * @param snapId - The Snap ID. + * @param id - The identifier for the WebSocket connection. + */ +export type WebSocketServiceCloseAction = { + type: `WebSocketService:close`; + handler: WebSocketService['close']; +}; + +/** + * Send a message from a given Snap ID to a WebSocket connection. + * + * @param snapId - The Snap ID. + * @param id - The identifier for the WebSocket connection. + * @param data - The message to send. + */ +export type WebSocketServiceSendMessageAction = { + type: `WebSocketService:sendMessage`; + handler: WebSocketService['sendMessage']; +}; + +/** + * Get a list of all open WebSocket connections for a Snap ID. + * + * @param snapId - The Snap ID. + * @returns A list of WebSocket connections. + */ +export type WebSocketServiceGetAllAction = { + type: `WebSocketService:getAll`; + handler: WebSocketService['getAll']; +}; + +/** + * Union of all WebSocketService action types. + */ +export type WebSocketServiceMethodActions = + | WebSocketServiceOpenAction + | WebSocketServiceCloseAction + | WebSocketServiceSendMessageAction + | WebSocketServiceGetAllAction; diff --git a/packages/snaps-controllers/src/websocket/WebSocketService.ts b/packages/snaps-controllers/src/websocket/WebSocketService.ts index 15addd7c4f..9c83195526 100644 --- a/packages/snaps-controllers/src/websocket/WebSocketService.ts +++ b/packages/snaps-controllers/src/websocket/WebSocketService.ts @@ -1,69 +1,41 @@ import type { Messenger } from '@metamask/messenger'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { - GetWebSocketsResult, - SnapId, - WebSocketEvent, -} from '@metamask/snaps-sdk'; +import type { SnapId, WebSocketEvent } from '@metamask/snaps-sdk'; import { HandlerType, isEqual, logError } from '@metamask/snaps-utils'; import { assert, createDeferredPromise } from '@metamask/utils'; import { nanoid } from 'nanoid'; +import type { WebSocketServiceMethodActions } from './WebSocketService-method-action-types'; import type { - HandleSnapRequest, - SnapInstalled, - SnapUninstalled, - SnapUpdated, + SnapControllerHandleRequestAction, + SnapControllerSnapInstalledEvent, + SnapControllerSnapUninstalledEvent, + SnapControllerSnapUpdatedEvent, } from '../snaps'; import { METAMASK_ORIGIN } from '../snaps'; const serviceName = 'WebSocketService'; -export type WebSocketServiceOpenAction = { - type: `${typeof serviceName}:open`; - handler: ( - snapId: SnapId, - url: string, - protocols?: string[], - ) => Promise; -}; - -export type WebSocketServiceCloseAction = { - type: `${typeof serviceName}:close`; - handler: (snapId: SnapId, id: string) => void; -}; - -export type WebSocketServiceSendMessageAction = { - type: `${typeof serviceName}:sendMessage`; - handler: ( - snapId: SnapId, - id: string, - data: string | number[], - ) => Promise; -}; - -export type WebSocketServiceGetAllAction = { - type: `${typeof serviceName}:getAll`; - handler: (snapId: SnapId) => GetWebSocketsResult; -}; +const MESSENGER_EXPOSED_METHODS = [ + 'open', + 'close', + 'sendMessage', + 'getAll', +] as const; -export type WebSocketServiceActions = - | WebSocketServiceOpenAction - | WebSocketServiceCloseAction - | WebSocketServiceSendMessageAction - | WebSocketServiceGetAllAction; +export type WebSocketServiceActions = WebSocketServiceMethodActions; -export type WebSocketServiceAllowedActions = HandleSnapRequest; +type AllowedActions = SnapControllerHandleRequestAction; -export type WebSocketServiceEvents = - | SnapUninstalled - | SnapUpdated - | SnapInstalled; +type AllowedEvents = + | SnapControllerSnapUninstalledEvent + | SnapControllerSnapUpdatedEvent + | SnapControllerSnapInstalledEvent; export type WebSocketServiceMessenger = Messenger< 'WebSocketService', - WebSocketServiceActions | WebSocketServiceAllowedActions, - WebSocketServiceEvents + WebSocketServiceActions | AllowedActions, + AllowedEvents >; type WebSocketServiceArgs = { @@ -93,22 +65,9 @@ export class WebSocketService { this.#messenger = messenger; this.#sockets = new Map(); - this.#messenger.registerActionHandler( - `${serviceName}:open`, - async (...args) => this.#open(...args), - ); - - this.#messenger.registerActionHandler(`${serviceName}:close`, (...args) => - this.#close(...args), - ); - - this.#messenger.registerActionHandler( - `${serviceName}:sendMessage`, - async (...args) => this.#sendMessage(...args), - ); - - this.#messenger.registerActionHandler(`${serviceName}:getAll`, (...args) => - this.#getAll(...args), + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); this.#messenger.subscribe('SnapController:snapUpdated', (snap) => { @@ -154,7 +113,7 @@ export class WebSocketService { * @returns True if a matching connection already exists, otherwise false. */ #exists(snapId: SnapId, url: string, protocols: string[]) { - return this.#getAll(snapId).some( + return this.getAll(snapId).some( (socket) => socket.url === url && isEqual(socket.protocols, protocols), ); } @@ -190,7 +149,7 @@ export class WebSocketService { * @returns The identifier for the opened connection. * @throws If the connection fails. */ - async #open(snapId: SnapId, url: string, protocols: string[] = []) { + async open(snapId: SnapId, url: string, protocols: string[] = []) { assert( !this.#exists(snapId, url, protocols), `An open WebSocket connection to ${url} already exists.`, @@ -280,7 +239,7 @@ export class WebSocketService { * @param snapId - The Snap ID. * @param id - The identifier for the WebSocket connection. */ - #close(snapId: SnapId, id: string) { + close(snapId: SnapId, id: string) { const { socket } = this.#get(snapId, id); socket.close(); @@ -292,8 +251,8 @@ export class WebSocketService { * @param snapId - The Snap ID. */ #closeAll(snapId: SnapId) { - for (const socket of this.#getAll(snapId)) { - this.#close(snapId, socket.id); + for (const socket of this.getAll(snapId)) { + this.close(snapId, socket.id); } } @@ -304,7 +263,7 @@ export class WebSocketService { * @param id - The identifier for the WebSocket connection. * @param data - The message to send. */ - async #sendMessage(snapId: SnapId, id: string, data: string | number[]) { + async sendMessage(snapId: SnapId, id: string, data: string | number[]) { const { socket, openPromise } = this.#get(snapId, id); await openPromise; @@ -320,7 +279,7 @@ export class WebSocketService { * @param snapId - The Snap ID. * @returns A list of WebSocket connections. */ - #getAll(snapId: SnapId) { + getAll(snapId: SnapId) { return [...this.#sockets.values()] .filter((socket) => socket.snapId === snapId) .map((socket) => ({ diff --git a/packages/snaps-controllers/src/websocket/index.ts b/packages/snaps-controllers/src/websocket/index.ts index f0a947eb67..1d535dc632 100644 --- a/packages/snaps-controllers/src/websocket/index.ts +++ b/packages/snaps-controllers/src/websocket/index.ts @@ -1 +1,11 @@ -export * from './WebSocketService'; +export type { + WebSocketServiceActions, + WebSocketServiceMessenger, +} from './WebSocketService'; +export { WebSocketService } from './WebSocketService'; +export type { + WebSocketServiceCloseAction, + WebSocketServiceGetAllAction, + WebSocketServiceOpenAction, + WebSocketServiceSendMessageAction, +} from './WebSocketService-method-action-types'; diff --git a/packages/snaps-jest/src/environment.ts b/packages/snaps-jest/src/environment.ts index 130a433c16..000b4fa984 100644 --- a/packages/snaps-jest/src/environment.ts +++ b/packages/snaps-jest/src/environment.ts @@ -2,7 +2,7 @@ import type { EnvironmentContext, JestEnvironmentConfig, } from '@jest/environment'; -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { installSnap } from '@metamask/snaps-simulation'; import type { @@ -85,7 +85,7 @@ export class SnapsEnvironment extends NodeEnvironment { async installSnap< Service extends new ( ...args: any[] - ) => InstanceType, + ) => InstanceType, >( snapId: string = this.snapId, options: Partial> = {}, diff --git a/packages/snaps-jest/src/helpers.ts b/packages/snaps-jest/src/helpers.ts index 1508cfa2c0..4602a1a2b3 100644 --- a/packages/snaps-jest/src/helpers.ts +++ b/packages/snaps-jest/src/helpers.ts @@ -1,4 +1,4 @@ -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import type { AccountSelectorState, AssetSelectorState, @@ -35,9 +35,7 @@ const log = createModuleLogger(rootLogger, 'helpers'); * @returns The options. */ function getOptions< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId: SnapId | Partial> | undefined, options: Partial>, @@ -102,9 +100,7 @@ export async function installSnap(): Promise; * @throws If the built-in server is not running, and no snap ID is provided. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >(options: Partial>): Promise; /** @@ -140,9 +136,7 @@ export async function installSnap< * @throws If the built-in server is not running, and no snap ID is provided. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId: SnapId, options?: Partial>, @@ -181,9 +175,7 @@ export async function installSnap< * @throws If the built-in server is not running, and no snap ID is provided. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId?: SnapId | Partial>, options: Partial> = {}, diff --git a/packages/snaps-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/packages/snaps-rpc-methods/src/restricted/notify.ts b/packages/snaps-rpc-methods/src/restricted/notify.ts index 703b0d09f5..bc6575ddac 100644 --- a/packages/snaps-rpc-methods/src/restricted/notify.ts +++ b/packages/snaps-rpc-methods/src/restricted/notify.ts @@ -96,7 +96,7 @@ export type NotifyMethodHooks = { context?: InterfaceContext, contentType?: ContentType, ) => Promise; - getSnap: (snapId: string) => Snap | undefined; + getSnap: (snapId: string) => Snap | null; }; type SpecificationBuilderOptions = { diff --git a/packages/snaps-simulation/src/controllers.ts b/packages/snaps-simulation/src/controllers.ts index 1f415bef17..1b9fd8eb4f 100644 --- a/packages/snaps-simulation/src/controllers.ts +++ b/packages/snaps-simulation/src/controllers.ts @@ -2,25 +2,29 @@ import { caip25CaveatBuilder, Caip25CaveatType, } from '@metamask/chain-agnostic-permission'; +import type { + MessengerActions, + MessengerEvents, + MockAnyNamespace, +} from '@metamask/messenger'; import { Messenger } from '@metamask/messenger'; import type { CaveatSpecificationConstraint, + PermissionControllerMessenger, PermissionSpecificationConstraint, - PermissionControllerActions, - SubjectMetadataControllerActions, } from '@metamask/permission-controller'; import { PermissionController, SubjectMetadataController, SubjectType, } from '@metamask/permission-controller'; -import { SnapInterfaceController } from '@metamask/snaps-controllers'; import type { - ExecutionServiceActions, - SnapInterfaceControllerActions, - SnapInterfaceControllerAllowedActions, - SnapInterfaceControllerStateChangeEvent, + ExecutionServiceMessenger, + SnapControllerMessenger, + SnapInterfaceControllerMessenger, + SnapRegistryControllerMessenger, } from '@metamask/snaps-controllers'; +import { SnapInterfaceController } from '@metamask/snaps-controllers'; import { caveatSpecifications as snapsCaveatsSpecifications, endowmentCaveatSpecifications as snapsEndowmentCaveatSpecifications, @@ -37,20 +41,22 @@ import type { SimulationOptions } from './options'; import type { RestrictedMiddlewareHooks } from './simulation'; import type { RunSagaFunction } from './store'; -export type RootControllerAllowedActions = - | SnapInterfaceControllerActions - | SnapInterfaceControllerAllowedActions - | PermissionControllerActions - | ExecutionServiceActions - | SubjectMetadataControllerActions; - -export type RootControllerAllowedEvents = - SnapInterfaceControllerStateChangeEvent; - export type RootControllerMessenger = Messenger< - any, - RootControllerAllowedActions, - RootControllerAllowedEvents + MockAnyNamespace, + MessengerActions< + | ExecutionServiceMessenger + | PermissionControllerMessenger + | SnapControllerMessenger + | SnapInterfaceControllerMessenger + | SnapRegistryControllerMessenger + >, + MessengerEvents< + | ExecutionServiceMessenger + | PermissionControllerMessenger + | SnapControllerMessenger + | SnapInterfaceControllerMessenger + | SnapRegistryControllerMessenger + > >; export type GetControllersOptions = { @@ -85,10 +91,11 @@ export function getControllers(options: GetControllersOptions): Controllers { subjectCacheLimit: 100, }); - const interfaceControllerMessenger = new Messenger({ - namespace: 'SnapInterfaceController', - parent: controllerMessenger, - }); + const interfaceControllerMessenger: SnapInterfaceControllerMessenger = + new Messenger({ + namespace: 'SnapInterfaceController', + parent: controllerMessenger, + }); controllerMessenger.delegate({ messenger: interfaceControllerMessenger, diff --git a/packages/snaps-simulation/src/request.ts b/packages/snaps-simulation/src/request.ts index 4ff5608f42..8267ae1c70 100644 --- a/packages/snaps-simulation/src/request.ts +++ b/packages/snaps-simulation/src/request.ts @@ -1,4 +1,4 @@ -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import { type ComponentOrElement, ComponentOrElementStruct, @@ -38,7 +38,7 @@ import type { export type HandleRequestOptions = { snapId: SnapId; store: Store; - executionService: AbstractExecutionService; + executionService: ExecutionService; handler: HandlerType; controllerMessenger: RootControllerMessenger; simulationOptions: SimulationOptions; diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 9c2e097119..23d955d171 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -12,7 +12,7 @@ import { type Caveat, type RequestedPermissions, } from '@metamask/permission-controller'; -import type { AbstractExecutionService } from '@metamask/snaps-controllers'; +import type { ExecutionService } from '@metamask/snaps-controllers'; import { detectSnapLocation, fetchSnap, @@ -88,7 +88,7 @@ export type ExecutionServiceOptions< Service extends new (...args: any[]) => any, > = Omit< ConstructorParameters[0], - keyof ConstructorParameters>[0] + keyof ConstructorParameters[0] >; /** @@ -102,9 +102,7 @@ export type ExecutionServiceOptions< * @template Service - The type of the execution service. */ export type InstallSnapOptions< - Service extends new ( - ...args: any[] - ) => InstanceType>, + Service extends new (...args: any[]) => InstanceType, > = ExecutionServiceOptions extends Record ? { @@ -121,7 +119,7 @@ export type InstallSnapOptions< export type InstalledSnap = { snapId: SnapId; store: Store; - executionService: InstanceType; + executionService: InstanceType; controllerMessenger: Messenger< NamespacedName, ActionConstraint, @@ -406,9 +404,7 @@ export type MultichainMiddlewareHooks = { * @template Service - The type of the execution service. */ export async function installSnap< - Service extends new ( - ...args: any[] - ) => InstanceType, + Service extends new (...args: any[]) => InstanceType, >( snapId: SnapId, { @@ -481,8 +477,8 @@ export async function installSnap< }); // Create execution service. - const ExecutionService = executionService ?? NodeThreadExecutionService; - const service = new ExecutionService({ + const ActualService = executionService ?? NodeThreadExecutionService; + const service = new ActualService({ ...executionServiceOptions, messenger: new Messenger({ namespace: 'ExecutionService', diff --git a/packages/snaps-simulation/src/test-utils/controller.ts b/packages/snaps-simulation/src/test-utils/controller.ts index b926709522..73b3a52fc2 100644 --- a/packages/snaps-simulation/src/test-utils/controller.ts +++ b/packages/snaps-simulation/src/test-utils/controller.ts @@ -1,14 +1,11 @@ import { Messenger } from '@metamask/messenger'; -import type { SnapInterfaceControllerAllowedActions } from '@metamask/snaps-controllers'; +import type { SnapInterfaceControllerMessenger } from '@metamask/snaps-controllers'; import { MockControllerMessenger } from '@metamask/snaps-utils/test-utils'; -import type { RootControllerAllowedActions } from '../controllers'; +import type { RootControllerMessenger } from '../controllers'; export const getRootControllerMessenger = (mocked = true) => { - const messenger = new MockControllerMessenger< - RootControllerAllowedActions, - any - >(); + const messenger: RootControllerMessenger = new MockControllerMessenger(); if (mocked) { messenger.registerActionHandler('PhishingController:testOrigin', () => ({ @@ -42,12 +39,7 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( typeof getRootControllerMessenger > = getRootControllerMessenger(), ) => { - const controllerMessenger = new Messenger< - 'SnapInterfaceController', - SnapInterfaceControllerAllowedActions, - never, - any - >({ + const controllerMessenger: SnapInterfaceControllerMessenger = new Messenger({ namespace: 'SnapInterfaceController', parent: messenger, }); diff --git a/packages/snaps-utils/src/test-utils/snap.ts b/packages/snaps-utils/src/test-utils/snap.ts index f46f1ef56d..5294f6b011 100644 --- a/packages/snaps-utils/src/test-utils/snap.ts +++ b/packages/snaps-utils/src/test-utils/snap.ts @@ -86,13 +86,17 @@ export const getSnapObject = ({ } as const; }; -export const getTruncatedSnap = ({ - initialPermissions = getSnapManifest().initialPermissions, - id = MOCK_SNAP_ID, - version = getSnapManifest().version, - enabled = true, - blocked = false, -}: GetTruncatedSnapOptions = {}): TruncatedSnap => { +export const getTruncatedSnap = ( + options: GetTruncatedSnapOptions | null = null, +): TruncatedSnap => { + const { + initialPermissions = getSnapManifest().initialPermissions, + id = MOCK_SNAP_ID, + version = getSnapManifest().version, + enabled = true, + blocked = false, + } = options ?? {}; + return { initialPermissions, id, diff --git a/packages/snaps-utils/src/ui.tsx b/packages/snaps-utils/src/ui.tsx index 7e8982e953..ea910767f5 100644 --- a/packages/snaps-utils/src/ui.tsx +++ b/packages/snaps-utils/src/ui.tsx @@ -346,7 +346,7 @@ function getMarkdownLinks(text: string) { export function validateLink( link: string, isOnPhishingList: (url: string) => boolean, - getSnap: (id: string) => Snap | undefined, + getSnap: (id: string) => Snap | null, ) { try { const url = new URL(link); @@ -398,7 +398,7 @@ export function validateLink( export function validateTextLinks( text: string, isOnPhishingList: (url: string) => boolean, - getSnap: (id: string) => Snap | undefined, + getSnap: (id: string) => Snap | null, ) { const links = getMarkdownLinks(text); @@ -445,7 +445,7 @@ export function validateJsxElements( hasPermission, }: { isOnPhishingList: (url: string) => boolean; - getSnap: (id: string) => Snap | undefined; + getSnap: (id: string) => Snap | null; getAccountByAddress: ( address: CaipAccountId, ) => InternalAccount | undefined; 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