Skip to content

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

Merged
sirtimid merged 6 commits into
mainfrom
sirtimid/wallet-cli-wallet-factory
Jun 25, 2026
Merged

feat(wallet-cli): add wallet factory and daemon entry point#9226
sirtimid merged 6 commits 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
Touches keyring/SRP import, encrypted vault persistence, and exposes the full wallet messenger over a Unix socket with only filesystem permissions as the access boundary.

Overview
Adds createWallet and daemon-entry so the CLI can run a long-lived background wallet process backed by SQLite persistence.

createWallet opens a KeyValueStore, hydrates controller state (via a short-lived metadata probe Wallet), wires headless instanceOptions (in-memory storageService, AlwaysOnlineAdapter, Infura + remote feature flags), subscribes persistence, runs wallet.init() (startup aborts if any step fails), imports the SRP on first run when no vault exists, and returns { wallet, dispose } with ordered, idempotent teardown. First-run failures can delete on-disk DB files.

daemon-entry validates required env vars, removes password/SRP from process.env after capture, locks the data dir to 0o700, runs claimDaemonSlot (stale socket/PID cleanup, refuse live siblings), claims the daemon with an exclusive PID write before opening the DB, builds the wallet, and serves getStatus plus a call RPC that forwards arbitrary messenger actions. Shutdown/SIG handlers coalesce teardown and only remove PID files the process owns.

Also adds remote-feature-flag-controller and storage-service dependencies and a small Readonly typing tweak in persistence subscriptions.

Reviewed by Cursor Bugbot for commit 180ffc1. 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 2 times, most recently 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
Comment thread packages/wallet-cli/src/daemon/wallet-factory.ts Outdated
Comment thread packages/wallet-cli/src/daemon/daemon-entry.ts Outdated
sirtimid and others added 3 commits June 25, 2026 14:48
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>
…solved logger

Address PR review feedback:

- Extract the daemon data-dir mkdir + 0o700 chmod into a dedicated
  `ensureOwnerOnlyDirectory` in `data-dir.ts`, moving the access-control
  rationale into its JSDoc. This gives the owner-only guarantee a named
  import boundary the daemon-entry test can assert against directly.
- Forward the resolved `logFn` (not the raw optional `log`) to
  `subscribeToChanges` in `createWallet`, so persistence-write failures
  always use the daemon's logger instead of falling through to the
  console fallback inconsistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…let factory

`@metamask/wallet` 5.0.0 (#9001) makes
`instanceOptions.networkController.infuraProjectId` a required option.
Thread the daemon's already-validated `INFURA_PROJECT_ID` through
`createWallet` and `buildInstanceOptions` (and the metadata probe) into the
new `networkController` slot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-wallet-factory branch from 7854c96 to 340a7b4 Compare June 25, 2026 13:18
@sirtimid sirtimid requested a review from grypez June 25, 2026 13:18
@sirtimid sirtimid enabled auto-merge June 25, 2026 13:19
@sirtimid sirtimid disabled auto-merge June 25, 2026 14:34
sirtimid and others added 3 commits June 25, 2026 16:59
`@metamask/wallet` 5.0.0 (#9001) added `Wallet.init()`, which runs each
instance's `init()` — notably `NetworkController.init()`, which applies the
selected network so a provider proxy is available. Without it the daemon
would serve RPC against a wallet that has no usable provider.

Call `wallet.init()` after subscribing persistence (so init-driven state
changes are written through) and before first-run SRP import. `init()`
settles every step independently, so surface any rejected step via the
logger rather than letting it pass silently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the fixes surfaced by the PR review:

- Abort startup when a `wallet.init()` step rejects instead of logging and
  continuing. A failed `NetworkController.init` leaves no usable provider, and
  `getStatus`/`pingDaemon` would still report the daemon healthy — so serving
  RPC against it hid the failure. Now the rejected steps are logged and
  `createWallet` throws, letting the existing teardown + first-run DB cleanup
  run.
- Narrow the RPC `call` dispatch from `messenger as any` to a contained
  `ArbitraryDispatch` structural cast through `unknown`, restoring arity
  checks and removing the `no-explicit-any` disable.
- Widen `subscribeToChanges`/`subscribeToStateChanged` to accept a
  `Readonly` messenger so the factory no longer casts `wallet.messenger`.
- Extract `CreateWalletConfig` and a shared `Logger` type.
- Comment cleanups: note the intentionally-omitted `keyringController` slot,
  flag the `ClientType.Extension` CLI semantics, document the crash-mid-import
  vault gap, give the signal-handler istanbul-ignores reasons, and trim
  comments that restated the code.

Add tests for the subscribe→init→import ordering and both init-failure paths
(settled-rejected results and a rejected `init()` promise); coverage stays at
100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`MM_WALLET_PASSWORD` and `MM_WALLET_SRP` were read into locals but left in
`process.env` for the daemon's whole lifetime. Since the daemon is long-lived
and dispatches arbitrary messenger actions over its socket, delete both from
the environment once captured to limit their exposure to in-process code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid sirtimid added this pull request to the merge queue Jun 25, 2026
Merged via the queue into main with commit 13f811b Jun 25, 2026
396 checks passed
@sirtimid sirtimid deleted the sirtimid/wallet-cli-wallet-factory branch June 25, 2026 16:24
pull Bot pushed a commit to dmrazzy/core that referenced this pull request Jun 30, 2026
…oke test (MetaMask#9255)

## Explanation

The final landing slice for `@metamask/wallet-cli`, after the scaffold
(MetaMask#9065), persistence (MetaMask#9067), transport (MetaMask#9108), and factory (MetaMask#9226)
slices. It adds the user-facing command surface so the package is
runnable:

- **`src/commands/daemon/{start,stop,status,purge,call}.ts`** (+ tests)
— the `mm daemon` command suite over the daemon layers already on
`main`: `start` spawns the daemon and reports its socket,
`status`/`stop` ping and tear it down, `purge` deletes the daemon state
files, and `call` dispatches an arbitrary `Controller:method` messenger
action.
- **`bin/dev.mjs` + `bin/dev.cmd`** — the dev-mode launchers deferred
from the scaffold slice, run via the `tsx` loader (`knip`
`ignoreDependencies: ['tsx']`).
- **`wallet-factory.e2e.test.ts`** — a smoke test that feeds the wired
`instanceOptions` into a **real** `Wallet` (no mock) against `:memory:`,
imports the SRP, and dispatches a messenger action — closing the gap
flagged in MetaMask#9226 review. Offline/CI-safe (neither construction nor
`wallet.init()` reaches the network).
- **`src/test/run-command.ts`** + a README **Usage** section.

## Checklist

- [x] `build`, `test` (100% coverage), `lint`, and `changelog:validate`
pass.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> New CLI paths accept wallet secrets and can invoke arbitrary messenger
actions over a local socket; `purge` deletes persisted wallet state,
though safeguards limit deletion when the daemon is still responsive.
> 
> **Overview**
> Adds the runnable **`mm daemon`** command surface on top of the
existing spawn/client/stop layers: **`start`** (Infura/password/SRP via
flags or env), **`stop`**, **`status`** (orphan-PID and stale-PID
warnings), **`purge`** (confirm/`--force`, whitelist-only file deletion,
refuses purge while the daemon still responds), and **`call`** (JSON-RPC
`call` with optional JSON-array args and timeout, TTY vs piped JSON
output).
> 
> Also ships **`bin/dev.mjs`/`dev.cmd`** (oclif dev mode via **`tsx`**),
a **`runCommand`** oclif test harness, a **real `Wallet` `:memory:`
e2e** for `createWallet`, Jest **Web Crypto polyfill** environment for
KeyringController tests, README **Usage**, and knip/tsconfig tweaks for
**`wallet-cli`**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
15fbe92. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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