@metamask-previews/wallet-cli 0.0.0-preview-9eed894c0 → 0.0.0-preview-421b16793

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,8 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
- - 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))
13
- - **Security note:** the daemon approves every request without a per-request prompt; the trust boundary is its `0600`, same-user Unix socket.
14
12
  - 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))
15
13
  - Wire the `gasFeeController` slot in the daemon wallet's instance options, passing `clientId: 'cli'` so the CLI identifies itself to the gas estimation API, now that `@metamask/wallet` requires this option ([#9527](https://github.com/MetaMask/core/pull/9527))
16
14
  - 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))
@@ -27,7 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
25
  - `--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))
28
26
  - 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))
29
27
  - Report daemon socket connection errors consistently across `mm daemon call` and `mm daemon list` ([#9339](https://github.com/MetaMask/core/pull/9339))
30
- - Bump `@metamask/wallet` from `^3.0.0` to `^8.1.0` ([#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), [#9609](https://github.com/MetaMask/core/pull/9609), [#9629](https://github.com/MetaMask/core/pull/9629))
28
+ - Bump `@metamask/wallet` from `^3.0.0` to `^8.1.0` ([#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), [#9629](https://github.com/MetaMask/core/pull/9629))
31
29
  - Wrap daemon password and SRP in opaque `Password` and `Srp` types that redact on logging; validated and unwrapped only at trust boundaries ([#8863](https://github.com/MetaMask/core/pull/8863))
30
+ - Bump `@metamask/wallet` from `^7.0.1` to `8.0.0`. ([#9609](https://github.com/MetaMask/core/pull/9609))
32
31
 
33
32
  [Unreleased]: https://github.com/MetaMask/core/
package/README.md CHANGED
@@ -57,8 +57,6 @@ mm daemon purge # stop, then delete all daemon state files (--force to
57
57
 
58
58
  State (socket, PID file, log, and the SQLite database) lives in the per-user oclif data directory; override it with `MM_DATA_DIR`.
59
59
 
60
- > **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.
61
-
62
60
  ## Troubleshooting
63
61
 
64
62
  ### Rebuilding `better-sqlite3`
@@ -7,7 +7,6 @@ const wallet_1 = require("@metamask/wallet");
7
7
  const promises_1 = require("node:fs/promises");
8
8
  const KeyValueStore_js_1 = require("../persistence/KeyValueStore.cjs");
9
9
  const persistence_js_1 = require("../persistence/persistence.cjs");
10
- const auto_approval_js_1 = require("./auto-approval.cjs");
11
10
  const IN_MEMORY_DATABASE_PATH = ':memory:';
12
11
  /**
13
12
  * Build the per-instance options the daemon's `Wallet` is constructed with.
@@ -28,13 +27,7 @@ const IN_MEMORY_DATABASE_PATH = ':memory:';
28
27
  * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real
29
28
  * flags over the network.
30
29
  * - `approvalController` — a no-op `showApprovalRequest` (the daemon is
31
- * headless); pending requests are accepted separately by the auto-approval
32
- * subscription `createWallet` installs, not by this hook.
33
- * - `gasFeeController` — only `clientId: 'cli'` (sent as `X-Client-Id` to the
34
- * gas API); the API endpoints, poll interval, and compatibility callbacks are
35
- * left at the wallet package's platform-agnostic defaults (the default
36
- * `EIP1559APIEndpoint` is already the production URL, so the daemon does not
37
- * re-specify it).
30
+ * headless).
38
31
  * - `transactionController` — swaps processing disabled and no client hooks;
39
32
  * see the slot's inline comment for why the daemon relies on the
40
33
  * controller's defaults for everything else.
@@ -48,9 +41,6 @@ const IN_MEMORY_DATABASE_PATH = ':memory:';
48
41
  function buildInstanceOptions(infuraProjectId) {
49
42
  return {
50
43
  approvalController: {
51
- // The daemon is headless, so there is no UI to open: requests are
52
- // resolved by the auto-approval subscription (see `subscribeToAutoApproval`)
53
- // rather than by this hook, which stays a no-op.
54
44
  // TODO: surface approval requests over the daemon transport.
55
45
  showApprovalRequest: () => undefined,
56
46
  },
@@ -141,8 +131,7 @@ async function createWallet({ databasePath, password, srp, infuraProjectId, log,
141
131
  const logFn = log ?? ((message) => console.error(message));
142
132
  const store = new KeyValueStore_js_1.KeyValueStore(databasePath);
143
133
  let wallet;
144
- let persistenceUnsubscribe;
145
- let autoApprovalUnsubscribe;
134
+ let unsubscribe;
146
135
  let wasFirstRun = false;
147
136
  try {
148
137
  const state = await loadPersistedState(store, infuraProjectId, logFn);
@@ -158,9 +147,7 @@ async function createWallet({ databasePath, password, srp, infuraProjectId, log,
158
147
  state,
159
148
  instanceOptions: buildInstanceOptions(infuraProjectId),
160
149
  });
161
- persistenceUnsubscribe = (0, persistence_js_1.subscribeToChanges)(wallet.messenger, wallet.controllerMetadata, store, logFn);
162
- // Installed before `init` so any approval raised during initialization is covered.
163
- autoApprovalUnsubscribe = (0, auto_approval_js_1.subscribeToAutoApproval)(wallet.messenger, logFn);
150
+ unsubscribe = (0, persistence_js_1.subscribeToChanges)(wallet.messenger, wallet.controllerMetadata, store, logFn);
164
151
  // Complete post-construction controller setup before serving requests —
165
152
  // e.g. `NetworkController.init` applies the selected network so a provider
166
153
  // is available. `init` settles every step independently; a rejected step
@@ -194,11 +181,11 @@ async function createWallet({ databasePath, password, srp, infuraProjectId, log,
194
181
  let disposePromise;
195
182
  return {
196
183
  wallet,
197
- dispose: async () => (disposePromise ?? (disposePromise = teardown(persistenceUnsubscribe, autoApprovalUnsubscribe, wallet, store, logFn))),
184
+ dispose: async () => (disposePromise ?? (disposePromise = teardown(unsubscribe, wallet, store, logFn))),
198
185
  };
199
186
  }
200
187
  catch (error) {
201
- await teardown(persistenceUnsubscribe, autoApprovalUnsubscribe, wallet, store, logFn);
188
+ await teardown(unsubscribe, wallet, store, logFn);
202
189
  if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {
203
190
  // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a
204
191
  // partially-persisted KeyringController vault cannot mislead the next run
@@ -244,42 +231,26 @@ async function loadPersistedState(store, infuraProjectId, logFn) {
244
231
  });
245
232
  }
246
233
  }
247
- /**
248
- * Best-effort unsubscribe: run the function if present, logging (and
249
- * swallowing) any error so a later teardown step still runs.
250
- *
251
- * @param unsubscribe - The unsubscribe function, if one was registered.
252
- * @param label - Human-readable subscription name for the failure log.
253
- * @param logFn - Logger for a failure.
254
- */
255
- function runUnsubscribe(unsubscribe, label, logFn) {
256
- if (!unsubscribe) {
257
- return;
258
- }
259
- try {
260
- unsubscribe();
261
- }
262
- catch (error) {
263
- logFn(`${label} unsubscribe failed during teardown — subscription may remain ` +
264
- `live until the process exits: ${String(error)}`);
265
- }
266
- }
267
234
  /**
268
235
  * Persistence-safe teardown of a wallet and its store; see {@link
269
236
  * CreateWalletResult.dispose} for the ordering rationale. Each step is
270
237
  * best-effort, so a failure is logged and the remaining steps still run.
271
238
  *
272
- * @param persistenceUnsubscribe - The persistence-subscription unsubscribe
273
- * function, if one was registered.
274
- * @param autoApprovalUnsubscribe - The auto-approval-subscription unsubscribe
275
- * function, if one was registered.
239
+ * @param unsubscribe - The persistence-subscription unsubscribe function, if
240
+ * one was registered.
276
241
  * @param wallet - The wallet to destroy, if one was constructed.
277
242
  * @param store - The store to close.
278
243
  * @param logFn - Logger for step failures.
279
244
  */
280
- async function teardown(persistenceUnsubscribe, autoApprovalUnsubscribe, wallet, store, logFn) {
281
- runUnsubscribe(persistenceUnsubscribe, 'Persistence', logFn);
282
- runUnsubscribe(autoApprovalUnsubscribe, 'Auto-approval', logFn);
245
+ async function teardown(unsubscribe, wallet, store, logFn) {
246
+ if (unsubscribe) {
247
+ try {
248
+ unsubscribe();
249
+ }
250
+ catch (error) {
251
+ logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);
252
+ }
253
+ }
283
254
  if (wallet) {
284
255
  try {
285
256
  await wallet.destroy();
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-factory.cjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type { WalletOptions } from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport { subscribeToAutoApproval } from './auto-approval.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless); pending requests are accepted separately by the auto-approval\n * subscription `createWallet` installs, not by this hook.\n * - `gasFeeController` — only `clientId: 'cli'` (sent as `X-Client-Id` to the\n * gas API); the API endpoints, poll interval, and compatibility callbacks are\n * left at the wallet package's platform-agnostic defaults (the default\n * `EIP1559APIEndpoint` is already the production URL, so the daemon does not\n * re-specify it).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // The daemon is headless, so there is no UI to open: requests are\n // resolved by the auto-approval subscription (see `subscribeToAutoApproval`)\n // rather than by this hook, which stays a no-op.\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let persistenceUnsubscribe: (() => void) | undefined;\n let autoApprovalUnsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n persistenceUnsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Installed before `init` so any approval raised during initialization is covered.\n autoApprovalUnsubscribe = subscribeToAutoApproval(wallet.messenger, logFn);\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(\n persistenceUnsubscribe,\n autoApprovalUnsubscribe,\n wallet,\n store,\n logFn,\n )),\n };\n } catch (error) {\n await teardown(\n persistenceUnsubscribe,\n autoApprovalUnsubscribe,\n wallet,\n store,\n logFn,\n );\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Best-effort unsubscribe: run the function if present, logging (and\n * swallowing) any error so a later teardown step still runs.\n *\n * @param unsubscribe - The unsubscribe function, if one was registered.\n * @param label - Human-readable subscription name for the failure log.\n * @param logFn - Logger for a failure.\n */\nfunction runUnsubscribe(\n unsubscribe: (() => void) | undefined,\n label: string,\n logFn: Logger,\n): void {\n if (!unsubscribe) {\n return;\n }\n try {\n unsubscribe();\n } catch (error) {\n logFn(\n `${label} unsubscribe failed during teardown — subscription may remain ` +\n `live until the process exits: ${String(error)}`,\n );\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param persistenceUnsubscribe - The persistence-subscription unsubscribe\n * function, if one was registered.\n * @param autoApprovalUnsubscribe - The auto-approval-subscription unsubscribe\n * function, if one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n persistenceUnsubscribe: (() => void) | undefined,\n autoApprovalUnsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n runUnsubscribe(persistenceUnsubscribe, 'Persistence', logFn);\n runUnsubscribe(autoApprovalUnsubscribe, 'Auto-approval', logFn);\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
1
+ {"version":3,"file":"wallet-factory.cjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type { WalletOptions } from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let unsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n unsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(unsubscribe, wallet, store, logFn)),\n };\n } catch (error) {\n await teardown(unsubscribe, wallet, store, logFn);\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param unsubscribe - The persistence-subscription unsubscribe function, if\n * one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n unsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch (error) {\n logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);\n }\n }\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-factory.d.cts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,MAAM,EACP,yBAAyB;AAO1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAqB;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAIzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AA6FF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuHlD"}
1
+ {"version":3,"file":"wallet-factory.d.cts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,MAAM,EACP,yBAAyB;AAM1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAqB;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAIzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAoFF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuGlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-factory.d.mts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,MAAM,EACP,yBAAyB;AAO1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAqB;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAIzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AA6FF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuHlD"}
1
+ {"version":3,"file":"wallet-factory.d.mts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,MAAM,EACP,yBAAyB;AAM1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAqB;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAIzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAoFF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuGlD"}
@@ -4,7 +4,6 @@ import { AlwaysOnlineAdapter, importSecretRecoveryPhrase, Wallet } from "@metama
4
4
  import { rm } from "node:fs/promises";
5
5
  import { KeyValueStore } from "../persistence/KeyValueStore.mjs";
6
6
  import { loadState, subscribeToChanges } from "../persistence/persistence.mjs";
7
- import { subscribeToAutoApproval } from "./auto-approval.mjs";
8
7
  const IN_MEMORY_DATABASE_PATH = ':memory:';
9
8
  /**
10
9
  * Build the per-instance options the daemon's `Wallet` is constructed with.
@@ -25,13 +24,7 @@ const IN_MEMORY_DATABASE_PATH = ':memory:';
25
24
  * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real
26
25
  * flags over the network.
27
26
  * - `approvalController` — a no-op `showApprovalRequest` (the daemon is
28
- * headless); pending requests are accepted separately by the auto-approval
29
- * subscription `createWallet` installs, not by this hook.
30
- * - `gasFeeController` — only `clientId: 'cli'` (sent as `X-Client-Id` to the
31
- * gas API); the API endpoints, poll interval, and compatibility callbacks are
32
- * left at the wallet package's platform-agnostic defaults (the default
33
- * `EIP1559APIEndpoint` is already the production URL, so the daemon does not
34
- * re-specify it).
27
+ * headless).
35
28
  * - `transactionController` — swaps processing disabled and no client hooks;
36
29
  * see the slot's inline comment for why the daemon relies on the
37
30
  * controller's defaults for everything else.
@@ -45,9 +38,6 @@ const IN_MEMORY_DATABASE_PATH = ':memory:';
45
38
  function buildInstanceOptions(infuraProjectId) {
46
39
  return {
47
40
  approvalController: {
48
- // The daemon is headless, so there is no UI to open: requests are
49
- // resolved by the auto-approval subscription (see `subscribeToAutoApproval`)
50
- // rather than by this hook, which stays a no-op.
51
41
  // TODO: surface approval requests over the daemon transport.
52
42
  showApprovalRequest: () => undefined,
53
43
  },
@@ -138,8 +128,7 @@ export async function createWallet({ databasePath, password, srp, infuraProjectI
138
128
  const logFn = log ?? ((message) => console.error(message));
139
129
  const store = new KeyValueStore(databasePath);
140
130
  let wallet;
141
- let persistenceUnsubscribe;
142
- let autoApprovalUnsubscribe;
131
+ let unsubscribe;
143
132
  let wasFirstRun = false;
144
133
  try {
145
134
  const state = await loadPersistedState(store, infuraProjectId, logFn);
@@ -155,9 +144,7 @@ export async function createWallet({ databasePath, password, srp, infuraProjectI
155
144
  state,
156
145
  instanceOptions: buildInstanceOptions(infuraProjectId),
157
146
  });
158
- persistenceUnsubscribe = subscribeToChanges(wallet.messenger, wallet.controllerMetadata, store, logFn);
159
- // Installed before `init` so any approval raised during initialization is covered.
160
- autoApprovalUnsubscribe = subscribeToAutoApproval(wallet.messenger, logFn);
147
+ unsubscribe = subscribeToChanges(wallet.messenger, wallet.controllerMetadata, store, logFn);
161
148
  // Complete post-construction controller setup before serving requests —
162
149
  // e.g. `NetworkController.init` applies the selected network so a provider
163
150
  // is available. `init` settles every step independently; a rejected step
@@ -191,11 +178,11 @@ export async function createWallet({ databasePath, password, srp, infuraProjectI
191
178
  let disposePromise;
192
179
  return {
193
180
  wallet,
194
- dispose: async () => (disposePromise ?? (disposePromise = teardown(persistenceUnsubscribe, autoApprovalUnsubscribe, wallet, store, logFn))),
181
+ dispose: async () => (disposePromise ?? (disposePromise = teardown(unsubscribe, wallet, store, logFn))),
195
182
  };
196
183
  }
197
184
  catch (error) {
198
- await teardown(persistenceUnsubscribe, autoApprovalUnsubscribe, wallet, store, logFn);
185
+ await teardown(unsubscribe, wallet, store, logFn);
199
186
  if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {
200
187
  // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a
201
188
  // partially-persisted KeyringController vault cannot mislead the next run
@@ -240,42 +227,26 @@ async function loadPersistedState(store, infuraProjectId, logFn) {
240
227
  });
241
228
  }
242
229
  }
243
- /**
244
- * Best-effort unsubscribe: run the function if present, logging (and
245
- * swallowing) any error so a later teardown step still runs.
246
- *
247
- * @param unsubscribe - The unsubscribe function, if one was registered.
248
- * @param label - Human-readable subscription name for the failure log.
249
- * @param logFn - Logger for a failure.
250
- */
251
- function runUnsubscribe(unsubscribe, label, logFn) {
252
- if (!unsubscribe) {
253
- return;
254
- }
255
- try {
256
- unsubscribe();
257
- }
258
- catch (error) {
259
- logFn(`${label} unsubscribe failed during teardown — subscription may remain ` +
260
- `live until the process exits: ${String(error)}`);
261
- }
262
- }
263
230
  /**
264
231
  * Persistence-safe teardown of a wallet and its store; see {@link
265
232
  * CreateWalletResult.dispose} for the ordering rationale. Each step is
266
233
  * best-effort, so a failure is logged and the remaining steps still run.
267
234
  *
268
- * @param persistenceUnsubscribe - The persistence-subscription unsubscribe
269
- * function, if one was registered.
270
- * @param autoApprovalUnsubscribe - The auto-approval-subscription unsubscribe
271
- * function, if one was registered.
235
+ * @param unsubscribe - The persistence-subscription unsubscribe function, if
236
+ * one was registered.
272
237
  * @param wallet - The wallet to destroy, if one was constructed.
273
238
  * @param store - The store to close.
274
239
  * @param logFn - Logger for step failures.
275
240
  */
276
- async function teardown(persistenceUnsubscribe, autoApprovalUnsubscribe, wallet, store, logFn) {
277
- runUnsubscribe(persistenceUnsubscribe, 'Persistence', logFn);
278
- runUnsubscribe(autoApprovalUnsubscribe, 'Auto-approval', logFn);
241
+ async function teardown(unsubscribe, wallet, store, logFn) {
242
+ if (unsubscribe) {
243
+ try {
244
+ unsubscribe();
245
+ }
246
+ catch (error) {
247
+ logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);
248
+ }
249
+ }
279
250
  if (wallet) {
280
251
  try {
281
252
  await wallet.destroy();
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-factory.mjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type { WalletOptions } from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport { subscribeToAutoApproval } from './auto-approval.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless); pending requests are accepted separately by the auto-approval\n * subscription `createWallet` installs, not by this hook.\n * - `gasFeeController` — only `clientId: 'cli'` (sent as `X-Client-Id` to the\n * gas API); the API endpoints, poll interval, and compatibility callbacks are\n * left at the wallet package's platform-agnostic defaults (the default\n * `EIP1559APIEndpoint` is already the production URL, so the daemon does not\n * re-specify it).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // The daemon is headless, so there is no UI to open: requests are\n // resolved by the auto-approval subscription (see `subscribeToAutoApproval`)\n // rather than by this hook, which stays a no-op.\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let persistenceUnsubscribe: (() => void) | undefined;\n let autoApprovalUnsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n persistenceUnsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Installed before `init` so any approval raised during initialization is covered.\n autoApprovalUnsubscribe = subscribeToAutoApproval(wallet.messenger, logFn);\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(\n persistenceUnsubscribe,\n autoApprovalUnsubscribe,\n wallet,\n store,\n logFn,\n )),\n };\n } catch (error) {\n await teardown(\n persistenceUnsubscribe,\n autoApprovalUnsubscribe,\n wallet,\n store,\n logFn,\n );\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Best-effort unsubscribe: run the function if present, logging (and\n * swallowing) any error so a later teardown step still runs.\n *\n * @param unsubscribe - The unsubscribe function, if one was registered.\n * @param label - Human-readable subscription name for the failure log.\n * @param logFn - Logger for a failure.\n */\nfunction runUnsubscribe(\n unsubscribe: (() => void) | undefined,\n label: string,\n logFn: Logger,\n): void {\n if (!unsubscribe) {\n return;\n }\n try {\n unsubscribe();\n } catch (error) {\n logFn(\n `${label} unsubscribe failed during teardown — subscription may remain ` +\n `live until the process exits: ${String(error)}`,\n );\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param persistenceUnsubscribe - The persistence-subscription unsubscribe\n * function, if one was registered.\n * @param autoApprovalUnsubscribe - The auto-approval-subscription unsubscribe\n * function, if one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n persistenceUnsubscribe: (() => void) | undefined,\n autoApprovalUnsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n runUnsubscribe(persistenceUnsubscribe, 'Persistence', logFn);\n runUnsubscribe(autoApprovalUnsubscribe, 'Auto-approval', logFn);\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
1
+ {"version":3,"file":"wallet-factory.mjs","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientConfigApiService,\n ClientType,\n DistributionType,\n EnvironmentType,\n} from '@metamask/remote-feature-flag-controller';\nimport { InMemoryStorageAdapter } from '@metamask/storage-service';\nimport type { Json } from '@metamask/utils';\nimport {\n AlwaysOnlineAdapter,\n importSecretRecoveryPhrase,\n Wallet,\n} from '@metamask/wallet';\nimport type { WalletOptions } from '@metamask/wallet';\nimport { rm } from 'node:fs/promises';\n\nimport { KeyValueStore } from '../persistence/KeyValueStore.js';\nimport { loadState, subscribeToChanges } from '../persistence/persistence.js';\nimport type { Password, Srp } from './secrets.js';\nimport type { Logger } from './types.js';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password?: Password;\n srp: Srp;\n infuraProjectId: string;\n log?: Logger;\n};\n\nexport type CreateWalletResult = {\n wallet: Wallet;\n /**\n * Tear down everything `createWallet` set up, in the order that keeps\n * in-flight persistence writes valid: stop the state-change subscription,\n * destroy the wallet, then close the store (closing first would cause a\n * teardown-time persistence write to fail). Resilient — a failure in any\n * step is logged and the remaining steps still run — and idempotent (repeat\n * calls coalesce onto the same teardown).\n */\n dispose: () => Promise<void>;\n};\n\n/**\n * Build the per-instance options the daemon's `Wallet` is constructed with.\n *\n * Returns a fresh set on every call so the metadata probe and the real wallet\n * never share adapter/service instances (the probe is destroyed, which may\n * tear those instances down).\n *\n * Only the slots wired on `@metamask/wallet` today are populated:\n * - `storageService` — backed by an in-memory adapter. This is the wallet's\n * large-blob store (snap source, caches), distinct from the SQLite\n * `KeyValueStore` that persists controller state; no wired controller\n * offloads durable data to it yet, so in-memory suffices.\n * - `connectivityController` — the `AlwaysOnlineAdapter` exported for\n * node-like hosts that have no platform connectivity signal.\n * - `networkController` — the Infura project ID used to reach Infura RPC\n * endpoints.\n * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real\n * flags over the network.\n * - `approvalController` — a no-op `showApprovalRequest` (the daemon is\n * headless).\n * - `transactionController` — swaps processing disabled and no client hooks;\n * see the slot's inline comment for why the daemon relies on the\n * controller's defaults for everything else.\n *\n * The optional `keyringController` slot is intentionally omitted so the\n * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.\n *\n * @param infuraProjectId - The Infura project ID for the `NetworkController`.\n * @returns The `instanceOptions` for the `Wallet` constructor.\n */\nfunction buildInstanceOptions(\n infuraProjectId: string,\n): WalletOptions['instanceOptions'] {\n return {\n approvalController: {\n // TODO: surface approval requests over the daemon transport.\n showApprovalRequest: (): undefined => undefined,\n },\n connectivityController: {\n connectivityAdapter: new AlwaysOnlineAdapter(),\n },\n gasFeeController: {\n // Identifies the CLI to the gas estimation API via the `X-Client-Id`\n // header.\n clientId: 'cli',\n },\n networkController: {\n infuraProjectId,\n },\n remoteFeatureFlagController: {\n clientConfigApiService: new ClientConfigApiService({\n fetch: globalThis.fetch,\n config: {\n // TODO: switch to a CLI-specific `ClientType` once one exists; until\n // then `Extension` buckets feature flags as an extension client.\n client: ClientType.Extension,\n distribution: DistributionType.Main,\n environment: EnvironmentType.Production,\n },\n }),\n getMetaMetricsId: (): string => 'cli',\n clientVersion: '0.0.0',\n },\n storageService: {\n storage: new InMemoryStorageAdapter(),\n },\n transactionController: {\n // The CLI exposes no swaps surface, so skip the swaps-specific\n // post-processing a full wallet client runs (mobile makes the same\n // choice; the extension keeps swaps enabled).\n disableSwaps: true,\n // No CLI-specific transaction hooks: the controller's built-in publish\n // path broadcasts through the wired `NetworkController` provider, and the\n // remaining hooks (metrics, notifications, gas-fee tokens) are client-UI\n // concerns the headless daemon has no equivalent for. Every other option\n // (`getPermittedAccounts`, `isSimulationEnabled`, `trace`, …) is left at\n // the controller's default.\n hooks: {},\n },\n };\n}\n\n/**\n * Create a configured `Wallet` for daemon use, backed by a SQLite key-value\n * store for controller-state persistence.\n *\n * Loads any previously-persisted controller state from the store, seeds the\n * wallet with it, then subscribes the store to subsequent state changes so all\n * persist-flagged properties are written through.\n *\n * If the store does not yet contain a keyring vault (first run), the supplied\n * secret recovery phrase is imported using the supplied password. On\n * subsequent runs, the persisted vault is reused: when a password is\n * supplied, the wallet is unlocked via `KeyringController:submitPassword` so\n * keyring-bound messenger actions work immediately; when no password is\n * supplied, the wallet starts locked and the caller is expected to invoke\n * `mm wallet unlock` before any keyring-bound operation. First-run startup\n * without a password is rejected (the SRP cannot be imported without one).\n * On a subsequent run, a wrong password rejects startup with a `Failed to\n * unlock the persisted vault` error that wraps the `submitPassword` rejection.\n *\n * On any failure after the store is opened, the store is closed (and the wallet\n * destroyed, if constructed). On a first-run failure, the on-disk database is\n * also removed so a retry does not latch onto an orphaned partial vault — this\n * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import\n * can still leave a vault on disk.\n *\n * @param config - Wallet configuration.\n * @param config.databasePath - Path to the SQLite database file (or\n * `':memory:'` for ephemeral use).\n * @param config.password - The wallet password. Optional on subsequent runs;\n * when omitted, the daemon starts with a locked keyring. Required on first\n * run (to import the SRP).\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic). Used\n * only on first run.\n * @param config.infuraProjectId - The Infura project ID for the\n * `NetworkController`.\n * @param config.log - Optional logger for persistence-write and teardown\n * failures. Without it, failures fall back to `console.error` (which a detached\n * daemon's `stdio: 'ignore'` discards).\n * @returns The `Wallet` and a `dispose` handle that tears it down.\n */\nexport async function createWallet({\n databasePath,\n password,\n srp,\n infuraProjectId,\n log,\n}: CreateWalletConfig): Promise<CreateWalletResult> {\n const logFn = log ?? ((message: string): void => console.error(message));\n const store = new KeyValueStore(databasePath);\n let wallet: Wallet | undefined;\n let unsubscribe: (() => void) | undefined;\n let wasFirstRun = false;\n\n try {\n const state = await loadPersistedState(store, infuraProjectId, logFn);\n wasFirstRun = !hasPersistedKeyring(state);\n\n // Validate the first-run precondition BEFORE constructing the wallet,\n // so a doomed startup doesn't build a Wallet (and wire persistence\n // handlers) just to tear it down.\n if (wasFirstRun && password === undefined) {\n throw new Error(\n 'A password is required on first run to import the secret recovery phrase. ' +\n 'Pass `--password` (or `MM_WALLET_PASSWORD`) on `mm daemon start`.',\n );\n }\n\n wallet = new Wallet({\n state,\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n unsubscribe = subscribeToChanges(\n wallet.messenger,\n wallet.controllerMetadata,\n store,\n logFn,\n );\n\n // Complete post-construction controller setup before serving requests —\n // e.g. `NetworkController.init` applies the selected network so a provider\n // is available. `init` settles every step independently; a rejected step\n // leaves the wallet only partially usable, so abort startup (the catch\n // below tears down and, on first run, removes the partial database) rather\n // than serving a degraded daemon.\n const initResults = await wallet.init();\n const initFailures = initResults.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n );\n for (const failure of initFailures) {\n logFn(`Wallet init step failed: ${String(failure.reason)}`);\n }\n if (initFailures.length > 0) {\n const firstReason = String(initFailures[0].reason);\n throw new Error(\n `Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`,\n );\n }\n\n if (wasFirstRun) {\n // The precondition check above throws when `wasFirstRun && password ===\n // undefined`, so `password` is defined here. TS does not correlate the\n // two separate variables, so it cannot narrow `password` from\n // `wasFirstRun` alone — hence the assertion.\n await importSecretRecoveryPhrase(\n wallet,\n (password as Password).unwrap(),\n srp.unwrap(),\n );\n } else if (password !== undefined) {\n try {\n await wallet.messenger.call(\n 'KeyringController:submitPassword',\n password.unwrap(),\n );\n } catch (error) {\n throw new Error(\n `Failed to unlock the persisted vault: ${String(error)}`,\n );\n }\n }\n\n let disposePromise: Promise<void> | undefined;\n return {\n wallet,\n dispose: async () =>\n (disposePromise ??= teardown(unsubscribe, wallet, store, logFn)),\n };\n } catch (error) {\n await teardown(unsubscribe, wallet, store, logFn);\n\n if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {\n // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a\n // partially-persisted KeyringController vault cannot mislead the next run\n // into skipping SRP import. Covers in-process failures only — a crash\n // (SIGKILL/power loss) mid-import leaves the vault on disk.\n await Promise.all(\n [databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map(\n (path) =>\n rm(path, { force: true }).catch((rmError: unknown) => {\n logFn(\n `Failed to remove ${path} during first-run cleanup: ${String(rmError)}`,\n );\n }),\n ),\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Load persisted controller state, filtered to currently persist-flagged\n * properties.\n *\n * `loadState` filters against the live controller metadata, but that metadata\n * is only knowable from a constructed `Wallet` — and its output is what seeds\n * the real wallet. So this constructs a short-lived probe purely to read the\n * metadata, then tears it down.\n *\n * TODO: drop the probe once `@metamask/wallet` exposes controller metadata\n * without constructing a `Wallet`.\n *\n * @param store - The key-value store to read from.\n * @param infuraProjectId - The Infura project ID for the probe's\n * `NetworkController`.\n * @param logFn - Logger for a probe-teardown failure.\n * @returns The filtered persisted state, suitable for the `Wallet` `state`\n * option.\n */\nasync function loadPersistedState(\n store: KeyValueStore,\n infuraProjectId: string,\n logFn: Logger,\n): Promise<Record<string, Record<string, Json>>> {\n const probe = new Wallet({\n instanceOptions: buildInstanceOptions(infuraProjectId),\n });\n try {\n return loadState(store, probe.controllerMetadata);\n } finally {\n await probe.destroy().catch((error: unknown) => {\n logFn(`Metadata probe destroy failed: ${String(error)}`);\n });\n }\n}\n\n/**\n * Persistence-safe teardown of a wallet and its store; see {@link\n * CreateWalletResult.dispose} for the ordering rationale. Each step is\n * best-effort, so a failure is logged and the remaining steps still run.\n *\n * @param unsubscribe - The persistence-subscription unsubscribe function, if\n * one was registered.\n * @param wallet - The wallet to destroy, if one was constructed.\n * @param store - The store to close.\n * @param logFn - Logger for step failures.\n */\nasync function teardown(\n unsubscribe: (() => void) | undefined,\n wallet: Wallet | undefined,\n store: KeyValueStore,\n logFn: Logger,\n): Promise<void> {\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch (error) {\n logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);\n }\n }\n if (wallet) {\n try {\n await wallet.destroy();\n } catch (error) {\n logFn(`wallet.destroy() failed during teardown: ${String(error)}`);\n }\n }\n try {\n store.close();\n } catch (error) {\n logFn(`store.close() failed during teardown: ${String(error)}`);\n }\n}\n\n/**\n * Determine whether the loaded state already contains a keyring vault.\n *\n * The KeyringController persists its `vault` once an SRP has been imported, so\n * its presence indicates that first-run setup completed before.\n *\n * @param state - The state loaded from the key-value store.\n * @returns True if a KeyringController vault string is present.\n */\nfunction hasPersistedKeyring(\n state: Record<string, Record<string, unknown>>,\n): boolean {\n return typeof state.KeyringController?.vault === 'string';\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/wallet-cli",
3
- "version": "0.0.0-preview-9eed894c0",
3
+ "version": "0.0.0-preview-421b16793",
4
4
  "description": "The CLI of @metamask/wallet",
5
5
  "keywords": [
6
6
  "Ethereum",
@@ -1,86 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.subscribeToAutoApproval = void 0;
4
- /**
5
- * Subscribe the daemon to auto-accept every pending approval request.
6
- *
7
- * Without this, any `ApprovalController:addRequest` call hangs forever on a
8
- * headless daemon. **Everything is approved without a prompt** — trust boundary
9
- * is the `0600` same-user socket. A scoped policy is tracked in
10
- * {@link https://github.com/MetaMask/core/issues/9513}.
11
- *
12
- * The `inFlight` guard makes accepting each id idempotent: `acceptRequest`
13
- * deletes the request on resolve, which itself re-emits `stateChanged`. Without
14
- * it the same id would be accepted twice and the second accept would reject.
15
- * Both sync throws and async rejections are logged and swallowed so one bad
16
- * request cannot crash the daemon or wedge the subscription.
17
- *
18
- * @param messenger - The wallet root messenger.
19
- * @param log - Optional logger for accept failures. Defaults to `console.error`.
20
- * @returns A function that unsubscribes the auto-approval handler.
21
- */
22
- function subscribeToAutoApproval(messenger, log) {
23
- const logFn = log ??
24
- ((message) => {
25
- console.error(message);
26
- });
27
- const inFlight = new Set();
28
- const logFailure = (id, error) => {
29
- logFn(`Failed to auto-accept approval request ${id}: ${String(error)}`);
30
- };
31
- const acceptRequest = (id) => {
32
- if (inFlight.has(id)) {
33
- return;
34
- }
35
- inFlight.add(id);
36
- try {
37
- messenger
38
- .call('ApprovalController:acceptRequest', id)
39
- .catch((error) => logFailure(id, error))
40
- .finally(() => inFlight.delete(id));
41
- }
42
- catch (error) {
43
- // Synchronous throw means the Promise chain above was never built
44
- // (no .catch/.finally attached), so clean up the in-flight guard here.
45
- // The error is intentionally suppressed — a race-condition reject must
46
- // not crash the daemon or permanently wedge the subscription.
47
- inFlight.delete(id);
48
- logFailure(id, error);
49
- }
50
- };
51
- const handler = (state) => {
52
- for (const id of Object.keys(state.pendingApprovals)) {
53
- acceptRequest(id);
54
- }
55
- };
56
- return subscribeToApprovalStateChanged(messenger, handler);
57
- }
58
- exports.subscribeToAutoApproval = subscribeToAutoApproval;
59
- /**
60
- * Subscribe a handler to `ApprovalController:stateChanged`.
61
- *
62
- * `ApprovalControllerEvents` only declares `ApprovalController:stateChange`
63
- * (via `ControllerStateChangeEvent`), not the non-deprecated
64
- * `ApprovalController:stateChanged` variant that `BaseController` publishes at
65
- * runtime. This helper localizes the unavoidable cast — using the same
66
- * technique as `subscribeToStateChanged` in the persistence layer — behind a
67
- * typed {@link ApprovalStateChangeHandler}, keeping the `state` payload
68
- * compile-checked at the call site instead of erased by a statement-level
69
- * `@ts-expect-error`.
70
- *
71
- * TODO: Remove this cast once `ApprovalControllerEvents` includes
72
- * `ControllerStateChangedEvent` (`:stateChanged`), matching the union already
73
- * exported by `BaseController`.
74
- *
75
- * @param messenger - The wallet root messenger.
76
- * @param handler - The state-change handler to register.
77
- * @returns A function that unsubscribes the handler.
78
- */
79
- function subscribeToApprovalStateChanged(messenger, handler) {
80
- const subscriber = messenger;
81
- subscriber.subscribe('ApprovalController:stateChanged', handler);
82
- return () => {
83
- subscriber.unsubscribe('ApprovalController:stateChanged', handler);
84
- };
85
- }
86
- //# sourceMappingURL=auto-approval.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auto-approval.cjs","sourceRoot":"","sources":["../../src/daemon/auto-approval.ts"],"names":[],"mappings":";;;AA2BA;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,uBAAuB,CACrC,SAAiE,EACjE,GAAY;IAEZ,MAAM,KAAK,GACT,GAAG;QACH,CAAC,CAAC,OAAe,EAAQ,EAAE;YACzB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IAEL,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAEnC,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,KAAc,EAAQ,EAAE;QACtD,KAAK,CAAC,0CAA0C,EAAE,KAAK,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,CAAC,EAAU,EAAQ,EAAE;QACzC,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEjB,IAAI,CAAC;YACH,SAAS;iBACN,IAAI,CAAC,kCAAkC,EAAE,EAAE,CAAC;iBAC5C,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;iBAChD,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,kEAAkE;YAClE,uEAAuE;YACvE,uEAAuE;YACvE,8DAA8D;YAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACpB,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACxB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,OAAO,GAA+B,CAAC,KAAK,EAAE,EAAE;QACpD,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACrD,aAAa,CAAC,EAAE,CAAC,CAAC;QACpB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,+BAA+B,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAC7D,CAAC;AA5CD,0DA4CC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,+BAA+B,CACtC,SAAiE,EACjE,OAAmC;IAEnC,MAAM,UAAU,GAAG,SAMlB,CAAC;IACF,UAAU,CAAC,SAAS,CAAC,iCAAiC,EAAE,OAAO,CAAC,CAAC;IACjE,OAAO,GAAG,EAAE;QACV,UAAU,CAAC,WAAW,CAAC,iCAAiC,EAAE,OAAO,CAAC,CAAC;IACrE,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n} from '@metamask/wallet';\n\nimport type { Logger } from './types.js';\n\n/**\n * The slice of `ApprovalController` state this module reads. The full\n * state-change payload is the controller's `ApprovalControllerState`; auto\n * approval only needs the ids of the pending requests, so it looks at\n * `pendingApprovals` alone (the request bodies are irrelevant to accepting\n * them).\n */\ntype PendingApprovalsState = {\n pendingApprovals: Record<string, unknown>;\n};\n\n/**\n * Handler for `ApprovalController`'s state-change event, narrowed to the slice\n * auto approval reads. The `Patch[]` second argument from the raw event is\n * intentionally absent — auto approval re-accepts on every state change and\n * does not filter by patch.\n */\ntype ApprovalStateChangeHandler = (state: PendingApprovalsState) => void;\n\n/**\n * Subscribe the daemon to auto-accept every pending approval request.\n *\n * Without this, any `ApprovalController:addRequest` call hangs forever on a\n * headless daemon. **Everything is approved without a prompt** — trust boundary\n * is the `0600` same-user socket. A scoped policy is tracked in\n * {@link https://github.com/MetaMask/core/issues/9513}.\n *\n * The `inFlight` guard makes accepting each id idempotent: `acceptRequest`\n * deletes the request on resolve, which itself re-emits `stateChanged`. Without\n * it the same id would be accepted twice and the second accept would reject.\n * Both sync throws and async rejections are logged and swallowed so one bad\n * request cannot crash the daemon or wedge the subscription.\n *\n * @param messenger - The wallet root messenger.\n * @param log - Optional logger for accept failures. Defaults to `console.error`.\n * @returns A function that unsubscribes the auto-approval handler.\n */\nexport function subscribeToAutoApproval(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n log?: Logger,\n): () => void {\n const logFn =\n log ??\n ((message: string): void => {\n console.error(message);\n });\n\n const inFlight = new Set<string>();\n\n const logFailure = (id: string, error: unknown): void => {\n logFn(`Failed to auto-accept approval request ${id}: ${String(error)}`);\n };\n\n const acceptRequest = (id: string): void => {\n if (inFlight.has(id)) {\n return;\n }\n inFlight.add(id);\n\n try {\n messenger\n .call('ApprovalController:acceptRequest', id)\n .catch((error: unknown) => logFailure(id, error))\n .finally(() => inFlight.delete(id));\n } catch (error) {\n // Synchronous throw means the Promise chain above was never built\n // (no .catch/.finally attached), so clean up the in-flight guard here.\n // The error is intentionally suppressed — a race-condition reject must\n // not crash the daemon or permanently wedge the subscription.\n inFlight.delete(id);\n logFailure(id, error);\n }\n };\n\n const handler: ApprovalStateChangeHandler = (state) => {\n for (const id of Object.keys(state.pendingApprovals)) {\n acceptRequest(id);\n }\n };\n\n return subscribeToApprovalStateChanged(messenger, handler);\n}\n\n/**\n * Subscribe a handler to `ApprovalController:stateChanged`.\n *\n * `ApprovalControllerEvents` only declares `ApprovalController:stateChange`\n * (via `ControllerStateChangeEvent`), not the non-deprecated\n * `ApprovalController:stateChanged` variant that `BaseController` publishes at\n * runtime. This helper localizes the unavoidable cast — using the same\n * technique as `subscribeToStateChanged` in the persistence layer — behind a\n * typed {@link ApprovalStateChangeHandler}, keeping the `state` payload\n * compile-checked at the call site instead of erased by a statement-level\n * `@ts-expect-error`.\n *\n * TODO: Remove this cast once `ApprovalControllerEvents` includes\n * `ControllerStateChangedEvent` (`:stateChanged`), matching the union already\n * exported by `BaseController`.\n *\n * @param messenger - The wallet root messenger.\n * @param handler - The state-change handler to register.\n * @returns A function that unsubscribes the handler.\n */\nfunction subscribeToApprovalStateChanged(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n handler: ApprovalStateChangeHandler,\n): () => void {\n const subscriber = messenger as unknown as {\n subscribe: (eventType: string, handler: ApprovalStateChangeHandler) => void;\n unsubscribe: (\n eventType: string,\n handler: ApprovalStateChangeHandler,\n ) => void;\n };\n subscriber.subscribe('ApprovalController:stateChanged', handler);\n return () => {\n subscriber.unsubscribe('ApprovalController:stateChanged', handler);\n };\n}\n"]}
@@ -1,22 +0,0 @@
1
- import type { DefaultActions, DefaultEvents, RootMessenger } from "@metamask/wallet";
2
- import type { Logger } from "./types.cjs";
3
- /**
4
- * Subscribe the daemon to auto-accept every pending approval request.
5
- *
6
- * Without this, any `ApprovalController:addRequest` call hangs forever on a
7
- * headless daemon. **Everything is approved without a prompt** — trust boundary
8
- * is the `0600` same-user socket. A scoped policy is tracked in
9
- * {@link https://github.com/MetaMask/core/issues/9513}.
10
- *
11
- * The `inFlight` guard makes accepting each id idempotent: `acceptRequest`
12
- * deletes the request on resolve, which itself re-emits `stateChanged`. Without
13
- * it the same id would be accepted twice and the second accept would reject.
14
- * Both sync throws and async rejections are logged and swallowed so one bad
15
- * request cannot crash the daemon or wedge the subscription.
16
- *
17
- * @param messenger - The wallet root messenger.
18
- * @param log - Optional logger for accept failures. Defaults to `console.error`.
19
- * @returns A function that unsubscribes the auto-approval handler.
20
- */
21
- export declare function subscribeToAutoApproval(messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>, log?: Logger): () => void;
22
- //# sourceMappingURL=auto-approval.d.cts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auto-approval.d.cts","sourceRoot":"","sources":["../../src/daemon/auto-approval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACd,yBAAyB;AAE1B,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAqBzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,QAAQ,CAAC,aAAa,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC,EACjE,GAAG,CAAC,EAAE,MAAM,GACX,MAAM,IAAI,CAyCZ"}
@@ -1,22 +0,0 @@
1
- import type { DefaultActions, DefaultEvents, RootMessenger } from "@metamask/wallet";
2
- import type { Logger } from "./types.mjs";
3
- /**
4
- * Subscribe the daemon to auto-accept every pending approval request.
5
- *
6
- * Without this, any `ApprovalController:addRequest` call hangs forever on a
7
- * headless daemon. **Everything is approved without a prompt** — trust boundary
8
- * is the `0600` same-user socket. A scoped policy is tracked in
9
- * {@link https://github.com/MetaMask/core/issues/9513}.
10
- *
11
- * The `inFlight` guard makes accepting each id idempotent: `acceptRequest`
12
- * deletes the request on resolve, which itself re-emits `stateChanged`. Without
13
- * it the same id would be accepted twice and the second accept would reject.
14
- * Both sync throws and async rejections are logged and swallowed so one bad
15
- * request cannot crash the daemon or wedge the subscription.
16
- *
17
- * @param messenger - The wallet root messenger.
18
- * @param log - Optional logger for accept failures. Defaults to `console.error`.
19
- * @returns A function that unsubscribes the auto-approval handler.
20
- */
21
- export declare function subscribeToAutoApproval(messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>, log?: Logger): () => void;
22
- //# sourceMappingURL=auto-approval.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auto-approval.d.mts","sourceRoot":"","sources":["../../src/daemon/auto-approval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACd,yBAAyB;AAE1B,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAmB;AAqBzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,QAAQ,CAAC,aAAa,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC,EACjE,GAAG,CAAC,EAAE,MAAM,GACX,MAAM,IAAI,CAyCZ"}
@@ -1,82 +0,0 @@
1
- /**
2
- * Subscribe the daemon to auto-accept every pending approval request.
3
- *
4
- * Without this, any `ApprovalController:addRequest` call hangs forever on a
5
- * headless daemon. **Everything is approved without a prompt** — trust boundary
6
- * is the `0600` same-user socket. A scoped policy is tracked in
7
- * {@link https://github.com/MetaMask/core/issues/9513}.
8
- *
9
- * The `inFlight` guard makes accepting each id idempotent: `acceptRequest`
10
- * deletes the request on resolve, which itself re-emits `stateChanged`. Without
11
- * it the same id would be accepted twice and the second accept would reject.
12
- * Both sync throws and async rejections are logged and swallowed so one bad
13
- * request cannot crash the daemon or wedge the subscription.
14
- *
15
- * @param messenger - The wallet root messenger.
16
- * @param log - Optional logger for accept failures. Defaults to `console.error`.
17
- * @returns A function that unsubscribes the auto-approval handler.
18
- */
19
- export function subscribeToAutoApproval(messenger, log) {
20
- const logFn = log ??
21
- ((message) => {
22
- console.error(message);
23
- });
24
- const inFlight = new Set();
25
- const logFailure = (id, error) => {
26
- logFn(`Failed to auto-accept approval request ${id}: ${String(error)}`);
27
- };
28
- const acceptRequest = (id) => {
29
- if (inFlight.has(id)) {
30
- return;
31
- }
32
- inFlight.add(id);
33
- try {
34
- messenger
35
- .call('ApprovalController:acceptRequest', id)
36
- .catch((error) => logFailure(id, error))
37
- .finally(() => inFlight.delete(id));
38
- }
39
- catch (error) {
40
- // Synchronous throw means the Promise chain above was never built
41
- // (no .catch/.finally attached), so clean up the in-flight guard here.
42
- // The error is intentionally suppressed — a race-condition reject must
43
- // not crash the daemon or permanently wedge the subscription.
44
- inFlight.delete(id);
45
- logFailure(id, error);
46
- }
47
- };
48
- const handler = (state) => {
49
- for (const id of Object.keys(state.pendingApprovals)) {
50
- acceptRequest(id);
51
- }
52
- };
53
- return subscribeToApprovalStateChanged(messenger, handler);
54
- }
55
- /**
56
- * Subscribe a handler to `ApprovalController:stateChanged`.
57
- *
58
- * `ApprovalControllerEvents` only declares `ApprovalController:stateChange`
59
- * (via `ControllerStateChangeEvent`), not the non-deprecated
60
- * `ApprovalController:stateChanged` variant that `BaseController` publishes at
61
- * runtime. This helper localizes the unavoidable cast — using the same
62
- * technique as `subscribeToStateChanged` in the persistence layer — behind a
63
- * typed {@link ApprovalStateChangeHandler}, keeping the `state` payload
64
- * compile-checked at the call site instead of erased by a statement-level
65
- * `@ts-expect-error`.
66
- *
67
- * TODO: Remove this cast once `ApprovalControllerEvents` includes
68
- * `ControllerStateChangedEvent` (`:stateChanged`), matching the union already
69
- * exported by `BaseController`.
70
- *
71
- * @param messenger - The wallet root messenger.
72
- * @param handler - The state-change handler to register.
73
- * @returns A function that unsubscribes the handler.
74
- */
75
- function subscribeToApprovalStateChanged(messenger, handler) {
76
- const subscriber = messenger;
77
- subscriber.subscribe('ApprovalController:stateChanged', handler);
78
- return () => {
79
- subscriber.unsubscribe('ApprovalController:stateChanged', handler);
80
- };
81
- }
82
- //# sourceMappingURL=auto-approval.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auto-approval.mjs","sourceRoot":"","sources":["../../src/daemon/auto-approval.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,uBAAuB,CACrC,SAAiE,EACjE,GAAY;IAEZ,MAAM,KAAK,GACT,GAAG;QACH,CAAC,CAAC,OAAe,EAAQ,EAAE;YACzB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IAEL,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAEnC,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,KAAc,EAAQ,EAAE;QACtD,KAAK,CAAC,0CAA0C,EAAE,KAAK,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,CAAC,EAAU,EAAQ,EAAE;QACzC,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEjB,IAAI,CAAC;YACH,SAAS;iBACN,IAAI,CAAC,kCAAkC,EAAE,EAAE,CAAC;iBAC5C,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;iBAChD,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,kEAAkE;YAClE,uEAAuE;YACvE,uEAAuE;YACvE,8DAA8D;YAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACpB,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACxB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,OAAO,GAA+B,CAAC,KAAK,EAAE,EAAE;QACpD,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACrD,aAAa,CAAC,EAAE,CAAC,CAAC;QACpB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,+BAA+B,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,+BAA+B,CACtC,SAAiE,EACjE,OAAmC;IAEnC,MAAM,UAAU,GAAG,SAMlB,CAAC;IACF,UAAU,CAAC,SAAS,CAAC,iCAAiC,EAAE,OAAO,CAAC,CAAC;IACjE,OAAO,GAAG,EAAE;QACV,UAAU,CAAC,WAAW,CAAC,iCAAiC,EAAE,OAAO,CAAC,CAAC;IACrE,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n} from '@metamask/wallet';\n\nimport type { Logger } from './types.js';\n\n/**\n * The slice of `ApprovalController` state this module reads. The full\n * state-change payload is the controller's `ApprovalControllerState`; auto\n * approval only needs the ids of the pending requests, so it looks at\n * `pendingApprovals` alone (the request bodies are irrelevant to accepting\n * them).\n */\ntype PendingApprovalsState = {\n pendingApprovals: Record<string, unknown>;\n};\n\n/**\n * Handler for `ApprovalController`'s state-change event, narrowed to the slice\n * auto approval reads. The `Patch[]` second argument from the raw event is\n * intentionally absent — auto approval re-accepts on every state change and\n * does not filter by patch.\n */\ntype ApprovalStateChangeHandler = (state: PendingApprovalsState) => void;\n\n/**\n * Subscribe the daemon to auto-accept every pending approval request.\n *\n * Without this, any `ApprovalController:addRequest` call hangs forever on a\n * headless daemon. **Everything is approved without a prompt** — trust boundary\n * is the `0600` same-user socket. A scoped policy is tracked in\n * {@link https://github.com/MetaMask/core/issues/9513}.\n *\n * The `inFlight` guard makes accepting each id idempotent: `acceptRequest`\n * deletes the request on resolve, which itself re-emits `stateChanged`. Without\n * it the same id would be accepted twice and the second accept would reject.\n * Both sync throws and async rejections are logged and swallowed so one bad\n * request cannot crash the daemon or wedge the subscription.\n *\n * @param messenger - The wallet root messenger.\n * @param log - Optional logger for accept failures. Defaults to `console.error`.\n * @returns A function that unsubscribes the auto-approval handler.\n */\nexport function subscribeToAutoApproval(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n log?: Logger,\n): () => void {\n const logFn =\n log ??\n ((message: string): void => {\n console.error(message);\n });\n\n const inFlight = new Set<string>();\n\n const logFailure = (id: string, error: unknown): void => {\n logFn(`Failed to auto-accept approval request ${id}: ${String(error)}`);\n };\n\n const acceptRequest = (id: string): void => {\n if (inFlight.has(id)) {\n return;\n }\n inFlight.add(id);\n\n try {\n messenger\n .call('ApprovalController:acceptRequest', id)\n .catch((error: unknown) => logFailure(id, error))\n .finally(() => inFlight.delete(id));\n } catch (error) {\n // Synchronous throw means the Promise chain above was never built\n // (no .catch/.finally attached), so clean up the in-flight guard here.\n // The error is intentionally suppressed — a race-condition reject must\n // not crash the daemon or permanently wedge the subscription.\n inFlight.delete(id);\n logFailure(id, error);\n }\n };\n\n const handler: ApprovalStateChangeHandler = (state) => {\n for (const id of Object.keys(state.pendingApprovals)) {\n acceptRequest(id);\n }\n };\n\n return subscribeToApprovalStateChanged(messenger, handler);\n}\n\n/**\n * Subscribe a handler to `ApprovalController:stateChanged`.\n *\n * `ApprovalControllerEvents` only declares `ApprovalController:stateChange`\n * (via `ControllerStateChangeEvent`), not the non-deprecated\n * `ApprovalController:stateChanged` variant that `BaseController` publishes at\n * runtime. This helper localizes the unavoidable cast — using the same\n * technique as `subscribeToStateChanged` in the persistence layer — behind a\n * typed {@link ApprovalStateChangeHandler}, keeping the `state` payload\n * compile-checked at the call site instead of erased by a statement-level\n * `@ts-expect-error`.\n *\n * TODO: Remove this cast once `ApprovalControllerEvents` includes\n * `ControllerStateChangedEvent` (`:stateChanged`), matching the union already\n * exported by `BaseController`.\n *\n * @param messenger - The wallet root messenger.\n * @param handler - The state-change handler to register.\n * @returns A function that unsubscribes the handler.\n */\nfunction subscribeToApprovalStateChanged(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n handler: ApprovalStateChangeHandler,\n): () => void {\n const subscriber = messenger as unknown as {\n subscribe: (eventType: string, handler: ApprovalStateChangeHandler) => void;\n unsubscribe: (\n eventType: string,\n handler: ApprovalStateChangeHandler,\n ) => void;\n };\n subscriber.subscribe('ApprovalController:stateChanged', handler);\n return () => {\n subscriber.unsubscribe('ApprovalController:stateChanged', handler);\n };\n}\n"]}