From 3344a44dc6ef6b7d0de53d4f185ed6dba785a209 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 13:59:59 -0500 Subject: [PATCH 01/25] Add `@metamask/utils` to composable-controller deps --- packages/composable-controller/package.json | 3 ++- yarn.lock | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index 432b6b3c03f..d1e56cd004f 100644 --- a/packages/composable-controller/package.json +++ b/packages/composable-controller/package.json @@ -31,7 +31,8 @@ "test:watch": "jest --watch" }, "dependencies": { - "@metamask/base-controller": "^4.1.1" + "@metamask/base-controller": "^4.1.1", + "@metamask/utils": "^8.3.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/yarn.lock b/yarn.lock index fcc17862f20..4305ac86e80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1708,6 +1708,7 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.4.4 "@metamask/base-controller": ^4.1.1 + "@metamask/utils": ^8.3.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 immer: ^9.0.6 From f50716abab684be10abba5b4b42d1d0011c8ee81 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 20:32:28 -0500 Subject: [PATCH 02/25] Define and apply `isBaseController` type guard --- .../src/ComposableController.ts | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 6d4f6eb7d83..15d4d7f3360 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -36,6 +36,23 @@ function isBaseControllerV1( return controller instanceof BaseControllerV1; } +/** + * Determines if the given controller is an instance of BaseController + * @param controller - Controller instance to check + * @returns True if the controller is an instance of BaseController + */ +function isBaseController( + controller: ControllerInstance, +): controller is BaseController { + return ( + 'name' in controller && + typeof controller.name === 'string' && + 'state' in controller && + typeof controller.state === 'object' && + controller instanceof BaseController + ); +} + export type ComposableControllerState = { [name: string]: ControllerInstance['state']; }; @@ -113,15 +130,16 @@ export class ComposableController extends BaseController< [name]: childState, })); }); + } else if (isBaseController(controller)) { + this.messagingSystem.subscribe(`${name}:stateChange`, (childState) => { + this.update((state) => ({ + ...state, + [name]: childState, + })); + }); } else { - this.messagingSystem.subscribe( - `${String(name)}:stateChange`, - (childState: Record) => { - this.update((state) => ({ - ...state, - [name]: childState, - })); - }, + throw new Error( + 'Invalid controller: controller must extend from BaseController or BaseControllerV1', ); } } From 7be4f2821411df1e1fc28f5e032206b56906e817 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 20:34:49 -0500 Subject: [PATCH 03/25] test: Add test for composing a non-controller --- .../src/ComposableController.test.ts | 31 +++++++++++++++++++ packages/composable-controller/tsconfig.json | 3 ++ 2 files changed, 34 insertions(+) diff --git a/packages/composable-controller/src/ComposableController.test.ts b/packages/composable-controller/src/ComposableController.test.ts index 4e5d944fa1f..41b6e47f639 100644 --- a/packages/composable-controller/src/ComposableController.test.ts +++ b/packages/composable-controller/src/ComposableController.test.ts @@ -10,6 +10,7 @@ import { import type { Patch } from 'immer'; import * as sinon from 'sinon'; +import { JsonRpcEngine } from '../../json-rpc-engine/src/JsonRpcEngine'; import type { ComposableControllerStateChangeEvent } from './ComposableController'; import { ComposableController } from './ComposableController'; @@ -335,5 +336,35 @@ describe('ComposableController', () => { }), ).toThrow('Messaging system is required'); }); + + it('should throw if attempting to compose a controller that does not extend from BaseController', () => { + const notController = new JsonRpcEngine(); + const controllerMessenger = new ControllerMessenger< + never, + FooControllerEvent + >(); + const fooControllerMessenger = controllerMessenger.getRestricted< + 'FooController', + never, + never + >({ + name: 'FooController', + }); + const fooController = new FooController(fooControllerMessenger); + const composableControllerMessenger = controllerMessenger.getRestricted({ + name: 'ComposableController', + allowedEvents: ['FooController:stateChange'], + }); + expect( + () => + new ComposableController({ + // @ts-expect-error - Suppressing type error to test for runtime error handling + controllers: [notController, fooController], + messenger: composableControllerMessenger, + }), + ).toThrow( + 'Invalid controller: controller must extend from BaseController or BaseControllerV1', + ); + }); }); }); diff --git a/packages/composable-controller/tsconfig.json b/packages/composable-controller/tsconfig.json index f2d7b67ff66..cc814f313b7 100644 --- a/packages/composable-controller/tsconfig.json +++ b/packages/composable-controller/tsconfig.json @@ -6,6 +6,9 @@ "references": [ { "path": "../base-controller" + }, + { + "path": "../json-rpc-engine" } ], "include": ["../../types", "./src"] From 416382eb7674911a2b00bd9eb3e896b61c2934df Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 20:35:35 -0500 Subject: [PATCH 04/25] Improve BaseControllerV1 typing, type guard, comments --- .../src/ComposableController.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 15d4d7f3360..2ba9b9d5e25 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -2,6 +2,8 @@ import { BaseController, BaseControllerV1 } from '@metamask/base-controller'; import type { ControllerStateChangeEvent, RestrictedControllerMessenger, + BaseState, + BaseConfig, } from '@metamask/base-controller'; export const controllerName = 'ComposableController'; @@ -14,7 +16,7 @@ export const controllerName = 'ComposableController'; * that we use in the ComposableController (name and state). */ type ControllerInstance = - // TODO: Replace `any` with type + // As explained above, `any` is used to include all `BaseControllerV1` instances. // eslint-disable-next-line @typescript-eslint/no-explicit-any BaseControllerV1 | { name: string; state: Record }; @@ -30,10 +32,21 @@ export type ControllerList = ControllerInstance[]; */ function isBaseControllerV1( controller: ControllerInstance, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): controller is BaseControllerV1 { - return controller instanceof BaseControllerV1; +): controller is BaseControllerV1< + BaseConfig & Record, + BaseState & Record +> { + return ( + 'name' in controller && + typeof controller.name === 'string' && + 'defaultConfig' in controller && + typeof controller.defaultConfig === 'object' && + 'defaultState' in controller && + typeof controller.defaultState === 'object' && + 'disabled' in controller && + typeof controller.disabled === 'boolean' && + controller instanceof BaseControllerV1 + ); } /** @@ -54,7 +67,11 @@ function isBaseController( } export type ComposableControllerState = { - [name: string]: ControllerInstance['state']; + // `any` is used here to disable the `BaseController` type constraint which expects state properties to extend `Record`. + // `ComposableController` state needs to accommodate `BaseControllerV1` state objects, and there is no straightforward way + // to include objects in `BaseController` state when they may have properties that are wider than `Json`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [name: string]: Record; }; export type ComposableControllerStateChangeEvent = ControllerStateChangeEvent< From 7b9e33a2baff855dd7c88a3d60501921018d7a37 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 20:36:19 -0500 Subject: [PATCH 05/25] Remove unused `#controllers` class field --- .../composable-controller/src/ComposableController.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 2ba9b9d5e25..e88484d18a8 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -97,8 +97,6 @@ export class ComposableController extends BaseController< ComposableControllerState, ComposableControllerMessenger > { - readonly #controllers: ControllerList = []; - /** * Creates a ComposableController instance. * @@ -127,19 +125,19 @@ export class ComposableController extends BaseController< messenger, }); - this.#controllers = controllers; - this.#controllers.forEach((controller) => + controllers.forEach((controller) => this.#updateChildController(controller), ); } /** - * Adds a child controller instance to composable controller state, - * or updates the state of a child controller. + * Constructor helper that adds a child controller instance to composable controller state + * and subscribes to child controller state changes. * @param controller - Controller instance to update */ #updateChildController(controller: ControllerInstance): void { const { name } = controller; + if (isBaseControllerV1(controller)) { controller.subscribe((childState) => { this.update((state) => ({ From d4a8161b18004ffc2f2c69d03b12f93ff4f8a4cf Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 20:54:48 -0500 Subject: [PATCH 06/25] Export base-controller type guards --- packages/composable-controller/src/ComposableController.ts | 4 ++-- packages/composable-controller/src/index.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index e88484d18a8..620389b3069 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -30,7 +30,7 @@ export type ControllerList = ControllerInstance[]; * @param controller - Controller instance to check * @returns True if the controller is an instance of BaseControllerV1 */ -function isBaseControllerV1( +export function isBaseControllerV1( controller: ControllerInstance, ): controller is BaseControllerV1< BaseConfig & Record, @@ -54,7 +54,7 @@ function isBaseControllerV1( * @param controller - Controller instance to check * @returns True if the controller is an instance of BaseController */ -function isBaseController( +export function isBaseController( controller: ControllerInstance, ): controller is BaseController { return ( diff --git a/packages/composable-controller/src/index.ts b/packages/composable-controller/src/index.ts index da2d27e177e..5fc5a803c5d 100644 --- a/packages/composable-controller/src/index.ts +++ b/packages/composable-controller/src/index.ts @@ -5,4 +5,8 @@ export type { ComposableControllerEvents, ComposableControllerMessenger, } from './ComposableController'; -export { ComposableController } from './ComposableController'; +export { + ComposableController, + isBaseController, + isBaseControllerV1, +} from './ComposableController'; From c762c6eaf2482a95a04fc68856f3d4501f5369be Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 22:02:32 -0500 Subject: [PATCH 07/25] Remove deprecated `subscribe` property from BaseControllerV2 --- packages/base-controller/src/BaseControllerV2.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/base-controller/src/BaseControllerV2.ts b/packages/base-controller/src/BaseControllerV2.ts index 32467cf0d43..23b5c22f4c6 100644 --- a/packages/base-controller/src/BaseControllerV2.ts +++ b/packages/base-controller/src/BaseControllerV2.ts @@ -113,17 +113,6 @@ export class BaseController< public readonly metadata: StateMetadata; - /** - * The existence of the `subscribe` property is how the ComposableController used to detect - * whether a controller extends the old BaseController or the new one. We set it to `undefined` here to - * ensure the ComposableController never mistakes them for an older style controller. - * - * This property is no longer used, and will be removed in a future release. - * - * @deprecated This will be removed in a future release - */ - public readonly subscribe: undefined; - /** * Creates a BaseController instance. * From 53be42748478d11da750ceb251b246ef6f634b9c Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 22:07:26 -0500 Subject: [PATCH 08/25] Use `Record` typing for BaseControllerV2 state objects --- .../composable-controller/src/ComposableController.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 620389b3069..1452dd764e8 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -5,6 +5,7 @@ import type { BaseState, BaseConfig, } from '@metamask/base-controller'; +import type { Json } from '@metamask/utils'; export const controllerName = 'ComposableController'; @@ -18,7 +19,7 @@ export const controllerName = 'ComposableController'; type ControllerInstance = // As explained above, `any` is used to include all `BaseControllerV1` instances. // eslint-disable-next-line @typescript-eslint/no-explicit-any - BaseControllerV1 | { name: string; state: Record }; + BaseControllerV1 | { name: string; state: Record }; /** * List of child controller instances @@ -68,15 +69,14 @@ export function isBaseController( export type ComposableControllerState = { // `any` is used here to disable the `BaseController` type constraint which expects state properties to extend `Record`. - // `ComposableController` state needs to accommodate `BaseControllerV1` state objects, and there is no straightforward way - // to include objects in `BaseController` state when they may have properties that are wider than `Json`. + // `ComposableController` state needs to accommodate `BaseControllerV1` state objects that may have properties wider than `Json`. // eslint-disable-next-line @typescript-eslint/no-explicit-any [name: string]: Record; }; export type ComposableControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, - ComposableControllerState + Record)> >; export type ComposableControllerEvents = ComposableControllerStateChangeEvent; @@ -137,7 +137,6 @@ export class ComposableController extends BaseController< */ #updateChildController(controller: ControllerInstance): void { const { name } = controller; - if (isBaseControllerV1(controller)) { controller.subscribe((childState) => { this.update((state) => ({ From 4f5484122c9e73b3dbf4e37951f7ac151f4198fe Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 22:08:23 -0500 Subject: [PATCH 09/25] Use generic argument for typing `reduce` --- .../composable-controller/src/ComposableController.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 1452dd764e8..d55993a82e2 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -119,9 +119,12 @@ export class ComposableController extends BaseController< super({ name: controllerName, metadata: {}, - state: controllers.reduce((state, controller) => { - return { ...state, [controller.name]: controller.state }; - }, {} as ComposableControllerState), + state: controllers.reduce( + (state, controller) => { + return { ...state, [controller.name]: controller.state }; + }, + {}, + ), messenger, }); From f4530ab24a22686cdaa98ebc662d9960bce04a6c Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 22:13:10 -0500 Subject: [PATCH 10/25] Update CHANGELOGs --- packages/base-controller/CHANGELOG.md | 5 +++++ packages/composable-controller/CHANGELOG.md | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index 52ee840f1d7..bda0315b5e4 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- **BREAKING:** Remove the deprecated `subscribe` class field from `BaseControllerV2` ([#3904](https://github.com/MetaMask/core/pull/3904)) + - Replaced by the `isBaseController` type guard in `@metamask/composable-controller`. + ## [4.1.1] ### Changed diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md index 26688cf855f..d96288facb4 100644 --- a/packages/composable-controller/CHANGELOG.md +++ b/packages/composable-controller/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING:** Add `@metamask/utils` ^8.3.0 as a dependency. ([#3904](https://github.com/MetaMask/core/pull/3904)) +- Add exports `isBaseControllerV1` and `isBaseController`, which are type guards for validating controller instances ([#3904](https://github.com/MetaMask/core/pull/3904)) + +### Changed + +- Throws an error if a non-controller is passed into the `controllers` constructor option ([#3904](https://github.com/MetaMask/core/pull/3904)) + ## [5.0.1] ### Changed From 724f80eb2b37ffb8f8000e6b80bf837f9be59343 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 23:10:43 -0500 Subject: [PATCH 11/25] Refactor messenger typing for controller events and allowed events --- .../src/ComposableController.test.ts | 17 ++++++++++------- .../src/ComposableController.ts | 15 +++++++++++---- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.test.ts b/packages/composable-controller/src/ComposableController.test.ts index 41b6e47f639..6087de8313e 100644 --- a/packages/composable-controller/src/ComposableController.test.ts +++ b/packages/composable-controller/src/ComposableController.test.ts @@ -11,7 +11,7 @@ import type { Patch } from 'immer'; import * as sinon from 'sinon'; import { JsonRpcEngine } from '../../json-rpc-engine/src/JsonRpcEngine'; -import type { ComposableControllerStateChangeEvent } from './ComposableController'; +import type { ComposableControllerEvents } from './ComposableController'; import { ComposableController } from './ComposableController'; // Mock BaseController classes @@ -107,7 +107,10 @@ describe('ComposableController', () => { describe('BaseControllerV1', () => { it('should compose controller state', () => { - const composableMessenger = new ControllerMessenger().getRestricted({ + const composableMessenger = new ControllerMessenger< + never, + ComposableControllerEvents + >().getRestricted({ name: 'ComposableController', }); const controller = new ComposableController({ @@ -124,7 +127,7 @@ describe('ComposableController', () => { it('should notify listeners of nested state change', () => { const controllerMessenger = new ControllerMessenger< never, - ComposableControllerStateChangeEvent + ComposableControllerEvents >(); const composableMessenger = controllerMessenger.getRestricted({ name: 'ComposableController', @@ -177,7 +180,7 @@ describe('ComposableController', () => { it('should notify listeners of nested state change', () => { const controllerMessenger = new ControllerMessenger< never, - ComposableControllerStateChangeEvent | FooControllerEvent + ComposableControllerEvents | FooControllerEvent >(); const fooControllerMessenger = controllerMessenger.getRestricted< 'FooController', @@ -241,7 +244,7 @@ describe('ComposableController', () => { const barController = new BarController(); const controllerMessenger = new ControllerMessenger< never, - ComposableControllerStateChangeEvent | FooControllerEvent + ComposableControllerEvents | FooControllerEvent >(); const fooControllerMessenger = controllerMessenger.getRestricted< 'FooController', @@ -281,7 +284,7 @@ describe('ComposableController', () => { const barController = new BarController(); const controllerMessenger = new ControllerMessenger< never, - ComposableControllerStateChangeEvent | FooControllerEvent + ComposableControllerEvents | FooControllerEvent >(); const fooControllerMessenger = controllerMessenger.getRestricted< 'FooController', @@ -337,7 +340,7 @@ describe('ComposableController', () => { ).toThrow('Messaging system is required'); }); - it('should throw if attempting to compose a controller that does not extend from BaseController', () => { + it('should throw if composing a controller that does not extend from BaseController', () => { const notController = new JsonRpcEngine(); const controllerMessenger = new ControllerMessenger< never, diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index d55993a82e2..d796c76898a 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -76,17 +76,24 @@ export type ComposableControllerState = { export type ComposableControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, - Record)> + Record >; export type ComposableControllerEvents = ComposableControllerStateChangeEvent; +type AnyControllerStateChangeEvent = ControllerStateChangeEvent< + string, + Record +>; + +type AllowedEvents = AnyControllerStateChangeEvent; + export type ComposableControllerMessenger = RestrictedControllerMessenger< typeof controllerName, never, - ControllerStateChangeEvent>, - string, - string + ComposableControllerEvents | AllowedEvents, + never, + AllowedEvents['type'] >; /** From 48669b20a8d9a57254a6589209fcf20c309e6e36 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 23:11:17 -0500 Subject: [PATCH 12/25] Add `isValidJson` check for child state from BaseControllerV2 controllers --- .../src/ComposableController.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index d796c76898a..7e85c72e66b 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -5,7 +5,7 @@ import type { BaseState, BaseConfig, } from '@metamask/base-controller'; -import type { Json } from '@metamask/utils'; +import { isValidJson, type Json } from '@metamask/utils'; export const controllerName = 'ComposableController'; @@ -156,10 +156,12 @@ export class ComposableController extends BaseController< }); } else if (isBaseController(controller)) { this.messagingSystem.subscribe(`${name}:stateChange`, (childState) => { - this.update((state) => ({ - ...state, - [name]: childState, - })); + if (isValidJson(childState)) { + this.update((state) => ({ + ...state, + [name]: childState, + })); + } }); } else { throw new Error( From 1069fa40a96996373e99a011326bbcde0070ff16 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 7 Feb 2024 23:22:36 -0500 Subject: [PATCH 13/25] Update CHANGELOG --- packages/composable-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md index d96288facb4..369ca9143ef 100644 --- a/packages/composable-controller/CHANGELOG.md +++ b/packages/composable-controller/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Throws an error if a non-controller is passed into the `controllers` constructor option ([#3904](https://github.com/MetaMask/core/pull/3904)) +### Removed + +- **BREAKING:** The `AllowedActions` parameter of the `ComposableControllerMessenger` type is narrowed from `string` to `never`, as `ComposableController` does not use any external controller actions. ([#3904](https://github.com/MetaMask/core/pull/3904)) + ## [5.0.1] ### Changed From d4cf789a94bdf8d177d69650deafa1e7c2eb038f Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Thu, 8 Feb 2024 14:18:14 -0500 Subject: [PATCH 14/25] Add `@metamask/json-rpc-engine` as devDep --- packages/composable-controller/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index d1e56cd004f..53ad6a93be6 100644 --- a/packages/composable-controller/package.json +++ b/packages/composable-controller/package.json @@ -36,6 +36,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", + "@metamask/json-rpc-engine": "^7.3.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "immer": "^9.0.6", diff --git a/yarn.lock b/yarn.lock index 4305ac86e80..17c75c30e27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1708,6 +1708,7 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.4.4 "@metamask/base-controller": ^4.1.1 + "@metamask/json-rpc-engine": ^7.3.2 "@metamask/utils": ^8.3.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 From 559b4a1e211c78c84e70b6de7bc96d63fc9bdbec Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 20 Feb 2024 15:02:05 -0500 Subject: [PATCH 15/25] Revert "Remove deprecated `subscribe` property from BaseControllerV2" This reverts commit 280e8e4f764fe5682d3bff08558441d741782d2f. --- packages/base-controller/src/BaseControllerV2.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/base-controller/src/BaseControllerV2.ts b/packages/base-controller/src/BaseControllerV2.ts index 23b5c22f4c6..32467cf0d43 100644 --- a/packages/base-controller/src/BaseControllerV2.ts +++ b/packages/base-controller/src/BaseControllerV2.ts @@ -113,6 +113,17 @@ export class BaseController< public readonly metadata: StateMetadata; + /** + * The existence of the `subscribe` property is how the ComposableController used to detect + * whether a controller extends the old BaseController or the new one. We set it to `undefined` here to + * ensure the ComposableController never mistakes them for an older style controller. + * + * This property is no longer used, and will be removed in a future release. + * + * @deprecated This will be removed in a future release + */ + public readonly subscribe: undefined; + /** * Creates a BaseController instance. * From 71fc9a401c1bd6e38674313bf189b1726cc6e9a2 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 20 Feb 2024 12:06:34 -0800 Subject: [PATCH 16/25] Update packages/base-controller/CHANGELOG.md --- packages/base-controller/CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index bda0315b5e4..52ee840f1d7 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -7,11 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Removed - -- **BREAKING:** Remove the deprecated `subscribe` class field from `BaseControllerV2` ([#3904](https://github.com/MetaMask/core/pull/3904)) - - Replaced by the `isBaseController` type guard in `@metamask/composable-controller`. - ## [4.1.1] ### Changed From f7097cb295ac8fd74a9826df0a77071e50f1f417 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 20 Feb 2024 22:41:39 -0500 Subject: [PATCH 17/25] Add TODOs for removing `BaseControllerV1` related features once all controllers have migrated to V2 --- packages/composable-controller/src/ComposableController.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 7e85c72e66b..8ef24a758f2 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -18,6 +18,7 @@ export const controllerName = 'ComposableController'; */ type ControllerInstance = // As explained above, `any` is used to include all `BaseControllerV1` instances. + // TODO: Remove first union member once `BaseControllerV2` migrations are completed for all controllers. // eslint-disable-next-line @typescript-eslint/no-explicit-any BaseControllerV1 | { name: string; state: Record }; @@ -30,6 +31,7 @@ export type ControllerList = ControllerInstance[]; * Determines if the given controller is an instance of BaseControllerV1 * @param controller - Controller instance to check * @returns True if the controller is an instance of BaseControllerV1 + * TODO: Deprecate once `BaseControllerV2` migrations are completed for all controllers. */ export function isBaseControllerV1( controller: ControllerInstance, @@ -70,6 +72,7 @@ export function isBaseController( export type ComposableControllerState = { // `any` is used here to disable the `BaseController` type constraint which expects state properties to extend `Record`. // `ComposableController` state needs to accommodate `BaseControllerV1` state objects that may have properties wider than `Json`. + // TODO: Replace `any` with `Json` once `BaseControllerV2` migrations are completed for all controllers. // eslint-disable-next-line @typescript-eslint/no-explicit-any [name: string]: Record; }; @@ -144,6 +147,7 @@ export class ComposableController extends BaseController< * Constructor helper that adds a child controller instance to composable controller state * and subscribes to child controller state changes. * @param controller - Controller instance to update + * TODO: Remove `isBaseControllerV1` branch once `BaseControllerV2` migrations are completed for all controllers. */ #updateChildController(controller: ControllerInstance): void { const { name } = controller; From ada78a2b1960d2f9b65802e114b69a382b449991 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 20 Feb 2024 23:30:55 -0500 Subject: [PATCH 18/25] Remove `ControllerList` type, add `BaseControllerV{1,2}Instance` types --- packages/composable-controller/CHANGELOG.md | 3 ++- .../src/ComposableController.ts | 20 ++++++++++--------- packages/composable-controller/src/index.ts | 1 - 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md index 369ca9143ef..401ebee7b27 100644 --- a/packages/composable-controller/CHANGELOG.md +++ b/packages/composable-controller/CHANGELOG.md @@ -9,16 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **BREAKING:** Add `@metamask/utils` ^8.3.0 as a dependency. ([#3904](https://github.com/MetaMask/core/pull/3904)) - Add exports `isBaseControllerV1` and `isBaseController`, which are type guards for validating controller instances ([#3904](https://github.com/MetaMask/core/pull/3904)) ### Changed +- **BREAKING:** Add `@metamask/utils` ^8.3.0 as a dependency. ([#3904](https://github.com/MetaMask/core/pull/3904)) - Throws an error if a non-controller is passed into the `controllers` constructor option ([#3904](https://github.com/MetaMask/core/pull/3904)) ### Removed - **BREAKING:** The `AllowedActions` parameter of the `ComposableControllerMessenger` type is narrowed from `string` to `never`, as `ComposableController` does not use any external controller actions. ([#3904](https://github.com/MetaMask/core/pull/3904)) +- **BREAKING:** Remove `ControllerList` as an exported type. ([#3904](https://github.com/MetaMask/core/pull/3904)) ## [5.0.1] diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 8ef24a758f2..456539f7da4 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -10,22 +10,24 @@ import { isValidJson, type Json } from '@metamask/utils'; export const controllerName = 'ComposableController'; /* - * This type encompasses controllers based on either BaseControllerV1 or + * The following three types encompass controllers based on either BaseControllerV1 or * BaseController. The BaseController type can't be included directly * because the generic parameters it expects require knowing the exact state * shape, so instead we look for an object with the BaseController properties * that we use in the ComposableController (name and state). */ +// As explained above, `any` is used to include all `BaseControllerV1` instances. +// TODO: Remove this type once `BaseControllerV2` migrations are completed for all controllers. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type BaseControllerV1Instance = BaseControllerV1; + +type BaseControllerV2Instance = { name: string; state: Record }; + type ControllerInstance = // As explained above, `any` is used to include all `BaseControllerV1` instances. - // TODO: Remove first union member once `BaseControllerV2` migrations are completed for all controllers. + // TODO: Remove `BaseControllerV1Instance` once `BaseControllerV2` migrations are completed for all controllers. // eslint-disable-next-line @typescript-eslint/no-explicit-any - BaseControllerV1 | { name: string; state: Record }; - -/** - * List of child controller instances - */ -export type ControllerList = ControllerInstance[]; + BaseControllerV1Instance | BaseControllerV2Instance; /** * Determines if the given controller is an instance of BaseControllerV1 @@ -119,7 +121,7 @@ export class ComposableController extends BaseController< controllers, messenger, }: { - controllers: ControllerList; + controllers: ControllerInstance[]; messenger: ComposableControllerMessenger; }) { if (messenger === undefined) { diff --git a/packages/composable-controller/src/index.ts b/packages/composable-controller/src/index.ts index 5fc5a803c5d..9deb15385bb 100644 --- a/packages/composable-controller/src/index.ts +++ b/packages/composable-controller/src/index.ts @@ -1,5 +1,4 @@ export type { - ControllerList, ComposableControllerState, ComposableControllerStateChangeEvent, ComposableControllerEvents, From 68f2a04ef85654a1bad510286c2b64b8614dd0d8 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 21 Feb 2024 08:53:34 -0500 Subject: [PATCH 19/25] Populate metadata object in constructor with child controllers --- .../composable-controller/src/ComposableController.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 456539f7da4..ba12539ace4 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -4,6 +4,7 @@ import type { RestrictedControllerMessenger, BaseState, BaseConfig, + StateMetadata, } from '@metamask/base-controller'; import { isValidJson, type Json } from '@metamask/utils'; @@ -130,7 +131,15 @@ export class ComposableController extends BaseController< super({ name: controllerName, - metadata: {}, + metadata: controllers.reduce>( + (metadata, controller) => ({ + ...metadata, + [controller.name]: isBaseController(controller) + ? controller.metadata + : { persist: true, anonymous: true }, + }), + {}, + ), state: controllers.reduce( (state, controller) => { return { ...state, [controller.name]: controller.state }; From 523f74615912021ad9afbddb427b98beb9f115c2 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 21 Feb 2024 09:26:00 -0500 Subject: [PATCH 20/25] Fix jsdoc --- packages/composable-controller/src/ComposableController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index ba12539ace4..ff208a99aa1 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -155,8 +155,7 @@ export class ComposableController extends BaseController< } /** - * Constructor helper that adds a child controller instance to composable controller state - * and subscribes to child controller state changes. + * Constructor helper that subscribes to child controller state changes. * @param controller - Controller instance to update * TODO: Remove `isBaseControllerV1` branch once `BaseControllerV2` migrations are completed for all controllers. */ From 3b687f42f0d24cc519dd499fb492d2e1f29c3948 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 21 Feb 2024 14:47:20 -0500 Subject: [PATCH 21/25] Add jsdoc for types `BaseControllerV{1,2}Instance`, `ControllerInstance` --- .../src/ComposableController.ts | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index ff208a99aa1..6cecf0b5072 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -10,25 +10,33 @@ import { isValidJson, type Json } from '@metamask/utils'; export const controllerName = 'ComposableController'; -/* - * The following three types encompass controllers based on either BaseControllerV1 or - * BaseController. The BaseController type can't be included directly - * because the generic parameters it expects require knowing the exact state - * shape, so instead we look for an object with the BaseController properties - * that we use in the ComposableController (name and state). - */ -// As explained above, `any` is used to include all `BaseControllerV1` instances. // TODO: Remove this type once `BaseControllerV2` migrations are completed for all controllers. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type BaseControllerV1Instance = BaseControllerV1; +/** + * A type encompassing all controller instances that extend from `BaseControllerV1`. + */ +type BaseControllerV1Instance = + // `any` is used to include all `BaseControllerV1` instances. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + BaseControllerV1; -type BaseControllerV2Instance = { name: string; state: Record }; +/** + * A type encompassing all controller instances that extend from `BaseController` (formerly `BaseControllerV2`). + * + * The `BaseController` class itself can't be used directly as a type representing all of its subclasses, + * because the generic parameters it expects require knowing the exact shape of the controller's state and messenger. + * + * Instead, we look for an object with the `BaseController` properties that we use in the ComposableController (name and state). + */ +type BaseControllerV2Instance = { + name: string; + state: Record; +}; -type ControllerInstance = - // As explained above, `any` is used to include all `BaseControllerV1` instances. - // TODO: Remove `BaseControllerV1Instance` once `BaseControllerV2` migrations are completed for all controllers. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - BaseControllerV1Instance | BaseControllerV2Instance; +// TODO: Remove `BaseControllerV1Instance` member once `BaseControllerV2` migrations are completed for all controllers. +/** + * A type encompassing all controller instances that extend from `BaseControllerV1` or `BaseController`. + */ +type ControllerInstance = BaseControllerV1Instance | BaseControllerV2Instance; /** * Determines if the given controller is an instance of BaseControllerV1 From c5edafd7ea19679070a746435e2b5e5243709486 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 21 Feb 2024 14:47:47 -0500 Subject: [PATCH 22/25] Export types `ControllerInstance`, `BaseControllerV{1,2}Instance` --- packages/composable-controller/CHANGELOG.md | 3 ++- .../composable-controller/src/ComposableController.ts | 8 +++++--- packages/composable-controller/src/index.ts | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md index 401ebee7b27..09c143b4211 100644 --- a/packages/composable-controller/CHANGELOG.md +++ b/packages/composable-controller/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add exports `isBaseControllerV1` and `isBaseController`, which are type guards for validating controller instances ([#3904](https://github.com/MetaMask/core/pull/3904)) +- Add and export functions `isBaseControllerV1` and `isBaseController`, which are type guards for validating controller instances ([#3904](https://github.com/MetaMask/core/pull/3904)) +- Add and export types `BaseControllerV1Instance`, `BaseControllerV2Instance`, `ControllerInstance` which are the narrowest supertypes for all controllers extending from, respectively, `BaseControllerV1`, `BaseController`, and both ([#3904](https://github.com/MetaMask/core/pull/3904)) ### Changed diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index 6cecf0b5072..fc3fd2565be 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -14,7 +14,7 @@ export const controllerName = 'ComposableController'; /** * A type encompassing all controller instances that extend from `BaseControllerV1`. */ -type BaseControllerV1Instance = +export type BaseControllerV1Instance = // `any` is used to include all `BaseControllerV1` instances. // eslint-disable-next-line @typescript-eslint/no-explicit-any BaseControllerV1; @@ -27,7 +27,7 @@ type BaseControllerV1Instance = * * Instead, we look for an object with the `BaseController` properties that we use in the ComposableController (name and state). */ -type BaseControllerV2Instance = { +export type BaseControllerV2Instance = { name: string; state: Record; }; @@ -36,7 +36,9 @@ type BaseControllerV2Instance = { /** * A type encompassing all controller instances that extend from `BaseControllerV1` or `BaseController`. */ -type ControllerInstance = BaseControllerV1Instance | BaseControllerV2Instance; +export type ControllerInstance = + | BaseControllerV1Instance + | BaseControllerV2Instance; /** * Determines if the given controller is an instance of BaseControllerV1 diff --git a/packages/composable-controller/src/index.ts b/packages/composable-controller/src/index.ts index 9deb15385bb..c803f27d442 100644 --- a/packages/composable-controller/src/index.ts +++ b/packages/composable-controller/src/index.ts @@ -1,4 +1,7 @@ export type { + BaseControllerV1Instance, + BaseControllerV2Instance, + ControllerInstance, ComposableControllerState, ComposableControllerStateChangeEvent, ComposableControllerEvents, From 9e7a6255738acece3912bc29f002cd6107a8273f Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 21 Feb 2024 11:50:07 -0800 Subject: [PATCH 23/25] Update packages/composable-controller/CHANGELOG.md Co-authored-by: Elliot Winkler --- packages/composable-controller/CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md index 09c143b4211..da123f4ceb4 100644 --- a/packages/composable-controller/CHANGELOG.md +++ b/packages/composable-controller/CHANGELOG.md @@ -14,12 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Add `@metamask/utils` ^8.3.0 as a dependency. ([#3904](https://github.com/MetaMask/core/pull/3904)) -- Throws an error if a non-controller is passed into the `controllers` constructor option ([#3904](https://github.com/MetaMask/core/pull/3904)) +- **BREAKING:** Passing a non-controller into `controllers` constructor option now throws an error ([#3904](https://github.com/MetaMask/core/pull/3904)) +- **BREAKING:** The `AllowedActions` parameter of the `ComposableControllerMessenger` type is narrowed from `string` to `never`, as `ComposableController` does not use any external controller actions. ([#3904](https://github.com/MetaMask/core/pull/3904)) +- Add `@metamask/utils` ^8.3.0 as a dependency. ([#3904](https://github.com/MetaMask/core/pull/3904)) ### Removed - -- **BREAKING:** The `AllowedActions` parameter of the `ComposableControllerMessenger` type is narrowed from `string` to `never`, as `ComposableController` does not use any external controller actions. ([#3904](https://github.com/MetaMask/core/pull/3904)) - **BREAKING:** Remove `ControllerList` as an exported type. ([#3904](https://github.com/MetaMask/core/pull/3904)) ## [5.0.1] From 8c5accecb2c7e2137aae6a014e9b33cd377a8b19 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 21 Feb 2024 11:50:47 -0800 Subject: [PATCH 24/25] Update import statement for non-dependency to use `@metamask/` prefix Co-authored-by: Elliot Winkler --- packages/composable-controller/src/ComposableController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/composable-controller/src/ComposableController.test.ts b/packages/composable-controller/src/ComposableController.test.ts index 6087de8313e..303e956951b 100644 --- a/packages/composable-controller/src/ComposableController.test.ts +++ b/packages/composable-controller/src/ComposableController.test.ts @@ -10,7 +10,7 @@ import { import type { Patch } from 'immer'; import * as sinon from 'sinon'; -import { JsonRpcEngine } from '../../json-rpc-engine/src/JsonRpcEngine'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { ComposableControllerEvents } from './ComposableController'; import { ComposableController } from './ComposableController'; From 856001b32713f59e71529a45cc31cab1d3d05592 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 21 Feb 2024 15:01:01 -0500 Subject: [PATCH 25/25] Linter fixes Linter fix --- packages/composable-controller/CHANGELOG.md | 1 + packages/composable-controller/src/ComposableController.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md index da123f4ceb4..32709313aef 100644 --- a/packages/composable-controller/CHANGELOG.md +++ b/packages/composable-controller/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `@metamask/utils` ^8.3.0 as a dependency. ([#3904](https://github.com/MetaMask/core/pull/3904)) ### Removed + - **BREAKING:** Remove `ControllerList` as an exported type. ([#3904](https://github.com/MetaMask/core/pull/3904)) ## [5.0.1] diff --git a/packages/composable-controller/src/ComposableController.test.ts b/packages/composable-controller/src/ComposableController.test.ts index 303e956951b..868605deb1e 100644 --- a/packages/composable-controller/src/ComposableController.test.ts +++ b/packages/composable-controller/src/ComposableController.test.ts @@ -7,10 +7,10 @@ import { BaseControllerV1, ControllerMessenger, } from '@metamask/base-controller'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { Patch } from 'immer'; import * as sinon from 'sinon'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { ComposableControllerEvents } from './ComposableController'; import { ComposableController } from './ComposableController';