MetaMask / MetaMask/metamask-extension
[Epic] `MetamaskController` decomposition: Extract cross-client modules as `wallet-services`
- Dominant language
- TypeScript
- Stars
- 13.2k
- Forks
- 5.6k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 451
Description
Extract the cross-client `wallet-services/` modules from `MetamaskController`, adopting published core packages where one already exists (snap-management → [`@metamask/snap-account-service`](https://github.com/MetaMask/core/tree/main/packages/snap-account-service), [#42522](https://github.com/MetaMask/metamask-extension/pull/42522)). Each extension-local extraction delivers full method bodies, structural messenger unit tests, `XXX_ACTIONS` constants, and `RootMessenger` union wiring. ESLint boundary rule locks the `wallet-services/` boundary in CI as the final bar.
**Parent Epic:** [#41735 — `MetamaskController` decomposition](https://github.com/MetaMask/metamask-extension/issues/41735)
## Tickets
| # | Ticket | Size |
| --- | ---------------------------------------------------------------------------- | ---- |
| A.1 | Extract `token-resolution` | S |
| A.2 | ~~Extract `snap-management`~~ → adopt core `snap-account-service` ([#42522](https://github.com/MetaMask/metamask-extension/pull/42522)) | S |
| A.3 | Extract `permission-management` | M |
| A.4 | Extract `account-management` | L |
| A.5 | Extract `transaction-lifecycle` | L |
| A.6 | Extract `vault-management` | XL |
| A.7 | Reconcile `registerWalletServices` messenger typing | M |
| A.8 | Remove `as never` cast from `register.ts` | S |
| A.9 | Add ESLint `import/no-restricted-paths` boundary rule for `wallet-services/` | S |
Each extraction ticket (A.1–A.6) delivers: (1) create the module directory, (2) port the method bodies from `metamask-controller.js` into the target module, (3) register the module's public methods as actions on a per-module delegating messenger that delegates to the root messenger (per the `WalletService`-messenger pattern from [`decisions#117`](https://github.com/MetaMask/decisions/pull/117)) — these are the actions the subsequent `getApi()` rewrites target, (4) audit every `messenger.call(...)` site against the target controller's registered actions, fixing wrong action names and using direct controller injection for class methods that are not yet registered as messenger actions, (5) delete the `MetamaskController`-side originals and rewrite the corresponding `getApi()` entries to call `messenger.call('Module:action', ...)` directly — no forwarding stub (a method whose entire body is a single `this.controllerMessenger.call(...)` delegation) may remain after the PR lands, and (6) add structural unit tests. No separate "final deletion" or "integration verification" ticket exists — each extraction PR is complete on its own.
**A.2 is the exception (consume-core path).** Its snap-keyring slice is delivered via the published core package `@metamask/snap-account-service` ([#42522](https://github.com/MetaMask/metamask-extension/pull/42522)): publish to core → wire via modular init → reroute call sites → delete MC bodies, with no extension-local body-port and no `snap-management/` module. Residual snap passthroughs fold into the [`decisions#117`](https://github.com/MetaMask/decisions/pull/117) `getApi()` rewrite; `handleWatchAssetRequest` moves to A.1. See [#41740](https://github.com/MetaMask/metamask-extension/issues/41740).
## Success bar
- All extension-local `wallet-services/` modules extracted to `main` with full bodies and structural unit tests (snap-management delivered via core `snap-account-service`, [#42522](https://github.com/MetaMask/metamask-extension/pull/42522), not a local module)
- [`register.ts`](https://github.com/MetaMask/metamask-extension/blob/main/app/scripts/lib/wallet-services/register.ts) has no `as never` cast
- `tsc` passes on all 6 modules
- `yarn test app/scripts/lib/wallet-services` green with structural mocks only
- `yarn lint` green with A.9 boundary rule active; deliberate violation caught
## Sequence note
A.6 (`vault-management`) is intentionally last — absorbs lessons from the smaller modules. A.7/A.8 (typing pass + cast removal) depend on all 6 bodies being final. A.9 (ESLint rule) is independent and can land any time.
Contributor guide
Research direction
This is a multi-ticket epic: start with app/scripts/lib/metamask-controller.js and app/scripts/lib/wallet-services/register.ts, then select an unclaimed extraction or follow-up ticket. Read the referenced WalletService messenger decision and structural tests under app/scripts/lib/wallet-services; completion requires the ticket’s module or wiring change, passing tsc and tests, and the stated lint boundary checks.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- eslint, typescript
- Domain
- backend-api-design, tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100