@metamask-previews/wallet-cli 0.0.0-preview-64d459746 → 0.0.0-preview-3680ba557

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
@@ -27,7 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
27
 
28
28
  ### Changed
29
29
 
30
- - Wire the `analyticsController` slot in the daemon wallet's instance options with a no-op platform adapter and fixed analytics ID, and remove the previous root-messenger stub handlers now that `@metamask/wallet` constructs `AnalyticsController` itself ([#10901](https://github.com/MetaMask/core/pull/10901))
31
30
  - **BREAKING:** Bump minimum Node.js version to 22 ([#9976](https://github.com/MetaMask/core/pull/9976))
32
31
  - The daemon client (`sendCommand`) now retries only on `ECONNREFUSED`, not `ECONNRESET`, since a reset can drop after the daemon has already acted on a request — re-sending could execute a non-idempotent action (e.g. a transaction broadcast) twice ([#9608](https://github.com/MetaMask/core/pull/9608))
33
32
  - `--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))
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-factory.d.ts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,MAAM,EACP,MAAM,kBAAkB,CAAC;AAO1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAsBzC,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;AAyGF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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.ts","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"AAeA,OAAO,EAGL,MAAM,EACP,MAAM,kBAAkB,CAAC;AAY1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;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;AAsIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAwHlD"}
@@ -1,4 +1,6 @@
1
+ import { getDefaultAnalyticsControllerState } from '@metamask/analytics-controller';
1
2
  import { ConfigRegistryApiEnv } from '@metamask/config-registry-controller';
3
+ import { Messenger } from '@metamask/messenger';
2
4
  import { ClientConfigApiService, ClientType, DistributionType, EnvironmentType, } from '@metamask/remote-feature-flag-controller';
3
5
  import { InMemoryStorageAdapter } from '@metamask/storage-service';
4
6
  import { AlwaysOnlineAdapter, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet';
@@ -6,22 +8,6 @@ import { rm } from 'node:fs/promises';
6
8
  import { KeyValueStore } from '../persistence/KeyValueStore.js';
7
9
  import { loadState, subscribeToChanges } from '../persistence/persistence.js';
8
10
  import { subscribeToAutoApproval } from './auto-approval.js';
9
- /**
10
- * No-op analytics adapter for the headless daemon. Delivery is a no-op, and the
11
- * controller starts opted out, so `NetworkController` RPC analytics calls that
12
- * reach `trackEvent` do not emit.
13
- */
14
- const NO_OP_ANALYTICS_PLATFORM_ADAPTER = {
15
- track: () => undefined,
16
- identify: () => undefined,
17
- view: () => undefined,
18
- onSetupCompleted: () => undefined,
19
- };
20
- /**
21
- * Fixed analytics ID for the daemon. Required by `AnalyticsController`; unused
22
- * for delivery because the adapter is a no-op and consent defaults to opted out.
23
- */
24
- const CLI_ANALYTICS_ID = '00000000-0000-4000-8000-000000000000';
25
11
  const IN_MEMORY_DATABASE_PATH = ':memory:';
26
12
  /**
27
13
  * Build the per-instance options the daemon's `Wallet` is constructed with.
@@ -31,8 +17,6 @@ const IN_MEMORY_DATABASE_PATH = ':memory:';
31
17
  * tear those instances down).
32
18
  *
33
19
  * Only the slots wired on `@metamask/wallet` today are populated:
34
- * - `analyticsController` — a no-op platform adapter and a fixed analytics ID,
35
- * since the daemon does not report analytics.
36
20
  * - `storageService` — backed by an in-memory adapter. This is the wallet's
37
21
  * large-blob store (snap source, caches), distinct from the SQLite
38
22
  * `KeyValueStore` that persists controller state; no wired controller
@@ -63,10 +47,6 @@ const IN_MEMORY_DATABASE_PATH = ':memory:';
63
47
  */
64
48
  function buildInstanceOptions(infuraProjectId) {
65
49
  return {
66
- analyticsController: {
67
- platformAdapter: NO_OP_ANALYTICS_PLATFORM_ADAPTER,
68
- analyticsId: CLI_ANALYTICS_ID,
69
- },
70
50
  configRegistryApiService: {
71
51
  env: ConfigRegistryApiEnv.PRD,
72
52
  },
@@ -123,6 +103,29 @@ function buildInstanceOptions(infuraProjectId) {
123
103
  },
124
104
  };
125
105
  }
106
+ /**
107
+ * Register handlers for the `AnalyticsController` actions that
108
+ * `NetworkController` calls to emit RPC service analytics. The daemon does not
109
+ * report analytics, so these do nothing: `getState` returns an empty analytics
110
+ * ID, which stops the controller before it tracks anything, and `trackEvent` is
111
+ * a no-op. Registering them keeps those messenger calls from throwing.
112
+ *
113
+ * @param messenger - The wallet's root messenger.
114
+ */
115
+ function registerAnalyticsStubHandlers(messenger) {
116
+ // Register on an `AnalyticsController`-namespaced messenger, not the root: a
117
+ // messenger can only register handlers for its own namespace, and doing so
118
+ // delegates them up to the root, where `NetworkController` can reach them.
119
+ const analyticsMessenger = new Messenger({
120
+ namespace: 'AnalyticsController',
121
+ parent: messenger,
122
+ });
123
+ analyticsMessenger.registerActionHandler('AnalyticsController:getState', () => ({
124
+ ...getDefaultAnalyticsControllerState(),
125
+ analyticsId: '',
126
+ }));
127
+ analyticsMessenger.registerActionHandler('AnalyticsController:trackEvent', () => undefined);
128
+ }
126
129
  /**
127
130
  * Create a configured `Wallet` for daemon use, backed by a SQLite key-value
128
131
  * store for controller-state persistence.
@@ -184,6 +187,7 @@ export async function createWallet({ databasePath, password, srp, infuraProjectI
184
187
  state,
185
188
  instanceOptions: buildInstanceOptions(infuraProjectId),
186
189
  });
190
+ registerAnalyticsStubHandlers(wallet.messenger);
187
191
  persistenceUnsubscribe = subscribeToChanges(wallet.messenger, wallet.controllerMetadata, store, logFn);
188
192
  // Installed before `init` so any approval raised during initialization is covered.
189
193
  autoApprovalUnsubscribe = subscribeToAutoApproval(wallet.messenger, logFn);
@@ -260,6 +264,7 @@ async function loadPersistedState(store, infuraProjectId, logFn) {
260
264
  const probe = new Wallet({
261
265
  instanceOptions: buildInstanceOptions(infuraProjectId),
262
266
  });
267
+ registerAnalyticsStubHandlers(probe.messenger);
263
268
  try {
264
269
  return loadState(store, probe.controllerMetadata);
265
270
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-factory.js","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AnalyticsPlatformAdapter } from '@metamask/analytics-controller';\nimport { ConfigRegistryApiEnv } from '@metamask/config-registry-controller';\nimport {\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\n/**\n * No-op analytics adapter for the headless daemon. Delivery is a no-op, and the\n * controller starts opted out, so `NetworkController` RPC analytics calls that\n * reach `trackEvent` do not emit.\n */\nconst NO_OP_ANALYTICS_PLATFORM_ADAPTER: AnalyticsPlatformAdapter = {\n track: (): void => undefined,\n identify: (): void => undefined,\n view: (): void => undefined,\n onSetupCompleted: (): void => undefined,\n};\n\n/**\n * Fixed analytics ID for the daemon. Required by `AnalyticsController`; unused\n * for delivery because the adapter is a no-op and consent defaults to opted out.\n */\nconst CLI_ANALYTICS_ID = '00000000-0000-4000-8000-000000000000';\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 * - `analyticsController` — a no-op platform adapter and a fixed analytics ID,\n * since the daemon does not report analytics.\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 analyticsController: {\n platformAdapter: NO_OP_ANALYTICS_PLATFORM_ADAPTER,\n analyticsId: CLI_ANALYTICS_ID,\n },\n configRegistryApiService: {\n env: ConfigRegistryApiEnv.PRD,\n },\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 selectedNetworkController: {\n domainProxyMap: new Map(),\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.js","sourceRoot":"","sources":["../../src/daemon/wallet-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDefaultAnalyticsControllerState } from '@metamask/analytics-controller';\nimport type {\n AnalyticsControllerGetStateAction,\n AnalyticsControllerTrackEventAction,\n} from '@metamask/analytics-controller';\nimport { ConfigRegistryApiEnv } from '@metamask/config-registry-controller';\nimport { Messenger } from '@metamask/messenger';\nimport {\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 {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n WalletOptions,\n} 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 configRegistryApiService: {\n env: ConfigRegistryApiEnv.PRD,\n },\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 selectedNetworkController: {\n domainProxyMap: new Map(),\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 * Register handlers for the `AnalyticsController` actions that\n * `NetworkController` calls to emit RPC service analytics. The daemon does not\n * report analytics, so these do nothing: `getState` returns an empty analytics\n * ID, which stops the controller before it tracks anything, and `trackEvent` is\n * a no-op. Registering them keeps those messenger calls from throwing.\n *\n * @param messenger - The wallet's root messenger.\n */\nfunction registerAnalyticsStubHandlers(messenger: Wallet['messenger']): void {\n // Register on an `AnalyticsController`-namespaced messenger, not the root: a\n // messenger can only register handlers for its own namespace, and doing so\n // delegates them up to the root, where `NetworkController` can reach them.\n const analyticsMessenger = new Messenger<\n 'AnalyticsController',\n AnalyticsControllerGetStateAction | AnalyticsControllerTrackEventAction,\n never,\n RootMessenger<DefaultActions, DefaultEvents>\n >({\n namespace: 'AnalyticsController',\n parent: messenger as RootMessenger<DefaultActions, DefaultEvents>,\n });\n analyticsMessenger.registerActionHandler(\n 'AnalyticsController:getState',\n () => ({\n ...getDefaultAnalyticsControllerState(),\n analyticsId: '',\n }),\n );\n analyticsMessenger.registerActionHandler(\n 'AnalyticsController:trackEvent',\n () => undefined,\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 registerAnalyticsStubHandlers(wallet.messenger);\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 registerAnalyticsStubHandlers(probe.messenger);\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/wallet-cli",
3
- "version": "0.0.0-preview-64d459746",
3
+ "version": "0.0.0-preview-3680ba557",
4
4
  "description": "The CLI of @metamask/wallet",
5
5
  "keywords": [
6
6
  "Ethereum",