Skip to content

fix(wallet-cli): Auto-unlock keyring on daemon restart + add mm wallet unlock#8821

Draft
sirtimid wants to merge 3 commits into
rekm/wallet-clifrom
sirtimid/wallet-cli-auto-unlock
Draft

fix(wallet-cli): Auto-unlock keyring on daemon restart + add mm wallet unlock#8821
sirtimid wants to merge 3 commits into
rekm/wallet-clifrom
sirtimid/wallet-cli-auto-unlock

Conversation

@sirtimid
Copy link
Copy Markdown

Summary

Closes the "daemon restart leaves wallet locked" gap from PR #8446, resolving #8776 (auto-unlock on subsequent daemon starts) and #8780 (mm wallet unlock command). Together these are the two halves of the same fix.

Targets rekm/wallet-clinot for core-team review yet per the wallet-library landing plan. Posting as draft so accounts-engineers/core-platform aren't auto-pinged by CODEOWNERS.

Commit-by-commit

  1. chore(keyring-controller): Mirror state from main onto rekm/wallet-clirekm/wallet-cli was two releases behind main on keyring-controller. Brings the package's src + package.json + CHANGELOG to byte-identical alignment with main (git diff main for packages/keyring-controller/ returns empty). Required because KeyringController:submitPassword (exposed via the messenger on main in chore: Expose missing KeyringController methods through the messenger #8674) is what the next two commits dispatch through. Disappears as a no-op when this branch eventually rebases onto main.

  2. fix(wallet-cli): Auto-unlock keyring on subsequent daemon starts (#8776) — On the not-first-run branch, wallet-factory.ts now calls KeyringController:submitPassword to unlock the persisted vault before returning. --password/MM_WALLET_PASSWORD are now optional on mm daemon start; first-run still requires them (enforced before Wallet construction so a doomed startup doesn't build then tear down a Wallet). Empty-string password is normalised to "no password" at the boundary so --password '' doesn't get sent to the controller as a wrong password.

  3. feat(wallet-cli): Add 'mm wallet unlock' command (#8780) — New oclif command under the wallet topic. Optional --password flag with MM_WALLET_PASSWORD env fallback; prompts interactively via @inquirer/password when neither is supplied. Empty --password '' triggers the prompt. ENOENT/ECONNREFUSED → "Daemon is not running"; EACCES → permission-denied hint with MM_DAEMON_DATA_DIR pointer; JSON-RPC failure → Failed to unlock: <msg> (code <n>) data=<json>. Placed under wallet not daemon because unlocking is a wallet/keyring-lifecycle operation.

Test plan

  • Unit tests: 273/273 pass, 100% coverage maintained on wallet-factory.ts, unlock.ts, daemon-spawn.ts, daemon-entry.ts, prompts.ts.
  • Real-Wallet integration tests (no @metamask/wallet mocks) — 4 cases covering: first-run SRP import + listAccounts; auto-unlock on restart with password; restart without password → locked → unlock via submitPassword → listAccounts works; wrong password rejects without corrupting the DB (retry with correct password succeeds). These caught a real bug during review: the issue's original proposed fix would have failed because submitPassword wasn't messenger-exposed on this branch's base (motivating the keyring-controller mirror in commit 1).
  • yarn lint:eslint clean for both packages.
  • yarn lint:misc:check clean.
  • yarn changelog:validate clean.
  • Manual test: mm daemon start --password X, kill it, mm daemon start --password X, mm daemon call AccountsController:listAccounts returns accounts.
  • Manual test: mm daemon start (no password, after first-run), mm daemon call AccountsController:listAccounts fails, mm wallet unlock --password X, mm daemon call AccountsController:listAccounts returns accounts.
  • Manual test: mm wallet unlock with no flag and no env var prompts interactively.

Known limitations (deferred technical debt)

  • yarn constraints reports 24 violations: consumers of @metamask/keyring-controller still reference ^25.2.0 while this branch's keyring-controller is now ^25.5.0. Main's consumers already reference ^25.5.0, so these resolve automatically when rekm/wallet-cli eventually rebases onto main. Bundling those 24 package.json bumps into this PR was rejected as scope creep — the fix is "rebase onto main" not "ship 24 bumps in a feature PR".
  • yarn workspace @metamask/wallet-cli run build:all fails with a pre-existing ts-bridge --clean + transitive-deps ordering bug (same failure on @metamask/wallet's own build:all on this branch). Root yarn build works; workspace yarn build (without :all) works. Not introduced by this PR.

🤖 Generated with Claude Code

sirtimid and others added 3 commits May 14, 2026 19:14
rekm/wallet-cli is behind main on the keyring-controller package by two
releases (25.4.0, 25.5.0). This brings the keyring-controller src,
package.json, and CHANGELOG into byte-identical alignment with main
(verified via `git diff main` returning empty for this directory) so the
reconciliation disappears as a clean no-op when rekm/wallet-cli is
eventually rebased onto main.

This is a prerequisite for the companion wallet-cli changes in this PR:
`KeyringController:submitPassword` (exposed via the messenger on main
in #8674) is what the daemon auto-unlock and `mm wallet unlock` command
dispatch through. Without this mirror the integration tests added in
the next commit would fail with "A handler for
KeyringController:submitPassword has not been delegated to Wallet".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Today the daemon imports the SRP on first run (which leaves the
keyring unlocked) but on subsequent runs only hydrates state from
`<dataDir>/wallet.db` and constructs the Wallet. The persisted
KeyringController vault is reused, but `KeyringController.isUnlocked`
is `persist: false` and defaults back to `false`, so any messenger
action touching keyring-bound state (signing,
`AccountsController:listAccounts`, etc.) failed after a daemon
restart even though the password was already supplied via
`--password` / `MM_WALLET_PASSWORD`.

Changes:

- `wallet-factory.ts` now calls `KeyringController:submitPassword` on
  the subsequent-run branch when a password is supplied, unlocking
  the keyring before returning. Wrong-password rejections surface as
  the rejection from `submitPassword`; the existing `catch` destroys
  the partial wallet and closes the store. First-run + no-password
  is rejected with a clear error *before* the Wallet is constructed,
  so a doomed startup doesn't build then tear down a Wallet.

- `--password` / `MM_WALLET_PASSWORD` are now optional on
  `mm daemon start`. On subsequent runs, omitting them leaves the
  keyring locked; the companion `mm wallet unlock` command (next
  commit) is the affordance to unlock later. First-run still
  requires a password (enforced by `wallet-factory.ts`).

- Empty-string password is normalised to `undefined` at the
  wallet-factory boundary so `--password ''` is treated as "no
  password supplied" rather than "the empty string is the password"
  (which the controller would reject as wrong).

- `daemon-spawn.ts` only forwards `MM_WALLET_PASSWORD` to the child
  env when the caller explicitly supplied one; it deletes the
  variable from the spread otherwise so a stray inherited value
  doesn't override an explicit omission.

Tests:

- Unit tests added in `wallet-factory.test.ts` covering all four
  branches (first-run +/- password, subsequent +/- password), the
  empty-string normalisation, and the wrong-password cleanup path.

- New `wallet-factory.integration.test.ts` (no mocks of
  `@metamask/wallet`) covers the full first-run -> destroy -> restart
  lifecycle, the auto-unlock path, the locked-then-unlock-via-
  submitPassword path, and that a wrong password rejects without
  corrupting the DB (a retry with the right password succeeds).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the auto-unlock fix in the previous commit. With
`--password` now optional on `mm daemon start`, a daemon can be
started with the keyring locked; this command is the affordance to
unlock it later. Also covers the general case of a daemon that was
unlocked, then locked via `mm daemon call KeyringController:setLocked`.

`packages/wallet-cli/src/commands/wallet/unlock.ts`
  Dispatches `KeyringController:submitPassword` over the daemon socket
  via `sendCommand`. `--password` flag is optional with
  `MM_WALLET_PASSWORD` env fallback; when neither is supplied, the
  command prompts interactively via the new `promptPassword` helper
  (masked input). Empty `--password ''` is treated as "no flag
  supplied" so the prompt fires instead of sending an empty string
  the controller would reject.

  Errors surface with friendly messages: ENOENT/ECONNREFUSED ->
  "Daemon is not running" hint; EACCES -> permission-denied hint
  with an `MM_DAEMON_DATA_DIR` pointer; JSON-RPC failures ->
  "Failed to unlock: <msg> (code <n>) data=<json>".

`packages/wallet-cli/src/daemon/prompts.ts`
  New `promptPassword` helper using the same dynamic-import +
  ESM-interop pattern as the existing `confirmPurge`.

  Adds `@inquirer/password@^4.0.16` as a dependency.

Placement under the new `wallet` oclif topic (rather than `daemon`)
because unlocking is a wallet/keyring-lifecycle operation, not a
daemon-lifecycle one. oclif auto-registers the topic from the
directory structure; no extra config needed.

Tests (14 cases) cover: happy path, env-var fallback, interactive
prompt when neither flag nor env is supplied, prompt when
`--password ''` is supplied, ENOENT/ECONNREFUSED/EACCES/other socket
errors, JSON-RPC failure rendering (including the `data` field),
non-Error throws, timeout flag, idempotent re-unlock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security
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@​metamask/​eth-simple-keyring@​12.0.1 ⏵ 12.0.29910010095100
Added@​inquirer/​password@​4.0.231001009896100

View full report

@socket-security
Copy link
Copy Markdown

Caution

MetaMask internal reviewing guidelines:

  • Do not ignore-all
  • Each alert has instructions on how to review if you don't know what it means. If lost, ask your Security Liaison or the supply-chain group
  • Copy-paste ignore lines for specific packages or a group of one kind with a note on what research you did to deem it safe.
    @SocketSecurity ignore npm/PACKAGE@VERSION
Action Severity Alert  (click "▶" to expand/collapse)
Block Low
Publisher changed: npm mute-stream is now published by npm-cli-ops instead of lukekarrys

New Author: npm-cli-ops

Previous Author: lukekarrys

From: ?npm/@inquirer/password@4.0.23npm/mute-stream@2.0.0

ℹ Read more on: This package | This alert | What is new author?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Scrutinize new collaborator additions to packages because they now have the ability to publish code into your dependency tree. Packages should avoid frequent or unnecessary additions or changes to publishing rights.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/mute-stream@2.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@sirtimid sirtimid changed the title fix(wallet-cli): Auto-unlock keyring on daemon restart + add mm wallet unlock (#8776, #8780) fix(wallet-cli): Auto-unlock keyring on daemon restart + add mm wallet unlock May 14, 2026
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