Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/wallet-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add the `mm wallet unlock` command, which dispatches `KeyringController:submitPassword` over the daemon socket, allowing the keyring to be unlocked after a daemon start with no password or after a `mm daemon call KeyringController:setLocked` ([#8821](https://github.com/MetaMask/core/pull/8821))
- Add the `mm daemon list` command, which prints the messenger actions the running daemon can dispatch via `daemon call`, enumerated from the live messenger so the list cannot drift from what `call` accepts ([#9339](https://github.com/MetaMask/core/pull/9339))
- Add the `mm daemon` command suite (`start`, `stop`, `status`, `purge`, and `call`) for running the wallet daemon and dispatching messenger actions over its socket ([#9255](https://github.com/MetaMask/core/pull/9255))
- Add a wallet factory and daemon entry point that construct a `@metamask/wallet` `Wallet` backed by the SQLite key-value store, hydrate it from persisted state, run controller initialization (aborting startup if any step fails), import the secret recovery phrase on first run, and expose a `dispose` teardown handle ([#9226](https://github.com/MetaMask/core/pull/9226))
Expand All @@ -18,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- `--password` / `MM_WALLET_PASSWORD` is now optional on `mm daemon start`; on subsequent runs, omitting it starts the daemon with a locked keyring, and the persisted vault is auto-unlocked when a password is supplied ([#8821](https://github.com/MetaMask/core/pull/8821))
- The daemon RPC server now validates `params` against each handler's superstruct before dispatch, returning a `-32602 invalidParams` error on mismatch instead of passing raw params to the handler ([#8846](https://github.com/MetaMask/core/pull/8846))
- Report daemon socket connection errors consistently across `mm daemon call` and `mm daemon list` ([#9339](https://github.com/MetaMask/core/pull/9339))
- Bump `@metamask/wallet` from `^3.0.0` to `^7.0.1` ([#9218](https://github.com/MetaMask/core/pull/9218), [#9263](https://github.com/MetaMask/core/pull/9263), [#9349](https://github.com/MetaMask/core/pull/9349), [#9396](https://github.com/MetaMask/core/pull/9396), [#9470](https://github.com/MetaMask/core/pull/9470))
Expand Down
14 changes: 13 additions & 1 deletion packages/wallet-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,24 @@ or

## Usage

The CLI drives a long-lived background **daemon** that holds an unlocked `@metamask/wallet` in memory and exposes its messenger over a per-user Unix socket. All commands live under the `mm daemon` topic; run `mm --help` (or `mm daemon <command> --help`) for the full reference.
The CLI drives a long-lived background daemon that holds a `@metamask/wallet` in memory and exposes its messenger over a per-user Unix socket. All commands live under the `mm daemon` and `mm wallet` topics; run `mm --help` (or `mm <topic> <command> --help`) for the full reference.

Start the daemon (flags may also be supplied as the `INFURA_PROJECT_ID`, `MM_WALLET_PASSWORD`, and `MM_WALLET_SRP` environment variables — preferred for secrets):

```sh
# First run — password required to import the secret recovery phrase:
mm daemon start --infura-project-id <key> --password <pw> --srp "<phrase>"

# Subsequent runs — password optional. Omit to start with a locked keyring
# and unlock later with `mm wallet unlock`:
mm daemon start --infura-project-id <key> --srp "<phrase>"
```

Unlock the keyring after a password-less start (or after `KeyringController:setLocked`):

```sh
mm wallet unlock --password <pw> # or: MM_WALLET_PASSWORD=<pw> mm wallet unlock
mm wallet unlock # prompts interactively (input masked)
```

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:
Expand Down
1 change: 1 addition & 0 deletions packages/wallet-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"dependencies": {
"@inquirer/confirm": "^6.0.11",
"@inquirer/password": "^5.1.1",
"@metamask/base-controller": "^9.1.0",
"@metamask/remote-feature-flag-controller": "^4.2.2",
"@metamask/rpc-errors": "^7.0.2",
Expand Down
37 changes: 37 additions & 0 deletions packages/wallet-cli/src/commands/daemon/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ describe('daemon start', () => {
const { stdout } = await runCommand(DaemonStart, FLAGS);

expect(stdout).toContain('Daemon running. Socket: /tmp/daemon.sock');
const config = mockEnsureDaemon.mock.calls[0][0];
expect(config.infuraProjectId).toBe('key');
expect(config.password?.unwrap()).toBe('pw');
expect(config.srp.unwrap()).toBe(SRP);
});

it('warns that flags were not applied when a daemon is already running', async () => {
Expand All @@ -34,4 +38,37 @@ describe('daemon start', () => {
expect(stdout).toContain('Daemon already running');
expect(stdout).toContain('not applied');
});

it('passes password: undefined to ensureDaemon when --password is omitted', async () => {
mockEnsureDaemon.mockResolvedValue({
state: 'started',
socketPath: '/tmp/daemon.sock',
});

await runCommand(DaemonStart, ['--infura-project-id', 'key', '--srp', SRP]);

expect(mockEnsureDaemon).toHaveBeenCalledWith(
expect.objectContaining({ password: undefined }),
);
});

it('passes password: undefined to ensureDaemon when --password is empty', async () => {
mockEnsureDaemon.mockResolvedValue({
state: 'started',
socketPath: '/tmp/daemon.sock',
});

await runCommand(DaemonStart, [
'--infura-project-id',
'key',
'--srp',
SRP,
'--password',
'',
]);

expect(mockEnsureDaemon).toHaveBeenCalledWith(
expect.objectContaining({ password: undefined }),
);
});
});
8 changes: 5 additions & 3 deletions packages/wallet-cli/src/commands/daemon/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class DaemonStart extends Command {
static override examples = [
'<%= config.bin %> daemon start --infura-project-id <key> --password <pw> --srp <phrase>',
'INFURA_PROJECT_ID=<key> MM_WALLET_PASSWORD=<pw> MM_WALLET_SRP=<phrase> <%= config.bin %> daemon start',
'<%= config.bin %> daemon start --infura-project-id <key> --srp <phrase> # then `mm wallet unlock` later',
];

static override flags = {
Expand All @@ -19,9 +20,10 @@ export default class DaemonStart extends Command {
}),
password: Flags.string({
description:
'Wallet password (testing only — use MM_WALLET_PASSWORD env var in production)',
'Wallet password (testing only — use MM_WALLET_PASSWORD env var in production). ' +
'Required on first run; on subsequent runs, omit (and leave MM_WALLET_PASSWORD unset) to start with a locked keyring and use `mm wallet unlock`.',
env: 'MM_WALLET_PASSWORD',
required: true,
required: false,
Comment thread
cursor[bot] marked this conversation as resolved.
}),
srp: Flags.string({
description:
Expand All @@ -34,7 +36,7 @@ export default class DaemonStart extends Command {
public async run(): Promise<void> {
const { flags } = await this.parse(DaemonStart);
const infuraProjectId = flags['infura-project-id'];
const password = Password.from(flags.password);
const password = flags.password ? Password.from(flags.password) : undefined;
const srp = Srp.from(flags.srp);

const { state, socketPath } = await ensureDaemon({
Expand Down
Loading