@cogcoin/client 1.1.4 → 1.1.5

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 (83) hide show
  1. package/README.md +4 -5
  2. package/dist/bitcoind/progress/tty-renderer.js +3 -2
  3. package/dist/bitcoind/service.js +1 -1
  4. package/dist/cli/command-registry.d.ts +39 -0
  5. package/dist/cli/command-registry.js +1132 -0
  6. package/dist/cli/commands/client-admin.js +6 -56
  7. package/dist/cli/commands/mining-admin.js +9 -32
  8. package/dist/cli/commands/mining-read.js +15 -56
  9. package/dist/cli/commands/mining-runtime.js +258 -57
  10. package/dist/cli/commands/service-runtime.js +1 -64
  11. package/dist/cli/commands/status.js +2 -15
  12. package/dist/cli/commands/update.js +6 -21
  13. package/dist/cli/commands/wallet-admin.js +18 -120
  14. package/dist/cli/commands/wallet-mutation.js +4 -7
  15. package/dist/cli/commands/wallet-read.js +31 -138
  16. package/dist/cli/context.js +2 -4
  17. package/dist/cli/mining-format.js +8 -2
  18. package/dist/cli/mutation-command-groups.d.ts +11 -11
  19. package/dist/cli/mutation-command-groups.js +9 -18
  20. package/dist/cli/mutation-json.d.ts +1 -17
  21. package/dist/cli/mutation-json.js +1 -28
  22. package/dist/cli/mutation-success.d.ts +0 -1
  23. package/dist/cli/mutation-success.js +0 -19
  24. package/dist/cli/output.d.ts +1 -10
  25. package/dist/cli/output.js +52 -481
  26. package/dist/cli/parse.d.ts +1 -1
  27. package/dist/cli/parse.js +38 -695
  28. package/dist/cli/runner.js +28 -113
  29. package/dist/cli/types.d.ts +7 -8
  30. package/dist/cli/update-notifier.js +1 -1
  31. package/dist/cli/wallet-format.js +1 -1
  32. package/dist/wallet/lifecycle/managed-core.d.ts +23 -0
  33. package/dist/wallet/lifecycle/managed-core.js +257 -0
  34. package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
  35. package/dist/wallet/lifecycle/repair-mining.js +304 -0
  36. package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
  37. package/dist/wallet/lifecycle/repair-runtime.js +206 -0
  38. package/dist/wallet/lifecycle/repair.d.ts +11 -0
  39. package/dist/wallet/lifecycle/repair.js +368 -0
  40. package/dist/wallet/lifecycle/setup.d.ts +16 -0
  41. package/dist/wallet/lifecycle/setup.js +430 -0
  42. package/dist/wallet/lifecycle/types.d.ts +125 -0
  43. package/dist/wallet/lifecycle/types.js +1 -0
  44. package/dist/wallet/lifecycle.d.ts +4 -165
  45. package/dist/wallet/lifecycle.js +3 -1656
  46. package/dist/wallet/mining/candidate.d.ts +60 -0
  47. package/dist/wallet/mining/candidate.js +290 -0
  48. package/dist/wallet/mining/competitiveness.d.ts +22 -0
  49. package/dist/wallet/mining/competitiveness.js +640 -0
  50. package/dist/wallet/mining/control.js +7 -251
  51. package/dist/wallet/mining/cycle.d.ts +39 -0
  52. package/dist/wallet/mining/cycle.js +542 -0
  53. package/dist/wallet/mining/engine-state.d.ts +66 -0
  54. package/dist/wallet/mining/engine-state.js +211 -0
  55. package/dist/wallet/mining/engine-types.d.ts +173 -0
  56. package/dist/wallet/mining/engine-types.js +1 -0
  57. package/dist/wallet/mining/engine-utils.d.ts +7 -0
  58. package/dist/wallet/mining/engine-utils.js +75 -0
  59. package/dist/wallet/mining/events.d.ts +2 -0
  60. package/dist/wallet/mining/events.js +19 -0
  61. package/dist/wallet/mining/lifecycle.d.ts +71 -0
  62. package/dist/wallet/mining/lifecycle.js +355 -0
  63. package/dist/wallet/mining/projection.d.ts +61 -0
  64. package/dist/wallet/mining/projection.js +319 -0
  65. package/dist/wallet/mining/publish.d.ts +79 -0
  66. package/dist/wallet/mining/publish.js +614 -0
  67. package/dist/wallet/mining/runner.d.ts +12 -418
  68. package/dist/wallet/mining/runner.js +274 -3433
  69. package/dist/wallet/mining/supervisor.d.ts +134 -0
  70. package/dist/wallet/mining/supervisor.js +558 -0
  71. package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
  72. package/dist/wallet/mining/visualizer-sync.js +166 -0
  73. package/dist/wallet/mining/visualizer.d.ts +1 -0
  74. package/dist/wallet/mining/visualizer.js +33 -18
  75. package/dist/wallet/reset.d.ts +1 -1
  76. package/dist/wallet/reset.js +35 -11
  77. package/dist/wallet/runtime.d.ts +0 -6
  78. package/dist/wallet/runtime.js +2 -38
  79. package/dist/wallet/tx/common.d.ts +18 -0
  80. package/dist/wallet/tx/common.js +40 -26
  81. package/package.json +1 -1
  82. package/dist/wallet/state/seed-index.d.ts +0 -43
  83. package/dist/wallet/state/seed-index.js +0 -151
@@ -0,0 +1,134 @@
1
+ import { spawn } from "node:child_process";
2
+ import { rm } from "node:fs/promises";
3
+ import { createRpcClient } from "../../bitcoind/node.js";
4
+ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
5
+ import type { ProgressOutputMode } from "../../bitcoind/types.js";
6
+ import { acquireFileLock, clearOrphanedFileLock, readLockMetadata } from "../fs/lock.js";
7
+ import { openWalletReadContext } from "../read/index.js";
8
+ import type { WalletRuntimePaths } from "../runtime.js";
9
+ import type { WalletSecretProvider } from "../state/provider.js";
10
+ import { requestMiningGenerationPreemption } from "./coordination.js";
11
+ import { inspectMiningControlPlane } from "./control.js";
12
+ import { saveStopSnapshot } from "./lifecycle.js";
13
+ import type { MiningRpcClient } from "./engine-types.js";
14
+ import { loadMiningRuntimeStatus, saveMiningRuntimeStatus } from "./runtime-artifacts.js";
15
+ import type { MiningRuntimeStatusV1 } from "./types.js";
16
+ import { MiningFollowVisualizer } from "./visualizer.js";
17
+ type OpenReadContext = typeof openWalletReadContext;
18
+ type AttachService = typeof attachOrStartManagedBitcoindService;
19
+ type RpcFactory = (config: Parameters<typeof createRpcClient>[0]) => MiningRpcClient;
20
+ type RequestMiningPreemption = typeof requestMiningGenerationPreemption;
21
+ type SaveStopSnapshot = typeof saveStopSnapshot;
22
+ type SpawnWorkerProcess = typeof spawn;
23
+ type ProcessKill = typeof process.kill;
24
+ type InspectMiningControlPlane = typeof inspectMiningControlPlane;
25
+ interface MiningLoopRunnerOptions {
26
+ dataDir: string;
27
+ databasePath: string;
28
+ provider: WalletSecretProvider;
29
+ paths: WalletRuntimePaths;
30
+ runMode: "foreground" | "background";
31
+ backgroundWorkerPid: number | null;
32
+ backgroundWorkerRunId: string | null;
33
+ signal?: AbortSignal;
34
+ fetchImpl?: typeof fetch;
35
+ openReadContext: OpenReadContext;
36
+ attachService: AttachService;
37
+ rpcFactory: RpcFactory;
38
+ stdout?: {
39
+ write(chunk: string): void;
40
+ };
41
+ visualizer?: MiningFollowVisualizer;
42
+ }
43
+ type RunMiningLoop = (options: MiningLoopRunnerOptions) => Promise<void>;
44
+ export interface MiningSupervisorRuntimeContext {
45
+ provider: WalletSecretProvider;
46
+ paths: WalletRuntimePaths;
47
+ openReadContext: OpenReadContext;
48
+ attachService: AttachService;
49
+ rpcFactory: RpcFactory;
50
+ }
51
+ interface MiningSupervisorDependencies {
52
+ requestMiningPreemption: RequestMiningPreemption;
53
+ saveStopSnapshot: SaveStopSnapshot;
54
+ spawnWorkerProcess: SpawnWorkerProcess;
55
+ runMiningLoop: RunMiningLoop;
56
+ inspectMiningControlPlane: InspectMiningControlPlane;
57
+ loadRuntimeStatus: typeof loadMiningRuntimeStatus;
58
+ saveRuntimeStatus: typeof saveMiningRuntimeStatus;
59
+ acquireLock: typeof acquireFileLock;
60
+ clearOrphanedLock: typeof clearOrphanedFileLock;
61
+ readLockMetadata: typeof readLockMetadata;
62
+ sleep: typeof sleep;
63
+ removeFile: typeof rm;
64
+ nowUnixMs: () => number;
65
+ processKill: ProcessKill;
66
+ processPid: number;
67
+ processExecPath: string;
68
+ resolveWorkerMainPath: () => string;
69
+ }
70
+ export interface MiningSupervisorStartResult {
71
+ started: boolean;
72
+ snapshot: MiningRuntimeStatusV1 | null;
73
+ }
74
+ export interface MiningSupervisorTakeoverResult {
75
+ controlLockCleared: boolean;
76
+ replaced: boolean;
77
+ snapshot: MiningRuntimeStatusV1 | null;
78
+ terminatedPids: number[];
79
+ }
80
+ declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
81
+ export declare function takeOverMiningRuntime(options: {
82
+ paths: WalletRuntimePaths;
83
+ reason: string;
84
+ clearControlLockFile?: boolean;
85
+ controlLockMetadata?: Awaited<ReturnType<typeof readLockMetadata>>;
86
+ shutdownGraceMs?: number;
87
+ deps?: Partial<MiningSupervisorDependencies>;
88
+ }): Promise<MiningSupervisorTakeoverResult>;
89
+ export declare function waitForBackgroundHealthy(paths: WalletRuntimePaths, depsOverrides?: Partial<MiningSupervisorDependencies>): Promise<MiningRuntimeStatusV1 | null>;
90
+ export declare function runForegroundMining(options: {
91
+ dataDir: string;
92
+ databasePath: string;
93
+ clientVersion?: string | null;
94
+ updateAvailable?: boolean;
95
+ stdout?: {
96
+ write(chunk: string): void;
97
+ };
98
+ stderr?: {
99
+ isTTY?: boolean;
100
+ columns?: number;
101
+ write(chunk: string): boolean | void;
102
+ };
103
+ signal?: AbortSignal;
104
+ progressOutput?: ProgressOutputMode;
105
+ visualizer?: MiningFollowVisualizer;
106
+ fetchImpl?: typeof fetch;
107
+ shutdownGraceMs?: number;
108
+ runtime: MiningSupervisorRuntimeContext;
109
+ deps?: Partial<MiningSupervisorDependencies>;
110
+ }): Promise<void>;
111
+ export declare function startBackgroundMining(options: {
112
+ dataDir: string;
113
+ databasePath: string;
114
+ shutdownGraceMs?: number;
115
+ waitForBackgroundHealthy?: (paths: WalletRuntimePaths) => Promise<MiningRuntimeStatusV1 | null>;
116
+ runtime: MiningSupervisorRuntimeContext;
117
+ deps?: Partial<MiningSupervisorDependencies>;
118
+ }): Promise<MiningSupervisorStartResult>;
119
+ export declare function stopBackgroundMining(options: {
120
+ dataDir: string;
121
+ databasePath: string;
122
+ shutdownGraceMs?: number;
123
+ runtime: MiningSupervisorRuntimeContext;
124
+ deps?: Partial<MiningSupervisorDependencies>;
125
+ }): Promise<MiningRuntimeStatusV1 | null>;
126
+ export declare function runBackgroundMiningWorker(options: {
127
+ dataDir: string;
128
+ databasePath: string;
129
+ runId: string;
130
+ fetchImpl?: typeof fetch;
131
+ runtime: MiningSupervisorRuntimeContext;
132
+ deps?: Partial<MiningSupervisorDependencies>;
133
+ }): Promise<void>;
134
+ export {};
@@ -0,0 +1,558 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRpcClient } from "../../bitcoind/node.js";
7
+ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
8
+ import { FileLockBusyError, acquireFileLock, clearOrphanedFileLock, readLockMetadata, } from "../fs/lock.js";
9
+ import { openWalletReadContext } from "../read/index.js";
10
+ import { readMiningGenerationActivity, requestMiningGenerationPreemption, } from "./coordination.js";
11
+ import { inspectMiningControlPlane } from "./control.js";
12
+ import { MINING_SHUTDOWN_GRACE_MS, MINING_WORKER_API_VERSION, } from "./constants.js";
13
+ import { saveStopSnapshot } from "./lifecycle.js";
14
+ import { loadMiningRuntimeStatus, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
15
+ import { MiningFollowVisualizer } from "./visualizer.js";
16
+ const BACKGROUND_START_TIMEOUT_MS = 15_000;
17
+ function sleep(ms, signal) {
18
+ return new Promise((resolve) => {
19
+ const timer = setTimeout(resolve, ms);
20
+ signal?.addEventListener("abort", () => {
21
+ clearTimeout(timer);
22
+ resolve();
23
+ }, { once: true });
24
+ });
25
+ }
26
+ function resolveSupervisorDependencies(overrides = {}) {
27
+ return {
28
+ requestMiningPreemption: overrides.requestMiningPreemption ?? requestMiningGenerationPreemption,
29
+ saveStopSnapshot: overrides.saveStopSnapshot ?? saveStopSnapshot,
30
+ spawnWorkerProcess: overrides.spawnWorkerProcess ?? spawn,
31
+ runMiningLoop: overrides.runMiningLoop ?? (() => {
32
+ throw new Error("mining_supervisor_run_loop_missing");
33
+ }),
34
+ inspectMiningControlPlane: overrides.inspectMiningControlPlane ?? inspectMiningControlPlane,
35
+ loadRuntimeStatus: overrides.loadRuntimeStatus ?? loadMiningRuntimeStatus,
36
+ saveRuntimeStatus: overrides.saveRuntimeStatus ?? saveMiningRuntimeStatus,
37
+ acquireLock: overrides.acquireLock ?? acquireFileLock,
38
+ clearOrphanedLock: overrides.clearOrphanedLock ?? clearOrphanedFileLock,
39
+ readLockMetadata: overrides.readLockMetadata ?? readLockMetadata,
40
+ sleep: overrides.sleep ?? sleep,
41
+ removeFile: overrides.removeFile ?? rm,
42
+ nowUnixMs: overrides.nowUnixMs ?? Date.now,
43
+ processKill: overrides.processKill ?? process.kill.bind(process),
44
+ processPid: overrides.processPid ?? process.pid,
45
+ processExecPath: overrides.processExecPath ?? process.execPath,
46
+ resolveWorkerMainPath: overrides.resolveWorkerMainPath
47
+ ?? (() => fileURLToPath(new URL("./worker-main.js", import.meta.url))),
48
+ };
49
+ }
50
+ async function isProcessAlive(pid, deps) {
51
+ if (pid === null) {
52
+ return false;
53
+ }
54
+ try {
55
+ deps.processKill(pid, 0);
56
+ return true;
57
+ }
58
+ catch (error) {
59
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ }
65
+ function normalizeMiningPid(value) {
66
+ return typeof value === "number" && Number.isInteger(value) && value > 0
67
+ ? value
68
+ : null;
69
+ }
70
+ function resolveMiningGenerationRequestPath(paths) {
71
+ return join(paths.miningRoot, "generation-request.json");
72
+ }
73
+ function resolveMiningGenerationActivityPath(paths) {
74
+ return join(paths.miningRoot, "generation-activity.json");
75
+ }
76
+ function createTakeoverStoppedMiningNote(livePublishInMempool) {
77
+ return livePublishInMempool
78
+ ? "Mining runtime replaced. The last mining transaction may still confirm from mempool."
79
+ : "Mining runtime replaced.";
80
+ }
81
+ function createStoppedMiningRuntimeSnapshotForTakeover(options) {
82
+ const note = createTakeoverStoppedMiningNote(options.snapshot?.livePublishInMempool);
83
+ if (options.snapshot !== null) {
84
+ return {
85
+ ...options.snapshot,
86
+ updatedAtUnixMs: options.nowUnixMs,
87
+ runMode: "stopped",
88
+ backgroundWorkerPid: null,
89
+ backgroundWorkerRunId: null,
90
+ backgroundWorkerHeartbeatAtUnixMs: null,
91
+ backgroundWorkerHealth: null,
92
+ currentPhase: "idle",
93
+ note,
94
+ };
95
+ }
96
+ return {
97
+ schemaVersion: 1,
98
+ walletRootId: options.walletRootId,
99
+ workerApiVersion: null,
100
+ workerBinaryVersion: null,
101
+ workerBuildId: null,
102
+ updatedAtUnixMs: options.nowUnixMs,
103
+ runMode: "stopped",
104
+ backgroundWorkerPid: null,
105
+ backgroundWorkerRunId: null,
106
+ backgroundWorkerHeartbeatAtUnixMs: null,
107
+ backgroundWorkerHealth: null,
108
+ indexerDaemonState: null,
109
+ indexerDaemonInstanceId: null,
110
+ indexerSnapshotSeq: null,
111
+ indexerSnapshotOpenedAtUnixMs: null,
112
+ indexerTruthSource: undefined,
113
+ indexerHeartbeatAtUnixMs: null,
114
+ coreBestHeight: null,
115
+ coreBestHash: null,
116
+ indexerTipHeight: null,
117
+ indexerTipHash: null,
118
+ indexerReorgDepth: null,
119
+ indexerTipAligned: null,
120
+ corePublishState: null,
121
+ providerState: null,
122
+ lastSuspendDetectedAtUnixMs: null,
123
+ reconnectSettledUntilUnixMs: null,
124
+ tipSettledUntilUnixMs: null,
125
+ miningState: "idle",
126
+ currentPhase: "idle",
127
+ currentPublishState: "none",
128
+ targetBlockHeight: null,
129
+ referencedBlockHashDisplay: null,
130
+ currentDomainId: null,
131
+ currentDomainName: null,
132
+ currentSentenceDisplay: null,
133
+ currentCanonicalBlend: null,
134
+ currentTxid: null,
135
+ currentWtxid: null,
136
+ livePublishInMempool: null,
137
+ currentFeeRateSatVb: null,
138
+ currentAbsoluteFeeSats: null,
139
+ currentBlockFeeSpentSats: "0",
140
+ sessionFeeSpentSats: "0",
141
+ lifetimeFeeSpentSats: "0",
142
+ sameDomainCompetitorSuppressed: null,
143
+ higherRankedCompetitorDomainCount: null,
144
+ dedupedCompetitorDomainCount: null,
145
+ competitivenessGateIndeterminate: null,
146
+ mempoolSequenceCacheStatus: null,
147
+ currentPublishDecision: null,
148
+ lastMempoolSequence: null,
149
+ lastCompetitivenessGateAtUnixMs: null,
150
+ pauseReason: null,
151
+ providerConfigured: false,
152
+ providerKind: null,
153
+ bitcoindHealth: "unavailable",
154
+ bitcoindServiceState: null,
155
+ bitcoindReplicaStatus: null,
156
+ nodeHealth: "unavailable",
157
+ indexerHealth: "unavailable",
158
+ tipsAligned: null,
159
+ lastEventAtUnixMs: null,
160
+ lastError: null,
161
+ note,
162
+ };
163
+ }
164
+ async function waitForMiningProcessExit(pid, timeoutMs, deps) {
165
+ const deadline = deps.nowUnixMs() + timeoutMs;
166
+ while (deps.nowUnixMs() < deadline) {
167
+ if (!await isProcessAlive(pid, deps)) {
168
+ return true;
169
+ }
170
+ await deps.sleep(Math.min(250, Math.max(timeoutMs, 1)));
171
+ }
172
+ return !await isProcessAlive(pid, deps);
173
+ }
174
+ async function terminateMiningRuntimePid(options) {
175
+ if (!await isProcessAlive(options.pid, options.deps)) {
176
+ return false;
177
+ }
178
+ try {
179
+ options.deps.processKill(options.pid, "SIGTERM");
180
+ }
181
+ catch (error) {
182
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
183
+ throw error;
184
+ }
185
+ }
186
+ if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.deps)) {
187
+ return true;
188
+ }
189
+ try {
190
+ options.deps.processKill(options.pid, "SIGKILL");
191
+ }
192
+ catch (error) {
193
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
194
+ throw error;
195
+ }
196
+ }
197
+ if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.deps)) {
198
+ return true;
199
+ }
200
+ throw new Error("mining_process_stop_timeout");
201
+ }
202
+ export async function takeOverMiningRuntime(options) {
203
+ const deps = resolveSupervisorDependencies(options.deps);
204
+ const snapshot = await deps.loadRuntimeStatus(options.paths.miningStatusPath).catch(() => null);
205
+ const controlLockMetadata = options.controlLockMetadata ?? (options.clearControlLockFile === true
206
+ ? await deps.readLockMetadata(options.paths.miningControlLockPath).catch(() => null)
207
+ : null);
208
+ const generationActivity = await readMiningGenerationActivity(options.paths).catch(() => null);
209
+ const shutdownGraceMs = options.shutdownGraceMs ?? MINING_SHUTDOWN_GRACE_MS;
210
+ const controlLockPid = normalizeMiningPid(controlLockMetadata?.processId);
211
+ const backgroundWorkerPid = normalizeMiningPid(snapshot?.backgroundWorkerPid);
212
+ const generationOwnerPid = normalizeMiningPid(generationActivity?.generationOwnerPid);
213
+ const terminatedPids = [];
214
+ const discoveredPids = new Set();
215
+ for (const pid of [controlLockPid, backgroundWorkerPid, generationOwnerPid]) {
216
+ if (pid === null
217
+ || pid === deps.processPid
218
+ || discoveredPids.has(pid)
219
+ || !await isProcessAlive(pid, deps)) {
220
+ continue;
221
+ }
222
+ discoveredPids.add(pid);
223
+ }
224
+ const shouldPreemptGeneration = discoveredPids.size > 0 && (generationActivity?.generationActive === true
225
+ || snapshot?.currentPhase === "generating"
226
+ || snapshot?.currentPhase === "scoring");
227
+ const preemption = shouldPreemptGeneration
228
+ ? await deps.requestMiningPreemption({
229
+ paths: options.paths,
230
+ reason: options.reason,
231
+ timeoutMs: Math.min(shutdownGraceMs, 15_000),
232
+ }).catch(() => null)
233
+ : null;
234
+ try {
235
+ for (const pid of discoveredPids) {
236
+ if (await terminateMiningRuntimePid({
237
+ pid,
238
+ shutdownGraceMs,
239
+ deps,
240
+ })) {
241
+ terminatedPids.push(pid);
242
+ }
243
+ }
244
+ }
245
+ finally {
246
+ await preemption?.release().catch(() => undefined);
247
+ }
248
+ const controlLockCleared = options.clearControlLockFile === true
249
+ ? await deps.clearOrphanedLock(options.paths.miningControlLockPath, async (pid) => await isProcessAlive(pid, deps)).catch(() => false)
250
+ : false;
251
+ await deps.removeFile(resolveMiningGenerationRequestPath(options.paths), { force: true }).catch(() => undefined);
252
+ await deps.removeFile(resolveMiningGenerationActivityPath(options.paths), { force: true }).catch(() => undefined);
253
+ const walletRootId = snapshot?.walletRootId
254
+ ?? (typeof controlLockMetadata?.walletRootId === "string" ? controlLockMetadata.walletRootId : null);
255
+ if (snapshot !== null || walletRootId !== null || terminatedPids.length > 0 || controlLockCleared) {
256
+ await deps.saveRuntimeStatus(options.paths.miningStatusPath, createStoppedMiningRuntimeSnapshotForTakeover({
257
+ snapshot,
258
+ walletRootId,
259
+ nowUnixMs: deps.nowUnixMs(),
260
+ }));
261
+ }
262
+ return {
263
+ controlLockCleared,
264
+ replaced: terminatedPids.length > 0,
265
+ snapshot,
266
+ terminatedPids,
267
+ };
268
+ }
269
+ async function acquireMiningStartControlLock(options) {
270
+ while (true) {
271
+ try {
272
+ return await options.deps.acquireLock(options.paths.miningControlLockPath, {
273
+ purpose: options.purpose,
274
+ });
275
+ }
276
+ catch (error) {
277
+ if (!(error instanceof FileLockBusyError)) {
278
+ throw error;
279
+ }
280
+ if (error.existingMetadata?.processId === options.deps.processPid) {
281
+ throw error;
282
+ }
283
+ const takeover = await takeOverMiningRuntime({
284
+ paths: options.paths,
285
+ reason: options.takeoverReason,
286
+ clearControlLockFile: true,
287
+ controlLockMetadata: error.existingMetadata,
288
+ shutdownGraceMs: options.shutdownGraceMs,
289
+ deps: options.deps,
290
+ });
291
+ if (!takeover.replaced && !takeover.controlLockCleared) {
292
+ throw error;
293
+ }
294
+ }
295
+ }
296
+ }
297
+ export async function waitForBackgroundHealthy(paths, depsOverrides = {}) {
298
+ const deps = resolveSupervisorDependencies(depsOverrides);
299
+ const deadline = deps.nowUnixMs() + BACKGROUND_START_TIMEOUT_MS;
300
+ while (deps.nowUnixMs() < deadline) {
301
+ const snapshot = await deps.loadRuntimeStatus(paths.miningStatusPath).catch(() => null);
302
+ if (snapshot !== null
303
+ && snapshot.runMode === "background"
304
+ && snapshot.backgroundWorkerHealth === "healthy") {
305
+ return snapshot;
306
+ }
307
+ await deps.sleep(250);
308
+ }
309
+ return deps.loadRuntimeStatus(paths.miningStatusPath).catch(() => null);
310
+ }
311
+ export async function runForegroundMining(options) {
312
+ const deps = resolveSupervisorDependencies(options.deps);
313
+ let visualizer = options.visualizer ?? null;
314
+ const ownsVisualizer = visualizer === null;
315
+ const controlLock = await acquireMiningStartControlLock({
316
+ paths: options.runtime.paths,
317
+ purpose: "mine-foreground",
318
+ takeoverReason: "mine-foreground-replace",
319
+ shutdownGraceMs: options.shutdownGraceMs,
320
+ deps,
321
+ });
322
+ const abortController = new AbortController();
323
+ const abortListener = () => {
324
+ abortController.abort();
325
+ };
326
+ const handleSigint = () => abortController.abort();
327
+ const handleSigterm = () => abortController.abort();
328
+ try {
329
+ await takeOverMiningRuntime({
330
+ paths: options.runtime.paths,
331
+ reason: "mine-foreground-replace",
332
+ shutdownGraceMs: options.shutdownGraceMs,
333
+ deps,
334
+ });
335
+ if (visualizer === null) {
336
+ visualizer = new MiningFollowVisualizer({
337
+ clientVersion: options.clientVersion,
338
+ updateAvailable: options.updateAvailable,
339
+ progressOutput: options.progressOutput ?? "auto",
340
+ stream: options.stderr,
341
+ });
342
+ }
343
+ options.signal?.addEventListener("abort", abortListener, { once: true });
344
+ process.on("SIGINT", handleSigint);
345
+ process.on("SIGTERM", handleSigterm);
346
+ await deps.runMiningLoop({
347
+ dataDir: options.dataDir,
348
+ databasePath: options.databasePath,
349
+ provider: options.runtime.provider,
350
+ paths: options.runtime.paths,
351
+ runMode: "foreground",
352
+ backgroundWorkerPid: null,
353
+ backgroundWorkerRunId: null,
354
+ signal: abortController.signal,
355
+ fetchImpl: options.fetchImpl,
356
+ openReadContext: options.runtime.openReadContext,
357
+ attachService: options.runtime.attachService,
358
+ rpcFactory: options.runtime.rpcFactory,
359
+ stdout: options.stdout,
360
+ visualizer,
361
+ });
362
+ await deps.saveStopSnapshot({
363
+ dataDir: options.dataDir,
364
+ databasePath: options.databasePath,
365
+ provider: options.runtime.provider,
366
+ paths: options.runtime.paths,
367
+ runMode: "foreground",
368
+ backgroundWorkerPid: null,
369
+ backgroundWorkerRunId: null,
370
+ note: "Foreground mining stopped cleanly.",
371
+ });
372
+ }
373
+ finally {
374
+ options.signal?.removeEventListener("abort", abortListener);
375
+ process.off("SIGINT", handleSigint);
376
+ process.off("SIGTERM", handleSigterm);
377
+ if (ownsVisualizer) {
378
+ visualizer?.close();
379
+ }
380
+ await controlLock.release();
381
+ }
382
+ }
383
+ export async function startBackgroundMining(options) {
384
+ const deps = resolveSupervisorDependencies(options.deps);
385
+ const waitForHealthy = options.waitForBackgroundHealthy
386
+ ?? (async (paths) => await waitForBackgroundHealthy(paths, deps));
387
+ let controlLock;
388
+ try {
389
+ controlLock = await acquireMiningStartControlLock({
390
+ paths: options.runtime.paths,
391
+ purpose: "mine-start",
392
+ takeoverReason: "mine-start-replace",
393
+ shutdownGraceMs: options.shutdownGraceMs,
394
+ deps,
395
+ });
396
+ }
397
+ catch (error) {
398
+ if (error instanceof FileLockBusyError && error.existingMetadata?.processId === deps.processPid) {
399
+ return {
400
+ started: false,
401
+ snapshot: await deps.loadRuntimeStatus(options.runtime.paths.miningStatusPath).catch(() => null),
402
+ };
403
+ }
404
+ throw error;
405
+ }
406
+ try {
407
+ await takeOverMiningRuntime({
408
+ paths: options.runtime.paths,
409
+ reason: "mine-start-replace",
410
+ shutdownGraceMs: options.shutdownGraceMs,
411
+ deps,
412
+ });
413
+ const runId = randomBytes(16).toString("hex");
414
+ const child = deps.spawnWorkerProcess(deps.processExecPath, [
415
+ deps.resolveWorkerMainPath(),
416
+ `--data-dir=${options.dataDir}`,
417
+ `--database-path=${options.databasePath}`,
418
+ `--run-id=${runId}`,
419
+ ], {
420
+ detached: true,
421
+ stdio: "ignore",
422
+ });
423
+ child.unref();
424
+ const snapshot = await waitForHealthy(options.runtime.paths);
425
+ return {
426
+ started: true,
427
+ snapshot,
428
+ };
429
+ }
430
+ finally {
431
+ await controlLock.release();
432
+ }
433
+ }
434
+ export async function stopBackgroundMining(options) {
435
+ const deps = resolveSupervisorDependencies(options.deps);
436
+ const shutdownGraceMs = options.shutdownGraceMs ?? MINING_SHUTDOWN_GRACE_MS;
437
+ const controlLock = await deps.acquireLock(options.runtime.paths.miningControlLockPath, {
438
+ purpose: "mine-stop",
439
+ });
440
+ try {
441
+ const snapshot = await deps.loadRuntimeStatus(options.runtime.paths.miningStatusPath).catch(() => null);
442
+ if (snapshot === null || snapshot.runMode !== "background" || snapshot.backgroundWorkerPid === null) {
443
+ return snapshot;
444
+ }
445
+ const preemption = await deps.requestMiningPreemption({
446
+ paths: options.runtime.paths,
447
+ reason: "mine-stop",
448
+ timeoutMs: Math.min(shutdownGraceMs, 15_000),
449
+ }).catch(() => null);
450
+ try {
451
+ try {
452
+ deps.processKill(snapshot.backgroundWorkerPid, "SIGTERM");
453
+ }
454
+ catch (error) {
455
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
456
+ throw error;
457
+ }
458
+ }
459
+ const deadline = deps.nowUnixMs() + shutdownGraceMs;
460
+ while (deps.nowUnixMs() < deadline) {
461
+ if (!await isProcessAlive(snapshot.backgroundWorkerPid, deps)) {
462
+ break;
463
+ }
464
+ await deps.sleep(250);
465
+ }
466
+ if (await isProcessAlive(snapshot.backgroundWorkerPid, deps)) {
467
+ try {
468
+ deps.processKill(snapshot.backgroundWorkerPid, "SIGKILL");
469
+ }
470
+ catch {
471
+ // ignore
472
+ }
473
+ }
474
+ }
475
+ finally {
476
+ await preemption?.release().catch(() => undefined);
477
+ }
478
+ await deps.saveStopSnapshot({
479
+ dataDir: options.dataDir,
480
+ databasePath: options.databasePath,
481
+ provider: options.runtime.provider,
482
+ paths: options.runtime.paths,
483
+ runMode: "background",
484
+ backgroundWorkerPid: snapshot.backgroundWorkerPid,
485
+ backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
486
+ note: snapshot.livePublishInMempool
487
+ ? "Background mining stopped. The last mining transaction may still confirm from mempool."
488
+ : "Background mining stopped.",
489
+ });
490
+ return deps.loadRuntimeStatus(options.runtime.paths.miningStatusPath).catch(() => null);
491
+ }
492
+ finally {
493
+ await controlLock.release();
494
+ }
495
+ }
496
+ export async function runBackgroundMiningWorker(options) {
497
+ const deps = resolveSupervisorDependencies(options.deps);
498
+ const abortController = new AbortController();
499
+ process.on("SIGINT", () => abortController.abort());
500
+ process.on("SIGTERM", () => abortController.abort());
501
+ const initialContext = await options.runtime.openReadContext({
502
+ dataDir: options.dataDir,
503
+ databasePath: options.databasePath,
504
+ secretProvider: options.runtime.provider,
505
+ paths: options.runtime.paths,
506
+ });
507
+ try {
508
+ const initialView = await deps.inspectMiningControlPlane({
509
+ provider: options.runtime.provider,
510
+ localState: initialContext.localState,
511
+ bitcoind: initialContext.bitcoind,
512
+ nodeStatus: initialContext.nodeStatus,
513
+ nodeHealth: initialContext.nodeHealth,
514
+ indexer: initialContext.indexer,
515
+ paths: options.runtime.paths,
516
+ });
517
+ await deps.saveRuntimeStatus(options.runtime.paths.miningStatusPath, {
518
+ ...initialView.runtime,
519
+ walletRootId: initialContext.localState.walletRootId,
520
+ workerApiVersion: MINING_WORKER_API_VERSION,
521
+ workerBinaryVersion: process.version,
522
+ workerBuildId: options.runId,
523
+ runMode: "background",
524
+ backgroundWorkerPid: deps.processPid,
525
+ backgroundWorkerRunId: options.runId,
526
+ backgroundWorkerHeartbeatAtUnixMs: deps.nowUnixMs(),
527
+ currentPhase: "idle",
528
+ updatedAtUnixMs: deps.nowUnixMs(),
529
+ });
530
+ }
531
+ finally {
532
+ await initialContext.close();
533
+ }
534
+ await deps.runMiningLoop({
535
+ dataDir: options.dataDir,
536
+ databasePath: options.databasePath,
537
+ provider: options.runtime.provider,
538
+ paths: options.runtime.paths,
539
+ runMode: "background",
540
+ backgroundWorkerPid: deps.processPid,
541
+ backgroundWorkerRunId: options.runId,
542
+ signal: abortController.signal,
543
+ fetchImpl: options.fetchImpl,
544
+ openReadContext: options.runtime.openReadContext,
545
+ attachService: options.runtime.attachService,
546
+ rpcFactory: options.runtime.rpcFactory,
547
+ });
548
+ await deps.saveStopSnapshot({
549
+ dataDir: options.dataDir,
550
+ databasePath: options.databasePath,
551
+ provider: options.runtime.provider,
552
+ paths: options.runtime.paths,
553
+ runMode: "background",
554
+ backgroundWorkerPid: deps.processPid,
555
+ backgroundWorkerRunId: options.runId,
556
+ note: "Background mining worker stopped cleanly.",
557
+ });
558
+ }