Skip to content

feat(wallet-cli): headless auto-approval + document the gas-fee slot#9612

Open
sirtimid wants to merge 7 commits into
mainfrom
sirtimid/wallet-cli-daemon-transaction-capable
Open

feat(wallet-cli): headless auto-approval + document the gas-fee slot#9612
sirtimid wants to merge 7 commits into
mainfrom
sirtimid/wallet-cli-daemon-transaction-capable

Conversation

@sirtimid

@sirtimid sirtimid commented Jul 22, 2026

Copy link
Copy Markdown
Member

Explanation

Makes the @metamask/wallet-cli daemon able to process a transaction end-to-end by completing the two consumer-side pieces a wired TransactionController needs at runtime. This makes the daemon transaction-capable; the user-facing mm send command is a separate follow-on.

TransactionController is wired into @metamask/wallet and its messenger delegates to GasFeeController and ApprovalController. A send flow currently dead-ends in the daemon in two ways, both fixed here.

Piece 1 — consume GasFeeController

The gasFeeController slot (clientId: 'cli') already exists in buildInstanceOptions — it was added when GasFeeController was wired upstream (#9527) because clientId is required. This PR finishes the consumer side:

  • Documents the slot in the buildInstanceOptions JSDoc slot list.
  • Relies on the wallet package's platform-agnostic production default for EIP1559APIEndpoint (the default is already the prod URL) rather than re-specifying the string in the CLI, so the endpoint stays centrally owned.

With GasFeeController wired and released, a daemon-hosted Wallet now resolves GasFeeController:fetchGasFeeEstimates instead of throwing A handler for ... has not been registered.

Piece 2 — headless auto-approval

ApprovalController:addRequest is awaited by transaction/signature flows. The daemon's showApprovalRequest is a no-op (it only signals "a request needs attention"; it does not resolve anything), so with no UI the awaiting call hangs forever.

subscribeToAutoApproval subscribes to ApprovalController:stateChanged and immediately accepts every pending request via ApprovalController:acceptRequest. The showApprovalRequest hook stays a no-op (there is no UI); the id isn't available to that hook, so acceptance goes through the messenger instead. An inFlight guard keeps accepting idempotent across the re-entrant/rapid state changes a single flow emits, and both sync throws and async rejections from an accept are logged and swallowed so one bad request can't crash the daemon or wedge the subscription. The subscription is installed in createWallet and torn down in its dispose path.

Security consideration — auto-approval is a conscious trust decision

Auto-approval means the daemon accepts every approval request without confirmation — transactions and signatures included. For a headless daemon this is the intended model: it is driven only by its owner's local CLI over a 0600, same-user Unix socket, so the trust boundary is the socket, not a per-request prompt. This is documented as the daemon's explicit trust model in subscribeToAutoApproval and the README, and flagged as not "safe by default" — a scoped/opt-in policy (config flag, or accepting only specific approval types) is deferred until the user-facing send command exists.

References

  • packages/wallet-cli/src/daemon/auto-approval.ts — the auto-approval subscription + trust model.
  • packages/wallet-cli/src/daemon/wallet-factory.tsbuildInstanceOptions slot docs; createWallet/teardown wiring.

Related

Checklist

  • Tests cover both the gas slot and the auto-approval accept path (unit + real-Wallet integration); 100% coverage maintained.
  • build, package test, yarn lint:fix, yarn lint, changelog:validate pass.
  • Auto-approval trust model documented in code and README.
  • Teardown (dispose) unsubscribes the auto-approval listener.

🤖 Generated with Claude Code


Note

High Risk
Auto-approves every transaction and signature without user confirmation; anyone who can reach the daemon socket can move funds, which is an intentional but security-critical design choice.

Overview
The wallet daemon can now finish transaction and signature flows that await ApprovalController:addRequest, which previously hung forever because showApprovalRequest is a no-op with no UI.

subscribeToAutoApproval listens for ApprovalController:stateChanged and calls ApprovalController:acceptRequest for every pending id. An in-flight set avoids double-accept on re-entrant state updates; accept failures are logged and swallowed so one bad request cannot wedge the daemon. createWallet installs the subscription before wallet.init and tears it down in dispose via a shared runUnsubscribe helper alongside persistence.

Security: the daemon auto-approves all requests (transactions and signatures) with no per-request prompt; the documented trust boundary is the 0600 same-user Unix socket. README and CHANGELOG call this out; scoped approval policy is deferred.

Also documents the existing gasFeeController slot (clientId: 'cli') in buildInstanceOptions JSDoc so gas estimation is clearly part of the daemon’s wired stack.

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

sirtimid added a commit that referenced this pull request Jul 22, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sirtimid and others added 3 commits July 23, 2026 11:20
Complete the two consumer-side pieces a wired `TransactionController` needs
to process a transaction end-to-end in the daemon:

- Headless auto-approval: subscribe to `ApprovalController:stateChanged` and
  accept every pending request via `ApprovalController:acceptRequest`, so an
  awaited `addRequest` (raised by a transaction/signature flow) resolves
  instead of hanging on the headless daemon's no-op `showApprovalRequest`.
  The subscription is torn down in the `createWallet` `dispose` path.
- Gas-fee slot: the `gasFeeController` slot (`clientId: 'cli'`) is already
  wired; document it in `buildInstanceOptions` and rely on the wallet
  package's production-default EIP-1559 endpoint rather than re-specifying it.

The auto-approval trust model is documented in `subscribeToAutoApproval` and
the README: the daemon accepts every approval without a per-request prompt,
so the trust boundary is its `0600` same-user Unix socket.

Closes #9512

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Import `Logger` type alias in `auto-approval.ts` instead of redeclaring the
  inline shape, making compatibility with `wallet-factory.ts` explicit
- Fix misleading catch-block comment that implied a rethrow; the error is
  intentionally suppressed
- Clarify `subscribeToApprovalStateChanged` docstring: root cause is the
  missing `ControllerStateChangedEvent` in `ApprovalControllerEvents`, not
  dynamic string widening as in the persistence layer; add TODO pointing at
  the upstream fix
- Add dropped-Patch[]-parameter note to `ApprovalStateChangeHandler`
- Extend `runUnsubscribe` failure log to note the subscription may remain live
- Replace fragile 5x Promise.resolve() flush with setImmediate pattern
- Add missing startup-failure test: subscribeToAutoApproval throws means
  persistence listener is still cleaned up, wallet destroyed, store closed
- Extend auto-approval unsubscribe test to also assert wallet.destroy ran
- Add 5 s per-test timeout to the auto-approval integration test
- Split dense CHANGELOG entry into lead sentence + nested security note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sirtimid
sirtimid force-pushed the sirtimid/wallet-cli-daemon-transaction-capable branch from 3bd65df to 09b4bb2 Compare July 23, 2026 08:49
@sirtimid
sirtimid marked this pull request as ready for review July 23, 2026 08:50
@sirtimid
sirtimid requested review from a team as code owners July 23, 2026 08:50
@sirtimid
sirtimid temporarily deployed to default-branch July 23, 2026 08:50 — with GitHub Actions Inactive
sirtimid and others added 4 commits July 23, 2026 11:52
The feat commit dropped the TODO when it replaced the old comment, but the
future work (exposing approval requests over the daemon transport for proper
user confirmation) is still planned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep only the ordering rationale; the rest is already in the function name
and its JSDoc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop inline TODO that duplicated the JSDoc TODO in subscribeToApprovalStateChanged
- Remove four in-test comments that restate their it() descriptions
- Trim integration test comment block to only the non-obvious shouldShowRequest note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the long inline trust model with a one-paragraph summary and a
reference to #9513 (the mm-send issue) for the planned scoped policy.
Keep only the inFlight guard rationale and error-swallowing note, which
are non-obvious implementation details an editor of this code needs.

Co-Authored-By: Claude Sonnet 4.6 <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: make the daemon transaction-capable — consume GasFeeController + headless auto-approval

1 participant