You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Consume GasFeeController — add a gasFeeController slot to the daemon's buildInstanceOptions so gas-fee estimates resolve.
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
Upstream (blocks Piece 1):Wire GasFeeController into @metamask/wallet default initialization #9510 — wire GasFeeController into @metamask/wallet — must land and ship in a @metamask/wallet release the CLI depends on. Piece 2 (auto-approval) has no upstream dependency and can proceed independently.
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.ts — buildInstanceOptions (approvalController.showApprovalRequest no-op; transactionController slot for the pattern to mirror).
packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts — the injectable showApprovalRequest.
Summary
Make the
@metamask/wallet-clidaemon capable of processing a transaction end-to-end, by completing the two consumer-side pieces the wiredTransactionControllerneeds at runtime:GasFeeController— add agasFeeControllerslot to the daemon'sbuildInstanceOptionsso gas-fee estimates resolve.showApprovalRequestwith 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-facingmmsend command is a separate follow-on (see Dependencies).Background — why both are needed
TransactionControlleris wired into@metamask/walletand its messenger delegates to both controllers. A send flow (TransactionController:addTransaction) does two things that currently dead-end in the daemon:GasFeeController:fetchGasFeeEstimates. That action is only lazily registered via delegation; untilGasFeeControlleris actually wired and consumed, the call throwsA handler for GasFeeController:fetchGasFeeEstimates has not been registered.ApprovalController:addRequestand awaits the result. The daemon'sshowApprovalRequestis() => 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
GasFeeControllerAdd to
buildInstanceOptions:fetchis fine here (the daemon runs on Node 18+), so no fetch-injection work is needed on the CLI side.buildInstanceOptionsJSDoc slot list and add a unit assertion (mirrors how thetransactionControllerslot was wired).Piece 2 — headless auto-approval
Replace the no-op with logic that accepts pending approvals. Recommended mechanism (the
showApprovalRequesthook does not receive the request id, so accept via the messenger instead):ApprovalController:stateChange(or the pending-approvals signal) and, for each pending request, callApprovalController:acceptRequest(id).ApprovalController:addRequestandApprovalController:acceptRequestare available messenger actions on the wallet bus.disposepath.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:Dependencies
GasFeeControllerinto@metamask/walletdefault initialization #9510 — wireGasFeeControllerinto@metamask/wallet— must land and ship in a@metamask/walletrelease the CLI depends on. Piece 2 (auto-approval) has no upstream dependency and can proceed independently.wallet-cli: makesendCommandretry idempotency-safe before mutating RPC ships #9511 — makesendCommandretry idempotency-safe, so a dropped connection can't broadcast a transaction twice.Out of scope (follow-on)
mmsend/transfer command itself (CLI surface, argument parsing, output). This issue only makes the daemon able to process a transaction; the command that drives it is separate and gated bywallet-cli: makesendCommandretry idempotency-safe before mutating RPC ships #9511.Acceptance criteria
GasFeeControllerinto@metamask/walletdefault initialization #9510 released, a daemon-hostedWalletresolvesGasFeeController:fetchGasFeeEstimates(no "handler not registered").dispose) unsubscribes the auto-approval listener.build,test,yarn lint:fix,yarn lint,changelog:validatepass.References
packages/wallet-cli/src/daemon/wallet-factory.ts—buildInstanceOptions(approvalController.showApprovalRequestno-op;transactionControllerslot for the pattern to mirror).packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts— the injectableshowApprovalRequest.Owning teams:
@MetaMask/core-platform@MetaMask/ocap-kernel.Related (tracking)
GasFeeControllerinto@metamask/walletdefault initialization #9510 — blocked by: wireGasFeeControllerinto@metamask/wallet(upstream; needed for Piece 1)wallet-cli: add themmsend-transaction command #9513 — unblocks: themmsend command that this makes possiblewallet-cli: makesendCommandretry idempotency-safe before mutating RPC ships #9511 — the send command (wallet-cli: add themmsend-transaction command #9513) is additionally gated onsendCommandretry idempotencyTransactionControllerslot