@cogcoin/client 1.1.6 → 1.1.8

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 (125) hide show
  1. package/README.md +2 -2
  2. package/dist/bitcoind/indexer-daemon.js +29 -79
  3. package/dist/bitcoind/managed-runtime/bitcoind-runtime.d.ts +20 -0
  4. package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
  5. package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
  6. package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
  7. package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
  8. package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
  9. package/dist/bitcoind/managed-runtime/types.d.ts +40 -0
  10. package/dist/bitcoind/node.d.ts +2 -2
  11. package/dist/bitcoind/node.js +2 -2
  12. package/dist/bitcoind/rpc.d.ts +2 -1
  13. package/dist/bitcoind/rpc.js +53 -3
  14. package/dist/bitcoind/service.js +47 -127
  15. package/dist/cli/command-registry.d.ts +1 -1
  16. package/dist/cli/command-registry.js +2 -64
  17. package/dist/cli/commands/client-admin.js +3 -18
  18. package/dist/cli/commands/mining-runtime.js +4 -60
  19. package/dist/cli/commands/wallet-admin.js +6 -6
  20. package/dist/cli/context.js +1 -3
  21. package/dist/cli/mining-json.d.ts +1 -22
  22. package/dist/cli/mining-json.js +0 -23
  23. package/dist/cli/output.js +16 -2
  24. package/dist/cli/parse.js +0 -2
  25. package/dist/cli/preview-json.d.ts +1 -22
  26. package/dist/cli/preview-json.js +0 -19
  27. package/dist/cli/types.d.ts +1 -3
  28. package/dist/cli/wallet-format.js +1 -1
  29. package/dist/cli/workflow-hints.d.ts +1 -2
  30. package/dist/cli/workflow-hints.js +5 -8
  31. package/dist/wallet/lifecycle/context.js +0 -1
  32. package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
  33. package/dist/wallet/lifecycle/repair-mining.js +5 -39
  34. package/dist/wallet/lifecycle/repair.js +0 -3
  35. package/dist/wallet/lifecycle/setup.js +10 -8
  36. package/dist/wallet/lifecycle/types.d.ts +1 -4
  37. package/dist/wallet/managed-core-wallet.d.ts +2 -0
  38. package/dist/wallet/managed-core-wallet.js +27 -1
  39. package/dist/wallet/mining/candidate.d.ts +1 -0
  40. package/dist/wallet/mining/candidate.js +38 -6
  41. package/dist/wallet/mining/competitiveness.d.ts +1 -0
  42. package/dist/wallet/mining/competitiveness.js +6 -0
  43. package/dist/wallet/mining/cycle.d.ts +2 -0
  44. package/dist/wallet/mining/cycle.js +14 -4
  45. package/dist/wallet/mining/engine-state.js +10 -0
  46. package/dist/wallet/mining/engine-types.d.ts +1 -0
  47. package/dist/wallet/mining/index.d.ts +1 -1
  48. package/dist/wallet/mining/index.js +1 -1
  49. package/dist/wallet/mining/publish.d.ts +3 -0
  50. package/dist/wallet/mining/publish.js +78 -6
  51. package/dist/wallet/mining/runner.d.ts +0 -32
  52. package/dist/wallet/mining/runner.js +59 -104
  53. package/dist/wallet/mining/stop.d.ts +7 -0
  54. package/dist/wallet/mining/stop.js +23 -0
  55. package/dist/wallet/mining/supervisor.d.ts +2 -36
  56. package/dist/wallet/mining/supervisor.js +139 -246
  57. package/dist/wallet/mining/visualizer-sync.js +79 -15
  58. package/dist/wallet/read/context.d.ts +1 -5
  59. package/dist/wallet/read/context.js +21 -205
  60. package/dist/wallet/read/managed-services.d.ts +33 -0
  61. package/dist/wallet/read/managed-services.js +222 -0
  62. package/dist/wallet/reset/artifacts.d.ts +16 -0
  63. package/dist/wallet/reset/artifacts.js +141 -0
  64. package/dist/wallet/reset/execution.d.ts +38 -0
  65. package/dist/wallet/reset/execution.js +458 -0
  66. package/dist/wallet/reset/preflight.d.ts +7 -0
  67. package/dist/wallet/reset/preflight.js +116 -0
  68. package/dist/wallet/reset/preview.d.ts +2 -0
  69. package/dist/wallet/reset/preview.js +50 -0
  70. package/dist/wallet/reset/process-cleanup.d.ts +12 -0
  71. package/dist/wallet/reset/process-cleanup.js +179 -0
  72. package/dist/wallet/reset/types.d.ts +189 -0
  73. package/dist/wallet/reset/types.js +1 -0
  74. package/dist/wallet/reset.d.ts +4 -119
  75. package/dist/wallet/reset.js +4 -882
  76. package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
  77. package/dist/wallet/state/client-password/bootstrap.js +3 -0
  78. package/dist/wallet/state/client-password/context.d.ts +10 -0
  79. package/dist/wallet/state/client-password/context.js +46 -0
  80. package/dist/wallet/state/client-password/crypto.d.ts +34 -0
  81. package/dist/wallet/state/client-password/crypto.js +117 -0
  82. package/dist/wallet/state/client-password/files.d.ts +10 -0
  83. package/dist/wallet/state/client-password/files.js +109 -0
  84. package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
  85. package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
  86. package/dist/wallet/state/client-password/messages.d.ts +3 -0
  87. package/dist/wallet/state/client-password/messages.js +9 -0
  88. package/dist/wallet/state/client-password/migration.d.ts +4 -0
  89. package/dist/wallet/state/client-password/migration.js +32 -0
  90. package/dist/wallet/state/client-password/prompts.d.ts +12 -0
  91. package/dist/wallet/state/client-password/prompts.js +79 -0
  92. package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
  93. package/dist/wallet/state/client-password/protected-secrets.js +90 -0
  94. package/dist/wallet/state/client-password/readiness.d.ts +4 -0
  95. package/dist/wallet/state/client-password/readiness.js +48 -0
  96. package/dist/wallet/state/client-password/references.d.ts +1 -0
  97. package/dist/wallet/state/client-password/references.js +56 -0
  98. package/dist/wallet/state/client-password/rotation.d.ts +6 -0
  99. package/dist/wallet/state/client-password/rotation.js +98 -0
  100. package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
  101. package/dist/wallet/state/client-password/session-policy.js +28 -0
  102. package/dist/wallet/state/client-password/session.d.ts +19 -0
  103. package/dist/wallet/state/client-password/session.js +170 -0
  104. package/dist/wallet/state/client-password/setup.d.ts +8 -0
  105. package/dist/wallet/state/client-password/setup.js +49 -0
  106. package/dist/wallet/state/client-password/types.d.ts +82 -0
  107. package/dist/wallet/state/client-password/types.js +5 -0
  108. package/dist/wallet/state/client-password.d.ts +7 -38
  109. package/dist/wallet/state/client-password.js +52 -937
  110. package/dist/wallet/tx/anchor.js +123 -216
  111. package/dist/wallet/tx/cog.js +294 -489
  112. package/dist/wallet/tx/common.d.ts +2 -0
  113. package/dist/wallet/tx/common.js +2 -0
  114. package/dist/wallet/tx/domain-admin.js +111 -220
  115. package/dist/wallet/tx/domain-market.js +401 -681
  116. package/dist/wallet/tx/executor.d.ts +176 -0
  117. package/dist/wallet/tx/executor.js +302 -0
  118. package/dist/wallet/tx/field.js +109 -215
  119. package/dist/wallet/tx/register.js +158 -269
  120. package/dist/wallet/tx/reputation.js +120 -227
  121. package/package.json +1 -1
  122. package/dist/wallet/mining/worker-main.d.ts +0 -1
  123. package/dist/wallet/mining/worker-main.js +0 -17
  124. package/dist/wallet/state/client-password-agent.d.ts +0 -1
  125. package/dist/wallet/state/client-password-agent.js +0 -211
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import { rm } from "node:fs/promises";
3
2
  import { createRpcClient } from "../../bitcoind/node.js";
4
3
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
@@ -8,7 +7,6 @@ import { openWalletReadContext } from "../read/index.js";
8
7
  import type { WalletRuntimePaths } from "../runtime.js";
9
8
  import type { WalletSecretProvider } from "../state/provider.js";
10
9
  import { requestMiningGenerationPreemption } from "./coordination.js";
11
- import { inspectMiningControlPlane } from "./control.js";
12
10
  import { saveStopSnapshot } from "./lifecycle.js";
13
11
  import type { MiningRpcClient } from "./engine-types.js";
14
12
  import { loadMiningRuntimeStatus, saveMiningRuntimeStatus } from "./runtime-artifacts.js";
@@ -19,9 +17,8 @@ type AttachService = typeof attachOrStartManagedBitcoindService;
19
17
  type RpcFactory = (config: Parameters<typeof createRpcClient>[0]) => MiningRpcClient;
20
18
  type RequestMiningPreemption = typeof requestMiningGenerationPreemption;
21
19
  type SaveStopSnapshot = typeof saveStopSnapshot;
22
- type SpawnWorkerProcess = typeof spawn;
23
20
  type ProcessKill = typeof process.kill;
24
- type InspectMiningControlPlane = typeof inspectMiningControlPlane;
21
+ type ForceExit = (code: number) => never | void;
25
22
  interface MiningLoopRunnerOptions {
26
23
  dataDir: string;
27
24
  databasePath: string;
@@ -51,9 +48,7 @@ export interface MiningSupervisorRuntimeContext {
51
48
  interface MiningSupervisorDependencies {
52
49
  requestMiningPreemption: RequestMiningPreemption;
53
50
  saveStopSnapshot: SaveStopSnapshot;
54
- spawnWorkerProcess: SpawnWorkerProcess;
55
51
  runMiningLoop: RunMiningLoop;
56
- inspectMiningControlPlane: InspectMiningControlPlane;
57
52
  loadRuntimeStatus: typeof loadMiningRuntimeStatus;
58
53
  saveRuntimeStatus: typeof saveMiningRuntimeStatus;
59
54
  acquireLock: typeof acquireFileLock;
@@ -64,12 +59,7 @@ interface MiningSupervisorDependencies {
64
59
  nowUnixMs: () => number;
65
60
  processKill: ProcessKill;
66
61
  processPid: number;
67
- processExecPath: string;
68
- resolveWorkerMainPath: () => string;
69
- }
70
- export interface MiningSupervisorStartResult {
71
- started: boolean;
72
- snapshot: MiningRuntimeStatusV1 | null;
62
+ forceExit: ForceExit;
73
63
  }
74
64
  export interface MiningSupervisorTakeoverResult {
75
65
  controlLockCleared: boolean;
@@ -86,7 +76,6 @@ export declare function takeOverMiningRuntime(options: {
86
76
  shutdownGraceMs?: number;
87
77
  deps?: Partial<MiningSupervisorDependencies>;
88
78
  }): Promise<MiningSupervisorTakeoverResult>;
89
- export declare function waitForBackgroundHealthy(paths: WalletRuntimePaths, depsOverrides?: Partial<MiningSupervisorDependencies>): Promise<MiningRuntimeStatusV1 | null>;
90
79
  export declare function runForegroundMining(options: {
91
80
  dataDir: string;
92
81
  databasePath: string;
@@ -108,27 +97,4 @@ export declare function runForegroundMining(options: {
108
97
  runtime: MiningSupervisorRuntimeContext;
109
98
  deps?: Partial<MiningSupervisorDependencies>;
110
99
  }): 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
100
  export {};
@@ -1,21 +1,22 @@
1
- import { randomBytes } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
1
  import { rm } from "node:fs/promises";
4
2
  import { join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
3
  import { createRpcClient } from "../../bitcoind/node.js";
7
4
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
8
5
  import { FileLockBusyError, acquireFileLock, clearOrphanedFileLock, readLockMetadata, } from "../fs/lock.js";
9
6
  import { openWalletReadContext } from "../read/index.js";
7
+ import { destroyAllClientPasswordSessionsResolved } from "../state/client-password/session.js";
10
8
  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";
9
+ import { MINING_SHUTDOWN_GRACE_MS, } from "./constants.js";
13
10
  import { saveStopSnapshot } from "./lifecycle.js";
14
11
  import { loadMiningRuntimeStatus, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
12
+ import { createMiningStopRequestedError } from "./stop.js";
15
13
  import { MiningFollowVisualizer } from "./visualizer.js";
16
- const BACKGROUND_START_TIMEOUT_MS = 15_000;
17
14
  function sleep(ms, signal) {
18
15
  return new Promise((resolve) => {
16
+ if (signal?.aborted) {
17
+ resolve();
18
+ return;
19
+ }
19
20
  const timer = setTimeout(resolve, ms);
20
21
  signal?.addEventListener("abort", () => {
21
22
  clearTimeout(timer);
@@ -23,15 +24,23 @@ function sleep(ms, signal) {
23
24
  }, { once: true });
24
25
  });
25
26
  }
27
+ function createOneShotClientPasswordSessionDestroyer() {
28
+ let destroyed = false;
29
+ return () => {
30
+ if (destroyed) {
31
+ return;
32
+ }
33
+ destroyed = true;
34
+ destroyAllClientPasswordSessionsResolved();
35
+ };
36
+ }
26
37
  function resolveSupervisorDependencies(overrides = {}) {
27
38
  return {
28
39
  requestMiningPreemption: overrides.requestMiningPreemption ?? requestMiningGenerationPreemption,
29
40
  saveStopSnapshot: overrides.saveStopSnapshot ?? saveStopSnapshot,
30
- spawnWorkerProcess: overrides.spawnWorkerProcess ?? spawn,
31
41
  runMiningLoop: overrides.runMiningLoop ?? (() => {
32
42
  throw new Error("mining_supervisor_run_loop_missing");
33
43
  }),
34
- inspectMiningControlPlane: overrides.inspectMiningControlPlane ?? inspectMiningControlPlane,
35
44
  loadRuntimeStatus: overrides.loadRuntimeStatus ?? loadMiningRuntimeStatus,
36
45
  saveRuntimeStatus: overrides.saveRuntimeStatus ?? saveMiningRuntimeStatus,
37
46
  acquireLock: overrides.acquireLock ?? acquireFileLock,
@@ -42,11 +51,17 @@ function resolveSupervisorDependencies(overrides = {}) {
42
51
  nowUnixMs: overrides.nowUnixMs ?? Date.now,
43
52
  processKill: overrides.processKill ?? process.kill.bind(process),
44
53
  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))),
54
+ forceExit: overrides.forceExit ?? ((code) => {
55
+ process.exit(code);
56
+ }),
48
57
  };
49
58
  }
59
+ class ForegroundMiningShutdownTimeoutError extends Error {
60
+ constructor() {
61
+ super("mining_foreground_shutdown_timeout");
62
+ this.name = "ForegroundMiningShutdownTimeoutError";
63
+ }
64
+ }
50
65
  async function isProcessAlive(pid, deps) {
51
66
  if (pid === null) {
52
67
  return false;
@@ -294,265 +309,143 @@ async function acquireMiningStartControlLock(options) {
294
309
  }
295
310
  }
296
311
  }
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
312
  export async function runForegroundMining(options) {
312
313
  const deps = resolveSupervisorDependencies(options.deps);
314
+ const shutdownGraceMs = options.shutdownGraceMs ?? MINING_SHUTDOWN_GRACE_MS;
315
+ const usesExternalSignal = options.signal !== undefined;
313
316
  let visualizer = options.visualizer ?? null;
314
317
  const ownsVisualizer = visualizer === null;
318
+ const destroyClientPasswordSessions = createOneShotClientPasswordSessionDestroyer();
315
319
  const controlLock = await acquireMiningStartControlLock({
316
320
  paths: options.runtime.paths,
317
321
  purpose: "mine-foreground",
318
322
  takeoverReason: "mine-foreground-replace",
319
- shutdownGraceMs: options.shutdownGraceMs,
323
+ shutdownGraceMs,
320
324
  deps,
321
325
  });
322
326
  const abortController = new AbortController();
327
+ let stopRequested = false;
328
+ let shutdownDeadlineTimer = null;
329
+ let rejectShutdownDeadline = null;
330
+ let forceExitIssued = false;
331
+ const shutdownDeadline = new Promise((_, reject) => {
332
+ rejectShutdownDeadline = reject;
333
+ });
334
+ const requestStop = () => {
335
+ if (stopRequested) {
336
+ return;
337
+ }
338
+ stopRequested = true;
339
+ if (!abortController.signal.aborted) {
340
+ abortController.abort(createMiningStopRequestedError());
341
+ }
342
+ shutdownDeadlineTimer = setTimeout(() => {
343
+ rejectShutdownDeadline?.(new ForegroundMiningShutdownTimeoutError());
344
+ }, shutdownGraceMs);
345
+ shutdownDeadlineTimer.unref?.();
346
+ };
323
347
  const abortListener = () => {
324
- abortController.abort();
348
+ requestStop();
325
349
  };
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,
350
+ const handleSigint = () => {
351
+ requestStop();
352
+ };
353
+ const handleSigterm = () => {
354
+ requestStop();
355
+ };
356
+ const issueForcedExit = (code) => {
357
+ if (forceExitIssued) {
358
+ return;
359
+ }
360
+ forceExitIssued = true;
361
+ visualizer?.close();
362
+ destroyClientPasswordSessions();
363
+ return deps.forceExit(code);
364
+ };
365
+ const gracefulRun = async () => {
366
+ try {
367
+ await takeOverMiningRuntime({
368
+ paths: options.runtime.paths,
369
+ reason: "mine-foreground-replace",
370
+ shutdownGraceMs,
371
+ deps,
372
+ });
373
+ if (visualizer === null) {
374
+ visualizer = new MiningFollowVisualizer({
375
+ clientVersion: options.clientVersion,
376
+ updateAvailable: options.updateAvailable,
377
+ progressOutput: options.progressOutput ?? "auto",
378
+ stream: options.stderr,
379
+ });
380
+ }
381
+ await deps.runMiningLoop({
382
+ dataDir: options.dataDir,
383
+ databasePath: options.databasePath,
384
+ provider: options.runtime.provider,
385
+ paths: options.runtime.paths,
386
+ runMode: "foreground",
387
+ backgroundWorkerPid: null,
388
+ backgroundWorkerRunId: null,
389
+ signal: abortController.signal,
390
+ fetchImpl: options.fetchImpl,
391
+ openReadContext: options.runtime.openReadContext,
392
+ attachService: options.runtime.attachService,
393
+ rpcFactory: options.runtime.rpcFactory,
394
+ stdout: options.stdout,
395
+ visualizer,
396
+ });
397
+ await deps.saveStopSnapshot({
398
+ dataDir: options.dataDir,
399
+ databasePath: options.databasePath,
400
+ provider: options.runtime.provider,
401
+ paths: options.runtime.paths,
402
+ runMode: "foreground",
403
+ backgroundWorkerPid: null,
404
+ backgroundWorkerRunId: null,
405
+ note: "Foreground mining stopped cleanly.",
341
406
  });
342
407
  }
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();
408
+ finally {
409
+ if (ownsVisualizer) {
410
+ visualizer?.close();
411
+ }
412
+ await controlLock.release();
413
+ destroyClientPasswordSessions();
379
414
  }
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;
415
+ };
416
+ const gracefulRunPromise = gracefulRun();
388
417
  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
- });
418
+ if (options.signal?.aborted) {
419
+ requestStop();
420
+ }
421
+ else {
422
+ options.signal?.addEventListener("abort", abortListener, { once: true });
423
+ }
424
+ if (!usesExternalSignal) {
425
+ process.on("SIGINT", handleSigint);
426
+ process.on("SIGTERM", handleSigterm);
427
+ }
428
+ await Promise.race([
429
+ gracefulRunPromise,
430
+ shutdownDeadline,
431
+ ]);
396
432
  }
397
433
  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
- };
434
+ if (error instanceof ForegroundMiningShutdownTimeoutError) {
435
+ gracefulRunPromise.catch(() => undefined);
436
+ issueForcedExit(130);
437
+ return;
403
438
  }
404
439
  throw error;
405
440
  }
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
441
  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
- }
442
+ if (shutdownDeadlineTimer !== null) {
443
+ clearTimeout(shutdownDeadlineTimer);
474
444
  }
475
- finally {
476
- await preemption?.release().catch(() => undefined);
445
+ options.signal?.removeEventListener("abort", abortListener);
446
+ if (!usesExternalSignal) {
447
+ process.off("SIGINT", handleSigint);
448
+ process.off("SIGTERM", handleSigterm);
477
449
  }
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
450
  }
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
451
  }
@@ -4,6 +4,12 @@ import { FOLLOW_VISIBLE_PRIOR_BLOCKS } from "../../bitcoind/client/follow-block-
4
4
  import { buildMiningTipKey, resetMiningUiForTip } from "./engine-state.js";
5
5
  import { deriveMiningWordIndices, numberToSats, resolveBip39WordsFromIndices, } from "./engine-utils.js";
6
6
  import { createEmptyMiningFollowVisualizerState } from "./visualizer.js";
7
+ function cloneSettledBoardEntries(entries) {
8
+ return entries.map((entry) => ({
9
+ ...entry,
10
+ requiredWords: [...entry.requiredWords],
11
+ }));
12
+ }
7
13
  function resolveSettledWinnerRequiredWords(options) {
8
14
  const storedWords = resolveBip39WordsFromIndices(options.bip39WordIndices);
9
15
  if (storedWords.length > 0) {
@@ -20,6 +26,46 @@ function resolveSettledWinnerRequiredWords(options) {
20
26
  function fallbackSettledWinnerDomainName(domainId) {
21
27
  return `domain-${domainId}`;
22
28
  }
29
+ function resolveSettledBoardEntriesForHeight(options) {
30
+ if (options.snapshotState === null || options.snapshotState === undefined) {
31
+ return null;
32
+ }
33
+ const winners = getBlockWinners(options.snapshotState, options.blockHeight);
34
+ if (winners === null) {
35
+ return null;
36
+ }
37
+ const snapshotState = options.snapshotState;
38
+ return winners
39
+ .slice()
40
+ .sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
41
+ .slice(0, 5)
42
+ .map((winner) => ({
43
+ rank: winner.rank,
44
+ domainName: lookupDomainById(snapshotState, winner.domainId)?.name ?? fallbackSettledWinnerDomainName(winner.domainId),
45
+ sentence: winner.sentenceText ?? "[unavailable]",
46
+ requiredWords: resolveSettledWinnerRequiredWords({
47
+ domainId: winner.domainId,
48
+ bip39WordIndices: winner.bip39WordIndices,
49
+ snapshotTipPreviousHashHex: options.blockPreviousHashHex,
50
+ }),
51
+ }));
52
+ }
53
+ function resolveLatestPriorNonEmptySettledBoard(options) {
54
+ for (let blockHeight = options.snapshotTipHeight - 1; blockHeight >= 0; blockHeight -= 1) {
55
+ const settledBoardEntries = resolveSettledBoardEntriesForHeight({
56
+ snapshotState: options.snapshotState,
57
+ blockHeight,
58
+ blockPreviousHashHex: null,
59
+ });
60
+ if (settledBoardEntries !== null && settledBoardEntries.length > 0) {
61
+ return {
62
+ settledBlockHeight: blockHeight,
63
+ settledBoardEntries,
64
+ };
65
+ }
66
+ }
67
+ return null;
68
+ }
23
69
  function resolveCurrentMinedBlockBoard(options) {
24
70
  const settledBlockHeight = options.snapshotTipHeight ?? null;
25
71
  if (settledBlockHeight === null) {
@@ -34,23 +80,37 @@ function resolveCurrentMinedBlockBoard(options) {
34
80
  settledBoardEntries: [],
35
81
  };
36
82
  }
37
- const settledBoardEntries = (getBlockWinners(options.snapshotState, settledBlockHeight) ?? [])
38
- .slice()
39
- .sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
40
- .slice(0, 5)
41
- .map((winner) => ({
42
- rank: winner.rank,
43
- domainName: lookupDomainById(options.snapshotState, winner.domainId)?.name ?? fallbackSettledWinnerDomainName(winner.domainId),
44
- sentence: winner.sentenceText ?? "[unavailable]",
45
- requiredWords: resolveSettledWinnerRequiredWords({
46
- domainId: winner.domainId,
47
- bip39WordIndices: winner.bip39WordIndices,
48
- snapshotTipPreviousHashHex: options.snapshotTipPreviousHashHex,
49
- }),
50
- }));
83
+ const settledBoardEntries = resolveSettledBoardEntriesForHeight({
84
+ snapshotState: options.snapshotState,
85
+ blockHeight: settledBlockHeight,
86
+ blockPreviousHashHex: options.snapshotTipPreviousHashHex,
87
+ });
88
+ if (settledBoardEntries !== null) {
89
+ return {
90
+ settledBlockHeight,
91
+ settledBoardEntries,
92
+ };
93
+ }
94
+ const currentDisplayedBlockHeight = options.currentDisplayedBoard?.settledBlockHeight ?? null;
95
+ const currentDisplayedEntries = options.currentDisplayedBoard?.settledBoardEntries ?? [];
96
+ if (currentDisplayedBlockHeight !== null
97
+ && currentDisplayedBlockHeight <= settledBlockHeight
98
+ && currentDisplayedEntries.length > 0) {
99
+ return {
100
+ settledBlockHeight: currentDisplayedBlockHeight,
101
+ settledBoardEntries: cloneSettledBoardEntries(currentDisplayedEntries),
102
+ };
103
+ }
104
+ const latestPriorNonEmptyBoard = resolveLatestPriorNonEmptySettledBoard({
105
+ snapshotState: options.snapshotState,
106
+ snapshotTipHeight: settledBlockHeight,
107
+ });
108
+ if (latestPriorNonEmptyBoard !== null) {
109
+ return latestPriorNonEmptyBoard;
110
+ }
51
111
  return {
52
112
  settledBlockHeight,
53
- settledBoardEntries,
113
+ settledBoardEntries: [],
54
114
  };
55
115
  }
56
116
  export function resolveSettledBoard(options) {
@@ -66,6 +126,10 @@ function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight, s
66
126
  snapshotState,
67
127
  snapshotTipHeight,
68
128
  snapshotTipPreviousHashHex,
129
+ currentDisplayedBoard: {
130
+ settledBlockHeight: loopState.ui.settledBlockHeight,
131
+ settledBoardEntries: loopState.ui.settledBoardEntries,
132
+ },
69
133
  });
70
134
  loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
71
135
  loopState.ui.settledBoardEntries = settledBoard.settledBoardEntries;
@@ -1,6 +1,6 @@
1
1
  import { readSnapshotWithRetry } from "../../bitcoind/indexer-daemon.js";
2
2
  import { type WalletSecretProvider } from "../state/provider.js";
3
- import type { WalletBitcoindStatus, WalletLocalStateStatus, WalletNodeStatus, WalletReadContext, WalletServiceHealth } from "./types.js";
3
+ import type { WalletLocalStateStatus, WalletReadContext } from "./types.js";
4
4
  import type { WalletRuntimePaths } from "../runtime.js";
5
5
  declare function inspectWalletLocalState(options?: {
6
6
  dataDir?: string;
@@ -9,10 +9,6 @@ declare function inspectWalletLocalState(options?: {
9
9
  paths?: WalletRuntimePaths;
10
10
  walletControlLockHeld?: boolean;
11
11
  }): Promise<WalletLocalStateStatus>;
12
- export declare function deriveNodeHealthForTesting(status: WalletNodeStatus | null, bitcoindHealth: WalletBitcoindStatus["health"]): {
13
- health: WalletServiceHealth;
14
- message: string | null;
15
- };
16
12
  export declare function openWalletReadContext(options: {
17
13
  dataDir: string;
18
14
  databasePath: string;