@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.
- package/README.md +4 -5
- package/dist/bitcoind/progress/tty-renderer.js +3 -2
- package/dist/bitcoind/service.js +1 -1
- package/dist/cli/command-registry.d.ts +39 -0
- package/dist/cli/command-registry.js +1132 -0
- package/dist/cli/commands/client-admin.js +6 -56
- package/dist/cli/commands/mining-admin.js +9 -32
- package/dist/cli/commands/mining-read.js +15 -56
- package/dist/cli/commands/mining-runtime.js +258 -57
- package/dist/cli/commands/service-runtime.js +1 -64
- package/dist/cli/commands/status.js +2 -15
- package/dist/cli/commands/update.js +6 -21
- package/dist/cli/commands/wallet-admin.js +18 -120
- package/dist/cli/commands/wallet-mutation.js +4 -7
- package/dist/cli/commands/wallet-read.js +31 -138
- package/dist/cli/context.js +2 -4
- package/dist/cli/mining-format.js +8 -2
- package/dist/cli/mutation-command-groups.d.ts +11 -11
- package/dist/cli/mutation-command-groups.js +9 -18
- package/dist/cli/mutation-json.d.ts +1 -17
- package/dist/cli/mutation-json.js +1 -28
- package/dist/cli/mutation-success.d.ts +0 -1
- package/dist/cli/mutation-success.js +0 -19
- package/dist/cli/output.d.ts +1 -10
- package/dist/cli/output.js +52 -481
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +38 -695
- package/dist/cli/runner.js +28 -113
- package/dist/cli/types.d.ts +7 -8
- package/dist/cli/update-notifier.js +1 -1
- package/dist/cli/wallet-format.js +1 -1
- package/dist/wallet/lifecycle/managed-core.d.ts +23 -0
- package/dist/wallet/lifecycle/managed-core.js +257 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
- package/dist/wallet/lifecycle/repair-mining.js +304 -0
- package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
- package/dist/wallet/lifecycle/repair-runtime.js +206 -0
- package/dist/wallet/lifecycle/repair.d.ts +11 -0
- package/dist/wallet/lifecycle/repair.js +368 -0
- package/dist/wallet/lifecycle/setup.d.ts +16 -0
- package/dist/wallet/lifecycle/setup.js +430 -0
- package/dist/wallet/lifecycle/types.d.ts +125 -0
- package/dist/wallet/lifecycle/types.js +1 -0
- package/dist/wallet/lifecycle.d.ts +4 -165
- package/dist/wallet/lifecycle.js +3 -1656
- package/dist/wallet/mining/candidate.d.ts +60 -0
- package/dist/wallet/mining/candidate.js +290 -0
- package/dist/wallet/mining/competitiveness.d.ts +22 -0
- package/dist/wallet/mining/competitiveness.js +640 -0
- package/dist/wallet/mining/control.js +7 -251
- package/dist/wallet/mining/cycle.d.ts +39 -0
- package/dist/wallet/mining/cycle.js +542 -0
- package/dist/wallet/mining/engine-state.d.ts +66 -0
- package/dist/wallet/mining/engine-state.js +211 -0
- package/dist/wallet/mining/engine-types.d.ts +173 -0
- package/dist/wallet/mining/engine-types.js +1 -0
- package/dist/wallet/mining/engine-utils.d.ts +7 -0
- package/dist/wallet/mining/engine-utils.js +75 -0
- package/dist/wallet/mining/events.d.ts +2 -0
- package/dist/wallet/mining/events.js +19 -0
- package/dist/wallet/mining/lifecycle.d.ts +71 -0
- package/dist/wallet/mining/lifecycle.js +355 -0
- package/dist/wallet/mining/projection.d.ts +61 -0
- package/dist/wallet/mining/projection.js +319 -0
- package/dist/wallet/mining/publish.d.ts +79 -0
- package/dist/wallet/mining/publish.js +614 -0
- package/dist/wallet/mining/runner.d.ts +12 -418
- package/dist/wallet/mining/runner.js +274 -3433
- package/dist/wallet/mining/supervisor.d.ts +134 -0
- package/dist/wallet/mining/supervisor.js +558 -0
- package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
- package/dist/wallet/mining/visualizer-sync.js +166 -0
- package/dist/wallet/mining/visualizer.d.ts +1 -0
- package/dist/wallet/mining/visualizer.js +33 -18
- package/dist/wallet/reset.d.ts +1 -1
- package/dist/wallet/reset.js +35 -11
- package/dist/wallet/runtime.d.ts +0 -6
- package/dist/wallet/runtime.js +2 -38
- package/dist/wallet/tx/common.d.ts +18 -0
- package/dist/wallet/tx/common.js +40 -26
- package/package.json +1 -1
- package/dist/wallet/state/seed-index.d.ts +0 -43
- 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
|
+
}
|