@metamask-previews/wallet-cli 0.0.0-preview-2dc3d26a4 → 0.0.0-preview-aa202ec10

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.
@@ -0,0 +1,328 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _WalletSend_instances, _WalletSend_dispatchSend;
7
+ import { is } from "@metamask/superstruct";
8
+ import { bigIntToHex, isJsonRpcFailure, toWei } from "@metamask/utils";
9
+ import { Command, Flags } from "@oclif/core";
10
+ import { sendCommand } from "../../daemon/daemon-client.mjs";
11
+ import { getDaemonPaths } from "../../daemon/paths.mjs";
12
+ import { confirmSend } from "../../daemon/prompts.mjs";
13
+ import { SendTransactionBroadcastResultStruct, SendTransactionDryRunResultStruct } from "../../daemon/send-transaction.mjs";
14
+ import { emptyToUndefined, formatJsonRpcError, makeDaemonConnectionError } from "../../daemon/utils.mjs";
15
+ const ETHER_DECIMALS = 18;
16
+ /**
17
+ * Fallback response timeout (ms) for the broadcast call. Broadcasting waits for
18
+ * signing and `eth_sendRawTransaction` server-side, which can far outlast the
19
+ * 30s default used for cheap RPCs; too short a timeout would make a slow but
20
+ * successful send look like a failure and tempt a duplicate re-send.
21
+ * `--timeout` overrides it.
22
+ */
23
+ const BROADCAST_TIMEOUT_MS = 120000;
24
+ /**
25
+ * Convert a decimal ether amount to a `0x`-prefixed wei quantity.
26
+ *
27
+ * The fixed-point conversion is delegated to `@metamask/utils`' `toWei` rather
28
+ * than re-deriving the wei math here. The daemon's `sendTransaction` boundary
29
+ * expects canonical hex wei; this is where the human-friendly `--value` (ether)
30
+ * is turned into it.
31
+ *
32
+ * `toWei` silently accepts negatives and throws opaque messages on other
33
+ * malformed input, so the value is guarded up front for a clear error and a
34
+ * strictly non-negative amount.
35
+ *
36
+ * @param amount - A non-negative decimal ether amount (e.g. `"0.1"`, `"1"`).
37
+ * @returns The equivalent wei as a `0x`-prefixed hex string.
38
+ * @throws If `amount` is not a non-negative decimal or has more than 18
39
+ * fractional digits.
40
+ */
41
+ export function parseEtherToWeiHex(amount) {
42
+ const trimmed = amount.trim();
43
+ if (!/^\d+(?:\.\d+)?$/u.test(trimmed)) {
44
+ throw new Error(`Invalid value "${amount}": expected a non-negative decimal amount of ether (e.g. 0.1).`);
45
+ }
46
+ if ((trimmed.split('.')[1] ?? '').length > ETHER_DECIMALS) {
47
+ throw new Error(`Invalid value "${amount}": ether has at most ${ETHER_DECIMALS} decimal places.`);
48
+ }
49
+ return bigIntToHex(toWei(trimmed, 'ether'));
50
+ }
51
+ class WalletSend extends Command {
52
+ constructor() {
53
+ super(...arguments);
54
+ _WalletSend_instances.add(this);
55
+ }
56
+ async run() {
57
+ const { flags } = await this.parse(WalletSend);
58
+ const networkClientId = emptyToUndefined(flags['network-client-id']);
59
+ const chainId = emptyToUndefined(flags['chain-id']);
60
+ if ((networkClientId === undefined) === (chainId === undefined)) {
61
+ this.error('Provide exactly one of --network-client-id or --chain-id.');
62
+ }
63
+ let value;
64
+ try {
65
+ value = parseEtherToWeiHex(flags.value);
66
+ }
67
+ catch (error) {
68
+ // `parseEtherToWeiHex` only ever throws `Error`.
69
+ this.error(error.message);
70
+ }
71
+ // Everything except the network selector. The selector is added per call:
72
+ // a confirmed broadcast pins the `networkClientId` the preview resolved
73
+ // rather than re-sending `--chain-id` and re-resolving it.
74
+ const baseParams = {
75
+ to: flags.to,
76
+ value,
77
+ ...(flags.from ? { from: flags.from } : {}),
78
+ ...(flags.data ? { data: flags.data } : {}),
79
+ ...(flags.gas ? { gas: flags.gas } : {}),
80
+ ...(flags['max-fee-per-gas']
81
+ ? { maxFeePerGas: flags['max-fee-per-gas'] }
82
+ : {}),
83
+ ...(flags['max-priority-fee-per-gas']
84
+ ? { maxPriorityFeePerGas: flags['max-priority-fee-per-gas'] }
85
+ : {}),
86
+ ...(flags['gas-price'] ? { gasPrice: flags['gas-price'] } : {}),
87
+ };
88
+ // Exactly one selector is defined (guarded above); guard each spread by its
89
+ // own `undefined` check so the object never carries an `undefined` value.
90
+ const params = {
91
+ ...baseParams,
92
+ ...(networkClientId === undefined ? {} : { networkClientId }),
93
+ ...(chainId === undefined ? {} : { chainId }),
94
+ };
95
+ const planExtras = {
96
+ data: flags.data,
97
+ gas: flags.gas,
98
+ maxFeePerGas: flags['max-fee-per-gas'],
99
+ maxPriorityFeePerGas: flags['max-priority-fee-per-gas'],
100
+ gasPrice: flags['gas-price'],
101
+ };
102
+ const { socketPath } = getDaemonPaths(this.config.dataDir);
103
+ const timeoutMs = flags.timeout;
104
+ // A `dryRun` resolves the sender and network client server-side without
105
+ // broadcasting. `--dry-run` stops after previewing; an interactive run
106
+ // previews first so the confirmation shows (and then pins) the resolved
107
+ // sender/network; `--yes` skips straight to the broadcast.
108
+ if (flags['dry-run']) {
109
+ const preview = await __classPrivateFieldGet(this, _WalletSend_instances, "m", _WalletSend_dispatchSend).call(this, {
110
+ socketPath,
111
+ params: { ...params, dryRun: true },
112
+ timeoutMs,
113
+ struct: SendTransactionDryRunResultStruct,
114
+ broadcast: false,
115
+ });
116
+ this.log(formatPlan(preview, flags.value, planExtras));
117
+ return;
118
+ }
119
+ let resolved;
120
+ if (!flags.yes) {
121
+ const preview = await __classPrivateFieldGet(this, _WalletSend_instances, "m", _WalletSend_dispatchSend).call(this, {
122
+ socketPath,
123
+ params: { ...params, dryRun: true },
124
+ timeoutMs,
125
+ struct: SendTransactionDryRunResultStruct,
126
+ broadcast: false,
127
+ });
128
+ let confirmed;
129
+ try {
130
+ confirmed = await confirmSend(formatPlan(preview, flags.value, planExtras));
131
+ }
132
+ catch (error) {
133
+ // Ctrl+C at the prompt (@inquirer/core's ExitPromptError) is a clean
134
+ // abort, not a failure; anything else should surface.
135
+ if (error instanceof Error && error.name === 'ExitPromptError') {
136
+ this.log('Aborted.');
137
+ return;
138
+ }
139
+ throw error;
140
+ }
141
+ if (!confirmed) {
142
+ this.log('Aborted.');
143
+ return;
144
+ }
145
+ resolved = preview;
146
+ }
147
+ // Broadcast the exact sender/network the user reviewed (when they confirmed
148
+ // a preview), so what is signed matches what was shown. `--yes` skips the
149
+ // preview, so there it re-resolves server-side from `params`.
150
+ const broadcastParams = resolved
151
+ ? {
152
+ ...baseParams,
153
+ from: resolved.from,
154
+ networkClientId: resolved.networkClientId,
155
+ }
156
+ : params;
157
+ const result = await __classPrivateFieldGet(this, _WalletSend_instances, "m", _WalletSend_dispatchSend).call(this, {
158
+ socketPath,
159
+ params: broadcastParams,
160
+ timeoutMs: timeoutMs ?? BROADCAST_TIMEOUT_MS,
161
+ struct: SendTransactionBroadcastResultStruct,
162
+ broadcast: true,
163
+ });
164
+ this.log('Transaction broadcast.');
165
+ this.log(`Hash: ${result.transactionHash}`);
166
+ this.log(`Id: ${result.transactionId}`);
167
+ this.log(`Status: ${result.status}`);
168
+ }
169
+ }
170
+ _WalletSend_instances = new WeakSet(), _WalletSend_dispatchSend =
171
+ /**
172
+ * Send a `sendTransaction` RPC to the daemon and return its validated result,
173
+ * translating connection errors, broadcast timeouts, JSON-RPC failures, and
174
+ * unexpected payloads into command errors.
175
+ *
176
+ * @param options - Dispatch options.
177
+ * @param options.socketPath - The daemon Unix socket path.
178
+ * @param options.params - The `sendTransaction` params (with or without
179
+ * `dryRun`).
180
+ * @param options.timeoutMs - Optional response timeout in milliseconds.
181
+ * @param options.struct - Struct the `result` payload must satisfy; the
182
+ * return type is inferred from it.
183
+ * @param options.broadcast - Whether this is the real (fund-moving) broadcast.
184
+ * When true, a read timeout is reported with guidance that the send may
185
+ * already be in flight, so the user does not blindly re-send.
186
+ * @returns The validated `result` payload.
187
+ */
188
+ async function _WalletSend_dispatchSend(options) {
189
+ const { socketPath, params, timeoutMs, struct, broadcast } = options;
190
+ let response;
191
+ try {
192
+ response = await sendCommand({
193
+ socketPath,
194
+ method: 'sendTransaction',
195
+ params,
196
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
197
+ });
198
+ }
199
+ catch (error) {
200
+ if (broadcast && isReadTimeout(error)) {
201
+ this.error(broadcastTimeoutMessage());
202
+ }
203
+ this.error(makeDaemonConnectionError(error));
204
+ }
205
+ if (isJsonRpcFailure(response)) {
206
+ this.error(formatJsonRpcError(response.error));
207
+ }
208
+ const { result } = response;
209
+ if (!is(result, struct)) {
210
+ this.error(`The daemon returned an unexpected ${broadcast ? 'send' : 'dry-run'} ` +
211
+ 'result. It may be running an incompatible version.');
212
+ }
213
+ return result;
214
+ };
215
+ WalletSend.description = 'Send a transaction through the daemon-hosted TransactionController. ' +
216
+ 'Estimates gas automatically unless overridden, signs, broadcasts, and ' +
217
+ 'prints the resulting transaction hash. The daemon auto-approves, so the ' +
218
+ 'confirmation boundary is this command.';
219
+ WalletSend.examples = [
220
+ '<%= config.bin %> wallet send --to 0xRecipient --value 0.01 --chain-id 0x1',
221
+ '<%= config.bin %> wallet send --to 0xRecipient --value 0.01 --network-client-id mainnet --yes',
222
+ '<%= config.bin %> wallet send --to 0xContract --data 0xabcdef --value 0 --chain-id 0x1 --dry-run',
223
+ ];
224
+ WalletSend.flags = {
225
+ to: Flags.string({
226
+ description: 'Recipient address (0x-prefixed)',
227
+ required: true,
228
+ }),
229
+ value: Flags.string({
230
+ description: 'Amount to send, in ether (e.g. 0.01). Defaults to 0.',
231
+ default: '0',
232
+ }),
233
+ from: Flags.string({
234
+ description: 'Sender address (0x-prefixed). Defaults to the selected account.',
235
+ }),
236
+ data: Flags.string({
237
+ description: 'Calldata as a 0x-prefixed hex string (for contract calls)',
238
+ }),
239
+ 'network-client-id': Flags.string({
240
+ description: 'Network client to send on. Provide this or --chain-id, not both.',
241
+ }),
242
+ 'chain-id': Flags.string({
243
+ description: 'Chain ID (0x-prefixed hex) to resolve to a network client. Provide this or --network-client-id, not both.',
244
+ }),
245
+ gas: Flags.string({
246
+ description: 'Gas limit override, as a 0x-prefixed hex quantity',
247
+ }),
248
+ 'max-fee-per-gas': Flags.string({
249
+ description: 'maxFeePerGas override, as a 0x-prefixed hex wei quantity',
250
+ }),
251
+ 'max-priority-fee-per-gas': Flags.string({
252
+ description: 'maxPriorityFeePerGas override, as a 0x-prefixed hex wei quantity',
253
+ }),
254
+ 'gas-price': Flags.string({
255
+ description: 'Legacy gasPrice override, as a 0x-prefixed hex wei quantity',
256
+ }),
257
+ 'dry-run': Flags.boolean({
258
+ description: 'Resolve the network client and sender and validate params, but do not broadcast.',
259
+ }),
260
+ yes: Flags.boolean({
261
+ char: 'y',
262
+ description: 'Skip the confirmation prompt and broadcast immediately.',
263
+ }),
264
+ timeout: Flags.integer({
265
+ char: 't',
266
+ description: 'Response timeout in milliseconds',
267
+ }),
268
+ };
269
+ export default WalletSend;
270
+ /**
271
+ * Whether an error is the daemon-client socket read timeout. It carries no
272
+ * errno code, so its message is the only signal.
273
+ *
274
+ * @param error - The caught value.
275
+ * @returns True if it is the read timeout.
276
+ */
277
+ function isReadTimeout(error) {
278
+ return error instanceof Error && error.message === 'Socket read timed out';
279
+ }
280
+ /**
281
+ * The message shown when the broadcast call times out. A send is not
282
+ * idempotent, and the daemon may still be broadcasting after the client gives
283
+ * up waiting, so this warns against a blind re-run rather than reading as a
284
+ * plain connection failure.
285
+ *
286
+ * @returns The user-facing guidance.
287
+ */
288
+ function broadcastTimeoutMessage() {
289
+ return ('The daemon did not respond in time, but your transaction may still be ' +
290
+ 'broadcasting. Do NOT re-run this command — you could send it twice. ' +
291
+ 'Check `mm daemon status`, the daemon log, or your account on-chain to ' +
292
+ 'confirm, then use --timeout to wait longer if needed.');
293
+ }
294
+ /**
295
+ * Format a dry-run plan for display (and as the confirmation prompt body).
296
+ *
297
+ * @param plan - The dry-run result returned by the daemon.
298
+ * @param etherAmount - The original `--value` (ether), shown alongside the
299
+ * resolved wei so the user sees the human amount they typed.
300
+ * @param extras - The `--data` / gas overrides the daemon does not echo back,
301
+ * shown so the user confirms exactly what will be sent.
302
+ * @returns A multi-line summary of the transaction to be sent.
303
+ */
304
+ function formatPlan(plan, etherAmount, extras) {
305
+ const lines = [
306
+ 'About to send:',
307
+ ` To: ${plan.to}`,
308
+ ` From: ${plan.from}`,
309
+ ` Value: ${etherAmount} ETH (${plan.value} wei)`,
310
+ ` Network: ${plan.networkClientId}`,
311
+ ];
312
+ if (extras.data) {
313
+ lines.push(` Data: ${extras.data}`);
314
+ }
315
+ const gasParts = [
316
+ extras.gas ? `gas=${extras.gas}` : undefined,
317
+ extras.maxFeePerGas ? `maxFeePerGas=${extras.maxFeePerGas}` : undefined,
318
+ extras.maxPriorityFeePerGas
319
+ ? `maxPriorityFeePerGas=${extras.maxPriorityFeePerGas}`
320
+ : undefined,
321
+ extras.gasPrice ? `gasPrice=${extras.gasPrice}` : undefined,
322
+ ].filter((part) => part !== undefined);
323
+ if (gasParts.length > 0) {
324
+ lines.push(` Gas: ${gasParts.join(', ')}`);
325
+ }
326
+ return lines.join('\n');
327
+ }
328
+ //# sourceMappingURL=send.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.mjs","sourceRoot":"","sources":["../../../src/commands/wallet/send.ts"],"names":[],"mappings":"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","sourcesContent":["import { is } from '@metamask/superstruct';\nimport type { Struct } from '@metamask/superstruct';\nimport { bigIntToHex, isJsonRpcFailure, toWei } from '@metamask/utils';\nimport type { Hex, Json, JsonRpcParams } from '@metamask/utils';\nimport { Command, Flags } from '@oclif/core';\n\nimport { sendCommand } from '../../daemon/daemon-client.js';\nimport { getDaemonPaths } from '../../daemon/paths.js';\nimport { confirmSend } from '../../daemon/prompts.js';\nimport {\n SendTransactionBroadcastResultStruct,\n SendTransactionDryRunResultStruct,\n} from '../../daemon/send-transaction.js';\nimport type { SendTransactionDryRunResult } from '../../daemon/send-transaction.js';\nimport {\n emptyToUndefined,\n formatJsonRpcError,\n makeDaemonConnectionError,\n} from '../../daemon/utils.js';\n\nconst ETHER_DECIMALS = 18;\n\n/**\n * Fallback response timeout (ms) for the broadcast call. Broadcasting waits for\n * signing and `eth_sendRawTransaction` server-side, which can far outlast the\n * 30s default used for cheap RPCs; too short a timeout would make a slow but\n * successful send look like a failure and tempt a duplicate re-send.\n * `--timeout` overrides it.\n */\nconst BROADCAST_TIMEOUT_MS = 120_000;\n\n/**\n * The transaction fields shown in the confirmation preview that the daemon\n * does not echo back (they are the raw `--data` / gas overrides the user\n * passed, so the CLI supplies them for display).\n */\ntype PlanExtras = {\n data?: string | undefined;\n gas?: string | undefined;\n maxFeePerGas?: string | undefined;\n maxPriorityFeePerGas?: string | undefined;\n gasPrice?: string | undefined;\n};\n\n/**\n * Convert a decimal ether amount to a `0x`-prefixed wei quantity.\n *\n * The fixed-point conversion is delegated to `@metamask/utils`' `toWei` rather\n * than re-deriving the wei math here. The daemon's `sendTransaction` boundary\n * expects canonical hex wei; this is where the human-friendly `--value` (ether)\n * is turned into it.\n *\n * `toWei` silently accepts negatives and throws opaque messages on other\n * malformed input, so the value is guarded up front for a clear error and a\n * strictly non-negative amount.\n *\n * @param amount - A non-negative decimal ether amount (e.g. `\"0.1\"`, `\"1\"`).\n * @returns The equivalent wei as a `0x`-prefixed hex string.\n * @throws If `amount` is not a non-negative decimal or has more than 18\n * fractional digits.\n */\nexport function parseEtherToWeiHex(amount: string): Hex {\n const trimmed = amount.trim();\n if (!/^\\d+(?:\\.\\d+)?$/u.test(trimmed)) {\n throw new Error(\n `Invalid value \"${amount}\": expected a non-negative decimal amount of ether (e.g. 0.1).`,\n );\n }\n\n if ((trimmed.split('.')[1] ?? '').length > ETHER_DECIMALS) {\n throw new Error(\n `Invalid value \"${amount}\": ether has at most ${ETHER_DECIMALS} decimal places.`,\n );\n }\n\n return bigIntToHex(toWei(trimmed, 'ether'));\n}\n\nexport default class WalletSend extends Command {\n static override description =\n 'Send a transaction through the daemon-hosted TransactionController. ' +\n 'Estimates gas automatically unless overridden, signs, broadcasts, and ' +\n 'prints the resulting transaction hash. The daemon auto-approves, so the ' +\n 'confirmation boundary is this command.';\n\n static override examples = [\n '<%= config.bin %> wallet send --to 0xRecipient --value 0.01 --chain-id 0x1',\n '<%= config.bin %> wallet send --to 0xRecipient --value 0.01 --network-client-id mainnet --yes',\n '<%= config.bin %> wallet send --to 0xContract --data 0xabcdef --value 0 --chain-id 0x1 --dry-run',\n ];\n\n static override flags = {\n to: Flags.string({\n description: 'Recipient address (0x-prefixed)',\n required: true,\n }),\n value: Flags.string({\n description: 'Amount to send, in ether (e.g. 0.01). Defaults to 0.',\n default: '0',\n }),\n from: Flags.string({\n description:\n 'Sender address (0x-prefixed). Defaults to the selected account.',\n }),\n data: Flags.string({\n description: 'Calldata as a 0x-prefixed hex string (for contract calls)',\n }),\n 'network-client-id': Flags.string({\n description:\n 'Network client to send on. Provide this or --chain-id, not both.',\n }),\n 'chain-id': Flags.string({\n description:\n 'Chain ID (0x-prefixed hex) to resolve to a network client. Provide this or --network-client-id, not both.',\n }),\n gas: Flags.string({\n description: 'Gas limit override, as a 0x-prefixed hex quantity',\n }),\n 'max-fee-per-gas': Flags.string({\n description: 'maxFeePerGas override, as a 0x-prefixed hex wei quantity',\n }),\n 'max-priority-fee-per-gas': Flags.string({\n description:\n 'maxPriorityFeePerGas override, as a 0x-prefixed hex wei quantity',\n }),\n 'gas-price': Flags.string({\n description:\n 'Legacy gasPrice override, as a 0x-prefixed hex wei quantity',\n }),\n 'dry-run': Flags.boolean({\n description:\n 'Resolve the network client and sender and validate params, but do not broadcast.',\n }),\n yes: Flags.boolean({\n char: 'y',\n description: 'Skip the confirmation prompt and broadcast immediately.',\n }),\n timeout: Flags.integer({\n char: 't',\n description: 'Response timeout in milliseconds',\n }),\n };\n\n public async run(): Promise<void> {\n const { flags } = await this.parse(WalletSend);\n\n const networkClientId = emptyToUndefined(flags['network-client-id']);\n const chainId = emptyToUndefined(flags['chain-id']);\n if ((networkClientId === undefined) === (chainId === undefined)) {\n this.error('Provide exactly one of --network-client-id or --chain-id.');\n }\n\n let value: string;\n try {\n value = parseEtherToWeiHex(flags.value);\n } catch (error) {\n // `parseEtherToWeiHex` only ever throws `Error`.\n this.error((error as Error).message);\n }\n\n // Everything except the network selector. The selector is added per call:\n // a confirmed broadcast pins the `networkClientId` the preview resolved\n // rather than re-sending `--chain-id` and re-resolving it.\n const baseParams: Record<string, Json> = {\n to: flags.to,\n value,\n ...(flags.from ? { from: flags.from } : {}),\n ...(flags.data ? { data: flags.data } : {}),\n ...(flags.gas ? { gas: flags.gas } : {}),\n ...(flags['max-fee-per-gas']\n ? { maxFeePerGas: flags['max-fee-per-gas'] }\n : {}),\n ...(flags['max-priority-fee-per-gas']\n ? { maxPriorityFeePerGas: flags['max-priority-fee-per-gas'] }\n : {}),\n ...(flags['gas-price'] ? { gasPrice: flags['gas-price'] } : {}),\n };\n // Exactly one selector is defined (guarded above); guard each spread by its\n // own `undefined` check so the object never carries an `undefined` value.\n const params: Record<string, Json> = {\n ...baseParams,\n ...(networkClientId === undefined ? {} : { networkClientId }),\n ...(chainId === undefined ? {} : { chainId }),\n };\n const planExtras: PlanExtras = {\n data: flags.data,\n gas: flags.gas,\n maxFeePerGas: flags['max-fee-per-gas'],\n maxPriorityFeePerGas: flags['max-priority-fee-per-gas'],\n gasPrice: flags['gas-price'],\n };\n\n const { socketPath } = getDaemonPaths(this.config.dataDir);\n const timeoutMs = flags.timeout;\n\n // A `dryRun` resolves the sender and network client server-side without\n // broadcasting. `--dry-run` stops after previewing; an interactive run\n // previews first so the confirmation shows (and then pins) the resolved\n // sender/network; `--yes` skips straight to the broadcast.\n if (flags['dry-run']) {\n const preview = await this.#dispatchSend({\n socketPath,\n params: { ...params, dryRun: true },\n timeoutMs,\n struct: SendTransactionDryRunResultStruct,\n broadcast: false,\n });\n this.log(formatPlan(preview, flags.value, planExtras));\n return;\n }\n\n let resolved: SendTransactionDryRunResult | undefined;\n if (!flags.yes) {\n const preview = await this.#dispatchSend({\n socketPath,\n params: { ...params, dryRun: true },\n timeoutMs,\n struct: SendTransactionDryRunResultStruct,\n broadcast: false,\n });\n\n let confirmed: boolean;\n try {\n confirmed = await confirmSend(\n formatPlan(preview, flags.value, planExtras),\n );\n } catch (error) {\n // Ctrl+C at the prompt (@inquirer/core's ExitPromptError) is a clean\n // abort, not a failure; anything else should surface.\n if (error instanceof Error && error.name === 'ExitPromptError') {\n this.log('Aborted.');\n return;\n }\n throw error;\n }\n if (!confirmed) {\n this.log('Aborted.');\n return;\n }\n resolved = preview;\n }\n\n // Broadcast the exact sender/network the user reviewed (when they confirmed\n // a preview), so what is signed matches what was shown. `--yes` skips the\n // preview, so there it re-resolves server-side from `params`.\n const broadcastParams: Record<string, Json> = resolved\n ? {\n ...baseParams,\n from: resolved.from,\n networkClientId: resolved.networkClientId,\n }\n : params;\n\n const result = await this.#dispatchSend({\n socketPath,\n params: broadcastParams,\n timeoutMs: timeoutMs ?? BROADCAST_TIMEOUT_MS,\n struct: SendTransactionBroadcastResultStruct,\n broadcast: true,\n });\n this.log('Transaction broadcast.');\n this.log(`Hash: ${result.transactionHash}`);\n this.log(`Id: ${result.transactionId}`);\n this.log(`Status: ${result.status}`);\n }\n\n /**\n * Send a `sendTransaction` RPC to the daemon and return its validated result,\n * translating connection errors, broadcast timeouts, JSON-RPC failures, and\n * unexpected payloads into command errors.\n *\n * @param options - Dispatch options.\n * @param options.socketPath - The daemon Unix socket path.\n * @param options.params - The `sendTransaction` params (with or without\n * `dryRun`).\n * @param options.timeoutMs - Optional response timeout in milliseconds.\n * @param options.struct - Struct the `result` payload must satisfy; the\n * return type is inferred from it.\n * @param options.broadcast - Whether this is the real (fund-moving) broadcast.\n * When true, a read timeout is reported with guidance that the send may\n * already be in flight, so the user does not blindly re-send.\n * @returns The validated `result` payload.\n */\n async #dispatchSend<Value>(options: {\n socketPath: string;\n params: JsonRpcParams;\n timeoutMs: number | undefined;\n struct: Struct<Value>;\n broadcast: boolean;\n }): Promise<Value> {\n const { socketPath, params, timeoutMs, struct, broadcast } = options;\n let response;\n try {\n response = await sendCommand({\n socketPath,\n method: 'sendTransaction',\n params,\n ...(timeoutMs === undefined ? {} : { timeoutMs }),\n });\n } catch (error) {\n if (broadcast && isReadTimeout(error)) {\n this.error(broadcastTimeoutMessage());\n }\n this.error(makeDaemonConnectionError(error));\n }\n\n if (isJsonRpcFailure(response)) {\n this.error(formatJsonRpcError(response.error));\n }\n\n const { result } = response;\n if (!is(result, struct)) {\n this.error(\n `The daemon returned an unexpected ${broadcast ? 'send' : 'dry-run'} ` +\n 'result. It may be running an incompatible version.',\n );\n }\n return result;\n }\n}\n\n/**\n * Whether an error is the daemon-client socket read timeout. It carries no\n * errno code, so its message is the only signal.\n *\n * @param error - The caught value.\n * @returns True if it is the read timeout.\n */\nfunction isReadTimeout(error: unknown): boolean {\n return error instanceof Error && error.message === 'Socket read timed out';\n}\n\n/**\n * The message shown when the broadcast call times out. A send is not\n * idempotent, and the daemon may still be broadcasting after the client gives\n * up waiting, so this warns against a blind re-run rather than reading as a\n * plain connection failure.\n *\n * @returns The user-facing guidance.\n */\nfunction broadcastTimeoutMessage(): string {\n return (\n 'The daemon did not respond in time, but your transaction may still be ' +\n 'broadcasting. Do NOT re-run this command — you could send it twice. ' +\n 'Check `mm daemon status`, the daemon log, or your account on-chain to ' +\n 'confirm, then use --timeout to wait longer if needed.'\n );\n}\n\n/**\n * Format a dry-run plan for display (and as the confirmation prompt body).\n *\n * @param plan - The dry-run result returned by the daemon.\n * @param etherAmount - The original `--value` (ether), shown alongside the\n * resolved wei so the user sees the human amount they typed.\n * @param extras - The `--data` / gas overrides the daemon does not echo back,\n * shown so the user confirms exactly what will be sent.\n * @returns A multi-line summary of the transaction to be sent.\n */\nfunction formatPlan(\n plan: SendTransactionDryRunResult,\n etherAmount: string,\n extras: PlanExtras,\n): string {\n const lines = [\n 'About to send:',\n ` To: ${plan.to}`,\n ` From: ${plan.from}`,\n ` Value: ${etherAmount} ETH (${plan.value} wei)`,\n ` Network: ${plan.networkClientId}`,\n ];\n if (extras.data) {\n lines.push(` Data: ${extras.data}`);\n }\n const gasParts = [\n extras.gas ? `gas=${extras.gas}` : undefined,\n extras.maxFeePerGas ? `maxFeePerGas=${extras.maxFeePerGas}` : undefined,\n extras.maxPriorityFeePerGas\n ? `maxPriorityFeePerGas=${extras.maxPriorityFeePerGas}`\n : undefined,\n extras.gasPrice ? `gasPrice=${extras.gasPrice}` : undefined,\n ].filter((part): part is string => part !== undefined);\n if (gasParts.length > 0) {\n lines.push(` Gas: ${gasParts.join(', ')}`);\n }\n return lines.join('\\n');\n}\n"]}
@@ -7,6 +7,7 @@ const data_dir_js_1 = require("./data-dir.cjs");
7
7
  const paths_js_1 = require("./paths.cjs");
8
8
  const rpc_socket_server_js_1 = require("./rpc-socket-server.cjs");
9
9
  const secrets_js_1 = require("./secrets.cjs");
10
+ const send_transaction_js_1 = require("./send-transaction.cjs");
10
11
  const types_js_1 = require("./types.cjs");
11
12
  const utils_js_1 = require("./utils.cjs");
12
13
  const wallet_factory_js_1 = require("./wallet-factory.cjs");
@@ -116,6 +117,9 @@ async function main() {
116
117
  // controllers are wired, so consumers need a way to see it without a
117
118
  // hand-kept catalog that would rot.
118
119
  listActions: (0, types_js_1.defineHandler)((0, superstruct_1.literal)(null), async () => constructedWallet.messenger.getRegisteredActionTypes()),
120
+ // Dedicated send handler; see `runSendTransaction` for why the generic
121
+ // `call` dispatch cannot carry a broadcast result.
122
+ sendTransaction: (0, types_js_1.defineHandler)(send_transaction_js_1.SendTransactionParamsStruct, async (params) => (0, send_transaction_js_1.runSendTransaction)(constructedWallet.messenger, params)),
119
123
  };
120
124
  // `startRpcSocketServer` restricts the socket to the owner (chmod 0o600)
121
125
  // on bind and never leaves a live server/socket behind if it rejects, so
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-entry.cjs","sourceRoot":"","sources":["../../src/daemon/daemon-entry.ts"],"names":[],"mappings":"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","sourcesContent":["import { define, literal } from '@metamask/superstruct';\nimport 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.js';\nimport { ensureOwnerOnlyDirectory } from './data-dir.js';\nimport { getDaemonPaths } from './paths.js';\nimport { startRpcSocketServer } from './rpc-socket-server.js';\nimport type { RpcSocketServerHandle } from './rpc-socket-server.js';\nimport { Password, Srp } from './secrets.js';\nimport { defineHandler } from './types.js';\nimport type {\n DaemonStatusInfo,\n Logger,\n RpcDispatcher,\n RpcHandlerMap,\n} from './types.js';\nimport { isErrorWithCode, isProcessAlive, readPidFile } from './utils.js';\nimport { createWallet } from './wallet-factory.js';\n\n/**\n * Params struct for the `call` RPC method. `params` must be a non-empty array\n * whose first element is the messenger action name; remaining elements are\n * positional action arguments forwarded as-is to `messenger.call`.\n */\nconst callParamsStruct = define<[string, ...unknown[]]>(\n 'CallParams',\n (value) => {\n if (!Array.isArray(value)) {\n return 'Expected an array';\n }\n if (value.length === 0) {\n return 'Expected a non-empty array';\n }\n if (typeof value[0] !== 'string') {\n return 'Expected the first element to be a string action name';\n }\n return true;\n },\n);\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 // Password is optional: when absent, the daemon starts without unlocking\n // the keyring (e.g. when the user prefers to call `mm wallet unlock`\n // interactively rather than embed the password in their environment).\n // First-run startup still requires a password; wallet-factory enforces\n // that and surfaces a clear error.\n const passwordRaw = process.env.MM_WALLET_PASSWORD;\n\n const srpRaw = process.env.MM_WALLET_SRP;\n if (!srpRaw) {\n throw new Error('MM_WALLET_SRP environment variable is required');\n }\n\n // Scrub before validation so a throw from Password.from / Srp.from (bad\n // value) does not leave the raw secrets in the long-lived daemon's env.\n delete process.env.MM_WALLET_PASSWORD;\n delete process.env.MM_WALLET_SRP;\n\n const password = passwordRaw ? Password.from(passwordRaw) : undefined;\n const srp = Srp.from(srpRaw);\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 // 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 them,\n // but there is no in-process auth check beyond that filesystem-permission\n // barrier. The messenger is strongly typed by action name; we narrow it\n // once here to the RpcDispatcher shape the `call` handler needs.\n const dispatch = constructedWallet.messenger.call.bind(\n constructedWallet.messenger,\n ) as unknown as RpcDispatcher;\n\n const handlers: RpcHandlerMap = {\n getStatus: defineHandler(\n literal(null),\n async (): Promise<DaemonStatusInfo> => ({\n pid: process.pid,\n uptime: Math.floor((Date.now() - startTime) / 1000),\n }),\n ),\n call: defineHandler(callParamsStruct, async (params) => {\n const [action, ...args] = params;\n return await dispatch(action, ...(args as Json[]));\n }),\n // Exposes the callable surface for discovery: it grows silently as\n // controllers are wired, so consumers need a way to see it without a\n // hand-kept catalog that would rot.\n listActions: defineHandler(\n literal(null),\n async (): Promise<Json> =>\n constructedWallet.messenger.getRegisteredActionTypes(),\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 try {\n await activeDispose();\n } catch (disposeError) {\n log(`dispose() failed during shutdown: ${String(disposeError)}`);\n }\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
+ {"version":3,"file":"daemon-entry.cjs","sourceRoot":"","sources":["../../src/daemon/daemon-entry.ts"],"names":[],"mappings":";;AAAA,uDAAwD;AAGxD,+CAAuE;AAEvE,0DAAgD;AAChD,gDAAyD;AACzD,0CAA4C;AAC5C,kEAA8D;AAE9D,8CAA6C;AAC7C,gEAG+B;AAC/B,0CAA2C;AAO3C,0CAA0E;AAC1E,4DAAmD;AAEnD;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,IAAA,oBAAM,EAC7B,YAAY,EACZ,CAAC,KAAK,EAAE,EAAE;IACR,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,mBAAmB,CAAC;IAC7B,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,4BAA4B,CAAC;IACtC,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,uDAAuD,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CACF,CAAC;AAEF,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;AAE7B,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC/C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IACtD,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,yEAAyE;IACzE,qEAAqE;IACrE,sEAAsE;IACtE,uEAAuE;IACvE,mCAAmC;IACnC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAEnD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IAED,wEAAwE;IACxE,wEAAwE;IACxE,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACtC,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAEjC,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,qBAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACtE,MAAM,GAAG,GAAG,gBAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAE7B,MAAM,IAAA,sCAAwB,EAAC,OAAO,CAAC,CAAC;IAExC,MAAM,EACJ,UAAU,EAAE,iBAAiB,EAC7B,OAAO,EACP,OAAO,EACP,MAAM,GACP,GAAG,IAAA,yBAAc,EAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,iBAAiB,CAAC;IAE1E,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAChC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAE1B,2EAA2E;IAC3E,2EAA2E;IAC3E,uEAAuE;IACvE,MAAM,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAEhD,MAAM,eAAe,GAAG,GAAG,OAAO,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC;IAEzD,kEAAkE;IAClE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,oEAAoE;IACpE,IAAI,CAAC;QACH,MAAM,IAAA,oBAAS,EAAC,OAAO,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,KAAK,YAAY,KAAK;YAC1B,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE;gBACnB,OAAO,EAAE,kCAAkC,OAAO,KAAK,KAAK,CAAC,OAAO,EAAE;aACvE,CAAC;YACJ,CAAC,CAAC,2EAA2E;gBAC3E,IAAI,KAAK,CACP,kCAAkC,OAAO,KAAK,MAAM,CAAC,KAAK,CAAC,EAAE,CAC9D,CAAC;IACR,CAAC;IAED,IAAI,MAA0B,CAAC;IAC/B,IAAI,OAA0C,CAAC;IAC/C,IAAI,MAAyC,CAAC;IAE9C,IAAI,CAAC;QACH,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAA,gCAAY,EAAC;YACxC,YAAY,EAAE,MAAM;YACpB,QAAQ;YACR,GAAG;YACH,eAAe;YACf,GAAG;SACJ,CAAC,CAAC,CAAC;QAEJ,MAAM,iBAAiB,GAAG,MAAM,CAAC;QACjC,wEAAwE;QACxE,sEAAsE;QACtE,sEAAsE;QACtE,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,iEAAiE;QACjE,MAAM,QAAQ,GAAG,iBAAiB,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CACpD,iBAAiB,CAAC,SAAS,CACA,CAAC;QAE9B,MAAM,QAAQ,GAAkB;YAC9B,SAAS,EAAE,IAAA,wBAAa,EACtB,IAAA,qBAAO,EAAC,IAAI,CAAC,EACb,KAAK,IAA+B,EAAE,CAAC,CAAC;gBACtC,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;aACpD,CAAC,CACH;YACD,IAAI,EAAE,IAAA,wBAAa,EAAC,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;gBACrD,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC;gBACjC,OAAO,MAAM,QAAQ,CAAC,MAAM,EAAE,GAAI,IAAe,CAAC,CAAC;YACrD,CAAC,CAAC;YACF,mEAAmE;YACnE,qEAAqE;YACrE,oCAAoC;YACpC,WAAW,EAAE,IAAA,wBAAa,EACxB,IAAA,qBAAO,EAAC,IAAI,CAAC,EACb,KAAK,IAAmB,EAAE,CACxB,iBAAiB,CAAC,SAAS,CAAC,wBAAwB,EAAE,CACzD;YACD,uEAAuE;YACvE,mDAAmD;YACnD,eAAe,EAAE,IAAA,wBAAa,EAC5B,iDAA2B,EAC3B,KAAK,EAAE,MAAM,EAAE,EAAE,CACf,IAAA,wCAAkB,EAAC,iBAAiB,CAAC,SAAS,EAAE,MAAM,CAAC,CAC1D;SACF,CAAC;QAEF,yEAAyE;QACzE,yEAAyE;QACzE,mDAAmD;QACnD,MAAM,GAAG,MAAM,IAAA,2CAAoB,EAAC;YAClC,UAAU;YACV,QAAQ;YACR,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;YAChD,GAAG;SACJ,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,wEAAwE;QACxE,gDAAgD;QAChD,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,OAAO,EAAE,CAAC;QAClB,CAAC;QACD,yEAAyE;QACzE,+CAA+C;QAC/C,MAAM,kBAAkB,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,KAAK,CACtD,CAAC,OAAgB,EAAE,EAAE;YACnB,GAAG,CAAC,6CAA6C,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACtE,CAAC,CACF,CAAC;QACF,MAAM,KAAK,CAAC;IACd,CAAC;IAED,2EAA2E;IAC3E,uCAAuC;IACvC,MAAM,YAAY,GAAG,MAAM,CAAC;IAC5B,MAAM,aAAa,GAAG,OAAO,CAAC;IAE9B,GAAG,CAAC,2BAA2B,UAAU,EAAE,CAAC,CAAC;IAE7C,IAAI,eAA0C,CAAC;IAE/C;;;;;OAKG;IACH,KAAK,UAAU,QAAQ,CAAC,MAAc;QACpC,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;YAClC,GAAG,CAAC,kBAAkB,MAAM,MAAM,CAAC,CAAC;YACpC,eAAe,GAAG,CAAC,KAAK,IAAmB,EAAE;gBAC3C,IAAI,CAAC;oBACH,MAAM,YAAY,CAAC,KAAK,EAAE,CAAC;gBAC7B,CAAC;gBAAC,OAAO,UAAU,EAAE,CAAC;oBACpB,GAAG,CAAC,0BAA0B,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;gBACtD,CAAC;gBACD,IAAI,CAAC;oBACH,MAAM,aAAa,EAAE,CAAC;gBACxB,CAAC;gBAAC,OAAO,YAAY,EAAE,CAAC;oBACtB,GAAG,CAAC,qCAAqC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;gBACnE,CAAC;gBACD,MAAM,OAAO,CAAC,GAAG,CAAC;oBAChB,kBAAkB,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,KAAK,CAChD,CAAC,OAAgB,EAAE,EAAE;wBACnB,GAAG,CAAC,8BAA8B,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;oBACvD,CAAC,CACF;oBACD,IAAA,aAAE,EAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,OAAgB,EAAE,EAAE;wBACzD,GAAG,CAAC,iCAAiC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;oBAC1D,CAAC,CAAC;iBACH,CAAC,CAAC;YACL,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,iJAAiJ;QACjJ,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,iJAAiJ;QACjJ,QAAQ,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,eAAe,CAC5B,OAAe,EACf,UAAkB,EAClB,GAAW;IAEX,MAAM,WAAW,GAAG,MAAM,IAAA,sBAAW,EAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,IAAA,6BAAU,EAAC,UAAU,CAAC,CAAC;IAE1C,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QACjC,MAAM,OAAO,GACX,WAAW,KAAK,SAAS;YACvB,CAAC,CAAC,uBAAuB;YACzB,CAAC,CAAC,QAAQ,WAAW,GAAG,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,kCAAkC,UAAU,IAAI,OAAO,EAAE,CAAC,CAAC;IAC7E,CAAC;IAED,wEAAwE;IACxE,iDAAiD;IACjD,wEAAwE;IACxE,qCAAqC;IACrC,wEAAwE;IACxE,wEAAwE;IACxE,6CAA6C;IAC7C,IAAI,WAAW,KAAK,SAAS,IAAI,IAAA,yBAAc,EAAC,WAAW,CAAC,EAAE,CAAC;QAC7D,MAAM,MAAM,GACV,IAAI,CAAC,MAAM,KAAK,aAAa;YAC3B,CAAC,CAAC,aAAa,UAAU,qBAAqB,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG;YACnE,CAAC,CAAC,gBAAgB,UAAU,0BAA0B,CAAC;QAC3D,MAAM,IAAI,KAAK,CACb,oCAAoC,WAAW,MAAM,MAAM,IAAI;YAC7D,+EAA+E,CAClF,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;QAClC,GAAG,CAAC,4BAA4B,UAAU,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;IACzE,CAAC;IACD,wEAAwE;IACxE,2EAA2E;IAC3E,yEAAyE;IACzE,qDAAqD;IACrD,MAAM,OAAO,CAAC,GAAG,CAAC;QAChB,IAAA,aAAE,EAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAC5B,IAAA,aAAE,EAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;KAChC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,kBAAkB,CAC/B,OAAe,EACf,gBAAwB;IAExB,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,IAAA,mBAAQ,EAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,IAAI,IAAA,0BAAe,EAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;YACrC,OAAO;QACT,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;QAChC,MAAM,IAAA,aAAE,EAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CAAC,OAAe;IACjC,OAAO,CAAC,OAAe,EAAQ,EAAE;QAC/B,MAAM,IAAI,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,OAAO,IAAI,CAAC;QAC1D,IAAA,qBAAU,EAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACjD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,MAAM,CAAC,KAAK,CAAC,KAAK,OAAO,IAAI,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { define, literal } from '@metamask/superstruct';\nimport 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.js';\nimport { ensureOwnerOnlyDirectory } from './data-dir.js';\nimport { getDaemonPaths } from './paths.js';\nimport { startRpcSocketServer } from './rpc-socket-server.js';\nimport type { RpcSocketServerHandle } from './rpc-socket-server.js';\nimport { Password, Srp } from './secrets.js';\nimport {\n runSendTransaction,\n SendTransactionParamsStruct,\n} from './send-transaction.js';\nimport { defineHandler } from './types.js';\nimport type {\n DaemonStatusInfo,\n Logger,\n RpcDispatcher,\n RpcHandlerMap,\n} from './types.js';\nimport { isErrorWithCode, isProcessAlive, readPidFile } from './utils.js';\nimport { createWallet } from './wallet-factory.js';\n\n/**\n * Params struct for the `call` RPC method. `params` must be a non-empty array\n * whose first element is the messenger action name; remaining elements are\n * positional action arguments forwarded as-is to `messenger.call`.\n */\nconst callParamsStruct = define<[string, ...unknown[]]>(\n 'CallParams',\n (value) => {\n if (!Array.isArray(value)) {\n return 'Expected an array';\n }\n if (value.length === 0) {\n return 'Expected a non-empty array';\n }\n if (typeof value[0] !== 'string') {\n return 'Expected the first element to be a string action name';\n }\n return true;\n },\n);\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 // Password is optional: when absent, the daemon starts without unlocking\n // the keyring (e.g. when the user prefers to call `mm wallet unlock`\n // interactively rather than embed the password in their environment).\n // First-run startup still requires a password; wallet-factory enforces\n // that and surfaces a clear error.\n const passwordRaw = process.env.MM_WALLET_PASSWORD;\n\n const srpRaw = process.env.MM_WALLET_SRP;\n if (!srpRaw) {\n throw new Error('MM_WALLET_SRP environment variable is required');\n }\n\n // Scrub before validation so a throw from Password.from / Srp.from (bad\n // value) does not leave the raw secrets in the long-lived daemon's env.\n delete process.env.MM_WALLET_PASSWORD;\n delete process.env.MM_WALLET_SRP;\n\n const password = passwordRaw ? Password.from(passwordRaw) : undefined;\n const srp = Srp.from(srpRaw);\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 // 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 them,\n // but there is no in-process auth check beyond that filesystem-permission\n // barrier. The messenger is strongly typed by action name; we narrow it\n // once here to the RpcDispatcher shape the `call` handler needs.\n const dispatch = constructedWallet.messenger.call.bind(\n constructedWallet.messenger,\n ) as unknown as RpcDispatcher;\n\n const handlers: RpcHandlerMap = {\n getStatus: defineHandler(\n literal(null),\n async (): Promise<DaemonStatusInfo> => ({\n pid: process.pid,\n uptime: Math.floor((Date.now() - startTime) / 1000),\n }),\n ),\n call: defineHandler(callParamsStruct, async (params) => {\n const [action, ...args] = params;\n return await dispatch(action, ...(args as Json[]));\n }),\n // Exposes the callable surface for discovery: it grows silently as\n // controllers are wired, so consumers need a way to see it without a\n // hand-kept catalog that would rot.\n listActions: defineHandler(\n literal(null),\n async (): Promise<Json> =>\n constructedWallet.messenger.getRegisteredActionTypes(),\n ),\n // Dedicated send handler; see `runSendTransaction` for why the generic\n // `call` dispatch cannot carry a broadcast result.\n sendTransaction: defineHandler(\n SendTransactionParamsStruct,\n async (params) =>\n runSendTransaction(constructedWallet.messenger, params),\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 try {\n await activeDispose();\n } catch (disposeError) {\n log(`dispose() failed during shutdown: ${String(disposeError)}`);\n }\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"]}
@@ -5,6 +5,7 @@ import { ensureOwnerOnlyDirectory } from "./data-dir.mjs";
5
5
  import { getDaemonPaths } from "./paths.mjs";
6
6
  import { startRpcSocketServer } from "./rpc-socket-server.mjs";
7
7
  import { Password, Srp } from "./secrets.mjs";
8
+ import { runSendTransaction, SendTransactionParamsStruct } from "./send-transaction.mjs";
8
9
  import { defineHandler } from "./types.mjs";
9
10
  import { isErrorWithCode, isProcessAlive, readPidFile } from "./utils.mjs";
10
11
  import { createWallet } from "./wallet-factory.mjs";
@@ -114,6 +115,9 @@ async function main() {
114
115
  // controllers are wired, so consumers need a way to see it without a
115
116
  // hand-kept catalog that would rot.
116
117
  listActions: defineHandler(literal(null), async () => constructedWallet.messenger.getRegisteredActionTypes()),
118
+ // Dedicated send handler; see `runSendTransaction` for why the generic
119
+ // `call` dispatch cannot carry a broadcast result.
120
+ sendTransaction: defineHandler(SendTransactionParamsStruct, async (params) => runSendTransaction(constructedWallet.messenger, params)),
117
121
  };
118
122
  // `startRpcSocketServer` restricts the socket to the owner (chmod 0o600)
119
123
  // on bind and never leaves a live server/socket behind if it rejects, so
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-entry.mjs","sourceRoot":"","sources":["../../src/daemon/daemon-entry.ts"],"names":[],"mappings":"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","sourcesContent":["import { define, literal } from '@metamask/superstruct';\nimport 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.js';\nimport { ensureOwnerOnlyDirectory } from './data-dir.js';\nimport { getDaemonPaths } from './paths.js';\nimport { startRpcSocketServer } from './rpc-socket-server.js';\nimport type { RpcSocketServerHandle } from './rpc-socket-server.js';\nimport { Password, Srp } from './secrets.js';\nimport { defineHandler } from './types.js';\nimport type {\n DaemonStatusInfo,\n Logger,\n RpcDispatcher,\n RpcHandlerMap,\n} from './types.js';\nimport { isErrorWithCode, isProcessAlive, readPidFile } from './utils.js';\nimport { createWallet } from './wallet-factory.js';\n\n/**\n * Params struct for the `call` RPC method. `params` must be a non-empty array\n * whose first element is the messenger action name; remaining elements are\n * positional action arguments forwarded as-is to `messenger.call`.\n */\nconst callParamsStruct = define<[string, ...unknown[]]>(\n 'CallParams',\n (value) => {\n if (!Array.isArray(value)) {\n return 'Expected an array';\n }\n if (value.length === 0) {\n return 'Expected a non-empty array';\n }\n if (typeof value[0] !== 'string') {\n return 'Expected the first element to be a string action name';\n }\n return true;\n },\n);\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 // Password is optional: when absent, the daemon starts without unlocking\n // the keyring (e.g. when the user prefers to call `mm wallet unlock`\n // interactively rather than embed the password in their environment).\n // First-run startup still requires a password; wallet-factory enforces\n // that and surfaces a clear error.\n const passwordRaw = process.env.MM_WALLET_PASSWORD;\n\n const srpRaw = process.env.MM_WALLET_SRP;\n if (!srpRaw) {\n throw new Error('MM_WALLET_SRP environment variable is required');\n }\n\n // Scrub before validation so a throw from Password.from / Srp.from (bad\n // value) does not leave the raw secrets in the long-lived daemon's env.\n delete process.env.MM_WALLET_PASSWORD;\n delete process.env.MM_WALLET_SRP;\n\n const password = passwordRaw ? Password.from(passwordRaw) : undefined;\n const srp = Srp.from(srpRaw);\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 // 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 them,\n // but there is no in-process auth check beyond that filesystem-permission\n // barrier. The messenger is strongly typed by action name; we narrow it\n // once here to the RpcDispatcher shape the `call` handler needs.\n const dispatch = constructedWallet.messenger.call.bind(\n constructedWallet.messenger,\n ) as unknown as RpcDispatcher;\n\n const handlers: RpcHandlerMap = {\n getStatus: defineHandler(\n literal(null),\n async (): Promise<DaemonStatusInfo> => ({\n pid: process.pid,\n uptime: Math.floor((Date.now() - startTime) / 1000),\n }),\n ),\n call: defineHandler(callParamsStruct, async (params) => {\n const [action, ...args] = params;\n return await dispatch(action, ...(args as Json[]));\n }),\n // Exposes the callable surface for discovery: it grows silently as\n // controllers are wired, so consumers need a way to see it without a\n // hand-kept catalog that would rot.\n listActions: defineHandler(\n literal(null),\n async (): Promise<Json> =>\n constructedWallet.messenger.getRegisteredActionTypes(),\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 try {\n await activeDispose();\n } catch (disposeError) {\n log(`dispose() failed during shutdown: ${String(disposeError)}`);\n }\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
+ {"version":3,"file":"daemon-entry.mjs","sourceRoot":"","sources":["../../src/daemon/daemon-entry.ts"],"names":[],"mappings":"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","sourcesContent":["import { define, literal } from '@metamask/superstruct';\nimport 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.js';\nimport { ensureOwnerOnlyDirectory } from './data-dir.js';\nimport { getDaemonPaths } from './paths.js';\nimport { startRpcSocketServer } from './rpc-socket-server.js';\nimport type { RpcSocketServerHandle } from './rpc-socket-server.js';\nimport { Password, Srp } from './secrets.js';\nimport {\n runSendTransaction,\n SendTransactionParamsStruct,\n} from './send-transaction.js';\nimport { defineHandler } from './types.js';\nimport type {\n DaemonStatusInfo,\n Logger,\n RpcDispatcher,\n RpcHandlerMap,\n} from './types.js';\nimport { isErrorWithCode, isProcessAlive, readPidFile } from './utils.js';\nimport { createWallet } from './wallet-factory.js';\n\n/**\n * Params struct for the `call` RPC method. `params` must be a non-empty array\n * whose first element is the messenger action name; remaining elements are\n * positional action arguments forwarded as-is to `messenger.call`.\n */\nconst callParamsStruct = define<[string, ...unknown[]]>(\n 'CallParams',\n (value) => {\n if (!Array.isArray(value)) {\n return 'Expected an array';\n }\n if (value.length === 0) {\n return 'Expected a non-empty array';\n }\n if (typeof value[0] !== 'string') {\n return 'Expected the first element to be a string action name';\n }\n return true;\n },\n);\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 // Password is optional: when absent, the daemon starts without unlocking\n // the keyring (e.g. when the user prefers to call `mm wallet unlock`\n // interactively rather than embed the password in their environment).\n // First-run startup still requires a password; wallet-factory enforces\n // that and surfaces a clear error.\n const passwordRaw = process.env.MM_WALLET_PASSWORD;\n\n const srpRaw = process.env.MM_WALLET_SRP;\n if (!srpRaw) {\n throw new Error('MM_WALLET_SRP environment variable is required');\n }\n\n // Scrub before validation so a throw from Password.from / Srp.from (bad\n // value) does not leave the raw secrets in the long-lived daemon's env.\n delete process.env.MM_WALLET_PASSWORD;\n delete process.env.MM_WALLET_SRP;\n\n const password = passwordRaw ? Password.from(passwordRaw) : undefined;\n const srp = Srp.from(srpRaw);\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 // 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 them,\n // but there is no in-process auth check beyond that filesystem-permission\n // barrier. The messenger is strongly typed by action name; we narrow it\n // once here to the RpcDispatcher shape the `call` handler needs.\n const dispatch = constructedWallet.messenger.call.bind(\n constructedWallet.messenger,\n ) as unknown as RpcDispatcher;\n\n const handlers: RpcHandlerMap = {\n getStatus: defineHandler(\n literal(null),\n async (): Promise<DaemonStatusInfo> => ({\n pid: process.pid,\n uptime: Math.floor((Date.now() - startTime) / 1000),\n }),\n ),\n call: defineHandler(callParamsStruct, async (params) => {\n const [action, ...args] = params;\n return await dispatch(action, ...(args as Json[]));\n }),\n // Exposes the callable surface for discovery: it grows silently as\n // controllers are wired, so consumers need a way to see it without a\n // hand-kept catalog that would rot.\n listActions: defineHandler(\n literal(null),\n async (): Promise<Json> =>\n constructedWallet.messenger.getRegisteredActionTypes(),\n ),\n // Dedicated send handler; see `runSendTransaction` for why the generic\n // `call` dispatch cannot carry a broadcast result.\n sendTransaction: defineHandler(\n SendTransactionParamsStruct,\n async (params) =>\n runSendTransaction(constructedWallet.messenger, params),\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 try {\n await activeDispose();\n } catch (disposeError) {\n log(`dispose() failed during shutdown: ${String(disposeError)}`);\n }\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"]}