@metamask-previews/wallet-cli 0.0.0-preview-2226dd478 → 0.0.0-preview-18dd0df9c

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/dist/daemon/types.cjs.map +1 -1
  3. package/dist/daemon/types.d.cts +0 -5
  4. package/dist/daemon/types.d.cts.map +1 -1
  5. package/dist/daemon/types.d.mts +0 -5
  6. package/dist/daemon/types.d.mts.map +1 -1
  7. package/dist/daemon/types.mjs.map +1 -1
  8. package/dist/persistence/persistence.cjs.map +1 -1
  9. package/dist/persistence/persistence.d.cts +1 -1
  10. package/dist/persistence/persistence.d.cts.map +1 -1
  11. package/dist/persistence/persistence.d.mts +1 -1
  12. package/dist/persistence/persistence.d.mts.map +1 -1
  13. package/dist/persistence/persistence.mjs.map +1 -1
  14. package/package.json +1 -3
  15. package/dist/daemon/daemon-entry.cjs +0 -245
  16. package/dist/daemon/daemon-entry.cjs.map +0 -1
  17. package/dist/daemon/daemon-entry.d.cts +0 -2
  18. package/dist/daemon/daemon-entry.d.cts.map +0 -1
  19. package/dist/daemon/daemon-entry.d.mts +0 -2
  20. package/dist/daemon/daemon-entry.d.mts.map +0 -1
  21. package/dist/daemon/daemon-entry.mjs +0 -243
  22. package/dist/daemon/daemon-entry.mjs.map +0 -1
  23. package/dist/daemon/data-dir.cjs +0 -23
  24. package/dist/daemon/data-dir.cjs.map +0 -1
  25. package/dist/daemon/data-dir.d.cts +0 -14
  26. package/dist/daemon/data-dir.d.cts.map +0 -1
  27. package/dist/daemon/data-dir.d.mts +0 -14
  28. package/dist/daemon/data-dir.d.mts.map +0 -1
  29. package/dist/daemon/data-dir.mjs +0 -19
  30. package/dist/daemon/data-dir.mjs.map +0 -1
  31. package/dist/daemon/wallet-factory.cjs +0 -234
  32. package/dist/daemon/wallet-factory.cjs.map +0 -1
  33. package/dist/daemon/wallet-factory.d.cts +0 -55
  34. package/dist/daemon/wallet-factory.d.cts.map +0 -1
  35. package/dist/daemon/wallet-factory.d.mts +0 -55
  36. package/dist/daemon/wallet-factory.d.mts.map +0 -1
  37. package/dist/daemon/wallet-factory.mjs +0 -230
  38. package/dist/daemon/wallet-factory.mjs.map +0 -1
@@ -1,243 +0,0 @@
1
- import { appendFile, readFile, rm, writeFile } from "node:fs/promises";
2
- import { pingDaemon } from "./daemon-client.mjs";
3
- import { ensureOwnerOnlyDirectory } from "./data-dir.mjs";
4
- import { getDaemonPaths } from "./paths.mjs";
5
- import { startRpcSocketServer } from "./rpc-socket-server.mjs";
6
- import { isErrorWithCode, isProcessAlive, readPidFile } from "./utils.mjs";
7
- import { createWallet } from "./wallet-factory.mjs";
8
- const startTime = Date.now();
9
- main().catch((error) => {
10
- process.stderr.write(`Daemon fatal: ${String(error)}\n`);
11
- process.exitCode = 1;
12
- });
13
- async function main() {
14
- const dataDir = process.env.MM_DAEMON_DATA_DIR;
15
- if (!dataDir) {
16
- throw new Error('MM_DAEMON_DATA_DIR environment variable is required');
17
- }
18
- const infuraProjectId = process.env.INFURA_PROJECT_ID;
19
- if (!infuraProjectId) {
20
- throw new Error('INFURA_PROJECT_ID environment variable is required');
21
- }
22
- const password = process.env.MM_WALLET_PASSWORD;
23
- if (!password) {
24
- throw new Error('MM_WALLET_PASSWORD environment variable is required');
25
- }
26
- const srp = process.env.MM_WALLET_SRP;
27
- if (!srp) {
28
- throw new Error('MM_WALLET_SRP environment variable is required');
29
- }
30
- // Scrub the wallet secrets from the environment now they are captured. The
31
- // daemon is long-lived and dispatches arbitrary messenger actions over its
32
- // socket, so leaving the SRP/password in `process.env` for its whole lifetime
33
- // needlessly widens their exposure to any in-process code.
34
- delete process.env.MM_WALLET_PASSWORD;
35
- delete process.env.MM_WALLET_SRP;
36
- await ensureOwnerOnlyDirectory(dataDir);
37
- const { socketPath: defaultSocketPath, pidPath, logPath, dbPath, } = getDaemonPaths(dataDir);
38
- const socketPath = process.env.MM_DAEMON_SOCKET_PATH ?? defaultSocketPath;
39
- const log = makeLogger(logPath);
40
- log('Starting daemon...');
41
- // Pre-flight: refuse to take over if a responsive daemon already owns this
42
- // socket. If the existing PID file is stale (or the socket is dead), clean
43
- // it up so the exclusive PID-file write below has a chance to succeed.
44
- await claimDaemonSlot(pidPath, socketPath, log);
45
- const pidFileContents = `${process.pid}\n${startTime}\n`;
46
- // Claim the slot atomically BEFORE opening the SQLite database or
47
- // constructing the Wallet. Two concurrent `daemon start` invocations can
48
- // both pass `claimDaemonSlot` (the gap between its preflight and the slot
49
- // write is racy); without this ordering, both would open `wallet.db` and
50
- // both would run first-run SRP import before one loses the wx race.
51
- try {
52
- await writeFile(pidPath, pidFileContents, { flag: 'wx' });
53
- }
54
- catch (error) {
55
- throw error instanceof Error
56
- ? Object.assign(error, {
57
- message: `Failed to claim daemon slot at ${pidPath}: ${error.message}`,
58
- })
59
- : /* istanbul ignore next -- node:fs/promises always rejects with an Error */
60
- new Error(`Failed to claim daemon slot at ${pidPath}: ${String(error)}`);
61
- }
62
- let wallet;
63
- let dispose;
64
- let handle;
65
- try {
66
- ({ wallet, dispose } = await createWallet({
67
- databasePath: dbPath,
68
- password,
69
- srp,
70
- infuraProjectId,
71
- log,
72
- }));
73
- const constructedWallet = wallet;
74
- const handlers = {
75
- getStatus: async () => ({
76
- pid: process.pid,
77
- uptime: Math.floor((Date.now() - startTime) / 1000),
78
- }),
79
- // Arbitrary messenger dispatch is intentional: the CLI exposes the full
80
- // messenger surface over a Unix socket inside the per-user oclif data
81
- // directory. The dataDir is chmodded to 0o700 above and the socket to
82
- // 0o600 by the RPC server on bind, so only the owning user can open
83
- // them, but there is no in-process auth check beyond that
84
- // filesystem-permission barrier.
85
- call: async (params) => {
86
- if (!Array.isArray(params) || typeof params[0] !== 'string') {
87
- throw new Error('Expected params to be an array with an action name');
88
- }
89
- const [action, ...args] = params;
90
- const result = constructedWallet.messenger.call(action, ...args);
91
- return (result instanceof Promise ? await result : result);
92
- },
93
- };
94
- // `startRpcSocketServer` restricts the socket to the owner (chmod 0o600)
95
- // on bind and never leaves a live server/socket behind if it rejects, so
96
- // the catch below has nothing of its own to close.
97
- handle = await startRpcSocketServer({
98
- socketPath,
99
- handlers,
100
- onShutdown: async () => shutdown('RPC shutdown'),
101
- log,
102
- });
103
- }
104
- catch (error) {
105
- // `dispose` is undefined only when `createWallet` itself threw — it has
106
- // already torn down its own store in that case.
107
- if (dispose) {
108
- await dispose();
109
- }
110
- // Only remove the PID file if it's still ours (we may have lost the race
111
- // and the file now belongs to another daemon).
112
- await removeOwnedPidFile(pidPath, pidFileContents).catch((rmError) => {
113
- log(`Failed to remove PID file during cleanup: ${String(rmError)}`);
114
- });
115
- throw error;
116
- }
117
- // Stable non-undefined refs for the shutdown closures (TS won't narrow the
118
- // outer `let`s across closure escape).
119
- const activeHandle = handle;
120
- const activeDispose = dispose;
121
- log(`Daemon started. Socket: ${socketPath}`);
122
- let shutdownPromise;
123
- /**
124
- * Shut down the daemon idempotently. Concurrent calls coalesce.
125
- *
126
- * @param reason - A label describing why shutdown was triggered.
127
- * @returns A promise that resolves when shutdown completes.
128
- */
129
- async function shutdown(reason) {
130
- if (shutdownPromise === undefined) {
131
- log(`Shutting down (${reason})...`);
132
- shutdownPromise = (async () => {
133
- try {
134
- await activeHandle.close();
135
- }
136
- catch (closeError) {
137
- log(`handle.close() failed: ${String(closeError)}`);
138
- }
139
- await activeDispose();
140
- await Promise.all([
141
- removeOwnedPidFile(pidPath, pidFileContents).catch((rmError) => {
142
- log(`Failed to remove PID file: ${String(rmError)}`);
143
- }),
144
- rm(socketPath, { force: true }).catch((rmError) => {
145
- log(`Failed to remove socket file: ${String(rmError)}`);
146
- }),
147
- ]);
148
- })();
149
- }
150
- return shutdownPromise;
151
- }
152
- process.on('SIGTERM', () => {
153
- /* istanbul ignore next -- `shutdown` logs each step internally; the catch only guards against an unhandled rejection from the signal handler. */
154
- shutdown('SIGTERM').catch(() => undefined);
155
- });
156
- process.on('SIGINT', () => {
157
- /* istanbul ignore next -- `shutdown` logs each step internally; the catch only guards against an unhandled rejection from the signal handler. */
158
- shutdown('SIGINT').catch(() => undefined);
159
- });
160
- }
161
- /**
162
- * Refuse to start if a responsive daemon already owns the socket. Otherwise
163
- * clear any stale PID/socket files so the exclusive PID-file write can
164
- * proceed.
165
- *
166
- * @param pidPath - The PID file path.
167
- * @param socketPath - The socket path.
168
- * @param log - Logger for diagnostic messages.
169
- */
170
- async function claimDaemonSlot(pidPath, socketPath, log) {
171
- const existingPid = await readPidFile(pidPath);
172
- const ping = await pingDaemon(socketPath);
173
- if (ping.status === 'responsive') {
174
- const pidPart = existingPid === undefined
175
- ? '(no PID file present)'
176
- : `(pid ${existingPid})`;
177
- throw new Error(`A daemon is already running on ${socketPath} ${pidPart}`);
178
- }
179
- // Refuse to clobber when the recorded PID is still alive, regardless of
180
- // whether the socket exists. Possible scenarios:
181
- // - `unreachable`: wedged or mid-startup sibling daemon (socket present
182
- // but not responding to JSON-RPC).
183
- // - `absent`: a sibling daemon that hasn't yet bound its socket, or one
184
- // whose socket was manually removed. In either case, removing its PID
185
- // file would orphan it from `daemon stop`.
186
- if (existingPid !== undefined && isProcessAlive(existingPid)) {
187
- const detail = ping.status === 'unreachable'
188
- ? `socket at ${socketPath} is unresponsive (${ping.error.message})`
189
- : `no socket at ${socketPath}, but pid is still alive`;
190
- throw new Error(`A daemon is already running (pid ${existingPid}): ${detail}. ` +
191
- `Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon.`);
192
- }
193
- if (ping.status === 'unreachable') {
194
- log(`Removing stale socket at ${socketPath} (${ping.error.message}).`);
195
- }
196
- // Always clear both files before claiming the slot. The PID file may be
197
- // corrupt (truncated, partial write from a crashed run); without this, the
198
- // exclusive `wx` write below would fail with EEXIST and the daemon could
199
- // not start until a human manually deleted the file.
200
- await Promise.all([
201
- rm(pidPath, { force: true }),
202
- rm(socketPath, { force: true }),
203
- ]);
204
- }
205
- /**
206
- * Remove the PID file only if it still contains our exact contents. Guards
207
- * against a racing daemon's PID file being removed by this daemon during
208
- * cleanup.
209
- *
210
- * @param pidPath - Path to the PID file.
211
- * @param expectedContents - The contents we wrote when claiming the slot.
212
- */
213
- async function removeOwnedPidFile(pidPath, expectedContents) {
214
- let actual;
215
- try {
216
- actual = await readFile(pidPath, 'utf-8');
217
- }
218
- catch (error) {
219
- if (isErrorWithCode(error, 'ENOENT')) {
220
- return;
221
- }
222
- throw error;
223
- }
224
- if (actual === expectedContents) {
225
- await rm(pidPath, { force: true });
226
- }
227
- }
228
- /**
229
- * Create a file logger that appends timestamped lines to `logPath`, falling
230
- * back to stderr if the append fails.
231
- *
232
- * @param logPath - The log file path.
233
- * @returns A logging function.
234
- */
235
- function makeLogger(logPath) {
236
- return (message) => {
237
- const line = `[${new Date().toISOString()}] ${message}\n`;
238
- appendFile(logPath, line).catch((error) => {
239
- process.stderr.write(`[log write failed: ${String(error)}] ${message}\n`);
240
- });
241
- };
242
- }
243
- //# sourceMappingURL=daemon-entry.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"daemon-entry.mjs","sourceRoot":"","sources":["../../src/daemon/daemon-entry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Json } from '@metamask/utils';\nimport type { Wallet } from '@metamask/wallet';\nimport { appendFile, readFile, rm, writeFile } from 'node:fs/promises';\n\nimport { pingDaemon } from './daemon-client';\nimport { ensureOwnerOnlyDirectory } from './data-dir';\nimport { getDaemonPaths } from './paths';\nimport { startRpcSocketServer } from './rpc-socket-server';\nimport type { RpcSocketServerHandle } from './rpc-socket-server';\nimport type { DaemonStatusInfo, Logger, RpcHandlerMap } from './types';\nimport { isErrorWithCode, isProcessAlive, readPidFile } from './utils';\nimport { createWallet } from './wallet-factory';\n\nconst startTime = Date.now();\n\nmain().catch((error: unknown) => {\n process.stderr.write(`Daemon fatal: ${String(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const dataDir = process.env.MM_DAEMON_DATA_DIR;\n if (!dataDir) {\n throw new Error('MM_DAEMON_DATA_DIR environment variable is required');\n }\n\n const infuraProjectId = process.env.INFURA_PROJECT_ID;\n if (!infuraProjectId) {\n throw new Error('INFURA_PROJECT_ID environment variable is required');\n }\n\n const password = process.env.MM_WALLET_PASSWORD;\n if (!password) {\n throw new Error('MM_WALLET_PASSWORD environment variable is required');\n }\n\n const srp = process.env.MM_WALLET_SRP;\n if (!srp) {\n throw new Error('MM_WALLET_SRP environment variable is required');\n }\n\n // Scrub the wallet secrets from the environment now they are captured. The\n // daemon is long-lived and dispatches arbitrary messenger actions over its\n // socket, so leaving the SRP/password in `process.env` for its whole lifetime\n // needlessly widens their exposure to any in-process code.\n delete process.env.MM_WALLET_PASSWORD;\n delete process.env.MM_WALLET_SRP;\n\n await ensureOwnerOnlyDirectory(dataDir);\n\n const {\n socketPath: defaultSocketPath,\n pidPath,\n logPath,\n dbPath,\n } = getDaemonPaths(dataDir);\n const socketPath = process.env.MM_DAEMON_SOCKET_PATH ?? defaultSocketPath;\n\n const log = makeLogger(logPath);\n log('Starting daemon...');\n\n // Pre-flight: refuse to take over if a responsive daemon already owns this\n // socket. If the existing PID file is stale (or the socket is dead), clean\n // it up so the exclusive PID-file write below has a chance to succeed.\n await claimDaemonSlot(pidPath, socketPath, log);\n\n const pidFileContents = `${process.pid}\\n${startTime}\\n`;\n\n // Claim the slot atomically BEFORE opening the SQLite database or\n // constructing the Wallet. Two concurrent `daemon start` invocations can\n // both pass `claimDaemonSlot` (the gap between its preflight and the slot\n // write is racy); without this ordering, both would open `wallet.db` and\n // both would run first-run SRP import before one loses the wx race.\n try {\n await writeFile(pidPath, pidFileContents, { flag: 'wx' });\n } catch (error) {\n throw error instanceof Error\n ? Object.assign(error, {\n message: `Failed to claim daemon slot at ${pidPath}: ${error.message}`,\n })\n : /* istanbul ignore next -- node:fs/promises always rejects with an Error */\n new Error(\n `Failed to claim daemon slot at ${pidPath}: ${String(error)}`,\n );\n }\n\n let wallet: Wallet | undefined;\n let dispose: (() => Promise<void>) | undefined;\n let handle: RpcSocketServerHandle | undefined;\n\n try {\n ({ wallet, dispose } = await createWallet({\n databasePath: dbPath,\n password,\n srp,\n infuraProjectId,\n log,\n }));\n\n const constructedWallet = wallet;\n const handlers: RpcHandlerMap = {\n getStatus: async (): Promise<DaemonStatusInfo> => ({\n pid: process.pid,\n uptime: Math.floor((Date.now() - startTime) / 1000),\n }),\n // Arbitrary messenger dispatch is intentional: the CLI exposes the full\n // messenger surface over a Unix socket inside the per-user oclif data\n // directory. The dataDir is chmodded to 0o700 above and the socket to\n // 0o600 by the RPC server on bind, so only the owning user can open\n // them, but there is no in-process auth check beyond that\n // filesystem-permission barrier.\n call: async (params) => {\n if (!Array.isArray(params) || typeof params[0] !== 'string') {\n throw new Error('Expected params to be an array with an action name');\n }\n const [action, ...args] = params as [string, ...Json[]];\n // The messenger's `call` is typed to a literal action-name union; the\n // daemon dispatches arbitrary action names from RPC. Cast to a\n // string-keyed `call` (which preserves arity) rather than to `any`, so\n // the only untyped value is the `unknown` result narrowed below.\n type ArbitraryDispatch = {\n call: (actionName: string, ...callArgs: Json[]) => unknown;\n };\n const result = (\n constructedWallet.messenger as unknown as ArbitraryDispatch\n ).call(action, ...args);\n return (result instanceof Promise ? await result : result) as Json;\n },\n };\n\n // `startRpcSocketServer` restricts the socket to the owner (chmod 0o600)\n // on bind and never leaves a live server/socket behind if it rejects, so\n // the catch below has nothing of its own to close.\n handle = await startRpcSocketServer({\n socketPath,\n handlers,\n onShutdown: async () => shutdown('RPC shutdown'),\n log,\n });\n } catch (error) {\n // `dispose` is undefined only when `createWallet` itself threw — it has\n // already torn down its own store in that case.\n if (dispose) {\n await dispose();\n }\n // Only remove the PID file if it's still ours (we may have lost the race\n // and the file now belongs to another daemon).\n await removeOwnedPidFile(pidPath, pidFileContents).catch(\n (rmError: unknown) => {\n log(`Failed to remove PID file during cleanup: ${String(rmError)}`);\n },\n );\n throw error;\n }\n\n // Stable non-undefined refs for the shutdown closures (TS won't narrow the\n // outer `let`s across closure escape).\n const activeHandle = handle;\n const activeDispose = dispose;\n\n log(`Daemon started. Socket: ${socketPath}`);\n\n let shutdownPromise: Promise<void> | undefined;\n\n /**\n * Shut down the daemon idempotently. Concurrent calls coalesce.\n *\n * @param reason - A label describing why shutdown was triggered.\n * @returns A promise that resolves when shutdown completes.\n */\n async function shutdown(reason: string): Promise<void> {\n if (shutdownPromise === undefined) {\n log(`Shutting down (${reason})...`);\n shutdownPromise = (async (): Promise<void> => {\n try {\n await activeHandle.close();\n } catch (closeError) {\n log(`handle.close() failed: ${String(closeError)}`);\n }\n await activeDispose();\n await Promise.all([\n removeOwnedPidFile(pidPath, pidFileContents).catch(\n (rmError: unknown) => {\n log(`Failed to remove PID file: ${String(rmError)}`);\n },\n ),\n rm(socketPath, { force: true }).catch((rmError: unknown) => {\n log(`Failed to remove socket file: ${String(rmError)}`);\n }),\n ]);\n })();\n }\n return shutdownPromise;\n }\n\n process.on('SIGTERM', () => {\n /* istanbul ignore next -- `shutdown` logs each step internally; the catch only guards against an unhandled rejection from the signal handler. */\n shutdown('SIGTERM').catch(() => undefined);\n });\n process.on('SIGINT', () => {\n /* istanbul ignore next -- `shutdown` logs each step internally; the catch only guards against an unhandled rejection from the signal handler. */\n shutdown('SIGINT').catch(() => undefined);\n });\n}\n\n/**\n * Refuse to start if a responsive daemon already owns the socket. Otherwise\n * clear any stale PID/socket files so the exclusive PID-file write can\n * proceed.\n *\n * @param pidPath - The PID file path.\n * @param socketPath - The socket path.\n * @param log - Logger for diagnostic messages.\n */\nasync function claimDaemonSlot(\n pidPath: string,\n socketPath: string,\n log: Logger,\n): Promise<void> {\n const existingPid = await readPidFile(pidPath);\n const ping = await pingDaemon(socketPath);\n\n if (ping.status === 'responsive') {\n const pidPart =\n existingPid === undefined\n ? '(no PID file present)'\n : `(pid ${existingPid})`;\n throw new Error(`A daemon is already running on ${socketPath} ${pidPart}`);\n }\n\n // Refuse to clobber when the recorded PID is still alive, regardless of\n // whether the socket exists. Possible scenarios:\n // - `unreachable`: wedged or mid-startup sibling daemon (socket present\n // but not responding to JSON-RPC).\n // - `absent`: a sibling daemon that hasn't yet bound its socket, or one\n // whose socket was manually removed. In either case, removing its PID\n // file would orphan it from `daemon stop`.\n if (existingPid !== undefined && isProcessAlive(existingPid)) {\n const detail =\n ping.status === 'unreachable'\n ? `socket at ${socketPath} is unresponsive (${ping.error.message})`\n : `no socket at ${socketPath}, but pid is still alive`;\n throw new Error(\n `A daemon is already running (pid ${existingPid}): ${detail}. ` +\n `Run \\`mm daemon stop\\` (or \\`mm daemon purge\\`) before starting a new daemon.`,\n );\n }\n\n if (ping.status === 'unreachable') {\n log(`Removing stale socket at ${socketPath} (${ping.error.message}).`);\n }\n // Always clear both files before claiming the slot. The PID file may be\n // corrupt (truncated, partial write from a crashed run); without this, the\n // exclusive `wx` write below would fail with EEXIST and the daemon could\n // not start until a human manually deleted the file.\n await Promise.all([\n rm(pidPath, { force: true }),\n rm(socketPath, { force: true }),\n ]);\n}\n\n/**\n * Remove the PID file only if it still contains our exact contents. Guards\n * against a racing daemon's PID file being removed by this daemon during\n * cleanup.\n *\n * @param pidPath - Path to the PID file.\n * @param expectedContents - The contents we wrote when claiming the slot.\n */\nasync function removeOwnedPidFile(\n pidPath: string,\n expectedContents: string,\n): Promise<void> {\n let actual: string;\n try {\n actual = await readFile(pidPath, 'utf-8');\n } catch (error: unknown) {\n if (isErrorWithCode(error, 'ENOENT')) {\n return;\n }\n throw error;\n }\n if (actual === expectedContents) {\n await rm(pidPath, { force: true });\n }\n}\n\n/**\n * Create a file logger that appends timestamped lines to `logPath`, falling\n * back to stderr if the append fails.\n *\n * @param logPath - The log file path.\n * @returns A logging function.\n */\nfunction makeLogger(logPath: string): Logger {\n return (message: string): void => {\n const line = `[${new Date().toISOString()}] ${message}\\n`;\n appendFile(logPath, line).catch((error: unknown) => {\n process.stderr.write(`[log write failed: ${String(error)}] ${message}\\n`);\n });\n };\n}\n"]}
@@ -1,23 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensureOwnerOnlyDirectory = void 0;
4
- const node_fs_1 = require("node:fs");
5
- const promises_1 = require("node:fs/promises");
6
- /**
7
- * Create the daemon's data directory (if it does not exist) and restrict it to
8
- * the owning user.
9
- *
10
- * The mode is `0o700` (owner-only). The daemon exposes the full wallet
11
- * messenger over the socket inside this directory, so anyone who can traverse
12
- * the dir can also `connect()` to the socket. Restricting to the owning user is
13
- * the only access-control boundary. We `chmod` after `mkdir` because the `mode`
14
- * option is ignored when the directory already exists.
15
- *
16
- * @param dataDir - The data directory to create and lock down.
17
- */
18
- async function ensureOwnerOnlyDirectory(dataDir) {
19
- (0, node_fs_1.mkdirSync)(dataDir, { recursive: true, mode: 0o700 });
20
- await (0, promises_1.chmod)(dataDir, 0o700);
21
- }
22
- exports.ensureOwnerOnlyDirectory = ensureOwnerOnlyDirectory;
23
- //# sourceMappingURL=data-dir.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"data-dir.cjs","sourceRoot":"","sources":["../../src/daemon/data-dir.ts"],"names":[],"mappings":";;;AAAA,qCAAoC;AACpC,+CAAyC;AAEzC;;;;;;;;;;;GAWG;AACI,KAAK,UAAU,wBAAwB,CAAC,OAAe;IAC5D,IAAA,mBAAS,EAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACrD,MAAM,IAAA,gBAAK,EAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAC9B,CAAC;AAHD,4DAGC","sourcesContent":["import { mkdirSync } from 'node:fs';\nimport { chmod } from 'node:fs/promises';\n\n/**\n * Create the daemon's data directory (if it does not exist) and restrict it to\n * the owning user.\n *\n * The mode is `0o700` (owner-only). The daemon exposes the full wallet\n * messenger over the socket inside this directory, so anyone who can traverse\n * the dir can also `connect()` to the socket. Restricting to the owning user is\n * the only access-control boundary. We `chmod` after `mkdir` because the `mode`\n * option is ignored when the directory already exists.\n *\n * @param dataDir - The data directory to create and lock down.\n */\nexport async function ensureOwnerOnlyDirectory(dataDir: string): Promise<void> {\n mkdirSync(dataDir, { recursive: true, mode: 0o700 });\n await chmod(dataDir, 0o700);\n}\n"]}
@@ -1,14 +0,0 @@
1
- /**
2
- * Create the daemon's data directory (if it does not exist) and restrict it to
3
- * the owning user.
4
- *
5
- * The mode is `0o700` (owner-only). The daemon exposes the full wallet
6
- * messenger over the socket inside this directory, so anyone who can traverse
7
- * the dir can also `connect()` to the socket. Restricting to the owning user is
8
- * the only access-control boundary. We `chmod` after `mkdir` because the `mode`
9
- * option is ignored when the directory already exists.
10
- *
11
- * @param dataDir - The data directory to create and lock down.
12
- */
13
- export declare function ensureOwnerOnlyDirectory(dataDir: string): Promise<void>;
14
- //# sourceMappingURL=data-dir.d.cts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"data-dir.d.cts","sourceRoot":"","sources":["../../src/daemon/data-dir.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG7E"}
@@ -1,14 +0,0 @@
1
- /**
2
- * Create the daemon's data directory (if it does not exist) and restrict it to
3
- * the owning user.
4
- *
5
- * The mode is `0o700` (owner-only). The daemon exposes the full wallet
6
- * messenger over the socket inside this directory, so anyone who can traverse
7
- * the dir can also `connect()` to the socket. Restricting to the owning user is
8
- * the only access-control boundary. We `chmod` after `mkdir` because the `mode`
9
- * option is ignored when the directory already exists.
10
- *
11
- * @param dataDir - The data directory to create and lock down.
12
- */
13
- export declare function ensureOwnerOnlyDirectory(dataDir: string): Promise<void>;
14
- //# sourceMappingURL=data-dir.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"data-dir.d.mts","sourceRoot":"","sources":["../../src/daemon/data-dir.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG7E"}
@@ -1,19 +0,0 @@
1
- import { mkdirSync } from "node:fs";
2
- import { chmod } from "node:fs/promises";
3
- /**
4
- * Create the daemon's data directory (if it does not exist) and restrict it to
5
- * the owning user.
6
- *
7
- * The mode is `0o700` (owner-only). The daemon exposes the full wallet
8
- * messenger over the socket inside this directory, so anyone who can traverse
9
- * the dir can also `connect()` to the socket. Restricting to the owning user is
10
- * the only access-control boundary. We `chmod` after `mkdir` because the `mode`
11
- * option is ignored when the directory already exists.
12
- *
13
- * @param dataDir - The data directory to create and lock down.
14
- */
15
- export async function ensureOwnerOnlyDirectory(dataDir) {
16
- mkdirSync(dataDir, { recursive: true, mode: 0o700 });
17
- await chmod(dataDir, 0o700);
18
- }
19
- //# sourceMappingURL=data-dir.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"data-dir.mjs","sourceRoot":"","sources":["../../src/daemon/data-dir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB;AACpC,OAAO,EAAE,KAAK,EAAE,yBAAyB;AAEzC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAAe;IAC5D,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACrD,MAAM,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["import { mkdirSync } from 'node:fs';\nimport { chmod } from 'node:fs/promises';\n\n/**\n * Create the daemon's data directory (if it does not exist) and restrict it to\n * the owning user.\n *\n * The mode is `0o700` (owner-only). The daemon exposes the full wallet\n * messenger over the socket inside this directory, so anyone who can traverse\n * the dir can also `connect()` to the socket. Restricting to the owning user is\n * the only access-control boundary. We `chmod` after `mkdir` because the `mode`\n * option is ignored when the directory already exists.\n *\n * @param dataDir - The data directory to create and lock down.\n */\nexport async function ensureOwnerOnlyDirectory(dataDir: string): Promise<void> {\n mkdirSync(dataDir, { recursive: true, mode: 0o700 });\n await chmod(dataDir, 0o700);\n}\n"]}
@@ -1,234 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createWallet = void 0;
4
- const remote_feature_flag_controller_1 = require("@metamask/remote-feature-flag-controller");
5
- const storage_service_1 = require("@metamask/storage-service");
6
- const wallet_1 = require("@metamask/wallet");
7
- const promises_1 = require("node:fs/promises");
8
- const KeyValueStore_1 = require("../persistence/KeyValueStore.cjs");
9
- const persistence_1 = require("../persistence/persistence.cjs");
10
- const IN_MEMORY_DATABASE_PATH = ':memory:';
11
- /**
12
- * Build the per-instance options the daemon's `Wallet` is constructed with.
13
- *
14
- * Returns a fresh set on every call so the metadata probe and the real wallet
15
- * never share adapter/service instances (the probe is destroyed, which may
16
- * tear those instances down).
17
- *
18
- * Only the slots wired on `@metamask/wallet` today are populated:
19
- * - `storageService` — backed by an in-memory adapter. This is the wallet's
20
- * large-blob store (snap source, caches), distinct from the SQLite
21
- * `KeyValueStore` that persists controller state; no wired controller
22
- * offloads durable data to it yet, so in-memory suffices.
23
- * - `connectivityController` — the `AlwaysOnlineAdapter` exported for
24
- * node-like hosts that have no platform connectivity signal.
25
- * - `networkController` — the Infura project ID used to reach Infura RPC
26
- * endpoints.
27
- * - `remoteFeatureFlagController` — a `ClientConfigApiService` fetching real
28
- * flags over the network.
29
- * - `approvalController` — a no-op `showApprovalRequest` (the daemon is
30
- * headless).
31
- *
32
- * The optional `keyringController` slot is intentionally omitted so the
33
- * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply.
34
- *
35
- * @param infuraProjectId - The Infura project ID for the `NetworkController`.
36
- * @returns The `instanceOptions` for the `Wallet` constructor.
37
- */
38
- function buildInstanceOptions(infuraProjectId) {
39
- return {
40
- approvalController: {
41
- // TODO: surface approval requests over the daemon transport.
42
- showApprovalRequest: () => undefined,
43
- },
44
- connectivityController: {
45
- connectivityAdapter: new wallet_1.AlwaysOnlineAdapter(),
46
- },
47
- networkController: {
48
- infuraProjectId,
49
- },
50
- remoteFeatureFlagController: {
51
- clientConfigApiService: new remote_feature_flag_controller_1.ClientConfigApiService({
52
- fetch: globalThis.fetch,
53
- config: {
54
- // TODO: switch to a CLI-specific `ClientType` once one exists; until
55
- // then `Extension` buckets feature flags as an extension client.
56
- client: remote_feature_flag_controller_1.ClientType.Extension,
57
- distribution: remote_feature_flag_controller_1.DistributionType.Main,
58
- environment: remote_feature_flag_controller_1.EnvironmentType.Production,
59
- },
60
- }),
61
- getMetaMetricsId: () => 'cli',
62
- clientVersion: '0.0.0',
63
- },
64
- storageService: {
65
- storage: new storage_service_1.InMemoryStorageAdapter(),
66
- },
67
- // TODO(#8975): add the `transactionController` slot once it is wired.
68
- };
69
- }
70
- /**
71
- * Create a configured `Wallet` for daemon use, backed by a SQLite key-value
72
- * store for controller-state persistence.
73
- *
74
- * Loads any previously-persisted controller state from the store, seeds the
75
- * wallet with it, then subscribes the store to subsequent state changes so all
76
- * persist-flagged properties are written through.
77
- *
78
- * If the store does not yet contain a keyring vault (first run), the supplied
79
- * secret recovery phrase is imported. On subsequent runs the persisted vault is
80
- * reused — `password`/`srp` go unused and the wallet starts locked; the caller
81
- * unlocks it via `KeyringController:submitPassword` before any keyring-bound
82
- * operation.
83
- *
84
- * On any failure after the store is opened, the store is closed (and the wallet
85
- * destroyed, if constructed). On a first-run failure, the on-disk database is
86
- * also removed so a retry does not latch onto an orphaned partial vault — this
87
- * covers in-process failures only; a hard crash (SIGKILL/power loss) mid-import
88
- * can still leave a vault on disk.
89
- *
90
- * @param config - Wallet configuration.
91
- * @param config.databasePath - Path to the SQLite database file (or
92
- * `':memory:'` for ephemeral use).
93
- * @param config.password - The wallet password.
94
- * @param config.srp - The secret recovery phrase (BIP-39 mnemonic).
95
- * @param config.infuraProjectId - The Infura project ID for the
96
- * `NetworkController`.
97
- * @param config.log - Optional logger for persistence-write and teardown
98
- * failures. Without it, failures fall back to `console.error` (which a detached
99
- * daemon's `stdio: 'ignore'` discards).
100
- * @returns The `Wallet` and a `dispose` handle that tears it down.
101
- */
102
- async function createWallet({ databasePath, password, srp, infuraProjectId, log, }) {
103
- const logFn = log ?? ((message) => console.error(message));
104
- const store = new KeyValueStore_1.KeyValueStore(databasePath);
105
- let wallet;
106
- let unsubscribe;
107
- let wasFirstRun = false;
108
- try {
109
- const state = await loadPersistedState(store, infuraProjectId, logFn);
110
- wasFirstRun = !hasPersistedKeyring(state);
111
- wallet = new wallet_1.Wallet({
112
- state,
113
- instanceOptions: buildInstanceOptions(infuraProjectId),
114
- });
115
- unsubscribe = (0, persistence_1.subscribeToChanges)(wallet.messenger, wallet.controllerMetadata, store, logFn);
116
- // Complete post-construction controller setup before serving requests —
117
- // e.g. `NetworkController.init` applies the selected network so a provider
118
- // is available. `init` settles every step independently; a rejected step
119
- // leaves the wallet only partially usable, so abort startup (the catch
120
- // below tears down and, on first run, removes the partial database) rather
121
- // than serving a degraded daemon.
122
- const initResults = await wallet.init();
123
- const initFailures = initResults.filter((result) => result.status === 'rejected');
124
- for (const failure of initFailures) {
125
- logFn(`Wallet init step failed: ${String(failure.reason)}`);
126
- }
127
- if (initFailures.length > 0) {
128
- const firstReason = String(initFailures[0].reason);
129
- throw new Error(`Wallet initialization failed (${initFailures.length} step(s)); refusing to serve a partially initialized wallet. First failure: ${firstReason}`);
130
- }
131
- if (wasFirstRun) {
132
- await (0, wallet_1.importSecretRecoveryPhrase)(wallet, password, srp);
133
- }
134
- let disposePromise;
135
- return {
136
- wallet,
137
- dispose: async () => (disposePromise ?? (disposePromise = teardown(unsubscribe, wallet, store, logFn))),
138
- };
139
- }
140
- catch (error) {
141
- await teardown(unsubscribe, wallet, store, logFn);
142
- if (wasFirstRun && databasePath !== IN_MEMORY_DATABASE_PATH) {
143
- // Best-effort cleanup of the on-disk SQLite files (main, WAL, SHM) so a
144
- // partially-persisted KeyringController vault cannot mislead the next run
145
- // into skipping SRP import. Covers in-process failures only — a crash
146
- // (SIGKILL/power loss) mid-import leaves the vault on disk.
147
- await Promise.all([databasePath, `${databasePath}-wal`, `${databasePath}-shm`].map((path) => (0, promises_1.rm)(path, { force: true }).catch((rmError) => {
148
- logFn(`Failed to remove ${path} during first-run cleanup: ${String(rmError)}`);
149
- })));
150
- }
151
- throw error;
152
- }
153
- }
154
- exports.createWallet = createWallet;
155
- /**
156
- * Load persisted controller state, filtered to currently persist-flagged
157
- * properties.
158
- *
159
- * `loadState` filters against the live controller metadata, but that metadata
160
- * is only knowable from a constructed `Wallet` — and its output is what seeds
161
- * the real wallet. So this constructs a short-lived probe purely to read the
162
- * metadata, then tears it down.
163
- *
164
- * TODO: drop the probe once `@metamask/wallet` exposes controller metadata
165
- * without constructing a `Wallet`.
166
- *
167
- * @param store - The key-value store to read from.
168
- * @param infuraProjectId - The Infura project ID for the probe's
169
- * `NetworkController`.
170
- * @param logFn - Logger for a probe-teardown failure.
171
- * @returns The filtered persisted state, suitable for the `Wallet` `state`
172
- * option.
173
- */
174
- async function loadPersistedState(store, infuraProjectId, logFn) {
175
- const probe = new wallet_1.Wallet({
176
- instanceOptions: buildInstanceOptions(infuraProjectId),
177
- });
178
- try {
179
- return (0, persistence_1.loadState)(store, probe.controllerMetadata);
180
- }
181
- finally {
182
- await probe.destroy().catch((error) => {
183
- logFn(`Metadata probe destroy failed: ${String(error)}`);
184
- });
185
- }
186
- }
187
- /**
188
- * Persistence-safe teardown of a wallet and its store; see {@link
189
- * CreateWalletResult.dispose} for the ordering rationale. Each step is
190
- * best-effort, so a failure is logged and the remaining steps still run.
191
- *
192
- * @param unsubscribe - The persistence-subscription unsubscribe function, if
193
- * one was registered.
194
- * @param wallet - The wallet to destroy, if one was constructed.
195
- * @param store - The store to close.
196
- * @param logFn - Logger for step failures.
197
- */
198
- async function teardown(unsubscribe, wallet, store, logFn) {
199
- if (unsubscribe) {
200
- try {
201
- unsubscribe();
202
- }
203
- catch (error) {
204
- logFn(`Persistence unsubscribe failed during teardown: ${String(error)}`);
205
- }
206
- }
207
- if (wallet) {
208
- try {
209
- await wallet.destroy();
210
- }
211
- catch (error) {
212
- logFn(`wallet.destroy() failed during teardown: ${String(error)}`);
213
- }
214
- }
215
- try {
216
- store.close();
217
- }
218
- catch (error) {
219
- logFn(`store.close() failed during teardown: ${String(error)}`);
220
- }
221
- }
222
- /**
223
- * Determine whether the loaded state already contains a keyring vault.
224
- *
225
- * The KeyringController persists its `vault` once an SRP has been imported, so
226
- * its presence indicates that first-run setup completed before.
227
- *
228
- * @param state - The state loaded from the key-value store.
229
- * @returns True if a KeyringController vault string is present.
230
- */
231
- function hasPersistedKeyring(state) {
232
- return typeof state.KeyringController?.vault === 'string';
233
- }
234
- //# sourceMappingURL=wallet-factory.cjs.map
@@ -1 +0,0 @@
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 { Logger } from './types';\n\nconst IN_MEMORY_DATABASE_PATH = ':memory:';\n\nexport type CreateWalletConfig = {\n databasePath: string;\n password: string;\n srp: string;\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 *\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 // TODO(#8975): add the `transactionController` slot once it is wired.\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. On subsequent runs the persisted vault is\n * reused — `password`/`srp` go unused and the wallet starts locked; the caller\n * unlocks it via `KeyringController:submitPassword` before any keyring-bound\n * operation.\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.\n * @param config.srp - The secret recovery phrase (BIP-39 mnemonic).\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 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 await importSecretRecoveryPhrase(wallet, password, srp);\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"]}