Skip to content

feat(wallet-cli): add wallet factory and daemon entry point#9226

Open
sirtimid wants to merge 1 commit into
mainfrom
sirtimid/wallet-cli-wallet-factory
Open

feat(wallet-cli): add wallet factory and daemon entry point#9226
sirtimid wants to merge 1 commit into
mainfrom
sirtimid/wallet-cli-wallet-factory

Conversation

@sirtimid

@sirtimid sirtimid commented Jun 23, 2026

Copy link
Copy Markdown
Member

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.tscreateWallet() builds a @metamask/wallet Wallet over the SQLite KeyValueStore, 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 via dispose on shutdown.

It uses the current @metamask/wallet instanceOptions API — storageService (in-memory adapter), connectivityController (AlwaysOnlineAdapter), and remoteFeatureFlagController. NetworkController and TransactionController slots are left as TODOs until they're wired on main.

Supersedes draft #8847 (the { wallet, dispose } refactor), folded in here on top of main.

Closes #8779.

Checklist

  • build, test (100% coverage on both new files), lint, changelog:validate, and constraints pass.

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 createWallet in wallet-factory.ts and the long-running daemon-entry process so the CLI daemon can own a real @metamask/wallet instance.

The factory opens the SQLite KeyValueStore, hydrates controller state (via a short-lived metadata probe), wires wallet instanceOptions (in-memory storage, always-online connectivity, remote feature flags, no-op approvals), subscribes persistence, imports the SRP only when no KeyringController vault exists, and returns an idempotent dispose teardown. Failed first-run setup deletes on-disk DB files so a retry does not skip import.

daemon-entry validates required env vars, locks down the data directory, runs claimDaemonSlot (ping socket, refuse live siblings, clear stale PID/socket), then claims the PID with wx before opening the DB or building the wallet. It starts the Unix JSON-RPC server with getStatus and a call handler that forwards arbitrary messenger actions, and shuts down on signals/RPC with ownership-safe PID cleanup.

Package deps add @metamask/remote-feature-flag-controller and @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.

@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-wallet-factory branch from 39eb299 to a8f34af Compare June 23, 2026 09:36
@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-wallet-factory branch from a8f34af to 5f93797 Compare June 23, 2026 09:55
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>
@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-wallet-factory branch from 5f93797 to 6fbe77a Compare June 23, 2026 10:15
@sirtimid sirtimid marked this pull request as ready for review June 23, 2026 10:29
@sirtimid sirtimid requested review from a team as code owners June 23, 2026 10:29
@sirtimid sirtimid temporarily deployed to default-branch June 23, 2026 10:29 — with GitHub Actions Inactive

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/wallet-cli/src/daemon/daemon-entry.ts
wallet.messenger as RootMessenger<DefaultActions, DefaultEvents>,
wallet.controllerMetadata,
store,
log,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log,
log: logFn,

I think you meant this?

Comment on lines +47 to +53
// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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.

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.

wallet-cli: Replace { wallet, store } with { wallet, dispose }

2 participants