Skip to content

wallet-cli: make the daemon transaction-capable — consume GasFeeController + headless auto-approval #9512

Description

@sirtimid

Summary

Make the @metamask/wallet-cli daemon capable of processing a transaction end-to-end, by completing the two consumer-side pieces the wired TransactionController needs at runtime:

  1. Consume GasFeeController — add a gasFeeController slot to the daemon's buildInstanceOptions so gas-fee estimates resolve.
  2. Headless auto-approval — replace the daemon's no-op showApprovalRequest with logic that auto-accepts pending approval requests, so a transaction (or signature) request doesn't hang forever waiting on a UI that doesn't exist.

Both are edits to the same daemon area (packages/wallet-cli/src/daemon/wallet-factory.ts + daemon wiring), so they land together. This makes the daemon transaction-capable; the user-facing mm send command is a separate follow-on (see Dependencies).

Background — why both are needed

TransactionController is wired into @metamask/wallet and its messenger delegates to both controllers. A send flow (TransactionController:addTransaction) does two things that currently dead-end in the daemon:

  • It calls GasFeeController:fetchGasFeeEstimates. That action is only lazily registered via delegation; until GasFeeController is actually wired and consumed, the call throws A handler for GasFeeController:fetchGasFeeEstimates has not been registered.
  • It calls ApprovalController:addRequest and awaits the result. The daemon's showApprovalRequest is () => undefined — that hook only signals "a request needs UI attention," it does not resolve the request. With nothing accepting it, the transaction hangs.

Piece 1 — consume GasFeeController

Add to buildInstanceOptions:

gasFeeController: {
  EIP1559APIEndpoint: 'https://gas.api.cx.metamask.io/networks/<chain_id>/suggestedGasFees',
  clientId: 'cli',
  // legacyAPIEndpoint / interval left at controller defaults
},
  • Global fetch is fine here (the daemon runs on Node 18+), so no fetch-injection work is needed on the CLI side.
  • Update the buildInstanceOptions JSDoc slot list and add a unit assertion (mirrors how the transactionController slot was wired).

Piece 2 — headless auto-approval

Replace the no-op with logic that accepts pending approvals. Recommended mechanism (the showApprovalRequest hook does not receive the request id, so accept via the messenger instead):

  • Subscribe to ApprovalController:stateChange (or the pending-approvals signal) and, for each pending request, call ApprovalController:acceptRequest(id).
  • Both ApprovalController:addRequest and ApprovalController:acceptRequest are available messenger actions on the wallet bus.
  • Register this in the daemon setup and tear it down in the existing dispose path.

Security consideration (must be a conscious decision)

Auto-approval means the daemon accepts every approval request without confirmation — including transaction and signature requests. 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 should be:

  • documented as the daemon's explicit trust model, and
  • ideally scoped/opt-in later (e.g. a config flag, or accepting only specific approval types) once the send command exists — out of scope here, but note it so auto-approve isn't silently treated as "safe by default."

Dependencies

Out of scope (follow-on)

Acceptance criteria

  • With Wire GasFeeController into @metamask/wallet default initialization #9510 released, a daemon-hosted Wallet resolves GasFeeController:fetchGasFeeEstimates (no "handler not registered").
  • A transaction/approval request raised in the daemon is auto-accepted rather than hanging.
  • The auto-approval trust model is documented in code.
  • Teardown (dispose) unsubscribes the auto-approval listener.
  • Tests cover both the gas slot and the auto-approval accept path; 100% coverage maintained; build, test, yarn lint:fix, yarn lint, changelog:validate pass.

References

  • packages/wallet-cli/src/daemon/wallet-factory.tsbuildInstanceOptions (approvalController.showApprovalRequest no-op; transactionController slot for the pattern to mirror).
  • packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts — the injectable showApprovalRequest.

Owning teams: @MetaMask/core-platform @MetaMask/ocap-kernel.

Related (tracking)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions