feat(wallet-cli): add the mm wallet send command#9636
Draft
sirtimid wants to merge 13 commits into
Draft
Conversation
sirtimid
added a commit
that referenced
this pull request
Jul 23, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sirtimid
added a commit
that referenced
this pull request
Jul 23, 2026
Address review findings on #9636: - Broadcast now uses a 120s default timeout (was the 30s used for cheap RPCs; `--timeout` still overrides) and, on a read-timeout, warns the transaction may already be broadcasting and not to re-run — closing the double-send door that the ECONNRESET no-retry already guards. - Report `submitted` (never the stale `unapproved` creation snapshot) when the live record is gone after broadcast; the fallback test now uses a realistic snapshot and asserts the record is re-read by id. - Preview shows `data` and gas overrides, and the confirmed broadcast pins the sender/network the preview resolved instead of re-resolving them. - Validate daemon responses against shared structs; a malformed broadcast payload now fails loudly instead of printing `Hash: (unknown)`. - Tighten `networkClientId` to a non-empty string at the daemon boundary. - e2e hard-fails when `MM_E2E_REQUIRE_ANVIL` is set but anvil is absent; CI sets it whenever it installs anvil, so a broken install can no longer pass as a green no-op. - Add the `../foundryup` tsconfig project references and the root README dependency-graph edge required by the new workspace devDependency. - Comment/doc accuracy fixes (toWei, isValidHexAddress, e2e fetch reason, gas attribution) and trim repeated rationale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…then async rejection test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add the user-facing `mm wallet send` command and a dedicated daemon
`sendTransaction` RPC handler for sending a transaction through the
daemon-hosted `TransactionController`.
`addTransaction` returns `{ transactionMeta, result }` where `result` is a
`Promise<hash>` that cannot travel back over the generic `call` dispatch, so
the daemon exposes a dedicated `sendTransaction` handler that resolves the
network client (from `--network-client-id` or `--chain-id`) and sender
(defaulting to the selected account), submits the transaction as internal
(auto-approved by the daemon), awaits the broadcast server-side, and returns a
serializable `{ transactionHash, transactionId, status }`.
The command converts the ether `--value` to wei via `@metamask/utils`' shared
`toWei`, previews the resolved plan, and broadcasts after confirmation. `--yes`
skips the prompt; `--dry-run` resolves and validates without broadcasting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…status Add a true end-to-end test for `mm wallet send`: it boots a local anvil node (seeded with the same mnemonic the wallet imports, so the account is funded), points the daemon at it via a custom `NetworkController:addNetwork`, sends a transaction through the built CLI, and asserts it was signed, broadcast, and mined (receipt `status: 0x1`, recipient balance increased). The suite is skip-if-absent: when the `anvil` binary is not found it is skipped rather than failed. In CI, anvil is installed (via `@metamask/foundryup`) only when files under `packages/wallet-cli/` changed, so unrelated PRs don't pay the Foundry download and the test skips itself. Also report the post-broadcast transaction status by re-reading the live record via `TransactionController:getTransactions`: the `addTransaction` result's `transactionMeta` is the creation snapshot, whose status is still `unapproved`, so the command previously printed a misleading status after a successful send. - Scope the nock net-connect allowlist to exactly the anvil host:port, restored in `afterEach`, so the shared "no real network" safety net stays in place. - Document both e2e suites in `packages/wallet-cli/tests/README.md`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review findings on #9636: - Broadcast now uses a 120s default timeout (was the 30s used for cheap RPCs; `--timeout` still overrides) and, on a read-timeout, warns the transaction may already be broadcasting and not to re-run — closing the double-send door that the ECONNRESET no-retry already guards. - Report `submitted` (never the stale `unapproved` creation snapshot) when the live record is gone after broadcast; the fallback test now uses a realistic snapshot and asserts the record is re-read by id. - Preview shows `data` and gas overrides, and the confirmed broadcast pins the sender/network the preview resolved instead of re-resolving them. - Validate daemon responses against shared structs; a malformed broadcast payload now fails loudly instead of printing `Hash: (unknown)`. - Tighten `networkClientId` to a non-empty string at the daemon boundary. - e2e hard-fails when `MM_E2E_REQUIRE_ANVIL` is set but anvil is absent; CI sets it whenever it installs anvil, so a broken install can no longer pass as a green no-op. - Add the `../foundryup` tsconfig project references and the root README dependency-graph edge required by the new workspace devDependency. - Comment/doc accuracy fixes (toWei, isValidHexAddress, e2e fetch reason, gas attribution) and trim repeated rationale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Base automatically changed from
sirtimid/wallet-cli-daemon-transaction-capable
to
main
July 24, 2026 11:04
sirtimid
force-pushed
the
sirtimid/wallet-cli-send-transaction
branch
from
July 24, 2026 12:11
14da5e6 to
927bcba
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation
Adds the user-facing
mm wallet sendcommand that sends a transaction end-to-end through the daemon-hostedTransactionController, closing #9513. This is the CLI surface on top of the daemon's transaction capability (#9512 / #9612): it collects transaction parameters, dispatches them to the daemon, waits for the broadcast, and reports the resulting transaction hash.Dedicated
sendTransactionRPC handlerTransactionController: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 genericcalldispatch. The daemon therefore exposes a dedicatedsendTransactionhandler (src/daemon/send-transaction.ts) that, server-side:networkClientId, or fromchainIdviaNetworkController:findNetworkClientIdByChainId;--from, or the selected account;addTransaction(txParams, { networkClientId, origin: 'metamask', isInternal: true })(internal, so it skips origin/permitted-account validation and is auto-approved by the headless daemon);submitted), not theunapprovedcreation snapshot;{ transactionHash, transactionId, status }.Params are validated with superstruct at the daemon boundary (exactly one of
networkClientId/chainId;0xaddress and hex quantities).The
mm wallet sendcommandA thin client over that handler (
src/commands/wallet/send.ts):--to(required),--value(ether — converted to wei via@metamask/utils' sharedtoWei),--data,--from,--network-client-id/--chain-id, gas overrides (--gas/--max-fee-per-gas/--max-priority-fee-per-gas/--gas-price),--timeout.--yesskips the prompt;--dry-runresolves + validates without broadcasting.Testing
--yes,--dry-run, error surfaces); 100% coverage maintained.tests/wallet-send.e2e.test.ts): boots a localanvilnode, adds it as a custom network, and drives the builtmmCLI to sign, broadcast, and mine a real transaction (asserts receiptstatus: 0x1and a recipient balance increase). It is skip-if-absent; CI installsanvilfor it only whenpackages/wallet-cli/changed. Seepackages/wallet-cli/tests/README.md.build, packagetest,test:e2e,yarn lint,changelog:validatepass.References
wallet-cli: add themmsend-transaction command #9513wallet-cli: make the daemon transaction-capable — consumeGasFeeController+ headless auto-approval #9512 / feat!: wireNetworkEnablementControllertoConfigRegistryController#9611 (daemon transaction-capable) and feat(wallet-cli): wire the TransactionController slot in the daemon wallet #9509 (theTransactionControllerslot); the mutating-RPC safety prerequisitewallet-cli: makesendCommandretry idempotency-safe before mutating RPC ships #9511 landed in fix(wallet-cli): retrysendCommandonly onECONNREFUSED#9608.packages/wallet-cli/src/daemon/send-transaction.ts,src/commands/wallet/send.ts,tests/wallet-send.e2e.test.ts.Note
Stacked on
sirtimid/wallet-cli-daemon-transaction-capable(#9612); this PR is based on that branch so the diff is scoped. Once #9612 merges, this will be rebased ontomainand retargeted.Checklist
build,test,test:e2e,yarn lint:fix,yarn lint,changelog:validatepass.--yesto skip) and--dry-rungate a real, irreversible send.🤖 Generated with Claude Code