Skip to content

feat(wallet): implement Wallet class with controller ensemble#8853

Draft
grypez wants to merge 7 commits into
mainfrom
grypez/wallet-controllers
Draft

feat(wallet): implement Wallet class with controller ensemble#8853
grypez wants to merge 7 commits into
mainfrom
grypez/wallet-controllers

Conversation

@grypez
Copy link
Copy Markdown

@grypez grypez commented May 19, 2026

What this PR does

Introduces the @metamask/wallet package — a class that wires seven controllers behind a single shared messenger and exposes a minimal public API for constructing and managing a MetaMask wallet:

const wallet = new Wallet({
  infuraProjectId: '...',
  clientVersion: '1.0.0',
  showApprovalRequest: () => openApprovalModal(),
  clientConfigApiService,
  getMetaMetricsId: () => metricsId,
  state: storedState, // optional
});

// Read controllers via the root messenger
wallet.messenger.call('AccountsController:listAccounts');

// Persist state changes
wallet.messenger.subscribe('KeyringController:stateChanged', (state) => {
  storage.set('KeyringController', state);
});

// Tear down
await wallet.destroy();

Controllers bundled: AccountsController, ApprovalController, ConnectivityController, KeyringController, NetworkController, RemoteFeatureFlagController, TransactionController.

Architecture

Each controller is defined as an InitializationConfiguration<Instance, Messenger> — a plain object with two functions:

  • messenger(parent) — carves a typed child messenger off the root, declaring exactly which actions and events that controller is permitted to use.
  • init({ state, messenger, options }) — constructs the controller instance.

initialize() runs these in sequence and returns the resulting instances. Wallet wraps initialize() and exposes messenger, state, controllerMetadata, and destroy().

This pattern keeps the Wallet class thin and makes the ensemble open to extension: callers can substitute or supplement the default configuration set without forking the class.

No persistence layer. Callers inject initial state and subscribe to ${ControllerName}:stateChanged events to write changes back to their own storage. The package has no opinion on SQLite, AsyncStorage, IndexedDB, or anything else. See the README for the full state contract.

What is intentionally deferred

  • Injectable connectivity adapter: ConnectivityController currently uses an AlwaysOnlineAdapter stub. Making this injectable (for React Native, which has real connectivity events) is tracked and will be added alongside the fetch injection option in the next PR.

Testing

Integration tests run against a live local chain via Anvil (installed automatically by the pretest hook):

yarn workspace @metamask/wallet run test

For verbose output:

yarn workspace @metamask/wallet run test:verbose

The encryptorFactory unit tests (covering the PBKDF2 wiring for encrypt, encryptWithDetail, and isVaultUpdated) run without Anvil and are fast (~0.5s).

Coverage thresholds are set to the measured actuals. The two uncovered paths — ConnectivityController's onConnectivityChange and NetworkController's offline branch — are deferred pending the injectable connectivity adapter.

grypez and others added 7 commits May 19, 2026 16:44
Add package.json (with dependencies, scripts, and constraints-compliant
pretest hook), tsconfig files, README, CHANGELOG, and the
install-binaries.sh script that fetches the Anvil binary before tests
run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce the core types that the controller ensemble is built on:

- WalletOptions — the public constructor options type
- InitializationConfiguration<Instance, Messenger> — a plugin type
  pairing a messenger factory with a controller constructor; each
  controller in the ensemble implements this shape
- InitializeArgs — the arguments threaded into each init() call
- bindMessengerAction — helper that binds a typed messenger call to a
  plain function, bridging the gap between controller option callbacks
  and messenger actions

Also adds utilities.ts with importSecretRecoveryPhrase,
createSecretRecoveryPhrase, and sendTransaction — internal helpers used
by tests and by callers integrating the wallet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add an InitializationConfiguration for each of the seven controllers in
the ensemble: AccountsController, ApprovalController,
ConnectivityController, KeyringController, NetworkController,
RemoteFeatureFlagController, TransactionController.

Each configuration follows the same shape: a messenger() factory that
carves a typed child messenger off the root (delegating only the actions
and events that controller requires), and an init() function that
constructs the controller instance with that messenger and the relevant
slice of WalletOptions.

Notable wiring decisions:
- NetworkController receives a getRpcServiceOptions factory that reads
  connectivity state from ConnectivityController via the messenger
- ConnectivityController uses an AlwaysOnlineAdapter stub (TODO: make
  injectable)
- TransactionController's sign option is bridged from
  KeyringController:signTransaction via bindMessengerAction
- :stateChange delegations for KeyringController and NetworkController
  use the deprecated event name because their downstream consumers have
  not yet migrated to :stateChanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add the initialize() function that runs InitializationConfigurations in
sequence and returns a map of controller instances and their state, and
the DEFAULT_CONFIGURATIONS array that defines the canonical ensemble
ordering.

The Wallet class wraps initialize(), exposes the root messenger,
aggregated state, controllerMetadata, and a destroy() method that
gracefully tears down all controllers and publishes Wallet:destroyed
exactly once.

Update src/index.ts to export the public surface: Wallet, WalletOptions,
and InitializationConfiguration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Wallet.test.ts covering:
- Account population after SRP import
- Transaction signing and submission against a local Anvil chain
- Secret recovery phrase creation
- State exposure
- controllerMetadata shape and filtering
- Wallet:destroyed lifecycle (published once, even on sync/async errors)

Add test/anvil.ts — a helper that spawns and tears down a local Anvil
instance for tests, configuring it with the test mnemonic so account
addresses are deterministic.

Remove the placeholder index.test.ts (greeter stub from package
creation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Export encryptorFactory so its returned functions can be tested directly
without constructing a full Wallet.

Add keyring-controller.test.ts with a describe block per factory:
- encrypt: round-trips and embeds the PBKDF2 iteration count
- encryptWithDetail: round-trips via the vault field and embeds the count
- isVaultUpdated: true for matching iterations, false for mismatched

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Connectivity-controller's onConnectivityChange and network-controller's
isOffline offline branch are intentionally uncovered for now:
- onConnectivityChange: AlwaysOnlineAdapter no-op, deferred
- isOffline offline path: requires injectable connectivity adapter,
  which will be added in the next PR alongside the fetch option

Thresholds set to the measured actuals so CI does not regress below
the current baseline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​metamask/​foundryup@​0.0.0-use.local100100100100100

View full report

@socket-security
Copy link
Copy Markdown

Caution

MetaMask internal reviewing guidelines:

  • Do not ignore-all
  • Each alert has instructions on how to review if you don't know what it means. If lost, ask your Security Liaison or the supply-chain group
  • Copy-paste ignore lines for specific packages or a group of one kind with a note on what research you did to deem it safe.
    @SocketSecurity ignore npm/PACKAGE@VERSION
Action Severity Alert  (click "▶" to expand/collapse)
Block Medium
Network access: npm @metamask/transaction-controller in module globalThis["fetch"]

Module: globalThis["fetch"]

Location: Package overview

From: ?npm/@metamask/transaction-controller@66.0.0

ℹ Read more on: This package | This alert | What is network access?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should remove all network access that is functionally unnecessary. Consumers should audit network access to ensure legitimate use.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@metamask/transaction-controller@66.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Potential code anomaly (AI signal): npm @metamask/transaction-controller is 75.0% likely to have a medium risk anomaly

Notes: The code performs straightforward signature verification using ethers.js, returning true when the recovered signer matches the provided publicKey. While generally safe, the silent catch and potential mismatch between data formatting and signing process should be addressed to avoid silent failures. Overall, a benign utility with moderate input-format sensitivity.

Confidence: 0.75

Severity: 0.50

From: ?npm/@metamask/transaction-controller@66.0.0

ℹ Read more on: This package | This alert | What is an AI-detected potential code anomaly?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: An AI system found a low-risk anomaly in this package. It may still be fine to use, but you should check that it is safe before proceeding.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@metamask/transaction-controller@66.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant