MetaMask / MetaMask/metamask-extension

[Epic] `MetamaskController` decomposition: Extract cross-client modules as `wallet-services`

Open
#41,736 2 comments 0 reactions 0 assignees View on GitHub
for-migration INVALID-ISSUE-TEMPLATE mig-epic-6-child mig-epic-65 team-extension-platform
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.