Summary
Add the user-facing mm command that sends a transaction through the daemon-hosted TransactionController. This is the CLI surface on top of the daemon transaction capability: it collects transaction parameters, dispatches them to the daemon, waits for the broadcast, and reports the resulting transaction hash.
Background
TransactionController is wired into the daemon's Wallet (#9509), and once the daemon can process a transaction (#9512: gas estimates + headless auto-approval), the only missing piece is a command that drives it. TransactionController.addTransaction(txParams, options) is exposed as the TransactionController:addTransaction messenger action, so the daemon already has the capability internally — this issue exposes it over the CLI.
Why a dedicated RPC handler (not raw mm daemon call)
addTransaction returns a Result shaped like { transactionMeta, result }, where result is a Promise<hash> that resolves once the transaction is signed and broadcast. That promise is not JSON-serializable, so it cannot travel back over the daemon's JSON-RPC socket via the generic call dispatch.
So the daemon needs a dedicated RPC handler (e.g. sendTransaction) that, server-side:
- calls
TransactionController:addTransaction(txParams, { networkClientId, origin: 'metamask' /* internal */ }),
- awaits the inner
result promise (the broadcast) to completion,
- returns a serializable payload — at minimum the transaction hash and id/status.
The mm command is then a thin client over that handler, following the existing command pattern (src/commands/wallet/unlock.ts, src/commands/daemon/call.ts).
Proposed CLI shape
mm wallet send (or mm tx send) with flags:
--to <address> (required)
--value <wei|eth> (native amount)
--data <hex> (optional, for contract calls)
--network-client-id <id> or --chain-id <hex> to select the network client (resolve chainId → networkClientId via NetworkController:findNetworkClientIdByChainId)
- optional gas overrides (
--gas, --max-fee-per-gas, --max-priority-fee-per-gas); otherwise rely on GasFeeController estimates
- output: the transaction hash (and id / status), with a clear error if the keyring is locked, no network is selected, or the account is unfunded
Validate params with superstruct at dispatch time (consistent with the daemon's existing RPC param validation).
Dependencies (must land first)
Transitively also depends on #9510 (GasFeeController wired upstream), via #9512.
Security / UX considerations
Acceptance criteria
mm wallet send (name TBD) creates, signs, broadcasts a transaction via the daemon and prints the resulting hash.
- The daemon exposes a dedicated
sendTransaction RPC handler that awaits the broadcast and returns a serializable result (never tries to serialize the result promise).
- Params validated with superstruct; clear errors for locked keyring / missing network / invalid params.
- Tests cover the handler (happy path + broadcast failure) and the command (arg parsing, output, error surfaces); 100% coverage maintained;
build, test, yarn lint:fix, yarn lint, changelog:validate pass.
References
packages/transaction-controller/src/TransactionController.ts — addTransaction (~line 1055) and its exposed TransactionController:addTransaction method action.
packages/wallet-cli/src/commands/wallet/unlock.ts, src/commands/daemon/call.ts — command + RPC-dispatch patterns to mirror.
packages/wallet-cli/src/daemon/rpc-socket-server.ts — where a dedicated sendTransaction handler is registered.
Owning teams: @MetaMask/core-platform @MetaMask/ocap-kernel.
Related (tracking)
Summary
Add the user-facing
mmcommand that sends a transaction through the daemon-hostedTransactionController. This is the CLI surface on top of the daemon transaction capability: it collects transaction parameters, dispatches them to the daemon, waits for the broadcast, and reports the resulting transaction hash.Background
TransactionControlleris wired into the daemon'sWallet(#9509), and once the daemon can process a transaction (#9512: gas estimates + headless auto-approval), the only missing piece is a command that drives it.TransactionController.addTransaction(txParams, options)is exposed as theTransactionController:addTransactionmessenger action, so the daemon already has the capability internally — this issue exposes it over the CLI.Why a dedicated RPC handler (not raw
mm daemon call)addTransactionreturns aResultshaped like{ transactionMeta, result }, whereresultis aPromise<hash>that resolves once the transaction is signed and broadcast. That promise is not JSON-serializable, so it cannot travel back over the daemon's JSON-RPC socket via the genericcalldispatch.So the daemon needs a dedicated RPC handler (e.g.
sendTransaction) that, server-side:TransactionController:addTransaction(txParams, { networkClientId, origin: 'metamask' /* internal */ }),resultpromise (the broadcast) to completion,The
mmcommand is then a thin client over that handler, following the existing command pattern (src/commands/wallet/unlock.ts,src/commands/daemon/call.ts).Proposed CLI shape
mm wallet send(ormm tx send) with flags:--to <address>(required)--value <wei|eth>(native amount)--data <hex>(optional, for contract calls)--network-client-id <id>or--chain-id <hex>to select the network client (resolve chainId → networkClientId viaNetworkController:findNetworkClientIdByChainId)--gas,--max-fee-per-gas,--max-priority-fee-per-gas); otherwise rely onGasFeeControllerestimatesValidate params with superstruct at dispatch time (consistent with the daemon's existing RPC param validation).
Dependencies (must land first)
wallet-cli: make the daemon transaction-capable — consumeGasFeeController+ headless auto-approval #9512 — daemon transaction-capable:GasFeeControllerconsumed (so estimates resolve) and headless auto-approval (soaddTransaction's approval request doesn't hang).wallet-cli: makesendCommandretry idempotency-safe before mutating RPC ships #9511 —sendCommandretry idempotency: hard prerequisite. Sending is non-idempotent; the current retry-once must be tightened before a mutating RPC ships, or a dropped connection could broadcast the same transaction twice.Transitively also depends on #9510 (GasFeeController wired upstream), via #9512.
Security / UX considerations
wallet-cli: make the daemon transaction-capable — consumeGasFeeController+ headless auto-approval #9512), so the confirmation boundary is the CLI command itself — the command should require explicit, unambiguous parameters (no silent defaults for--to/--value) and print a clear summary of what it broadcast.--dry-run/ confirmation prompt before broadcast (real funds move irreversibly).Acceptance criteria
mm wallet send(name TBD) creates, signs, broadcasts a transaction via the daemon and prints the resulting hash.sendTransactionRPC handler that awaits the broadcast and returns a serializable result (never tries to serialize theresultpromise).build,test,yarn lint:fix,yarn lint,changelog:validatepass.References
packages/transaction-controller/src/TransactionController.ts—addTransaction(~line 1055) and its exposedTransactionController:addTransactionmethod action.packages/wallet-cli/src/commands/wallet/unlock.ts,src/commands/daemon/call.ts— command + RPC-dispatch patterns to mirror.packages/wallet-cli/src/daemon/rpc-socket-server.ts— where a dedicatedsendTransactionhandler is registered.Owning teams:
@MetaMask/core-platform@MetaMask/ocap-kernel.Related (tracking)
wallet-cli: make the daemon transaction-capable — consumeGasFeeController+ headless auto-approval #9512 — blocked by: daemon must be transaction-capable (gas + auto-approval)wallet-cli: makesendCommandretry idempotency-safe before mutating RPC ships #9511 — blocked by:sendCommandretry idempotency (mutating RPC safety)GasFeeControllerinto@metamask/walletdefault initialization #9510 — transitively:GasFeeControllerwired upstreamTransactionControllerslot