You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: domains/coding/skills/controller-integration/repos/metamask-extension.md
+10-3Lines changed: 10 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,21 +15,28 @@ The integration surface is `app/scripts/messenger-client-init/` (the pattern is
15
15
| Stateless service | No persisted state (e.g. an API service) | Same as modular, minus state steps |
16
16
| Wallet-managed | Name is one of: AccountsController, ApprovalController, ConnectivityController, KeyringController, NetworkController, RemoteFeatureFlagController, StorageService |`this.wallet.getInstance('<Name>')` — construction happens inside `@metamask/wallet`|
17
17
18
+
Independently of the path above, the controller class itself comes from one of two sources — the `messenger-client-init/` wiring in §3 is identical either way, only step 1 (and where types are imported from) differs:
19
+
20
+
| Source | When | Where the class lives |
21
+
|---|---|---|
22
+
| npm package | Controller is owned/shared outside the extension (most new controllers) |`@metamask/<name>-controller`, imported as a dependency |
23
+
| Repo-local | Controller is extension-only |`app/scripts/controllers/<name>/` (class + a `<name>-controller.types.ts` exporting the messenger type) — e.g. `app/scripts/controllers/rewards/rewards-controller.ts`|
|`app/scripts/messenger-client-init/transaction-pay-controller-init.ts` + `messengers/transaction-pay-controller-messenger.ts`| Separate init messenger used inside constructor callbacks, heavy cross-controller delegation |
24
-
|`app/scripts/messenger-client-init/rewards-controller-init.ts` + `messengers/rewards-controller-messenger.ts`|Remote-feature-flag gating via init messenger |
31
+
|`app/scripts/controllers/rewards/rewards-controller.ts` (+ `rewards-controller.types.ts` exporting `RewardsControllerMessenger`) + `app/scripts/messenger-client-init/rewards-controller-init.ts` + `messengers/rewards-controller-messenger.ts`|Repo-local controller pattern, plus remote-feature-flag gating via init messenger. The init/messenger files are indistinguishable in shape from the npm-package case — only the class import path differs|
|`app/scripts/messenger-client-init/data-deletion-service-init.ts`| Stateless service |
27
34
28
35
## 3. Ordered steps
29
36
30
37
| # | File | Action |
31
38
|---|---|---|
32
-
| 1 |`package.json`|`yarn add @metamask/<name>-controller`, then `yarn lint:lockfile:dedupe:fix && yarn allow-scripts auto && yarn lavamoat:auto && yarn attributions:generate` (skip if repo-local)|
39
+
| 1 |`package.json`, or `app/scripts/controllers/<name>/`|**npm package**: `yarn add @metamask/<name>-controller`, then `yarn lint:lockfile:dedupe:fix && yarn allow-scripts auto && yarn lavamoat:auto && yarn attributions:generate`. **Repo-local**: create the folder with the controller class and a `<name>-controller.types.ts` exporting the messenger type — mirror `app/scripts/controllers/rewards/` — no dependency commands needed|
33
40
| 2 |`app/scripts/messenger-client-init/messengers/<name>-controller-messenger.ts`| Create `getXControllerMessenger` (skeleton below). Add `getXControllerInitMessenger` (namespace `'XControllerInit'`) only if the init fn itself needs actions/events beyond the controller's messenger type |
34
41
| 3 |`app/scripts/messenger-client-init/messengers/<name>-controller-messenger.test.ts`|`expect(messenger).toBeInstanceOf(Messenger)`; optionally assert delegation via a `delegate` spy |
35
42
| 4 |`app/scripts/messenger-client-init/messengers/index.ts`| Export the factory fns; add to `MESSENGER_FACTORIES`: `{ getMessenger, getInitMessenger: noop }` (lodash `noop`) or the real init-messenger fn |
| Stateless service | No persisted state (e.g. an API service) | Same as modular, minus state steps |
14
14
| Wallet-owned | Name is one of: AccountsController, ApprovalController, ConnectivityController, KeyringController, NetworkController, RemoteFeatureFlagController, StorageService |`this.#wallet.getInstance('<Name>')` — never an init function |
15
15
16
+
Independently of the path above, the controller class itself comes from one of two sources — the wiring steps in §3 are identical either way, only step 1 (and where types are imported from) differs:
17
+
18
+
| Source | When | Where the class lives |
19
+
|---|---|---|
20
+
| npm package | Controller is owned/shared outside mobile (most new controllers) |`@metamask/<name>-controller`, imported as a dependency |
|`app/core/Engine/controllers/card-controller/` (`CardController.ts`, `types.ts`, `index.ts` as init) + `app/core/Engine/messengers/card-controller-messenger.ts`| Repo-local controller pattern: class + messenger type + default state co-located in one folder, init fn in the folder's `index.ts`|
30
+
|`app/core/Engine/controllers/rewards-controller/` (`RewardsController.ts`) + `app/core/Engine/messengers/rewards-controller-messenger.ts`| Repo-local controller with remote-flag gating |
22
31
|`app/features/SampleFeature/controllers/sample-petnames-controller-init.ts`|`ONLY_INCLUDE_IF(sample-feature)` fencing only — its tests/selector lag the pattern, do not copy those |
23
32
24
33
## 3. Ordered steps
25
34
26
35
| # | File | Action |
27
36
|---|---|---|
28
-
| 1 |`package.json`|`yarn add @metamask/<name>-controller` (skip if repo-local)|
37
+
| 1 |`package.json`, or `app/core/Engine/controllers/<name>-controller/`|**npm package**: `yarn add @metamask/<name>-controller`. **Repo-local**: create the folder with the controller class (extend `BaseController`), `types.ts` (state, messenger type, `ControllerStateChangeEvent`-based Events union), and default state — mirror `card-controller/` or `rewards-controller/`|
29
38
| 2 |`app/core/Engine/messengers/<name>-controller-messenger.ts`| Create `getXControllerMessenger` (skeleton below). Add `getXControllerInitMessenger` too if init-time gating is needed (own namespace `<Name>Initialization`) |
| 7 |`app/core/Engine/controllers/<name>-controller-init.test.ts`| Use `buildMessengerClientInitRequestMock` from `app/core/Engine/utils/test-utils.ts`|
41
+
| 5 |`app/core/Engine/types.ts`| Add to `GlobalActions`, `GlobalEvents`, `MessengerClients`, `EngineState`, `MessengerClientsToInitialize` (+ `RequiredControllers`/`OptionalControllers` if optional). Import the controller/messenger types from `@metamask/<name>-controller` (package) or from the local `controllers/<name>-controller/types.ts` (repo-local)|
42
+
| 6 |`app/core/Engine/controllers/<name>-controller-init.ts`, or `app/core/Engine/controllers/<name>-controller/index.ts` for a repo-local controller| Create `MessengerClientInitFunction` (skeleton below), return `{ controller }` only |
43
+
| 7 |`app/core/Engine/controllers/<name>-controller-init.test.ts`(co-located `index.test.ts` if repo-local) | Use `buildMessengerClientInitRequestMock` from `app/core/Engine/utils/test-utils.ts`|
35
44
| 8 |`app/core/Engine/constants.ts`| Append `'<Name>:stateChange'` to `BACKGROUND_STATE_CHANGE_EVENT_NAMES`. ⚠️ Copy the exact event name from the package's `Events` type — some emit `:stateChanged`|
36
45
| 9 |`app/core/Engine/Engine.ts`| (a) import init fn (b) add to `initFunctions`**after** every controller it depends on — insertion order is init order (c) add to `this.context` (d) add to **both** the destructuring and return object of `get state()`|
37
46
| 10 |`app/util/test/initial-background-state.json`| Add default state (run step in §7 verify to get the exact expected value) |
| Controller missing from `Engine.state` getter |`UPDATE_BG_STATE` writes `undefined` into Redux for that key |
105
114
| Calling a non-delegated action | Runtime throw from `@metamask/messenger` at call time |
106
115
| Dependency read before it's initialized |`Error: Messenger client requested before it was initialized: <Name>` — move the entry later in `initFunctions`|
107
-
| Controller metadata has no `persist: true` key | Redux updates but state never survives restart (silently skipped, logged only) |
116
+
| Controller metadata has no `persist: true` key | Redux updates but state never survives restart (silently skipped, logged only). For a repo-local controller this metadata is authored by you in the class's own `metadata` object, not inherited from a package|
0 commit comments