Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/lint-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,11 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Build wallet-cli and its dependencies
run: yarn workspaces foreach --topological-dev --recursive --from '@metamask/wallet-cli' run build
- name: Install anvil for the real-chain e2e
run: yarn workspace @metamask/wallet-cli run test:e2e:install-anvil
- run: yarn workspace @metamask/wallet-cli run test:e2e
env:
MM_E2E_REQUIRE_ANVIL: 'true'
- name: Require clean working directory
shell: bash
run: |
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ packages/*/*.tsbuildinfo
# Docusaurus
**/.docusaurus
packages/wallet-framework-docs/site/build

# Foundry (anvil) binaries cached by @metamask/foundryup for the wallet-cli
# real-chain e2e (enableGlobalCache is off, so the cache lands in-repo).
.metamask/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ linkStyle default opacity:0.5
wallet_cli --> remote_feature_flag_controller;
wallet_cli --> storage_service;
wallet_cli --> wallet;
wallet_cli --> foundryup;
```

<!-- end dependency graph -->
Expand Down
4 changes: 4 additions & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ const config: KnipConfig = {
ignoreDependencies: ['immer'],
},
'packages/wallet-cli': {
// `anvil` (from Foundry) is an external system binary the real-chain send
// e2e probes and spawns; it's installed via `foundryup`, not an npm
// package, so knip can't tie the invocation to a dependency.
ignoreBinaries: ['anvil'],
// `tsx` is the dev-mode loader: it's referenced only as a `node --import`
// argument string (in `daemon-spawn`'s source-entry path and `bin/dev`),
// never as a traceable import, so knip can't see it.
Expand Down
3 changes: 3 additions & 0 deletions packages/wallet-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add the `mm wallet send` command and a dedicated daemon `sendTransaction` RPC handler for sending a transaction through the daemon-hosted `TransactionController` ([#9636](https://github.com/MetaMask/core/pull/9636))
- The command converts the ether `--value` to wei, resolves the network client (`--network-client-id` or `--chain-id`) and sender (defaulting to the selected account), previews the resolved plan, and broadcasts after confirmation, printing the resulting transaction hash. `--yes` skips the prompt; `--dry-run` resolves and validates without broadcasting.
- The `sendTransaction` handler awaits the broadcast server-side and returns a serializable `{ transactionHash, transactionId, status }`, because `addTransaction`'s `result` promise cannot travel back over the generic `call` dispatch. Transactions are submitted as internal (auto-approved by the daemon).
- Auto-accept pending approval requests in the daemon so a transaction or signature flow resolves instead of hanging on the headless no-op `showApprovalRequest`; the subscription is torn down on `dispose` ([#9612](https://github.com/MetaMask/core/pull/9612))
- **Security note:** the daemon approves every request without a per-request prompt; the trust boundary is its `0600`, same-user Unix socket.
- Wire the `transactionController` slot in the daemon wallet's instance options, so the daemon runs the `TransactionController` with an explicit CLI-appropriate configuration (swaps processing disabled, no client hooks) rather than relying on the controller's implicit defaults ([#9509](https://github.com/MetaMask/core/pull/9509))
Expand Down
12 changes: 11 additions & 1 deletion packages/wallet-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ mm wallet unlock --password <pw> # or: MM_WALLET_PASSWORD=<pw> mm wallet unloc
mm wallet unlock # prompts interactively (input masked)
```

Send a transaction. `--value` is in ether; select the network with `--network-client-id` or `--chain-id`; the sender defaults to the selected account. The command previews the resolved plan and asks for confirmation before broadcasting, then prints the transaction hash:

```sh
mm wallet send --to 0xRecipient --value 0.01 --chain-id 0x1
mm wallet send --to 0xRecipient --value 0.01 --network-client-id mainnet --yes # skip the prompt
mm wallet send --to 0xContract --data 0xabcdef --value 0 --chain-id 0x1 --dry-run # resolve + validate only
```

Because the daemon auto-approves (see the security note below), the confirmation prompt — or your explicit `--yes` — is the only boundary before funds move; use `--dry-run` first if unsure. Gas is estimated automatically unless overridden with `--gas` / `--max-fee-per-gas` / `--max-priority-fee-per-gas` / `--gas-price` (each a `0x`-prefixed hex quantity).

Discover what the running wallet can do — `list` prints every messenger action currently dispatchable via `call`. This surface grows as more controllers are wired, so treat it as evolving rather than a stability contract:

```sh
Expand All @@ -57,7 +67,7 @@ mm daemon purge # stop, then delete all daemon state files (--force to

State (socket, PID file, log, and the SQLite database) lives in the per-user oclif data directory; override it with `MM_DATA_DIR`.

> **Security model — the daemon auto-approves everything.** Because it is headless, the daemon accepts every approval request (transactions and signatures included) with no per-request prompt; otherwise an awaited request would hang forever with no UI to resolve it. The trust boundary is therefore the daemon's `0600`, same-user Unix socket — anything that can reach the socket can move funds. A scoped/opt-in approval policy is planned for when a user-facing send command lands.
> **Security model — the daemon auto-approves everything.** Because it is headless, the daemon accepts every approval request (transactions and signatures included) with no per-request prompt; otherwise an awaited request would hang forever with no UI to resolve it. The trust boundary is therefore the daemon's `0600`, same-user Unix socket — anything that can reach the socket can move funds. For `mm wallet send` the confirmation prompt (or `--yes`) is the only per-transaction gate; the daemon itself still applies no per-request policy.

## Troubleshooting

Expand Down
3 changes: 3 additions & 0 deletions packages/wallet-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"test": "yarn test:prepare && NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter",
"test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache",
"test:e2e": "yarn test:prepare && NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.e2e.js",
"test:e2e:install-anvil": "mm-foundryup",
"test:prepare": "./scripts/install-binaries.sh",
"test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
Expand All @@ -62,12 +63,14 @@
},
"devDependencies": {
"@metamask/auto-changelog": "^6.1.0",
"@metamask/foundryup": "^1.0.1",
"@ts-bridge/cli": "^0.6.4",
"@types/better-sqlite3": "^7.6.13",
"@types/jest": "^30.0.0",
"deepmerge": "^4.2.2",
"jest": "^30.4.2",
"jest-environment-node": "^30.4.1",
"nock": "^13.3.1",
"ts-jest": "^29.4.11",
"tsx": "^4.20.5",
"typescript": "~5.3.3"
Expand Down
Loading