Skip to content

feat(wallet-cli): Add wallet CLI with daemon support#8446

Open
rekmarks wants to merge 21 commits intofeat/wallet-libraryfrom
rekm/wallet-cli
Open

feat(wallet-cli): Add wallet CLI with daemon support#8446
rekmarks wants to merge 21 commits intofeat/wallet-libraryfrom
rekm/wallet-cli

Conversation

@rekmarks
Copy link
Copy Markdown
Member

@rekmarks rekmarks commented Apr 14, 2026

Introduces an oclif CLI for @metamask/wallet. It is more or less a port of the Ocap Kernel equivalent.

Usage

First, cd packages/wallet-cli.

Then, to start the daemon:

yarn mm daemon start \
  --infura-project-id <YOUR_PROJECT_ID> \
  --password testpass \
  --srp 'test test test test test test test test test test test ball'

Try calling an available controller action:

yarn mm daemon call AccountsController:listAccounts

Actions params must be specified as a stringified JSON array:

yarn mm daemon call \
    KeyringController:signPersonalMessage \
    '[{"data": "0x48656c6c6f2c20776f726c6421", "from": "0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf"}]'

When done, stop the daemon and clean up:

yarn mm daemon purge --force

Summary

  • Create wallet-cli package scaffolded with oclif (mm binary)
  • Implement daemon infrastructure for running a @metamask/wallet instance as a detached background process, communicating over JSON-RPC via Unix domain sockets
  • Add daemon commands: start, stop, status, purge, call
  • call command dispatches arbitrary messenger actions to the running daemon (e.g. mm daemon call AccountsController:listAccounts)
  • Unit tests for all daemon modules (108 tests, 100% coverage on daemon code)

Note

High Risk
Adds a new CLI/daemon that starts a wallet instance and exposes arbitrary messenger actions over a local Unix socket, plus lifecycle management (spawn/stop/purge); mistakes here could impact key handling and local security expectations.

Overview
Introduces a new @metamask/wallet-cli package (Oclif mm binary) that can spawn and manage a detached wallet daemon and communicate with it over newline-delimited JSON-RPC on a Unix domain socket.

Implements daemon lifecycle and RPC plumbing: a socket server with getStatus, shutdown, and a generic call method that forwards arbitrary messenger action names/args, along with CLI commands daemon start|stop|status|purge|call and supporting utilities/tests.

Wires the package into the monorepo (TypeScript refs, ESLint overrides, README dependency graph, CODEOWNERS/teams.json, Yarn workspace rules/lockfile), and exports importSecretRecoveryPhrase from @metamask/wallet for daemon initialization.

Reviewed by Cursor Bugbot for commit f69d1cc. Bugbot is set up for automated code reviews on this repo. Configure here.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 14, 2026

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.0.11100 +11009895 -1100
Added@​oclif/​core@​4.10.59810010096100

View full report

@socket-security

This comment was marked as resolved.

@rekmarks rekmarks requested a review from a team as a code owner April 14, 2026 05:34
Base automatically changed from rekm/wallet-library-tweaks to feat/wallet-library April 14, 2026 10:50
rekmarks and others added 14 commits April 14, 2026 13:09
…rocess

Adds a daemon that runs a Wallet instance as a detached background
process, communicating over JSON-RPC via Unix domain sockets. This
mirrors the architecture of kernel-cli's daemon.

Infrastructure (src/daemon/):
- socket-line: newline-delimited socket I/O
- rpc-socket-server: generic Unix socket JSON-RPC server
- daemon-client: one-shot JSON-RPC client with retry
- daemon-entry: standalone entry point for the spawned process
- daemon-spawn: spawns daemon-entry as detached child
- stop-daemon: shared stop logic with escalation
- wallet-factory: creates configured Wallet from config
- utils, paths, types: process utilities and path resolution

Commands (src/commands/daemon/):
- start: start daemon (--infura-project-id or INFURA_PROJECT_ID env)
- stop: stop daemon (RPC shutdown -> SIGTERM -> SIGKILL)
- status: check daemon status
- purge: stop daemon and delete all state files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9 test files covering all daemon infrastructure: paths, socket-line,
utils, rpc-socket-server, daemon-client, stop-daemon, wallet-factory,
daemon-entry, and daemon-spawn. 96 tests total.

Config changes:
- jest.config.js: exclude commands/ from coverage (not yet tested)
- eslint.config.mjs: disable n/no-process-env and n/no-sync for
  wallet-cli test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire sendSignal into stopDaemon so EPERM errors from process.kill are
not silently treated as successful stops. Remove unused withTimeout
utility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thread wallet password and secret recovery phrase through the daemon
startup chain so the wallet is initialized with an imported SRP.

- Export importSecretRecoveryPhrase from @metamask/wallet
- Add --password and --srp required flags to `daemon start`
- Pass MM_WALLET_PASSWORD / MM_WALLET_SRP env vars to spawned daemon
- Make createWallet async; call importSecretRecoveryPhrase after init

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a `daemon call` CLI command that forwards arbitrary messenger
action calls to the running wallet daemon over JSON-RPC. The daemon
registers a `call` RPC handler that invokes `wallet.messenger.call()`
with the provided action name and arguments.

Usage:
  wallet-cli daemon call AccountsController:listAccounts
  wallet-cli daemon call NetworkController:getState --timeout 10000

Also fixes lint errors in wallet-factory.ts (missing return types).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove placeholder index.ts and library exports (CLI-only package)
- Add yarn constraints exception for wallet-cli exports
- Document intentional `as any` messenger dispatch in daemon-entry
- Remove duplicate socketPath param from ensureDaemon
- Remove unused logPath from DaemonSpawnConfig
- Add 30s server-side socket read timeout in rpc-socket-server
- Handle sendCommand throwing in status command
- Have purge remove entire data directory
- Tighten isRpcError to require both code and message
- Fix waitFor to return false on timeout instead of re-checking
- Clarify multi-request rejection test assertions
- Document password/srp CLI flags as testing-only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Destructuring null params (when a request omits the params field) would
throw a confusing TypeError. Add a runtime check that params is a
non-empty array before destructuring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rekmarks
Copy link
Copy Markdown
Member Author

@SocketSecurity ignore npm/jake@10.9.4

Transitive via @oclif/core. Usage not suspicious.

rekmarks and others added 2 commits April 14, 2026 13:38
… typedoc and JSDoc

- Await rm calls in shutdown finally block via Promise.all and in error
  cleanup path so callers know cleanup is complete
- Use rpcErrors.parse() (-32700) for JSON.parse failures per JSON-RPC spec
- Update typedoc.json entry points after index.ts removal
- Move CONNECTION_TIMEOUT_MS above JSDoc so doc attaches to the function

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reject params where the first element is not a string, preventing
confusing downstream errors from messenger.call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

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 5951891. Configure here.

rekmarks and others added 4 commits April 14, 2026 14:16
- Log errors in catch blocks instead of silently swallowing them across
  daemon-entry, stop-daemon, and rpc-socket-server
- Make shutdown cleanup independent: handle.close(), wallet.destroy(),
  and file removal each run in their own try/catch so one failure
  doesn't skip the others
- Add default 30s timeout to sendCommand to prevent indefinite hangs
- Use JsonRpcResponse from @metamask/utils as handleRequest return type
- Add shared DaemonStatusInfo type for the getStatus RPC contract
- Change RpcHandler to allow void return, document null params
- Filter expected socket errors (EPIPE/ECONNRESET), log unexpected ones
- Only suppress ENOENT in socket unlink, re-throw other errors
- Clean up socket file (not just PID file) when stopDaemon succeeds
- Add child.on('error') handler in daemon-spawn for spawn failures
- Switch makeLogger from sync appendFileSync to async appendFile
- Remove socketPath from DaemonSpawnConfig, derive from dataDir
- Include error details in status command catch block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the required --force flag with an interactive y/N confirmation
prompt using @inquirer/confirm. The --force flag now skips the prompt
instead of being mandatory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant