feat(wallet-cli): add wallet factory and daemon entry point#9226
feat(wallet-cli): add wallet factory and daemon entry point#9226sirtimid wants to merge 1 commit into
Conversation
39eb299 to
a8f34af
Compare
a8f34af to
5f93797
Compare
Add `createWallet`, which constructs a `@metamask/wallet` `Wallet` backed by the SQLite `KeyValueStore`, hydrates it from persisted state, imports the secret recovery phrase on first run, and returns a resilient `dispose` teardown handle (unsubscribe → wallet.destroy → store.close). Add the daemon process entry point (`daemon-entry.ts`) that wires `createWallet` into the JSON-RPC socket server with PID-slot claiming and graceful shutdown. Adapted from Erik Marks's #8446 modules with these deviations: - Use the current `@metamask/wallet` `instanceOptions` API, populating the now-required `storageService` (in-memory adapter), `connectivityController` (`AlwaysOnlineAdapter`), and `remoteFeatureFlagController` slots. Drop the old flat `Wallet({...})` options. - Drop `infuraProjectId` from the `Wallet` call (NetworkController is not yet wired on `main`); keep the env guard with a TODO(#9001). - Return `{ wallet, dispose }` instead of `{ wallet, store }`. - Construct a short-lived metadata probe so `loadState` can filter persisted rows against the live controller metadata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5f93797 to
6fbe77a
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6fbe77a. Configure here.
| wallet.messenger as RootMessenger<DefaultActions, DefaultEvents>, | ||
| wallet.controllerMetadata, | ||
| store, | ||
| log, |
There was a problem hiding this comment.
| log, | |
| log: logFn, |
I think you meant this?
| // 0o700: owner-only. The daemon exposes the full wallet messenger over | ||
| // the socket inside this directory, so anyone who can traverse the dir | ||
| // can also `connect()` to the socket. Restricting to the owning user is | ||
| // the only access-control boundary. We chmod after mkdir because the | ||
| // `mode` option is ignored when the directory already exists. | ||
| mkdirSync(dataDir, { recursive: true, mode: 0o700 }); | ||
| await chmod(dataDir, 0o700); |
There was a problem hiding this comment.
Awesome, this addresses the comment I raised in your last PR.
The associated unit test daemon-entry > creates data dir, wallet, server, and writes PID exclusively on successful startup asserts that mkdirSync is called with mode: 0o700, while this comment states this is not sufficient. That conflict makes this a good import boundary. Recommend moving these lines to a new function in data-dir.ts, with the comment in the jsdoc.
e.g.
| // 0o700: owner-only. The daemon exposes the full wallet messenger over | |
| // the socket inside this directory, so anyone who can traverse the dir | |
| // can also `connect()` to the socket. Restricting to the owning user is | |
| // the only access-control boundary. We chmod after mkdir because the | |
| // `mode` option is ignored when the directory already exists. | |
| mkdirSync(dataDir, { recursive: true, mode: 0o700 }); | |
| await chmod(dataDir, 0o700); | |
| await ensureOwnerOnlyDirectory(dataDir); |
and have the test assert that ensureOwnerOnlyDirectory was called.

Explanation
Adds the wallet factory and daemon entry point for
@metamask/wallet-cli, after the scaffold (#9065), persistence (#9067), and transport (#9108) slices:wallet-factory.ts—createWallet()builds a@metamask/walletWalletover the SQLiteKeyValueStore, hydrates it from persisted state, imports the SRP on first run, and returns{ wallet, dispose }.dispose(idempotent, resilient) runs unsubscribe →wallet.destroy()→store.close().daemon-entry.ts— the daemon process: claims the PID slot, builds the wallet, serves the messenger over the owner-only Unix socket, and tears down viadisposeon shutdown.It uses the current
@metamask/walletinstanceOptionsAPI —storageService(in-memory adapter),connectivityController(AlwaysOnlineAdapter), andremoteFeatureFlagController.NetworkControllerandTransactionControllerslots are left as TODOs until they're wired onmain.Supersedes draft #8847 (the
{ wallet, dispose }refactor), folded in here on top ofmain.Closes #8779.
Checklist
build,test(100% coverage on both new files),lint,changelog:validate, andconstraintspass.Note
High Risk
Handles SRP/password via environment variables and exposes the full wallet messenger over a local Unix socket (filesystem permissions only), which is security-sensitive keyring and RPC surface area despite intentional CLI design.
Overview
Adds
createWalletinwallet-factory.tsand the long-runningdaemon-entryprocess so the CLI daemon can own a real@metamask/walletinstance.The factory opens the SQLite
KeyValueStore, hydrates controller state (via a short-lived metadata probe), wires walletinstanceOptions(in-memory storage, always-online connectivity, remote feature flags, no-op approvals), subscribes persistence, imports the SRP only when noKeyringControllervault exists, and returns an idempotentdisposeteardown. Failed first-run setup deletes on-disk DB files so a retry does not skip import.daemon-entryvalidates required env vars, locks down the data directory, runsclaimDaemonSlot(ping socket, refuse live siblings, clear stale PID/socket), then claims the PID withwxbefore opening the DB or building the wallet. It starts the Unix JSON-RPC server withgetStatusand acallhandler that forwards arbitrary messenger actions, and shuts down on signals/RPC with ownership-safe PID cleanup.Package deps add
@metamask/remote-feature-flag-controllerand@metamask/storage-service; changelog and README dependency graph are updated. Large Jest suites cover factory lifecycle and daemon startup/race/shutdown paths.Reviewed by Cursor Bugbot for commit 6fbe77a. Bugbot is set up for automated code reviews on this repo. Configure here.