Skip to content

feat(wallet-cli): add daemon JSON-RPC socket transport#9108

Draft
sirtimid wants to merge 7 commits into
mainfrom
sirtimid/wallet-cli-daemon-transport
Draft

feat(wallet-cli): add daemon JSON-RPC socket transport#9108
sirtimid wants to merge 7 commits into
mainfrom
sirtimid/wallet-cli-daemon-transport

Conversation

@sirtimid

Copy link
Copy Markdown
Member

Explanation

@metamask/wallet-cli runs a background daemon that hosts a @metamask/wallet instance; the CLI talks to it over a Unix socket. This PR adds that transport layer. It is purely additive plumbing — no mm subcommand consumes it yet (commands land in a follow-up), so there is no user-visible behavior change.

  • socket-linewriteLine/readLine for newline-delimited framing over a net.Socket, with an optional read timeout and listener cleanup.
  • daemon-clientsendCommand opens a connection, writes one JSON-RPC request, reads one response, and closes; it correlates the response id with the request and retries once on transient connection errors (ECONNREFUSED/ECONNRESET). pingDaemon is a lightweight getStatus health probe that distinguishes "no daemon" (absent) from "daemon present but unreachable", classifying the latter by failure mode (refused / timeout / permission / protocol / other).
  • rpc-socket-serverstartRpcSocketServer listens on a Unix socket and dispatches one JSON-RPC request per connection to a handler map. It intercepts a built-in shutdown method, enforces one-request-per-connection, times out idle connections, and returns a close() handle.
  • daemon-spawnensureDaemon spawns a detached daemon process and polls until the socket is responsive, refusing to take over a wedged or foreign-owned socket. Resolves the entry point from dist (prod) or src via tsx (dev).
  • stop-daemonstopDaemon escalates from a graceful shutdown RPC through SIGTERM to SIGKILL, then removes the PID and socket files best-effort.
  • promptsconfirmPurge wraps the ESM-only @inquirer/confirm via dynamic import.
  • types — adds RpcHandler, RpcHandlerMap, DaemonStatusInfo, and DaemonSpawnConfig.

Adds @inquirer/confirm and @metamask/rpc-errors dependencies. Every daemon module is covered to the package's 100% coverage thresholds, plus a socket-integration.test.ts that exercises the client and server together over a real Unix socket (framing, id correlation, the one-request-per-connection invariant, and real shutdown timing).

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

🤖 Generated with Claude Code

sirtimid and others added 6 commits June 11, 2026 18:31
Add the persistence layer for the wallet daemon:

- `KeyValueStore`: a synchronous `better-sqlite3`-backed key-value store.
- `loadState`: rehydrates per-controller state from the store.
- `subscribeToChanges`: writes persist-flagged controller state through to
  disk on every `<Controller>:stateChanged` event, returning an unsubscribe
  handle. (The teardown handle, not a `Wallet:destroyed` event, owns
  unsubscription — that event does not exist in `@metamask/wallet`.)

`better-sqlite3` ships a native addon that Yarn does not build at install
time (`enableScripts: false`), so a `test:prepare` step fetches a matching
prebuild via `scripts/install-binaries.sh`, `engines.node` is bumped to
`>=20` (no Node 18 prebuilds), and the package is excluded from the CI
Node 18.x test matrix. `yarn.config.cjs` exempts the package from the
standard `test` script and engines constraints accordingly.

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

better-sqlite3 12.10.0 dropped its Node 20 (ABI v115) Linux prebuild that
12.9.0 still shipped, so on CI `prebuild-install` finds no matching binary and
the `test:prepare` script aborts under `set -e`. Mirror better-sqlite3's own
install step (`prebuild-install || node-gyp rebuild --release`) so the addon
is compiled from source whenever no prebuild is published for the active Node
ABI/platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
loadState merged every row from the store into the Wallet constructor
state, while subscribeToChanges only ever writes persist-flagged
properties. After a migration removed a controller or disabled a
property's persist flag, the stale row lingered on disk and was
resurrected on restart. loadState now takes controllerMetadata and skips
rows that are not currently persist-flagged, through a shared isPersisted
predicate used by both the read and write paths.

Also addresses review feedback:
- Narrow the persistence-write try/catch so a throwing StateDeriver
  surfaces on its own instead of being misreported as a write failure;
  cover the deriver-throws and serializes-to-undefined cases with tests.
- Replace the statement-level @ts-expect-error on messenger
  subscribe/unsubscribe with a typed subscribeToStateChanged helper that
  keeps the handler payload shape compile-checked.
- Surface prebuild-install's failure before falling back to a source
  build in install-binaries.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the daemon's IPC transport layer for `@metamask/wallet-cli`. This is
purely additive plumbing — no `mm` subcommand consumes it yet (commands
land in a later slice), so there is no user-visible behavior change.

- socket-line: newline-delimited read/write framing over a `net.Socket`.
- daemon-client: `sendCommand` (one request per connection, id-correlated,
  retries once on transient connection errors) and `pingDaemon` (a
  `getStatus` health probe that classifies unreachable daemons by failure
  mode: refused / timeout / permission / protocol / other).
- rpc-socket-server: `startRpcSocketServer` listens on a Unix socket and
  dispatches one JSON-RPC request per connection to a handler map, with a
  built-in `shutdown` method and idle-connection timeout.
- daemon-spawn: `ensureDaemon` spawns a detached daemon and polls until the
  socket is responsive, refusing to take over a wedged or foreign socket.
- stop-daemon: `stopDaemon` escalates from a graceful `shutdown` RPC through
  SIGTERM to SIGKILL, then cleans up the PID and socket files.
- prompts: `confirmPurge` wraps the ESM-only `@inquirer/confirm`.
- types: add `RpcHandler`, `RpcHandlerMap`, `DaemonStatusInfo`, and
  `DaemonSpawnConfig`.

Adds `@inquirer/confirm` and `@metamask/rpc-errors` dependencies. All daemon
modules are covered to the package's 100% thresholds, plus an end-to-end
socket integration test exercising both halves over a real Unix socket.

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

socket-security Bot commented Jun 12, 2026

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
Updated@​inquirer/​confirm@​6.0.7 ⏵ 6.1.1100 +11009895100

View full report

Collapse duplicate @inquirer/* entries introduced by the daemon
transport dependencies to their highest resolved versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-persistence branch from b9854de to fbcddc5 Compare June 16, 2026 18:04
Base automatically changed from sirtimid/wallet-cli-persistence to main June 16, 2026 18:19
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