feat(wallet): implement Wallet class with controller ensemble#8853
feat(wallet): implement Wallet class with controller ensemble#8853grypez wants to merge 7 commits into
Conversation
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>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Caution MetaMask internal reviewing guidelines:
|
What this PR does
Introduces the
@metamask/walletpackage — a class that wires seven controllers behind a single shared messenger and exposes a minimal public API for constructing and managing a MetaMask wallet: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.Walletwrapsinitialize()and exposesmessenger,state,controllerMetadata, anddestroy().This pattern keeps the
Walletclass 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}:stateChangedevents 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
ConnectivityControllercurrently uses anAlwaysOnlineAdapterstub. Making this injectable (for React Native, which has real connectivity events) is tracked and will be added alongside thefetchinjection option in the next PR.Testing
Integration tests run against a live local chain via Anvil (installed automatically by the
pretesthook):yarn workspace @metamask/wallet run testFor verbose output:
The
encryptorFactoryunit tests (covering the PBKDF2 wiring forencrypt,encryptWithDetail, andisVaultUpdated) run without Anvil and are fast (~0.5s).Coverage thresholds are set to the measured actuals. The two uncovered paths —
ConnectivityController'sonConnectivityChangeandNetworkController's offline branch — are deferred pending the injectable connectivity adapter.