@metamask-previews/wallet-cli 0.0.0-preview-ec4680201 → 0.0.0-preview-40d5fb1e2
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 +1 -4
- package/dist/commands/daemon/call.cjs +7 -7
- package/dist/commands/daemon/call.cjs.map +1 -1
- package/dist/commands/daemon/call.mjs.map +1 -1
- package/dist/commands/daemon/list.cjs +8 -8
- package/dist/commands/daemon/list.cjs.map +1 -1
- package/dist/commands/daemon/list.mjs.map +1 -1
- package/dist/commands/daemon/purge.cjs +8 -8
- package/dist/commands/daemon/purge.cjs.map +1 -1
- package/dist/commands/daemon/purge.mjs.map +1 -1
- package/dist/commands/daemon/start.cjs +5 -5
- package/dist/commands/daemon/start.cjs.map +1 -1
- package/dist/commands/daemon/start.mjs.map +1 -1
- package/dist/commands/daemon/status.cjs +8 -8
- package/dist/commands/daemon/status.cjs.map +1 -1
- package/dist/commands/daemon/status.mjs.map +1 -1
- package/dist/commands/daemon/stop.cjs +8 -8
- package/dist/commands/daemon/stop.cjs.map +1 -1
- package/dist/commands/daemon/stop.mjs.map +1 -1
- package/dist/commands/wallet/unlock.cjs +9 -9
- package/dist/commands/wallet/unlock.cjs.map +1 -1
- package/dist/commands/wallet/unlock.mjs.map +1 -1
- package/dist/daemon/daemon-client.cjs +12 -21
- package/dist/daemon/daemon-client.cjs.map +1 -1
- package/dist/daemon/daemon-client.d.cts +4 -9
- package/dist/daemon/daemon-client.d.cts.map +1 -1
- package/dist/daemon/daemon-client.d.mts +4 -9
- package/dist/daemon/daemon-client.d.mts.map +1 -1
- package/dist/daemon/daemon-client.mjs +4 -13
- package/dist/daemon/daemon-client.mjs.map +1 -1
- package/dist/daemon/daemon-entry.cjs +21 -21
- package/dist/daemon/daemon-entry.cjs.map +1 -1
- package/dist/daemon/daemon-entry.mjs.map +1 -1
- package/dist/daemon/daemon-spawn.cjs +7 -7
- package/dist/daemon/daemon-spawn.cjs.map +1 -1
- package/dist/daemon/daemon-spawn.d.cts.map +1 -1
- package/dist/daemon/daemon-spawn.d.mts.map +1 -1
- package/dist/daemon/daemon-spawn.mjs.map +1 -1
- package/dist/daemon/paths.cjs.map +1 -1
- package/dist/daemon/paths.d.cts.map +1 -1
- package/dist/daemon/paths.d.mts.map +1 -1
- package/dist/daemon/paths.mjs.map +1 -1
- package/dist/daemon/rpc-socket-server.cjs +2 -2
- package/dist/daemon/rpc-socket-server.cjs.map +1 -1
- package/dist/daemon/rpc-socket-server.d.cts.map +1 -1
- package/dist/daemon/rpc-socket-server.d.mts.map +1 -1
- package/dist/daemon/rpc-socket-server.mjs.map +1 -1
- package/dist/daemon/stop-daemon.cjs +12 -12
- package/dist/daemon/stop-daemon.cjs.map +1 -1
- package/dist/daemon/stop-daemon.mjs.map +1 -1
- package/dist/daemon/types.cjs.map +1 -1
- package/dist/daemon/types.d.cts.map +1 -1
- package/dist/daemon/types.d.mts.map +1 -1
- package/dist/daemon/types.mjs.map +1 -1
- package/dist/daemon/wallet-factory.cjs +5 -10
- package/dist/daemon/wallet-factory.cjs.map +1 -1
- package/dist/daemon/wallet-factory.d.cts.map +1 -1
- package/dist/daemon/wallet-factory.d.mts.map +1 -1
- package/dist/daemon/wallet-factory.mjs +0 -5
- package/dist/daemon/wallet-factory.mjs.map +1 -1
- package/dist/persistence/persistence.cjs.map +1 -1
- package/dist/persistence/persistence.d.cts.map +1 -1
- package/dist/persistence/persistence.d.mts.map +1 -1
- package/dist/persistence/persistence.mjs.map +1 -1
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stop-daemon.cjs","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":";;;AAAA,+CAAsC;AAEtC,
|
|
1
|
+
{"version":3,"file":"stop-daemon.cjs","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":"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","sourcesContent":["import { rm } from 'node:fs/promises';\n\nimport { pingDaemon, sendCommand } from './daemon-client';\nimport { isProcessAlive, readPidFile, sendSignal, waitFor } from './utils';\n\n/**\n * Stop the daemon, preferring a graceful shutdown.\n *\n * Resolution order when a live daemon is present:\n * 1. If the socket is responsive, request a graceful `shutdown` over it.\n * 2. If the recorded PID is still alive, escalate to SIGTERM.\n * 3. ...then SIGKILL.\n *\n * Signals (steps 2-3) are only ever sent against a PID that is observed alive.\n * The socket-absent + alive-PID branch trades a small risk of signalling a\n * recycled PID for the larger risk of leaving an orphan daemon holding the\n * SQLite database — which `daemon purge` would otherwise wipe out from under\n * it.\n *\n * When the socket is NOT responsive AND the recorded PID is dead (or there is\n * no PID file), there is no live daemon: a lingering socket or PID file is\n * stale leftovers from a daemon that already exited — typically one that\n * crashed without running its own cleanup. Those files are removed and the\n * stop is reported as successful, rather than failing on a daemon that is\n * already gone.\n *\n * @param socketPath - The daemon socket path.\n * @param pidPath - The daemon PID file path.\n * @param log - Optional logging function for status messages.\n * @returns True if the daemon was stopped (or was not running).\n */\nexport async function stopDaemon(\n socketPath: string,\n pidPath: string,\n log?: (message: string) => void,\n): Promise<boolean> {\n const pid = await readPidFile(pidPath);\n const ping = await pingDaemon(socketPath);\n const socketObserved =\n ping.status === 'responsive' || ping.status === 'unreachable';\n const processAlive = pid !== undefined && isProcessAlive(pid);\n\n // Only `absent` and `refused` prove no live daemon; `permission`/`timeout`/\n // `protocol` may be a wedged or foreign daemon, so those fall through.\n const socketProvenGone =\n ping.status === 'absent' ||\n (ping.status === 'unreachable' && ping.reason === 'refused');\n\n if (socketProvenGone && !processAlive) {\n await cleanupFile(pidPath, 'PID file', log);\n await cleanupFile(socketPath, 'socket file', log);\n return true;\n }\n\n log?.('Stopping daemon...');\n\n let stopped = false;\n\n // Strategy 1: Graceful socket-based shutdown.\n if (ping.status === 'responsive') {\n try {\n await sendCommand({ socketPath, method: 'shutdown' });\n } catch (error) {\n log?.(`Graceful shutdown request failed: ${String(error)}`);\n }\n // A quiet socket does not prove the process exited; require the recorded\n // pid to be gone too, so a daemon that outlived its socket falls through to\n // SIGTERM/SIGKILL rather than being orphaned.\n stopped = await waitFor(\n async () =>\n (await pingDaemon(socketPath)).status !== 'responsive' &&\n (pid === undefined || !isProcessAlive(pid)),\n 5_000,\n );\n }\n\n // Strategy 2: SIGTERM. Signal when either the socket was observed or the\n // recorded PID is alive; the absent+alive case typically means someone\n // removed the socket from under a live daemon.\n if (!stopped && processAlive && pid !== undefined) {\n if (!socketObserved) {\n log?.(\n `Socket at ${socketPath} is absent but recorded pid ${pid} is alive; signalling anyway.`,\n );\n }\n try {\n if (sendSignal(pid, 'SIGTERM')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 5_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGTERM failed: ${String(error)}`);\n }\n }\n\n // Strategy 3: SIGKILL.\n if (!stopped && processAlive && pid !== undefined) {\n try {\n if (sendSignal(pid, 'SIGKILL')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 2_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGKILL failed: ${String(error)}`);\n }\n }\n\n if (stopped) {\n await cleanupFile(pidPath, 'PID file', log);\n await cleanupFile(socketPath, 'socket file', log);\n log?.('Daemon stopped.');\n }\n\n return stopped;\n}\n\n/**\n * Remove a file best-effort, logging any failure rather than letting it\n * propagate. ENOENT is silently ignored via `force: true`.\n *\n * @param path - The file path to remove.\n * @param label - Human-readable label for log messages.\n * @param log - Optional log sink.\n */\nasync function cleanupFile(\n path: string,\n label: string,\n log: ((message: string) => void) | undefined,\n): Promise<void> {\n await rm(path, { force: true }).catch((error: unknown) => {\n log?.(`Failed to remove ${label}: ${String(error)}`);\n });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stop-daemon.mjs","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,yBAAyB;AAEtC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"stop-daemon.mjs","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":"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","sourcesContent":["import { rm } from 'node:fs/promises';\n\nimport { pingDaemon, sendCommand } from './daemon-client';\nimport { isProcessAlive, readPidFile, sendSignal, waitFor } from './utils';\n\n/**\n * Stop the daemon, preferring a graceful shutdown.\n *\n * Resolution order when a live daemon is present:\n * 1. If the socket is responsive, request a graceful `shutdown` over it.\n * 2. If the recorded PID is still alive, escalate to SIGTERM.\n * 3. ...then SIGKILL.\n *\n * Signals (steps 2-3) are only ever sent against a PID that is observed alive.\n * The socket-absent + alive-PID branch trades a small risk of signalling a\n * recycled PID for the larger risk of leaving an orphan daemon holding the\n * SQLite database — which `daemon purge` would otherwise wipe out from under\n * it.\n *\n * When the socket is NOT responsive AND the recorded PID is dead (or there is\n * no PID file), there is no live daemon: a lingering socket or PID file is\n * stale leftovers from a daemon that already exited — typically one that\n * crashed without running its own cleanup. Those files are removed and the\n * stop is reported as successful, rather than failing on a daemon that is\n * already gone.\n *\n * @param socketPath - The daemon socket path.\n * @param pidPath - The daemon PID file path.\n * @param log - Optional logging function for status messages.\n * @returns True if the daemon was stopped (or was not running).\n */\nexport async function stopDaemon(\n socketPath: string,\n pidPath: string,\n log?: (message: string) => void,\n): Promise<boolean> {\n const pid = await readPidFile(pidPath);\n const ping = await pingDaemon(socketPath);\n const socketObserved =\n ping.status === 'responsive' || ping.status === 'unreachable';\n const processAlive = pid !== undefined && isProcessAlive(pid);\n\n // Only `absent` and `refused` prove no live daemon; `permission`/`timeout`/\n // `protocol` may be a wedged or foreign daemon, so those fall through.\n const socketProvenGone =\n ping.status === 'absent' ||\n (ping.status === 'unreachable' && ping.reason === 'refused');\n\n if (socketProvenGone && !processAlive) {\n await cleanupFile(pidPath, 'PID file', log);\n await cleanupFile(socketPath, 'socket file', log);\n return true;\n }\n\n log?.('Stopping daemon...');\n\n let stopped = false;\n\n // Strategy 1: Graceful socket-based shutdown.\n if (ping.status === 'responsive') {\n try {\n await sendCommand({ socketPath, method: 'shutdown' });\n } catch (error) {\n log?.(`Graceful shutdown request failed: ${String(error)}`);\n }\n // A quiet socket does not prove the process exited; require the recorded\n // pid to be gone too, so a daemon that outlived its socket falls through to\n // SIGTERM/SIGKILL rather than being orphaned.\n stopped = await waitFor(\n async () =>\n (await pingDaemon(socketPath)).status !== 'responsive' &&\n (pid === undefined || !isProcessAlive(pid)),\n 5_000,\n );\n }\n\n // Strategy 2: SIGTERM. Signal when either the socket was observed or the\n // recorded PID is alive; the absent+alive case typically means someone\n // removed the socket from under a live daemon.\n if (!stopped && processAlive && pid !== undefined) {\n if (!socketObserved) {\n log?.(\n `Socket at ${socketPath} is absent but recorded pid ${pid} is alive; signalling anyway.`,\n );\n }\n try {\n if (sendSignal(pid, 'SIGTERM')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 5_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGTERM failed: ${String(error)}`);\n }\n }\n\n // Strategy 3: SIGKILL.\n if (!stopped && processAlive && pid !== undefined) {\n try {\n if (sendSignal(pid, 'SIGKILL')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 2_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGKILL failed: ${String(error)}`);\n }\n }\n\n if (stopped) {\n await cleanupFile(pidPath, 'PID file', log);\n await cleanupFile(socketPath, 'socket file', log);\n log?.('Daemon stopped.');\n }\n\n return stopped;\n}\n\n/**\n * Remove a file best-effort, logging any failure rather than letting it\n * propagate. ENOENT is silently ignored via `force: true`.\n *\n * @param path - The file path to remove.\n * @param label - Human-readable label for log messages.\n * @param log - Optional log sink.\n */\nasync function cleanupFile(\n path: string,\n label: string,\n log: ((message: string) => void) | undefined,\n): Promise<void> {\n await rm(path, { force: true }).catch((error: unknown) => {\n log?.(`Failed to remove ${label}: ${String(error)}`);\n });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.cjs","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":";;;AAiDA;;;;;;;;;;;GAWG;AACH,SAAgB,aAAa,CAC3B,YAA6B,EAC7B,GAAiC;IAEjC,OAAO,EAAE,YAAY,EAAE,GAAG,EAGzB,CAAC;AACJ,CAAC;AARD,sCAQC","sourcesContent":["import type { Struct } from '@metamask/superstruct';\nimport type { Json } from '@metamask/utils';\n\nimport type { Password, Srp } from './secrets
|
|
1
|
+
{"version":3,"file":"types.cjs","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":";;;AAiDA;;;;;;;;;;;GAWG;AACH,SAAgB,aAAa,CAC3B,YAA6B,EAC7B,GAAiC;IAEjC,OAAO,EAAE,YAAY,EAAE,GAAG,EAGzB,CAAC;AACJ,CAAC;AARD,sCAQC","sourcesContent":["import type { Struct } from '@metamask/superstruct';\nimport type { Json } from '@metamask/utils';\n\nimport type { Password, Srp } from './secrets';\n\n/**\n * Sink for daemon diagnostic messages. A backgrounded daemon's stdio may be\n * discarded, so hosts supply a logger that writes somewhere durable.\n */\nexport type Logger = (message: string) => void;\n\n/**\n * A function that handles a JSON-RPC method call after its params have been\n * validated by the corresponding {@link RpcHandlerDefinition.paramsStruct}.\n */\nexport type RpcHandler<TParams, TResult> = (\n params: TParams,\n) => Promise<TResult>;\n\n/**\n * Definition for a single JSON-RPC method: the struct that validates\n * incoming `params` plus the handler that runs once `params` is known to\n * match.\n *\n * The server (see `rpc-socket-server.ts`) validates the raw `params` against\n * `paramsStruct` before invoking `run`, so each handler body can trust the\n * shape of its input without re-checking.\n */\nexport type RpcHandlerDefinition<TParams, TResult extends Json | void> = {\n paramsStruct: Struct<TParams>;\n run: RpcHandler<TParams, TResult>;\n};\n\n/**\n * A map of RPC method names to their handler definitions.\n *\n * `TParams` is erased to `unknown` here so definitions with different narrow\n * params types (e.g. `null` vs. a tuple) can coexist in the same map. Consumers\n * therefore see each `run` as accepting `unknown` and must validate `params`\n * against the paired `paramsStruct` before invoking it — which is exactly what\n * the server (see `rpc-socket-server.ts`) does. The concrete `TParams` is\n * captured inside {@link defineHandler}, where the struct and handler are bound\n * together.\n */\nexport type RpcHandlerMap = Record<\n string,\n RpcHandlerDefinition<unknown, Json | void>\n>;\n\n/**\n * Bundle a params struct with the handler that runs once `params` is\n * validated. The server invokes `run` only after `paramsStruct` accepts the\n * value, so `run` can trust the type of its argument.\n *\n * The returned definition erases `TParams` to `unknown` so heterogeneous\n * handlers can share an {@link RpcHandlerMap}.\n *\n * @param paramsStruct - Struct that validates `params` for this method.\n * @param run - Handler invoked with the validated params.\n * @returns An {@link RpcHandlerDefinition} suitable for an {@link RpcHandlerMap}.\n */\nexport function defineHandler<TParams, TResult extends Json | void>(\n paramsStruct: Struct<TParams>,\n run: RpcHandler<TParams, TResult>,\n): RpcHandlerDefinition<unknown, TResult> {\n return { paramsStruct, run } as unknown as RpcHandlerDefinition<\n unknown,\n TResult\n >;\n}\n\n/**\n * Typed wrapper around `wallet.messenger.call` used by the `call` RPC.\n *\n * The messenger is strongly typed by action name; the daemon exposes the full\n * messenger surface over the socket and dispatches by string, so we consolidate\n * the unsafe cast into a single, documented escape hatch instead of repeating\n * it at each call site.\n */\nexport type RpcDispatcher = (action: string, ...args: Json[]) => Promise<Json>;\n\n/**\n * Resolved paths for daemon state files.\n */\nexport type DaemonPaths = {\n socketPath: string;\n pidPath: string;\n logPath: string;\n dbPath: string;\n};\n\n/**\n * Status information returned by the daemon's `getStatus` RPC method.\n */\nexport type DaemonStatusInfo = {\n pid: number;\n uptime: number;\n};\n\n/**\n * Configuration passed to the daemon spawner.\n *\n * `password` is optional: when omitted, the daemon starts without unlocking\n * the keyring, and the caller is expected to use `mm wallet unlock` before\n * any keyring-bound operation. First-run startup still requires both\n * `password` and `srp`; without `password`, the daemon will exit during\n * startup with a clear error. `srp` is always required by the type and\n * forwarded to the daemon; it is only consumed on first run.\n */\nexport type DaemonSpawnConfig = {\n dataDir: string;\n infuraProjectId: string;\n password?: Password;\n srp: Srp;\n packageRoot: string;\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.cts","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,8BAA8B;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.cts","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,8BAA8B;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAkB;AAE/C;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAE/C;;;GAGG;AACH,MAAM,MAAM,UAAU,CAAC,OAAO,EAAE,OAAO,IAAI,CACzC,MAAM,EAAE,OAAO,KACZ,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,CAAC,OAAO,EAAE,OAAO,SAAS,IAAI,GAAG,IAAI,IAAI;IACvE,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,GAAG,EAAE,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAChC,MAAM,EACN,oBAAoB,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC,CAC3C,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,SAAS,IAAI,GAAG,IAAI,EAChE,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,EAC7B,GAAG,EAAE,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,GAChC,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAKxC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE/E;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,8BAA8B;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,8BAA8B;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,sBAAkB;AAE/C;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAE/C;;;GAGG;AACH,MAAM,MAAM,UAAU,CAAC,OAAO,EAAE,OAAO,IAAI,CACzC,MAAM,EAAE,OAAO,KACZ,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,CAAC,OAAO,EAAE,OAAO,SAAS,IAAI,GAAG,IAAI,IAAI;IACvE,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,GAAG,EAAE,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAChC,MAAM,EACN,oBAAoB,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC,CAC3C,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,SAAS,IAAI,GAAG,IAAI,EAChE,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,EAC7B,GAAG,EAAE,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,GAChC,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAKxC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE/E;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.mjs","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAiDA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAC3B,YAA6B,EAC7B,GAAiC;IAEjC,OAAO,EAAE,YAAY,EAAE,GAAG,EAGzB,CAAC;AACJ,CAAC","sourcesContent":["import type { Struct } from '@metamask/superstruct';\nimport type { Json } from '@metamask/utils';\n\nimport type { Password, Srp } from './secrets
|
|
1
|
+
{"version":3,"file":"types.mjs","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAiDA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAC3B,YAA6B,EAC7B,GAAiC;IAEjC,OAAO,EAAE,YAAY,EAAE,GAAG,EAGzB,CAAC;AACJ,CAAC","sourcesContent":["import type { Struct } from '@metamask/superstruct';\nimport type { Json } from '@metamask/utils';\n\nimport type { Password, Srp } from './secrets';\n\n/**\n * Sink for daemon diagnostic messages. A backgrounded daemon's stdio may be\n * discarded, so hosts supply a logger that writes somewhere durable.\n */\nexport type Logger = (message: string) => void;\n\n/**\n * A function that handles a JSON-RPC method call after its params have been\n * validated by the corresponding {@link RpcHandlerDefinition.paramsStruct}.\n */\nexport type RpcHandler<TParams, TResult> = (\n params: TParams,\n) => Promise<TResult>;\n\n/**\n * Definition for a single JSON-RPC method: the struct that validates\n * incoming `params` plus the handler that runs once `params` is known to\n * match.\n *\n * The server (see `rpc-socket-server.ts`) validates the raw `params` against\n * `paramsStruct` before invoking `run`, so each handler body can trust the\n * shape of its input without re-checking.\n */\nexport type RpcHandlerDefinition<TParams, TResult extends Json | void> = {\n paramsStruct: Struct<TParams>;\n run: RpcHandler<TParams, TResult>;\n};\n\n/**\n * A map of RPC method names to their handler definitions.\n *\n * `TParams` is erased to `unknown` here so definitions with different narrow\n * params types (e.g. `null` vs. a tuple) can coexist in the same map. Consumers\n * therefore see each `run` as accepting `unknown` and must validate `params`\n * against the paired `paramsStruct` before invoking it — which is exactly what\n * the server (see `rpc-socket-server.ts`) does. The concrete `TParams` is\n * captured inside {@link defineHandler}, where the struct and handler are bound\n * together.\n */\nexport type RpcHandlerMap = Record<\n string,\n RpcHandlerDefinition<unknown, Json | void>\n>;\n\n/**\n * Bundle a params struct with the handler that runs once `params` is\n * validated. The server invokes `run` only after `paramsStruct` accepts the\n * value, so `run` can trust the type of its argument.\n *\n * The returned definition erases `TParams` to `unknown` so heterogeneous\n * handlers can share an {@link RpcHandlerMap}.\n *\n * @param paramsStruct - Struct that validates `params` for this method.\n * @param run - Handler invoked with the validated params.\n * @returns An {@link RpcHandlerDefinition} suitable for an {@link RpcHandlerMap}.\n */\nexport function defineHandler<TParams, TResult extends Json | void>(\n paramsStruct: Struct<TParams>,\n run: RpcHandler<TParams, TResult>,\n): RpcHandlerDefinition<unknown, TResult> {\n return { paramsStruct, run } as unknown as RpcHandlerDefinition<\n unknown,\n TResult\n >;\n}\n\n/**\n * Typed wrapper around `wallet.messenger.call` used by the `call` RPC.\n *\n * The messenger is strongly typed by action name; the daemon exposes the full\n * messenger surface over the socket and dispatches by string, so we consolidate\n * the unsafe cast into a single, documented escape hatch instead of repeating\n * it at each call site.\n */\nexport type RpcDispatcher = (action: string, ...args: Json[]) => Promise<Json>;\n\n/**\n * Resolved paths for daemon state files.\n */\nexport type DaemonPaths = {\n socketPath: string;\n pidPath: string;\n logPath: string;\n dbPath: string;\n};\n\n/**\n * Status information returned by the daemon's `getStatus` RPC method.\n */\nexport type DaemonStatusInfo = {\n pid: number;\n uptime: number;\n};\n\n/**\n * Configuration passed to the daemon spawner.\n *\n * `password` is optional: when omitted, the daemon starts without unlocking\n * the keyring, and the caller is expected to use `mm wallet unlock` before\n * any keyring-bound operation. First-run startup still requires both\n * `password` and `srp`; without `password`, the daemon will exit during\n * startup with a clear error. `srp` is always required by the type and\n * forwarded to the daemon; it is only consumed on first run.\n */\nexport type DaemonSpawnConfig = {\n dataDir: string;\n infuraProjectId: string;\n password?: Password;\n srp: Srp;\n packageRoot: string;\n};\n"]}
|
|
@@ -5,8 +5,8 @@ const remote_feature_flag_controller_1 = require("@metamask/remote-feature-flag-
|
|
|
5
5
|
const storage_service_1 = require("@metamask/storage-service");
|
|
6
6
|
const wallet_1 = require("@metamask/wallet");
|
|
7
7
|
const promises_1 = require("node:fs/promises");
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const KeyValueStore_1 = require("../persistence/KeyValueStore.cjs");
|
|
9
|
+
const persistence_1 = require("../persistence/persistence.cjs");
|
|
10
10
|
const IN_MEMORY_DATABASE_PATH = ':memory:';
|
|
11
11
|
/**
|
|
12
12
|
* Build the per-instance options the daemon's `Wallet` is constructed with.
|
|
@@ -47,11 +47,6 @@ function buildInstanceOptions(infuraProjectId) {
|
|
|
47
47
|
connectivityController: {
|
|
48
48
|
connectivityAdapter: new wallet_1.AlwaysOnlineAdapter(),
|
|
49
49
|
},
|
|
50
|
-
gasFeeController: {
|
|
51
|
-
// Identifies the CLI to the gas estimation API via the `X-Client-Id`
|
|
52
|
-
// header.
|
|
53
|
-
clientId: 'cli',
|
|
54
|
-
},
|
|
55
50
|
networkController: {
|
|
56
51
|
infuraProjectId,
|
|
57
52
|
},
|
|
@@ -129,7 +124,7 @@ function buildInstanceOptions(infuraProjectId) {
|
|
|
129
124
|
*/
|
|
130
125
|
async function createWallet({ databasePath, password, srp, infuraProjectId, log, }) {
|
|
131
126
|
const logFn = log ?? ((message) => console.error(message));
|
|
132
|
-
const store = new
|
|
127
|
+
const store = new KeyValueStore_1.KeyValueStore(databasePath);
|
|
133
128
|
let wallet;
|
|
134
129
|
let unsubscribe;
|
|
135
130
|
let wasFirstRun = false;
|
|
@@ -147,7 +142,7 @@ async function createWallet({ databasePath, password, srp, infuraProjectId, log,
|
|
|
147
142
|
state,
|
|
148
143
|
instanceOptions: buildInstanceOptions(infuraProjectId),
|
|
149
144
|
});
|
|
150
|
-
unsubscribe = (0,
|
|
145
|
+
unsubscribe = (0, persistence_1.subscribeToChanges)(wallet.messenger, wallet.controllerMetadata, store, logFn);
|
|
151
146
|
// Complete post-construction controller setup before serving requests —
|
|
152
147
|
// e.g. `NetworkController.init` applies the selected network so a provider
|
|
153
148
|
// is available. `init` settles every step independently; a rejected step
|
|
@@ -223,7 +218,7 @@ async function loadPersistedState(store, infuraProjectId, logFn) {
|
|
|
223
218
|
instanceOptions: buildInstanceOptions(infuraProjectId),
|
|
224
219
|
});
|
|
225
220
|
try {
|
|
226
|
-
return (0,
|
|
221
|
+
return (0, persistence_1.loadState)(store, probe.controllerMetadata);
|
|
227
222
|
}
|
|
228
223
|
finally {
|
|
229
224
|
await probe.destroy().catch((error) => {
|
|
@@ -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 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
|
+
{"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';\nimport { loadState, subscribeToChanges } from '../persistence/persistence';\nimport type { Password, Srp } from './secrets';\nimport type { Logger } from './types';\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 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;AAM1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,
|
|
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,sBAAkB;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAgB;AAItC,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;AA+EF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;AAM1B,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,
|
|
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,sBAAkB;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAgB;AAItC,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;AA+EF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,YAAY,CAAC,EACjC,YAAY,EACZ,QAAQ,EACR,GAAG,EACH,eAAe,EACf,GAAG,GACJ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuGlD"}
|
|
@@ -44,11 +44,6 @@ function buildInstanceOptions(infuraProjectId) {
|
|
|
44
44
|
connectivityController: {
|
|
45
45
|
connectivityAdapter: new AlwaysOnlineAdapter(),
|
|
46
46
|
},
|
|
47
|
-
gasFeeController: {
|
|
48
|
-
// Identifies the CLI to the gas estimation API via the `X-Client-Id`
|
|
49
|
-
// header.
|
|
50
|
-
clientId: 'cli',
|
|
51
|
-
},
|
|
52
47
|
networkController: {
|
|
53
48
|
infuraProjectId,
|
|
54
49
|
},
|
|
@@ -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 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
|
+
{"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';\nimport { loadState, subscribeToChanges } from '../persistence/persistence';\nimport type { Password, Srp } from './secrets';\nimport type { Logger } from './types';\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 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":"persistence.cjs","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StateMetadataConstraint } from '@metamask/base-controller';\nimport { hasProperty } from '@metamask/utils';\nimport type { Json } from '@metamask/utils';\nimport type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n} from '@metamask/wallet';\nimport type { Patch } from 'immer';\n\nimport type { KeyValueStore } from './KeyValueStore.js';\n\n/**\n * Handler for a controller's `stateChanged` event: the new controller state and\n * the Immer patches describing what changed.\n */\ntype StateChangedHandler = (\n state: Record<string, Json>,\n patches: Patch[],\n) => void;\n\n/**\n * Construct a store key from a controller name and property name.\n *\n * @param controllerName - The controller name.\n * @param propertyName - The property name.\n * @returns The store key in the format `ControllerName.propertyName`.\n */\nfunction storeKey(controllerName: string, propertyName: string): string {\n return `${controllerName}.${propertyName}`;\n}\n\n/**\n * Load persisted state from the key-value store and reconstruct it as\n * a record keyed by controller name.\n *\n * Keys in the store follow the format `ControllerName.propertyName`.\n * This function groups them into `{ [controllerName]: { [propertyName]: value } }`.\n *\n * Only properties that are currently persist-flagged in `controllerMetadata`\n * are rehydrated. Rows for controllers or properties that no longer exist — or\n * whose `persist` flag has since been disabled — are ignored. This keeps\n * loading symmetric with {@link subscribeToChanges}, which only ever writes\n * persist-flagged properties: without the filter, a migration that stops\n * persisting a property would leave its stale row on disk to be resurrected\n * into the `Wallet` constructor state on the next restart.\n *\n * @param store - The key-value store to read from.\n * @param controllerMetadata - A map from controller name to its state metadata,\n * used to filter out keys that are no longer persist-flagged.\n * @returns A record of controller states, keyed by controller name, suitable\n * for the `state` option of the `Wallet` constructor.\n */\nexport function loadState(\n store: KeyValueStore,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n): Record<string, Record<string, Json>> {\n const allPairs = store.getAll();\n const state: Record<string, Record<string, Json>> = {};\n\n for (const [key, value] of Object.entries(allPairs)) {\n const dotIndex = key.indexOf('.');\n if (dotIndex === -1) {\n throw new Error(\n `Invalid key in store: '${key}'. Expected format 'ControllerName.propertyName'.`,\n );\n }\n const controllerName = key.slice(0, dotIndex);\n const propertyName = key.slice(dotIndex + 1);\n\n if (!controllerName || !propertyName) {\n throw new Error(\n `Invalid key in store: '${key}'. Both controller name and property name must be non-empty.`,\n );\n }\n\n if (!isPersisted(controllerMetadata[controllerName], propertyName)) {\n continue;\n }\n\n if (!state[controllerName]) {\n state[controllerName] = {};\n }\n state[controllerName][propertyName] = value;\n }\n return state;\n}\n\n/**\n * Subscribe to all controller `stateChanged` events and persist changes\n * to the key-value store.\n *\n * For each controller's metadata, this function determines which state\n * properties are persist-flagged. When a `stateChanged` event fires, it uses\n * the Immer patches to identify which top-level properties changed, filters\n * to only persist-flagged properties, and writes them to the store.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param controllerMetadata - A map from controller name to its state metadata.\n * @param store - The key-value store to write to.\n * @param log - Optional logger for persistence-write failures. Defaults to\n * `console.error` when omitted. A daemon host should supply its own logger,\n * since a backgrounded daemon's stdio may be discarded.\n * @returns A function that unsubscribes all persistence handlers.\n */\nexport function subscribeToChanges(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n store: KeyValueStore,\n log?: (message: string) => void,\n): () => void {\n const unsubscribers: (() => void)[] = [];\n const logFn =\n log ??\n ((message: string): void => {\n console.error(message);\n });\n\n for (const [controllerName, metadata] of Object.entries(controllerMetadata)) {\n const persistedProperties = getPersistPropertyNames(metadata);\n if (persistedProperties.size === 0) {\n continue;\n }\n\n const eventType = `${controllerName}:stateChanged`;\n\n const handler: StateChangedHandler = (state, patches) => {\n const changed = getChangedProperties(patches, persistedProperties);\n\n for (const prop of changed) {\n const key = storeKey(controllerName, prop);\n const removed = !hasProperty(state, prop);\n\n // Derive the value before the try/catch so a throwing `StateDeriver`\n // surfaces as its own error instead of a misreported write failure.\n const persistFlag = metadata[prop]?.persist;\n const value =\n !removed && typeof persistFlag === 'function'\n ? persistFlag(state[prop] as never)\n : state[prop];\n\n try {\n if (removed) {\n store.delete(key);\n } else {\n store.set(key, value);\n }\n } catch (error) {\n // TODO: Surface persistence-write failures up the stack so callers\n // can decide to halt rather than continue with diverging in-memory\n // and on-disk state. For now, log and continue.\n logFn(`Failed to persist state for ${key}: ${String(error)}`);\n }\n }\n };\n\n unsubscribers.push(subscribeToStateChanged(messenger, eventType, handler));\n }\n\n const unsubscribeAll = (): void => {\n while (unsubscribers.length > 0) {\n unsubscribers.pop()?.();\n }\n };\n\n return unsubscribeAll;\n}\n\n/**\n * Subscribe a handler to a controller's `stateChanged` event.\n *\n * The event name is built from a runtime controller name, so it widens to\n * `string` and cannot be proven to be a literal member of the messenger's event\n * union at compile time. This helper localizes that single unavoidable cast\n * behind a typed {@link StateChangedHandler}, so the `(state, patches)` payload\n * shape stays compile-checked at every call site instead of being erased by a\n * statement-level `@ts-expect-error`.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param eventType - The `${controllerName}:stateChanged` event name.\n * @param handler - The state-change handler to register.\n * @returns A function that unsubscribes the handler.\n */\nfunction subscribeToStateChanged(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n eventType: string,\n handler: StateChangedHandler,\n): () => void {\n const subscriber = messenger as unknown as {\n subscribe: (eventType: string, handler: StateChangedHandler) => void;\n unsubscribe: (eventType: string, handler: StateChangedHandler) => void;\n };\n subscriber.subscribe(eventType, handler);\n return () => {\n subscriber.unsubscribe(eventType, handler);\n };\n}\n\n/**\n * Determine whether a property is currently persist-flagged.\n *\n * The `persist` flag is truthy when it is `true` or a `StateDeriver` function,\n * and falsy when it is `false` or when the controller or property is absent\n * from the metadata. `loadState` and `subscribeToChanges` share this predicate\n * so the read and write paths can never disagree on what counts as persisted.\n *\n * @param metadata - The controller's state metadata, or `undefined` when the\n * controller is absent from the metadata map.\n * @param propertyName - The property name to check.\n * @returns `true` if the property should be persisted.\n */\nfunction isPersisted(\n metadata: Readonly<StateMetadataConstraint> | undefined,\n propertyName: string,\n): boolean {\n return Boolean(metadata?.[propertyName]?.persist);\n}\n\n/**\n * Get the set of property names whose `persist` metadata is truthy\n * (either `true` or a `StateDeriver` function).\n *\n * @param metadata - The controller's state metadata.\n * @returns A set of property names that should be persisted.\n */\nfunction getPersistPropertyNames(\n metadata: StateMetadataConstraint,\n): ReadonlySet<string> {\n const names = new Set<string>();\n for (const key of Object.keys(metadata)) {\n if (isPersisted(metadata, key)) {\n names.add(key);\n }\n }\n return names;\n}\n\n/**\n * Extracts the set of persist-flagged top-level property names that changed\n * from an array of Immer patches.\n *\n * If any patch has an empty path (indicating a root state replacement),\n * all persist-flagged properties are returned.\n *\n * @param patches - Immer patches from a state update.\n * @param persistedProperties - The set of persist-flagged property names.\n * @returns A set of top-level property names that were modified.\n */\nfunction getChangedProperties(\n patches: Patch[],\n persistedProperties: ReadonlySet<string>,\n): ReadonlySet<string> {\n const changed = new Set<string>();\n for (const patch of patches) {\n if (patch.path.length === 0) {\n return persistedProperties;\n }\n\n const prop = String(patch.path[0]);\n if (persistedProperties.has(prop)) {\n changed.add(prop);\n }\n }\n return changed;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"persistence.cjs","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StateMetadataConstraint } from '@metamask/base-controller';\nimport { hasProperty } from '@metamask/utils';\nimport type { Json } from '@metamask/utils';\nimport type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n} from '@metamask/wallet';\nimport type { Patch } from 'immer';\n\nimport type { KeyValueStore } from './KeyValueStore';\n\n/**\n * Handler for a controller's `stateChanged` event: the new controller state and\n * the Immer patches describing what changed.\n */\ntype StateChangedHandler = (\n state: Record<string, Json>,\n patches: Patch[],\n) => void;\n\n/**\n * Construct a store key from a controller name and property name.\n *\n * @param controllerName - The controller name.\n * @param propertyName - The property name.\n * @returns The store key in the format `ControllerName.propertyName`.\n */\nfunction storeKey(controllerName: string, propertyName: string): string {\n return `${controllerName}.${propertyName}`;\n}\n\n/**\n * Load persisted state from the key-value store and reconstruct it as\n * a record keyed by controller name.\n *\n * Keys in the store follow the format `ControllerName.propertyName`.\n * This function groups them into `{ [controllerName]: { [propertyName]: value } }`.\n *\n * Only properties that are currently persist-flagged in `controllerMetadata`\n * are rehydrated. Rows for controllers or properties that no longer exist — or\n * whose `persist` flag has since been disabled — are ignored. This keeps\n * loading symmetric with {@link subscribeToChanges}, which only ever writes\n * persist-flagged properties: without the filter, a migration that stops\n * persisting a property would leave its stale row on disk to be resurrected\n * into the `Wallet` constructor state on the next restart.\n *\n * @param store - The key-value store to read from.\n * @param controllerMetadata - A map from controller name to its state metadata,\n * used to filter out keys that are no longer persist-flagged.\n * @returns A record of controller states, keyed by controller name, suitable\n * for the `state` option of the `Wallet` constructor.\n */\nexport function loadState(\n store: KeyValueStore,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n): Record<string, Record<string, Json>> {\n const allPairs = store.getAll();\n const state: Record<string, Record<string, Json>> = {};\n\n for (const [key, value] of Object.entries(allPairs)) {\n const dotIndex = key.indexOf('.');\n if (dotIndex === -1) {\n throw new Error(\n `Invalid key in store: '${key}'. Expected format 'ControllerName.propertyName'.`,\n );\n }\n const controllerName = key.slice(0, dotIndex);\n const propertyName = key.slice(dotIndex + 1);\n\n if (!controllerName || !propertyName) {\n throw new Error(\n `Invalid key in store: '${key}'. Both controller name and property name must be non-empty.`,\n );\n }\n\n if (!isPersisted(controllerMetadata[controllerName], propertyName)) {\n continue;\n }\n\n if (!state[controllerName]) {\n state[controllerName] = {};\n }\n state[controllerName][propertyName] = value;\n }\n return state;\n}\n\n/**\n * Subscribe to all controller `stateChanged` events and persist changes\n * to the key-value store.\n *\n * For each controller's metadata, this function determines which state\n * properties are persist-flagged. When a `stateChanged` event fires, it uses\n * the Immer patches to identify which top-level properties changed, filters\n * to only persist-flagged properties, and writes them to the store.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param controllerMetadata - A map from controller name to its state metadata.\n * @param store - The key-value store to write to.\n * @param log - Optional logger for persistence-write failures. Defaults to\n * `console.error` when omitted. A daemon host should supply its own logger,\n * since a backgrounded daemon's stdio may be discarded.\n * @returns A function that unsubscribes all persistence handlers.\n */\nexport function subscribeToChanges(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n store: KeyValueStore,\n log?: (message: string) => void,\n): () => void {\n const unsubscribers: (() => void)[] = [];\n const logFn =\n log ??\n ((message: string): void => {\n console.error(message);\n });\n\n for (const [controllerName, metadata] of Object.entries(controllerMetadata)) {\n const persistedProperties = getPersistPropertyNames(metadata);\n if (persistedProperties.size === 0) {\n continue;\n }\n\n const eventType = `${controllerName}:stateChanged`;\n\n const handler: StateChangedHandler = (state, patches) => {\n const changed = getChangedProperties(patches, persistedProperties);\n\n for (const prop of changed) {\n const key = storeKey(controllerName, prop);\n const removed = !hasProperty(state, prop);\n\n // Derive the value before the try/catch so a throwing `StateDeriver`\n // surfaces as its own error instead of a misreported write failure.\n const persistFlag = metadata[prop]?.persist;\n const value =\n !removed && typeof persistFlag === 'function'\n ? persistFlag(state[prop] as never)\n : state[prop];\n\n try {\n if (removed) {\n store.delete(key);\n } else {\n store.set(key, value);\n }\n } catch (error) {\n // TODO: Surface persistence-write failures up the stack so callers\n // can decide to halt rather than continue with diverging in-memory\n // and on-disk state. For now, log and continue.\n logFn(`Failed to persist state for ${key}: ${String(error)}`);\n }\n }\n };\n\n unsubscribers.push(subscribeToStateChanged(messenger, eventType, handler));\n }\n\n const unsubscribeAll = (): void => {\n while (unsubscribers.length > 0) {\n unsubscribers.pop()?.();\n }\n };\n\n return unsubscribeAll;\n}\n\n/**\n * Subscribe a handler to a controller's `stateChanged` event.\n *\n * The event name is built from a runtime controller name, so it widens to\n * `string` and cannot be proven to be a literal member of the messenger's event\n * union at compile time. This helper localizes that single unavoidable cast\n * behind a typed {@link StateChangedHandler}, so the `(state, patches)` payload\n * shape stays compile-checked at every call site instead of being erased by a\n * statement-level `@ts-expect-error`.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param eventType - The `${controllerName}:stateChanged` event name.\n * @param handler - The state-change handler to register.\n * @returns A function that unsubscribes the handler.\n */\nfunction subscribeToStateChanged(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n eventType: string,\n handler: StateChangedHandler,\n): () => void {\n const subscriber = messenger as unknown as {\n subscribe: (eventType: string, handler: StateChangedHandler) => void;\n unsubscribe: (eventType: string, handler: StateChangedHandler) => void;\n };\n subscriber.subscribe(eventType, handler);\n return () => {\n subscriber.unsubscribe(eventType, handler);\n };\n}\n\n/**\n * Determine whether a property is currently persist-flagged.\n *\n * The `persist` flag is truthy when it is `true` or a `StateDeriver` function,\n * and falsy when it is `false` or when the controller or property is absent\n * from the metadata. `loadState` and `subscribeToChanges` share this predicate\n * so the read and write paths can never disagree on what counts as persisted.\n *\n * @param metadata - The controller's state metadata, or `undefined` when the\n * controller is absent from the metadata map.\n * @param propertyName - The property name to check.\n * @returns `true` if the property should be persisted.\n */\nfunction isPersisted(\n metadata: Readonly<StateMetadataConstraint> | undefined,\n propertyName: string,\n): boolean {\n return Boolean(metadata?.[propertyName]?.persist);\n}\n\n/**\n * Get the set of property names whose `persist` metadata is truthy\n * (either `true` or a `StateDeriver` function).\n *\n * @param metadata - The controller's state metadata.\n * @returns A set of property names that should be persisted.\n */\nfunction getPersistPropertyNames(\n metadata: StateMetadataConstraint,\n): ReadonlySet<string> {\n const names = new Set<string>();\n for (const key of Object.keys(metadata)) {\n if (isPersisted(metadata, key)) {\n names.add(key);\n }\n }\n return names;\n}\n\n/**\n * Extracts the set of persist-flagged top-level property names that changed\n * from an array of Immer patches.\n *\n * If any patch has an empty path (indicating a root state replacement),\n * all persist-flagged properties are returned.\n *\n * @param patches - Immer patches from a state update.\n * @param persistedProperties - The set of persist-flagged property names.\n * @returns A set of top-level property names that were modified.\n */\nfunction getChangedProperties(\n patches: Patch[],\n persistedProperties: ReadonlySet<string>,\n): ReadonlySet<string> {\n const changed = new Set<string>();\n for (const patch of patches) {\n if (patch.path.length === 0) {\n return persistedProperties;\n }\n\n const prop = String(patch.path[0]);\n if (persistedProperties.has(prop)) {\n changed.add(prop);\n }\n }\n return changed;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.d.cts","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kCAAkC;AAEzE,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAC5C,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACd,yBAAyB;AAG1B,OAAO,KAAK,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"persistence.d.cts","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kCAAkC;AAEzE,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAC5C,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACd,yBAAyB;AAG1B,OAAO,KAAK,EAAE,aAAa,EAAE,4BAAwB;AAsBrD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,aAAa,EACpB,kBAAkB,EAAE,QAAQ,CAC1B,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,uBAAuB,CAAC,CAAC,CAClD,GACA,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CA8BtC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,QAAQ,CAAC,aAAa,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC,EACjE,kBAAkB,EAAE,QAAQ,CAC1B,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,uBAAuB,CAAC,CAAC,CAClD,EACD,KAAK,EAAE,aAAa,EACpB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,MAAM,IAAI,CAwDZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.d.mts","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kCAAkC;AAEzE,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAC5C,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACd,yBAAyB;AAG1B,OAAO,KAAK,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"persistence.d.mts","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kCAAkC;AAEzE,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAC5C,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACd,yBAAyB;AAG1B,OAAO,KAAK,EAAE,aAAa,EAAE,4BAAwB;AAsBrD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,aAAa,EACpB,kBAAkB,EAAE,QAAQ,CAC1B,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,uBAAuB,CAAC,CAAC,CAClD,GACA,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CA8BtC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,QAAQ,CAAC,aAAa,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC,EACjE,kBAAkB,EAAE,QAAQ,CAC1B,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,uBAAuB,CAAC,CAAC,CAClD,EACD,KAAK,EAAE,aAAa,EACpB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,MAAM,IAAI,CAwDZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.mjs","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StateMetadataConstraint } from '@metamask/base-controller';\nimport { hasProperty } from '@metamask/utils';\nimport type { Json } from '@metamask/utils';\nimport type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n} from '@metamask/wallet';\nimport type { Patch } from 'immer';\n\nimport type { KeyValueStore } from './KeyValueStore.js';\n\n/**\n * Handler for a controller's `stateChanged` event: the new controller state and\n * the Immer patches describing what changed.\n */\ntype StateChangedHandler = (\n state: Record<string, Json>,\n patches: Patch[],\n) => void;\n\n/**\n * Construct a store key from a controller name and property name.\n *\n * @param controllerName - The controller name.\n * @param propertyName - The property name.\n * @returns The store key in the format `ControllerName.propertyName`.\n */\nfunction storeKey(controllerName: string, propertyName: string): string {\n return `${controllerName}.${propertyName}`;\n}\n\n/**\n * Load persisted state from the key-value store and reconstruct it as\n * a record keyed by controller name.\n *\n * Keys in the store follow the format `ControllerName.propertyName`.\n * This function groups them into `{ [controllerName]: { [propertyName]: value } }`.\n *\n * Only properties that are currently persist-flagged in `controllerMetadata`\n * are rehydrated. Rows for controllers or properties that no longer exist — or\n * whose `persist` flag has since been disabled — are ignored. This keeps\n * loading symmetric with {@link subscribeToChanges}, which only ever writes\n * persist-flagged properties: without the filter, a migration that stops\n * persisting a property would leave its stale row on disk to be resurrected\n * into the `Wallet` constructor state on the next restart.\n *\n * @param store - The key-value store to read from.\n * @param controllerMetadata - A map from controller name to its state metadata,\n * used to filter out keys that are no longer persist-flagged.\n * @returns A record of controller states, keyed by controller name, suitable\n * for the `state` option of the `Wallet` constructor.\n */\nexport function loadState(\n store: KeyValueStore,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n): Record<string, Record<string, Json>> {\n const allPairs = store.getAll();\n const state: Record<string, Record<string, Json>> = {};\n\n for (const [key, value] of Object.entries(allPairs)) {\n const dotIndex = key.indexOf('.');\n if (dotIndex === -1) {\n throw new Error(\n `Invalid key in store: '${key}'. Expected format 'ControllerName.propertyName'.`,\n );\n }\n const controllerName = key.slice(0, dotIndex);\n const propertyName = key.slice(dotIndex + 1);\n\n if (!controllerName || !propertyName) {\n throw new Error(\n `Invalid key in store: '${key}'. Both controller name and property name must be non-empty.`,\n );\n }\n\n if (!isPersisted(controllerMetadata[controllerName], propertyName)) {\n continue;\n }\n\n if (!state[controllerName]) {\n state[controllerName] = {};\n }\n state[controllerName][propertyName] = value;\n }\n return state;\n}\n\n/**\n * Subscribe to all controller `stateChanged` events and persist changes\n * to the key-value store.\n *\n * For each controller's metadata, this function determines which state\n * properties are persist-flagged. When a `stateChanged` event fires, it uses\n * the Immer patches to identify which top-level properties changed, filters\n * to only persist-flagged properties, and writes them to the store.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param controllerMetadata - A map from controller name to its state metadata.\n * @param store - The key-value store to write to.\n * @param log - Optional logger for persistence-write failures. Defaults to\n * `console.error` when omitted. A daemon host should supply its own logger,\n * since a backgrounded daemon's stdio may be discarded.\n * @returns A function that unsubscribes all persistence handlers.\n */\nexport function subscribeToChanges(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n store: KeyValueStore,\n log?: (message: string) => void,\n): () => void {\n const unsubscribers: (() => void)[] = [];\n const logFn =\n log ??\n ((message: string): void => {\n console.error(message);\n });\n\n for (const [controllerName, metadata] of Object.entries(controllerMetadata)) {\n const persistedProperties = getPersistPropertyNames(metadata);\n if (persistedProperties.size === 0) {\n continue;\n }\n\n const eventType = `${controllerName}:stateChanged`;\n\n const handler: StateChangedHandler = (state, patches) => {\n const changed = getChangedProperties(patches, persistedProperties);\n\n for (const prop of changed) {\n const key = storeKey(controllerName, prop);\n const removed = !hasProperty(state, prop);\n\n // Derive the value before the try/catch so a throwing `StateDeriver`\n // surfaces as its own error instead of a misreported write failure.\n const persistFlag = metadata[prop]?.persist;\n const value =\n !removed && typeof persistFlag === 'function'\n ? persistFlag(state[prop] as never)\n : state[prop];\n\n try {\n if (removed) {\n store.delete(key);\n } else {\n store.set(key, value);\n }\n } catch (error) {\n // TODO: Surface persistence-write failures up the stack so callers\n // can decide to halt rather than continue with diverging in-memory\n // and on-disk state. For now, log and continue.\n logFn(`Failed to persist state for ${key}: ${String(error)}`);\n }\n }\n };\n\n unsubscribers.push(subscribeToStateChanged(messenger, eventType, handler));\n }\n\n const unsubscribeAll = (): void => {\n while (unsubscribers.length > 0) {\n unsubscribers.pop()?.();\n }\n };\n\n return unsubscribeAll;\n}\n\n/**\n * Subscribe a handler to a controller's `stateChanged` event.\n *\n * The event name is built from a runtime controller name, so it widens to\n * `string` and cannot be proven to be a literal member of the messenger's event\n * union at compile time. This helper localizes that single unavoidable cast\n * behind a typed {@link StateChangedHandler}, so the `(state, patches)` payload\n * shape stays compile-checked at every call site instead of being erased by a\n * statement-level `@ts-expect-error`.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param eventType - The `${controllerName}:stateChanged` event name.\n * @param handler - The state-change handler to register.\n * @returns A function that unsubscribes the handler.\n */\nfunction subscribeToStateChanged(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n eventType: string,\n handler: StateChangedHandler,\n): () => void {\n const subscriber = messenger as unknown as {\n subscribe: (eventType: string, handler: StateChangedHandler) => void;\n unsubscribe: (eventType: string, handler: StateChangedHandler) => void;\n };\n subscriber.subscribe(eventType, handler);\n return () => {\n subscriber.unsubscribe(eventType, handler);\n };\n}\n\n/**\n * Determine whether a property is currently persist-flagged.\n *\n * The `persist` flag is truthy when it is `true` or a `StateDeriver` function,\n * and falsy when it is `false` or when the controller or property is absent\n * from the metadata. `loadState` and `subscribeToChanges` share this predicate\n * so the read and write paths can never disagree on what counts as persisted.\n *\n * @param metadata - The controller's state metadata, or `undefined` when the\n * controller is absent from the metadata map.\n * @param propertyName - The property name to check.\n * @returns `true` if the property should be persisted.\n */\nfunction isPersisted(\n metadata: Readonly<StateMetadataConstraint> | undefined,\n propertyName: string,\n): boolean {\n return Boolean(metadata?.[propertyName]?.persist);\n}\n\n/**\n * Get the set of property names whose `persist` metadata is truthy\n * (either `true` or a `StateDeriver` function).\n *\n * @param metadata - The controller's state metadata.\n * @returns A set of property names that should be persisted.\n */\nfunction getPersistPropertyNames(\n metadata: StateMetadataConstraint,\n): ReadonlySet<string> {\n const names = new Set<string>();\n for (const key of Object.keys(metadata)) {\n if (isPersisted(metadata, key)) {\n names.add(key);\n }\n }\n return names;\n}\n\n/**\n * Extracts the set of persist-flagged top-level property names that changed\n * from an array of Immer patches.\n *\n * If any patch has an empty path (indicating a root state replacement),\n * all persist-flagged properties are returned.\n *\n * @param patches - Immer patches from a state update.\n * @param persistedProperties - The set of persist-flagged property names.\n * @returns A set of top-level property names that were modified.\n */\nfunction getChangedProperties(\n patches: Patch[],\n persistedProperties: ReadonlySet<string>,\n): ReadonlySet<string> {\n const changed = new Set<string>();\n for (const patch of patches) {\n if (patch.path.length === 0) {\n return persistedProperties;\n }\n\n const prop = String(patch.path[0]);\n if (persistedProperties.has(prop)) {\n changed.add(prop);\n }\n }\n return changed;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"persistence.mjs","sourceRoot":"","sources":["../../src/persistence/persistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StateMetadataConstraint } from '@metamask/base-controller';\nimport { hasProperty } from '@metamask/utils';\nimport type { Json } from '@metamask/utils';\nimport type {\n DefaultActions,\n DefaultEvents,\n RootMessenger,\n} from '@metamask/wallet';\nimport type { Patch } from 'immer';\n\nimport type { KeyValueStore } from './KeyValueStore';\n\n/**\n * Handler for a controller's `stateChanged` event: the new controller state and\n * the Immer patches describing what changed.\n */\ntype StateChangedHandler = (\n state: Record<string, Json>,\n patches: Patch[],\n) => void;\n\n/**\n * Construct a store key from a controller name and property name.\n *\n * @param controllerName - The controller name.\n * @param propertyName - The property name.\n * @returns The store key in the format `ControllerName.propertyName`.\n */\nfunction storeKey(controllerName: string, propertyName: string): string {\n return `${controllerName}.${propertyName}`;\n}\n\n/**\n * Load persisted state from the key-value store and reconstruct it as\n * a record keyed by controller name.\n *\n * Keys in the store follow the format `ControllerName.propertyName`.\n * This function groups them into `{ [controllerName]: { [propertyName]: value } }`.\n *\n * Only properties that are currently persist-flagged in `controllerMetadata`\n * are rehydrated. Rows for controllers or properties that no longer exist — or\n * whose `persist` flag has since been disabled — are ignored. This keeps\n * loading symmetric with {@link subscribeToChanges}, which only ever writes\n * persist-flagged properties: without the filter, a migration that stops\n * persisting a property would leave its stale row on disk to be resurrected\n * into the `Wallet` constructor state on the next restart.\n *\n * @param store - The key-value store to read from.\n * @param controllerMetadata - A map from controller name to its state metadata,\n * used to filter out keys that are no longer persist-flagged.\n * @returns A record of controller states, keyed by controller name, suitable\n * for the `state` option of the `Wallet` constructor.\n */\nexport function loadState(\n store: KeyValueStore,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n): Record<string, Record<string, Json>> {\n const allPairs = store.getAll();\n const state: Record<string, Record<string, Json>> = {};\n\n for (const [key, value] of Object.entries(allPairs)) {\n const dotIndex = key.indexOf('.');\n if (dotIndex === -1) {\n throw new Error(\n `Invalid key in store: '${key}'. Expected format 'ControllerName.propertyName'.`,\n );\n }\n const controllerName = key.slice(0, dotIndex);\n const propertyName = key.slice(dotIndex + 1);\n\n if (!controllerName || !propertyName) {\n throw new Error(\n `Invalid key in store: '${key}'. Both controller name and property name must be non-empty.`,\n );\n }\n\n if (!isPersisted(controllerMetadata[controllerName], propertyName)) {\n continue;\n }\n\n if (!state[controllerName]) {\n state[controllerName] = {};\n }\n state[controllerName][propertyName] = value;\n }\n return state;\n}\n\n/**\n * Subscribe to all controller `stateChanged` events and persist changes\n * to the key-value store.\n *\n * For each controller's metadata, this function determines which state\n * properties are persist-flagged. When a `stateChanged` event fires, it uses\n * the Immer patches to identify which top-level properties changed, filters\n * to only persist-flagged properties, and writes them to the store.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param controllerMetadata - A map from controller name to its state metadata.\n * @param store - The key-value store to write to.\n * @param log - Optional logger for persistence-write failures. Defaults to\n * `console.error` when omitted. A daemon host should supply its own logger,\n * since a backgrounded daemon's stdio may be discarded.\n * @returns A function that unsubscribes all persistence handlers.\n */\nexport function subscribeToChanges(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n controllerMetadata: Readonly<\n Record<string, Readonly<StateMetadataConstraint>>\n >,\n store: KeyValueStore,\n log?: (message: string) => void,\n): () => void {\n const unsubscribers: (() => void)[] = [];\n const logFn =\n log ??\n ((message: string): void => {\n console.error(message);\n });\n\n for (const [controllerName, metadata] of Object.entries(controllerMetadata)) {\n const persistedProperties = getPersistPropertyNames(metadata);\n if (persistedProperties.size === 0) {\n continue;\n }\n\n const eventType = `${controllerName}:stateChanged`;\n\n const handler: StateChangedHandler = (state, patches) => {\n const changed = getChangedProperties(patches, persistedProperties);\n\n for (const prop of changed) {\n const key = storeKey(controllerName, prop);\n const removed = !hasProperty(state, prop);\n\n // Derive the value before the try/catch so a throwing `StateDeriver`\n // surfaces as its own error instead of a misreported write failure.\n const persistFlag = metadata[prop]?.persist;\n const value =\n !removed && typeof persistFlag === 'function'\n ? persistFlag(state[prop] as never)\n : state[prop];\n\n try {\n if (removed) {\n store.delete(key);\n } else {\n store.set(key, value);\n }\n } catch (error) {\n // TODO: Surface persistence-write failures up the stack so callers\n // can decide to halt rather than continue with diverging in-memory\n // and on-disk state. For now, log and continue.\n logFn(`Failed to persist state for ${key}: ${String(error)}`);\n }\n }\n };\n\n unsubscribers.push(subscribeToStateChanged(messenger, eventType, handler));\n }\n\n const unsubscribeAll = (): void => {\n while (unsubscribers.length > 0) {\n unsubscribers.pop()?.();\n }\n };\n\n return unsubscribeAll;\n}\n\n/**\n * Subscribe a handler to a controller's `stateChanged` event.\n *\n * The event name is built from a runtime controller name, so it widens to\n * `string` and cannot be proven to be a literal member of the messenger's event\n * union at compile time. This helper localizes that single unavoidable cast\n * behind a typed {@link StateChangedHandler}, so the `(state, patches)` payload\n * shape stays compile-checked at every call site instead of being erased by a\n * statement-level `@ts-expect-error`.\n *\n * @param messenger - The root messenger to subscribe on.\n * @param eventType - The `${controllerName}:stateChanged` event name.\n * @param handler - The state-change handler to register.\n * @returns A function that unsubscribes the handler.\n */\nfunction subscribeToStateChanged(\n messenger: Readonly<RootMessenger<DefaultActions, DefaultEvents>>,\n eventType: string,\n handler: StateChangedHandler,\n): () => void {\n const subscriber = messenger as unknown as {\n subscribe: (eventType: string, handler: StateChangedHandler) => void;\n unsubscribe: (eventType: string, handler: StateChangedHandler) => void;\n };\n subscriber.subscribe(eventType, handler);\n return () => {\n subscriber.unsubscribe(eventType, handler);\n };\n}\n\n/**\n * Determine whether a property is currently persist-flagged.\n *\n * The `persist` flag is truthy when it is `true` or a `StateDeriver` function,\n * and falsy when it is `false` or when the controller or property is absent\n * from the metadata. `loadState` and `subscribeToChanges` share this predicate\n * so the read and write paths can never disagree on what counts as persisted.\n *\n * @param metadata - The controller's state metadata, or `undefined` when the\n * controller is absent from the metadata map.\n * @param propertyName - The property name to check.\n * @returns `true` if the property should be persisted.\n */\nfunction isPersisted(\n metadata: Readonly<StateMetadataConstraint> | undefined,\n propertyName: string,\n): boolean {\n return Boolean(metadata?.[propertyName]?.persist);\n}\n\n/**\n * Get the set of property names whose `persist` metadata is truthy\n * (either `true` or a `StateDeriver` function).\n *\n * @param metadata - The controller's state metadata.\n * @returns A set of property names that should be persisted.\n */\nfunction getPersistPropertyNames(\n metadata: StateMetadataConstraint,\n): ReadonlySet<string> {\n const names = new Set<string>();\n for (const key of Object.keys(metadata)) {\n if (isPersisted(metadata, key)) {\n names.add(key);\n }\n }\n return names;\n}\n\n/**\n * Extracts the set of persist-flagged top-level property names that changed\n * from an array of Immer patches.\n *\n * If any patch has an empty path (indicating a root state replacement),\n * all persist-flagged properties are returned.\n *\n * @param patches - Immer patches from a state update.\n * @param persistedProperties - The set of persist-flagged property names.\n * @returns A set of top-level property names that were modified.\n */\nfunction getChangedProperties(\n patches: Patch[],\n persistedProperties: ReadonlySet<string>,\n): ReadonlySet<string> {\n const changed = new Set<string>();\n for (const patch of patches) {\n if (patch.path.length === 0) {\n return persistedProperties;\n }\n\n const prop = String(patch.path[0]);\n if (persistedProperties.has(prop)) {\n changed.add(prop);\n }\n }\n return changed;\n}\n"]}
|