@cogcoin/client 1.0.0 → 1.0.2
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 +2 -1
- package/dist/bitcoind/indexer-daemon.d.ts +3 -0
- package/dist/bitcoind/indexer-daemon.js +58 -8
- package/dist/bitcoind/retryable-rpc.js +3 -0
- package/dist/bitcoind/service.d.ts +1 -0
- package/dist/bitcoind/service.js +31 -9
- package/dist/cli/commands/mining-admin.js +9 -0
- package/dist/cli/commands/mining-runtime.js +114 -12
- package/dist/cli/commands/sync.js +1 -91
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/context.js +33 -1
- package/dist/cli/mining-format.js +28 -0
- package/dist/cli/mining-json.js +6 -0
- package/dist/cli/output.js +50 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +5 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/read-json.d.ts +13 -0
- package/dist/cli/read-json.js +17 -0
- package/dist/cli/runner.js +4 -0
- package/dist/cli/sync-progress.d.ts +6 -0
- package/dist/cli/sync-progress.js +91 -0
- package/dist/cli/types.d.ts +8 -2
- package/dist/cli/update-notifier.js +7 -222
- package/dist/cli/update-service.d.ts +44 -0
- package/dist/cli/update-service.js +218 -0
- package/dist/cli/wallet-format.js +3 -0
- package/dist/client/initialization.js +5 -0
- package/dist/wallet/lifecycle.d.ts +10 -0
- package/dist/wallet/lifecycle.js +6 -0
- package/dist/wallet/mining/config.js +13 -3
- package/dist/wallet/mining/control.d.ts +2 -1
- package/dist/wallet/mining/control.js +143 -19
- package/dist/wallet/mining/index.d.ts +2 -2
- package/dist/wallet/mining/index.js +1 -1
- package/dist/wallet/mining/provider-model.d.ts +30 -0
- package/dist/wallet/mining/provider-model.js +134 -0
- package/dist/wallet/mining/runner.d.ts +105 -3
- package/dist/wallet/mining/runner.js +490 -88
- package/dist/wallet/mining/runtime-artifacts.js +1 -0
- package/dist/wallet/mining/sentences.d.ts +2 -2
- package/dist/wallet/mining/sentences.js +25 -2
- package/dist/wallet/mining/types.d.ts +9 -1
- package/dist/wallet/mining/visualizer.js +28 -5
- package/dist/wallet/read/context.js +3 -0
- package/dist/wallet/reset.js +1 -0
- package/dist/wallet/tx/anchor.js +1 -0
- package/dist/wallet/tx/bitcoin-transfer.js +1 -0
- package/dist/wallet/tx/cog.js +3 -0
- package/dist/wallet/tx/domain-admin.js +1 -0
- package/dist/wallet/tx/domain-market.js +3 -0
- package/dist/wallet/tx/field.js +1 -0
- package/dist/wallet/tx/register.js +1 -0
- package/dist/wallet/tx/reputation.js +1 -0
- package/package.json +3 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { getBalance, getBlockWinners, lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
|
|
5
7
|
import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
|
|
@@ -9,8 +11,8 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
|
9
11
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
10
12
|
import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
|
|
11
13
|
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
12
|
-
import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
13
|
-
import { acquireFileLock } from "../fs/lock.js";
|
|
14
|
+
import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, isInsufficientFundsError, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
15
|
+
import { FileLockBusyError, acquireFileLock, clearOrphanedFileLock, readLockMetadata, } from "../fs/lock.js";
|
|
14
16
|
import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
|
|
15
17
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
16
18
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
@@ -19,13 +21,16 @@ import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, }
|
|
|
19
21
|
import { loadClientConfig } from "./config.js";
|
|
20
22
|
import { MINING_LOOP_INTERVAL_MS, MINING_NETWORK_SETTLE_WINDOW_MS, MINING_PROVIDER_BACKOFF_BASE_MS, MINING_PROVIDER_BACKOFF_MAX_MS, MINING_SHUTDOWN_GRACE_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS, MINING_SUSPEND_GAP_THRESHOLD_MS, MINING_TIP_SETTLE_WINDOW_MS, MINING_WORKER_API_VERSION, } from "./constants.js";
|
|
21
23
|
import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
|
|
22
|
-
import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
|
|
24
|
+
import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningGenerationActivity, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
|
|
23
25
|
import { clearMiningPublishState, miningPublishIsInMempool, miningPublishMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
|
|
24
26
|
import { createMiningSentenceRequestLimits } from "./sentence-protocol.js";
|
|
25
27
|
import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
|
|
26
28
|
import { createEmptyMiningFollowVisualizerState, MiningFollowVisualizer, } from "./visualizer.js";
|
|
27
29
|
const BEST_BLOCK_POLL_INTERVAL_MS = 500;
|
|
28
30
|
const BACKGROUND_START_TIMEOUT_MS = 15_000;
|
|
31
|
+
function resolveSnapshotOverride(override, fallback) {
|
|
32
|
+
return override === undefined ? fallback : override;
|
|
33
|
+
}
|
|
29
34
|
class MiningSuspendDetectedError extends Error {
|
|
30
35
|
detectedAtUnixMs;
|
|
31
36
|
constructor(detectedAtUnixMs) {
|
|
@@ -88,6 +93,239 @@ async function isProcessAlive(pid) {
|
|
|
88
93
|
return true;
|
|
89
94
|
}
|
|
90
95
|
}
|
|
96
|
+
function normalizeMiningPid(value) {
|
|
97
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
98
|
+
? value
|
|
99
|
+
: null;
|
|
100
|
+
}
|
|
101
|
+
function resolveMiningGenerationRequestPath(paths) {
|
|
102
|
+
return join(paths.miningRoot, "generation-request.json");
|
|
103
|
+
}
|
|
104
|
+
function resolveMiningGenerationActivityPath(paths) {
|
|
105
|
+
return join(paths.miningRoot, "generation-activity.json");
|
|
106
|
+
}
|
|
107
|
+
function createTakeoverStoppedMiningNote(livePublishInMempool) {
|
|
108
|
+
return livePublishInMempool
|
|
109
|
+
? "Mining runtime replaced. The last mining transaction may still confirm from mempool."
|
|
110
|
+
: "Mining runtime replaced.";
|
|
111
|
+
}
|
|
112
|
+
function createStoppedMiningRuntimeSnapshotForTakeover(options) {
|
|
113
|
+
const note = createTakeoverStoppedMiningNote(options.snapshot?.livePublishInMempool);
|
|
114
|
+
if (options.snapshot !== null) {
|
|
115
|
+
return {
|
|
116
|
+
...options.snapshot,
|
|
117
|
+
updatedAtUnixMs: options.nowUnixMs,
|
|
118
|
+
runMode: "stopped",
|
|
119
|
+
backgroundWorkerPid: null,
|
|
120
|
+
backgroundWorkerRunId: null,
|
|
121
|
+
backgroundWorkerHeartbeatAtUnixMs: null,
|
|
122
|
+
backgroundWorkerHealth: null,
|
|
123
|
+
currentPhase: "idle",
|
|
124
|
+
note,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
walletRootId: options.walletRootId,
|
|
130
|
+
workerApiVersion: null,
|
|
131
|
+
workerBinaryVersion: null,
|
|
132
|
+
workerBuildId: null,
|
|
133
|
+
updatedAtUnixMs: options.nowUnixMs,
|
|
134
|
+
runMode: "stopped",
|
|
135
|
+
backgroundWorkerPid: null,
|
|
136
|
+
backgroundWorkerRunId: null,
|
|
137
|
+
backgroundWorkerHeartbeatAtUnixMs: null,
|
|
138
|
+
backgroundWorkerHealth: null,
|
|
139
|
+
indexerDaemonState: null,
|
|
140
|
+
indexerDaemonInstanceId: null,
|
|
141
|
+
indexerSnapshotSeq: null,
|
|
142
|
+
indexerSnapshotOpenedAtUnixMs: null,
|
|
143
|
+
indexerTruthSource: undefined,
|
|
144
|
+
indexerHeartbeatAtUnixMs: null,
|
|
145
|
+
coreBestHeight: null,
|
|
146
|
+
coreBestHash: null,
|
|
147
|
+
indexerTipHeight: null,
|
|
148
|
+
indexerTipHash: null,
|
|
149
|
+
indexerReorgDepth: null,
|
|
150
|
+
indexerTipAligned: null,
|
|
151
|
+
corePublishState: null,
|
|
152
|
+
providerState: null,
|
|
153
|
+
lastSuspendDetectedAtUnixMs: null,
|
|
154
|
+
reconnectSettledUntilUnixMs: null,
|
|
155
|
+
tipSettledUntilUnixMs: null,
|
|
156
|
+
miningState: "idle",
|
|
157
|
+
currentPhase: "idle",
|
|
158
|
+
currentPublishState: "none",
|
|
159
|
+
targetBlockHeight: null,
|
|
160
|
+
referencedBlockHashDisplay: null,
|
|
161
|
+
currentDomainId: null,
|
|
162
|
+
currentDomainName: null,
|
|
163
|
+
currentSentenceDisplay: null,
|
|
164
|
+
currentCanonicalBlend: null,
|
|
165
|
+
currentTxid: null,
|
|
166
|
+
currentWtxid: null,
|
|
167
|
+
livePublishInMempool: null,
|
|
168
|
+
currentFeeRateSatVb: null,
|
|
169
|
+
currentAbsoluteFeeSats: null,
|
|
170
|
+
currentBlockFeeSpentSats: "0",
|
|
171
|
+
sessionFeeSpentSats: "0",
|
|
172
|
+
lifetimeFeeSpentSats: "0",
|
|
173
|
+
sameDomainCompetitorSuppressed: null,
|
|
174
|
+
higherRankedCompetitorDomainCount: null,
|
|
175
|
+
dedupedCompetitorDomainCount: null,
|
|
176
|
+
competitivenessGateIndeterminate: null,
|
|
177
|
+
mempoolSequenceCacheStatus: null,
|
|
178
|
+
currentPublishDecision: null,
|
|
179
|
+
lastMempoolSequence: null,
|
|
180
|
+
lastCompetitivenessGateAtUnixMs: null,
|
|
181
|
+
pauseReason: null,
|
|
182
|
+
providerConfigured: false,
|
|
183
|
+
providerKind: null,
|
|
184
|
+
bitcoindHealth: "unavailable",
|
|
185
|
+
bitcoindServiceState: null,
|
|
186
|
+
bitcoindReplicaStatus: null,
|
|
187
|
+
nodeHealth: "unavailable",
|
|
188
|
+
indexerHealth: "unavailable",
|
|
189
|
+
tipsAligned: null,
|
|
190
|
+
lastEventAtUnixMs: null,
|
|
191
|
+
lastError: null,
|
|
192
|
+
note,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function waitForMiningProcessExit(pid, timeoutMs, sleepImpl = sleep) {
|
|
196
|
+
const deadline = Date.now() + timeoutMs;
|
|
197
|
+
while (Date.now() < deadline) {
|
|
198
|
+
if (!await isProcessAlive(pid)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
await sleepImpl(Math.min(250, Math.max(timeoutMs, 1)));
|
|
202
|
+
}
|
|
203
|
+
return !await isProcessAlive(pid);
|
|
204
|
+
}
|
|
205
|
+
async function terminateMiningRuntimePid(options) {
|
|
206
|
+
if (!await isProcessAlive(options.pid)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
process.kill(options.pid, "SIGTERM");
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
process.kill(options.pid, "SIGKILL");
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
throw new Error("mining_process_stop_timeout");
|
|
232
|
+
}
|
|
233
|
+
async function takeOverMiningRuntime(options) {
|
|
234
|
+
const snapshot = await loadMiningRuntimeStatus(options.paths.miningStatusPath).catch(() => null);
|
|
235
|
+
const controlLockMetadata = options.controlLockMetadata ?? (options.clearControlLockFile === true
|
|
236
|
+
? await readLockMetadata(options.paths.miningControlLockPath).catch(() => null)
|
|
237
|
+
: null);
|
|
238
|
+
const generationActivity = await readMiningGenerationActivity(options.paths).catch(() => null);
|
|
239
|
+
const shutdownGraceMs = options.shutdownGraceMs ?? MINING_SHUTDOWN_GRACE_MS;
|
|
240
|
+
const requestPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
|
|
241
|
+
const controlLockPid = normalizeMiningPid(controlLockMetadata?.processId);
|
|
242
|
+
const backgroundWorkerPid = normalizeMiningPid(snapshot?.backgroundWorkerPid);
|
|
243
|
+
const generationOwnerPid = normalizeMiningPid(generationActivity?.generationOwnerPid);
|
|
244
|
+
const terminatedPids = [];
|
|
245
|
+
const discoveredPids = new Set();
|
|
246
|
+
for (const pid of [controlLockPid, backgroundWorkerPid, generationOwnerPid]) {
|
|
247
|
+
if (pid === null
|
|
248
|
+
|| pid === process.pid
|
|
249
|
+
|| discoveredPids.has(pid)
|
|
250
|
+
|| !await isProcessAlive(pid)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
discoveredPids.add(pid);
|
|
254
|
+
}
|
|
255
|
+
const shouldPreemptGeneration = discoveredPids.size > 0 && (generationActivity?.generationActive === true
|
|
256
|
+
|| snapshot?.currentPhase === "generating"
|
|
257
|
+
|| snapshot?.currentPhase === "scoring");
|
|
258
|
+
const preemption = shouldPreemptGeneration
|
|
259
|
+
? await requestPreemption({
|
|
260
|
+
paths: options.paths,
|
|
261
|
+
reason: options.reason,
|
|
262
|
+
timeoutMs: Math.min(shutdownGraceMs, 15_000),
|
|
263
|
+
}).catch(() => null)
|
|
264
|
+
: null;
|
|
265
|
+
try {
|
|
266
|
+
for (const pid of discoveredPids) {
|
|
267
|
+
if (await terminateMiningRuntimePid({
|
|
268
|
+
pid,
|
|
269
|
+
shutdownGraceMs,
|
|
270
|
+
sleepImpl: options.sleepImpl,
|
|
271
|
+
})) {
|
|
272
|
+
terminatedPids.push(pid);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
await preemption?.release().catch(() => undefined);
|
|
278
|
+
}
|
|
279
|
+
const controlLockCleared = options.clearControlLockFile === true
|
|
280
|
+
? await clearOrphanedFileLock(options.paths.miningControlLockPath, isProcessAlive).catch(() => false)
|
|
281
|
+
: false;
|
|
282
|
+
await rm(resolveMiningGenerationRequestPath(options.paths), { force: true }).catch(() => undefined);
|
|
283
|
+
await rm(resolveMiningGenerationActivityPath(options.paths), { force: true }).catch(() => undefined);
|
|
284
|
+
const walletRootId = snapshot?.walletRootId
|
|
285
|
+
?? (typeof controlLockMetadata?.walletRootId === "string" ? controlLockMetadata.walletRootId : null);
|
|
286
|
+
if (snapshot !== null || walletRootId !== null || terminatedPids.length > 0 || controlLockCleared) {
|
|
287
|
+
await saveMiningRuntimeStatus(options.paths.miningStatusPath, createStoppedMiningRuntimeSnapshotForTakeover({
|
|
288
|
+
snapshot,
|
|
289
|
+
walletRootId,
|
|
290
|
+
nowUnixMs: Date.now(),
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
controlLockCleared,
|
|
295
|
+
replaced: terminatedPids.length > 0,
|
|
296
|
+
snapshot,
|
|
297
|
+
terminatedPids,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function acquireMiningStartControlLock(options) {
|
|
301
|
+
while (true) {
|
|
302
|
+
try {
|
|
303
|
+
return await acquireFileLock(options.paths.miningControlLockPath, {
|
|
304
|
+
purpose: options.purpose,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (!(error instanceof FileLockBusyError)) {
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
if (error.existingMetadata?.processId === process.pid) {
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
const takeover = await takeOverMiningRuntime({
|
|
315
|
+
paths: options.paths,
|
|
316
|
+
reason: options.takeoverReason,
|
|
317
|
+
clearControlLockFile: true,
|
|
318
|
+
controlLockMetadata: error.existingMetadata,
|
|
319
|
+
requestMiningPreemption: options.requestMiningPreemption,
|
|
320
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
321
|
+
sleepImpl: options.sleepImpl,
|
|
322
|
+
});
|
|
323
|
+
if (!takeover.replaced && !takeover.controlLockCleared) {
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
91
329
|
function writeStdout(stream, line) {
|
|
92
330
|
if (stream === undefined) {
|
|
93
331
|
return;
|
|
@@ -232,7 +470,7 @@ function fallbackSettledWinnerDomainName(domainId) {
|
|
|
232
470
|
return `domain-${domainId}`;
|
|
233
471
|
}
|
|
234
472
|
function resolveCurrentMinedBlockBoard(options) {
|
|
235
|
-
const settledBlockHeight = options.
|
|
473
|
+
const settledBlockHeight = options.snapshotTipHeight ?? null;
|
|
236
474
|
if (settledBlockHeight === null) {
|
|
237
475
|
return {
|
|
238
476
|
settledBlockHeight,
|
|
@@ -245,12 +483,6 @@ function resolveCurrentMinedBlockBoard(options) {
|
|
|
245
483
|
settledBoardEntries: [],
|
|
246
484
|
};
|
|
247
485
|
}
|
|
248
|
-
if (options.nodeBestHeight !== null && (options.snapshotTipHeight ?? -1) < options.nodeBestHeight) {
|
|
249
|
-
return {
|
|
250
|
-
settledBlockHeight,
|
|
251
|
-
settledBoardEntries: [],
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
486
|
const settledBoardEntries = (getBlockWinners(options.snapshotState, settledBlockHeight) ?? [])
|
|
255
487
|
.slice()
|
|
256
488
|
.sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
|
|
@@ -268,15 +500,33 @@ function resolveCurrentMinedBlockBoard(options) {
|
|
|
268
500
|
export function resolveSettledBoardForTesting(options) {
|
|
269
501
|
return resolveCurrentMinedBlockBoard(options);
|
|
270
502
|
}
|
|
271
|
-
function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight
|
|
503
|
+
function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight) {
|
|
272
504
|
const settledBoard = resolveCurrentMinedBlockBoard({
|
|
273
505
|
snapshotState,
|
|
274
506
|
snapshotTipHeight,
|
|
275
|
-
nodeBestHeight,
|
|
507
|
+
nodeBestHeight: null,
|
|
276
508
|
});
|
|
277
509
|
loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
|
|
278
510
|
loopState.ui.settledBoardEntries = settledBoard.settledBoardEntries;
|
|
279
511
|
}
|
|
512
|
+
function syncMiningUiForCurrentTip(options) {
|
|
513
|
+
const targetBlockHeight = options.nodeBestHeight === null
|
|
514
|
+
? null
|
|
515
|
+
: options.nodeBestHeight + 1;
|
|
516
|
+
const tipKey = buildMiningTipKey(options.nodeBestHash, targetBlockHeight);
|
|
517
|
+
if (tipKey !== options.loopState.currentTipKey) {
|
|
518
|
+
options.loopState.currentTipKey = tipKey;
|
|
519
|
+
resetMiningUiForTip(options.loopState, targetBlockHeight);
|
|
520
|
+
if (options.recentWin !== null) {
|
|
521
|
+
options.loopState.ui.recentWin = options.recentWin;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
syncMiningUiSettledBoard(options.loopState, options.snapshotState, options.snapshotTipHeight);
|
|
525
|
+
return {
|
|
526
|
+
targetBlockHeight,
|
|
527
|
+
tipKey,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
280
530
|
function setMiningUiCandidate(loopState, candidate) {
|
|
281
531
|
loopState.ui.latestSentence = candidate.sentence;
|
|
282
532
|
loopState.ui.provisionalRequiredWords = [...candidate.bip39Words];
|
|
@@ -348,6 +598,21 @@ function syncMiningVisualizerBalances(loopState, readContext, balanceSats) {
|
|
|
348
598
|
: getBalance(readContext.snapshot.state, readContext.localState.state.funding.scriptPubKeyHex);
|
|
349
599
|
loopState.ui.balanceSats = balanceSats;
|
|
350
600
|
}
|
|
601
|
+
function createIndexedMiningFollowVisualizerState(readContext) {
|
|
602
|
+
const uiState = createEmptyMiningFollowVisualizerState();
|
|
603
|
+
const localState = readContext.localState;
|
|
604
|
+
const settledBoard = resolveCurrentMinedBlockBoard({
|
|
605
|
+
snapshotState: readContext.snapshot?.state ?? null,
|
|
606
|
+
snapshotTipHeight: readContext.snapshot?.tip?.height ?? readContext.indexer.snapshotTip?.height ?? null,
|
|
607
|
+
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
608
|
+
});
|
|
609
|
+
uiState.settledBlockHeight = settledBoard.settledBlockHeight;
|
|
610
|
+
uiState.settledBoardEntries = settledBoard.settledBoardEntries;
|
|
611
|
+
if (readContext.snapshot !== null && localState.availability === "ready" && localState.state !== null) {
|
|
612
|
+
uiState.balanceCogtoshi = getBalance(readContext.snapshot.state, localState.state.funding.scriptPubKeyHex);
|
|
613
|
+
}
|
|
614
|
+
return uiState;
|
|
615
|
+
}
|
|
351
616
|
function syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight) {
|
|
352
617
|
loopState.ui.visibleBlockTimesByHeight = { ...blockTimesByHeight };
|
|
353
618
|
}
|
|
@@ -642,28 +907,71 @@ async function resolveOverlayAuthorizedMiningDomain(options) {
|
|
|
642
907
|
function buildStatusSnapshot(view, overrides = {}) {
|
|
643
908
|
return {
|
|
644
909
|
...view.runtime,
|
|
645
|
-
runMode: overrides.runMode
|
|
646
|
-
backgroundWorkerPid: overrides.backgroundWorkerPid
|
|
647
|
-
backgroundWorkerRunId: overrides.backgroundWorkerRunId
|
|
648
|
-
backgroundWorkerHeartbeatAtUnixMs: overrides.backgroundWorkerHeartbeatAtUnixMs
|
|
649
|
-
currentPhase: overrides.currentPhase
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
910
|
+
runMode: resolveSnapshotOverride(overrides.runMode, view.runtime.runMode),
|
|
911
|
+
backgroundWorkerPid: resolveSnapshotOverride(overrides.backgroundWorkerPid, view.runtime.backgroundWorkerPid),
|
|
912
|
+
backgroundWorkerRunId: resolveSnapshotOverride(overrides.backgroundWorkerRunId, view.runtime.backgroundWorkerRunId),
|
|
913
|
+
backgroundWorkerHeartbeatAtUnixMs: resolveSnapshotOverride(overrides.backgroundWorkerHeartbeatAtUnixMs, view.runtime.backgroundWorkerHeartbeatAtUnixMs),
|
|
914
|
+
currentPhase: resolveSnapshotOverride(overrides.currentPhase, view.runtime.currentPhase),
|
|
915
|
+
currentPublishState: resolveSnapshotOverride(overrides.currentPublishState, view.runtime.currentPublishState),
|
|
916
|
+
targetBlockHeight: resolveSnapshotOverride(overrides.targetBlockHeight, view.runtime.targetBlockHeight),
|
|
917
|
+
referencedBlockHashDisplay: resolveSnapshotOverride(overrides.referencedBlockHashDisplay, view.runtime.referencedBlockHashDisplay),
|
|
918
|
+
currentDomainId: resolveSnapshotOverride(overrides.currentDomainId, view.runtime.currentDomainId),
|
|
919
|
+
currentDomainName: resolveSnapshotOverride(overrides.currentDomainName, view.runtime.currentDomainName),
|
|
920
|
+
currentSentenceDisplay: resolveSnapshotOverride(overrides.currentSentenceDisplay, view.runtime.currentSentenceDisplay),
|
|
921
|
+
currentCanonicalBlend: resolveSnapshotOverride(overrides.currentCanonicalBlend, view.runtime.currentCanonicalBlend),
|
|
922
|
+
currentTxid: resolveSnapshotOverride(overrides.currentTxid, view.runtime.currentTxid),
|
|
923
|
+
currentWtxid: resolveSnapshotOverride(overrides.currentWtxid, view.runtime.currentWtxid),
|
|
924
|
+
currentFeeRateSatVb: resolveSnapshotOverride(overrides.currentFeeRateSatVb, view.runtime.currentFeeRateSatVb),
|
|
925
|
+
currentAbsoluteFeeSats: resolveSnapshotOverride(overrides.currentAbsoluteFeeSats, view.runtime.currentAbsoluteFeeSats),
|
|
926
|
+
currentBlockFeeSpentSats: resolveSnapshotOverride(overrides.currentBlockFeeSpentSats, view.runtime.currentBlockFeeSpentSats),
|
|
927
|
+
lastSuspendDetectedAtUnixMs: resolveSnapshotOverride(overrides.lastSuspendDetectedAtUnixMs, view.runtime.lastSuspendDetectedAtUnixMs),
|
|
928
|
+
providerState: resolveSnapshotOverride(overrides.providerState, view.runtime.providerState),
|
|
929
|
+
corePublishState: resolveSnapshotOverride(overrides.corePublishState, view.runtime.corePublishState),
|
|
930
|
+
currentPublishDecision: resolveSnapshotOverride(overrides.currentPublishDecision, view.runtime.currentPublishDecision),
|
|
931
|
+
sameDomainCompetitorSuppressed: resolveSnapshotOverride(overrides.sameDomainCompetitorSuppressed, view.runtime.sameDomainCompetitorSuppressed),
|
|
932
|
+
higherRankedCompetitorDomainCount: resolveSnapshotOverride(overrides.higherRankedCompetitorDomainCount, view.runtime.higherRankedCompetitorDomainCount),
|
|
933
|
+
dedupedCompetitorDomainCount: resolveSnapshotOverride(overrides.dedupedCompetitorDomainCount, view.runtime.dedupedCompetitorDomainCount),
|
|
934
|
+
competitivenessGateIndeterminate: resolveSnapshotOverride(overrides.competitivenessGateIndeterminate, view.runtime.competitivenessGateIndeterminate),
|
|
935
|
+
mempoolSequenceCacheStatus: resolveSnapshotOverride(overrides.mempoolSequenceCacheStatus, view.runtime.mempoolSequenceCacheStatus),
|
|
936
|
+
lastMempoolSequence: resolveSnapshotOverride(overrides.lastMempoolSequence, view.runtime.lastMempoolSequence),
|
|
937
|
+
lastCompetitivenessGateAtUnixMs: resolveSnapshotOverride(overrides.lastCompetitivenessGateAtUnixMs, view.runtime.lastCompetitivenessGateAtUnixMs),
|
|
938
|
+
lastError: resolveSnapshotOverride(overrides.lastError, view.runtime.lastError),
|
|
939
|
+
note: resolveSnapshotOverride(overrides.note, view.runtime.note),
|
|
940
|
+
livePublishInMempool: resolveSnapshotOverride(overrides.livePublishInMempool, view.runtime.livePublishInMempool),
|
|
664
941
|
updatedAtUnixMs: Date.now(),
|
|
665
942
|
};
|
|
666
943
|
}
|
|
944
|
+
function buildPrePublishStatusOverrides(options) {
|
|
945
|
+
const replacing = options.state.miningState.currentTxid !== null;
|
|
946
|
+
const replacingAcrossTips = replacing && !livePublishTargetsCandidateTip({
|
|
947
|
+
liveState: options.state.miningState,
|
|
948
|
+
candidate: options.candidate,
|
|
949
|
+
});
|
|
950
|
+
return {
|
|
951
|
+
currentPhase: replacing ? "replacing" : "publishing",
|
|
952
|
+
currentPublishDecision: replacing ? "replacing" : "publishing",
|
|
953
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
954
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
955
|
+
currentDomainId: options.candidate.domainId,
|
|
956
|
+
currentDomainName: options.candidate.domainName,
|
|
957
|
+
currentSentenceDisplay: options.candidate.sentence,
|
|
958
|
+
currentCanonicalBlend: options.candidate.canonicalBlend.toString(),
|
|
959
|
+
note: replacing
|
|
960
|
+
? "Replacing the live mining transaction for the current tip."
|
|
961
|
+
: "Broadcasting the best mining candidate for the current tip.",
|
|
962
|
+
...(replacingAcrossTips
|
|
963
|
+
? {
|
|
964
|
+
currentPublishState: "none",
|
|
965
|
+
currentTxid: null,
|
|
966
|
+
currentWtxid: null,
|
|
967
|
+
livePublishInMempool: false,
|
|
968
|
+
currentFeeRateSatVb: null,
|
|
969
|
+
currentAbsoluteFeeSats: null,
|
|
970
|
+
currentBlockFeeSpentSats: "0",
|
|
971
|
+
}
|
|
972
|
+
: {}),
|
|
973
|
+
};
|
|
974
|
+
}
|
|
667
975
|
async function refreshAndSaveStatus(options) {
|
|
668
976
|
const view = await inspectMiningControlPlane({
|
|
669
977
|
provider: options.provider,
|
|
@@ -705,6 +1013,7 @@ async function handleDetectedMiningRuntimeResume(options) {
|
|
|
705
1013
|
note: "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.",
|
|
706
1014
|
},
|
|
707
1015
|
visualizer: options.visualizer,
|
|
1016
|
+
visualizerState: createIndexedMiningFollowVisualizerState(readContext),
|
|
708
1017
|
});
|
|
709
1018
|
}
|
|
710
1019
|
finally {
|
|
@@ -897,6 +1206,12 @@ function createStaleMiningCandidateWaitingNote() {
|
|
|
897
1206
|
function createRetryableMiningPublishWaitingNote() {
|
|
898
1207
|
return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
|
|
899
1208
|
}
|
|
1209
|
+
function createInsufficientFundsMiningPublishWaitingNote() {
|
|
1210
|
+
return "Mining is waiting for enough confirmed safe BTC funding that Bitcoin Core can use for the next publish.";
|
|
1211
|
+
}
|
|
1212
|
+
function createInsufficientFundsMiningPublishErrorMessage() {
|
|
1213
|
+
return "Bitcoin Core could not fund the next mining publish with confirmed safe BTC.";
|
|
1214
|
+
}
|
|
900
1215
|
async function generateCandidatesForDomains(options) {
|
|
901
1216
|
const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
|
|
902
1217
|
if (bestBlockHash === null || bestBlockHash === undefined) {
|
|
@@ -1524,6 +1839,7 @@ async function publishCandidateOnce(options) {
|
|
|
1524
1839
|
dataDir: options.dataDir,
|
|
1525
1840
|
chain: "main",
|
|
1526
1841
|
startHeight: 0,
|
|
1842
|
+
serviceLifetime: "ephemeral",
|
|
1527
1843
|
walletRootId: options.readContext.localState.state.walletRootId,
|
|
1528
1844
|
});
|
|
1529
1845
|
const rpc = options.rpcFactory(service.rpc);
|
|
@@ -1801,6 +2117,29 @@ async function publishCandidate(options) {
|
|
|
1801
2117
|
candidate: refreshedCandidate,
|
|
1802
2118
|
};
|
|
1803
2119
|
}
|
|
2120
|
+
if (isInsufficientFundsError(error)) {
|
|
2121
|
+
const note = createInsufficientFundsMiningPublishWaitingNote();
|
|
2122
|
+
const lastError = createInsufficientFundsMiningPublishErrorMessage();
|
|
2123
|
+
await appendEventFn(options.paths, createEvent("publish-paused-insufficient-funds", "Paused mining publish because Bitcoin Core could not fund the next mining transaction with confirmed safe BTC.", {
|
|
2124
|
+
level: "warn",
|
|
2125
|
+
runId: options.runId,
|
|
2126
|
+
targetBlockHeight: refreshedCandidate.targetBlockHeight,
|
|
2127
|
+
referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
|
|
2128
|
+
domainId: refreshedCandidate.domainId,
|
|
2129
|
+
domainName: refreshedCandidate.domainName,
|
|
2130
|
+
score: refreshedCandidate.canonicalBlend.toString(),
|
|
2131
|
+
reason: "insufficient-funds",
|
|
2132
|
+
}));
|
|
2133
|
+
return {
|
|
2134
|
+
state: readyReadContext.localState.state,
|
|
2135
|
+
txid: null,
|
|
2136
|
+
decision: "publish-paused-insufficient-funds",
|
|
2137
|
+
note,
|
|
2138
|
+
lastError,
|
|
2139
|
+
skipped: true,
|
|
2140
|
+
candidate: null,
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
1804
2143
|
throw error;
|
|
1805
2144
|
}
|
|
1806
2145
|
}
|
|
@@ -1811,7 +2150,7 @@ async function publishCandidate(options) {
|
|
|
1811
2150
|
export async function publishCandidateForTesting(options) {
|
|
1812
2151
|
return await publishCandidate(options);
|
|
1813
2152
|
}
|
|
1814
|
-
async function
|
|
2153
|
+
export async function ensureBuiltInMiningSetupIfNeeded(options) {
|
|
1815
2154
|
const config = await loadClientConfig({
|
|
1816
2155
|
path: options.paths.clientConfigPath,
|
|
1817
2156
|
provider: options.provider,
|
|
@@ -1869,6 +2208,7 @@ async function performMiningCycle(options) {
|
|
|
1869
2208
|
dataDir: options.dataDir,
|
|
1870
2209
|
chain: "main",
|
|
1871
2210
|
startHeight: 0,
|
|
2211
|
+
serviceLifetime: "ephemeral",
|
|
1872
2212
|
walletRootId: readContext.localState.state.walletRootId,
|
|
1873
2213
|
});
|
|
1874
2214
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
@@ -1911,6 +2251,16 @@ async function performMiningCycle(options) {
|
|
|
1911
2251
|
indexedTipHashHex: indexedTip?.blockHashHex ?? null,
|
|
1912
2252
|
}).catch(() => ({}));
|
|
1913
2253
|
syncMiningVisualizerBlockTimes(options.loopState, visibleBlockTimes);
|
|
2254
|
+
const { targetBlockHeight, tipKey } = syncMiningUiForCurrentTip({
|
|
2255
|
+
loopState: options.loopState,
|
|
2256
|
+
snapshotState: effectiveReadContext.snapshot?.state ?? null,
|
|
2257
|
+
snapshotTipHeight: effectiveReadContext.snapshot?.tip?.height ?? effectiveReadContext.indexer.snapshotTip?.height ?? null,
|
|
2258
|
+
nodeBestHeight: effectiveReadContext.nodeStatus?.nodeBestHeight ?? null,
|
|
2259
|
+
nodeBestHash: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2260
|
+
recentWin: reconciliation.recentWin,
|
|
2261
|
+
});
|
|
2262
|
+
const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
|
|
2263
|
+
syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
|
|
1914
2264
|
if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
|
|
1915
2265
|
await refreshAndSaveStatus({
|
|
1916
2266
|
paths: options.paths,
|
|
@@ -2038,18 +2388,21 @@ async function performMiningCycle(options) {
|
|
|
2038
2388
|
});
|
|
2039
2389
|
return;
|
|
2040
2390
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2391
|
+
if (targetBlockHeight === null) {
|
|
2392
|
+
await refreshAndSaveStatus({
|
|
2393
|
+
paths: options.paths,
|
|
2394
|
+
provider: options.provider,
|
|
2395
|
+
readContext: effectiveReadContext,
|
|
2396
|
+
overrides: {
|
|
2397
|
+
runMode: options.runMode,
|
|
2398
|
+
currentPhase: "waiting-bitcoin-network",
|
|
2399
|
+
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
2400
|
+
},
|
|
2401
|
+
visualizer: options.visualizer,
|
|
2402
|
+
visualizerState: options.loopState.ui,
|
|
2403
|
+
});
|
|
2404
|
+
return;
|
|
2049
2405
|
}
|
|
2050
|
-
syncMiningUiSettledBoard(options.loopState, effectiveReadContext.snapshot?.state ?? null, effectiveReadContext.snapshot?.tip?.height ?? effectiveReadContext.indexer.snapshotTip?.height ?? null, effectiveReadContext.nodeStatus?.nodeBestHeight ?? null);
|
|
2051
|
-
const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
|
|
2052
|
-
syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
|
|
2053
2406
|
if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
|
|
2054
2407
|
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
2055
2408
|
state: "paused",
|
|
@@ -2393,12 +2746,10 @@ async function performMiningCycle(options) {
|
|
|
2393
2746
|
readContext: effectiveReadContext,
|
|
2394
2747
|
overrides: {
|
|
2395
2748
|
runMode: options.runMode,
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
:
|
|
2399
|
-
|
|
2400
|
-
? "Broadcasting the best mining candidate for the current tip."
|
|
2401
|
-
: "Replacing the live mining transaction for the current tip.",
|
|
2749
|
+
...buildPrePublishStatusOverrides({
|
|
2750
|
+
state: effectiveReadContext.localState.state,
|
|
2751
|
+
candidate: selectedCandidate,
|
|
2752
|
+
}),
|
|
2402
2753
|
},
|
|
2403
2754
|
visualizer: options.visualizer,
|
|
2404
2755
|
visualizerState: options.loopState.ui,
|
|
@@ -2465,6 +2816,9 @@ async function performMiningCycle(options) {
|
|
|
2465
2816
|
clearSelectedCandidate(options.loopState);
|
|
2466
2817
|
setMiningUiCandidate(options.loopState, selectedCandidate);
|
|
2467
2818
|
options.loopState.waitingNote = published.note;
|
|
2819
|
+
const lastError = published.decision === "publish-paused-insufficient-funds"
|
|
2820
|
+
? published.lastError ?? createInsufficientFundsMiningPublishErrorMessage()
|
|
2821
|
+
: undefined;
|
|
2468
2822
|
await refreshAndSaveStatus({
|
|
2469
2823
|
paths: options.paths,
|
|
2470
2824
|
provider: options.provider,
|
|
@@ -2486,6 +2840,7 @@ async function performMiningCycle(options) {
|
|
|
2486
2840
|
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
2487
2841
|
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
2488
2842
|
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2843
|
+
lastError,
|
|
2489
2844
|
note: published.note,
|
|
2490
2845
|
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
2491
2846
|
},
|
|
@@ -2580,6 +2935,7 @@ async function saveStopSnapshot(options) {
|
|
|
2580
2935
|
dataDir: options.dataDir,
|
|
2581
2936
|
chain: "main",
|
|
2582
2937
|
startHeight: 0,
|
|
2938
|
+
serviceLifetime: "ephemeral",
|
|
2583
2939
|
walletRootId: localState.state.walletRootId,
|
|
2584
2940
|
}).catch(() => null);
|
|
2585
2941
|
if (service !== null) {
|
|
@@ -2692,6 +3048,7 @@ async function runMiningLoop(options) {
|
|
|
2692
3048
|
dataDir: options.dataDir,
|
|
2693
3049
|
chain: "main",
|
|
2694
3050
|
startHeight: 0,
|
|
3051
|
+
serviceLifetime: "ephemeral",
|
|
2695
3052
|
walletRootId: undefined,
|
|
2696
3053
|
}).catch(() => null);
|
|
2697
3054
|
if (service !== null) {
|
|
@@ -2723,34 +3080,50 @@ export async function runForegroundMining(options) {
|
|
|
2723
3080
|
const openReadContext = options.openReadContext ?? openWalletReadContext;
|
|
2724
3081
|
const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
|
|
2725
3082
|
const rpcFactory = options.rpcFactory ?? createRpcClient;
|
|
2726
|
-
const
|
|
2727
|
-
|
|
2728
|
-
|
|
3083
|
+
const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
|
|
3084
|
+
const runMiningLoopImpl = options.runMiningLoopImpl ?? runMiningLoop;
|
|
3085
|
+
const saveStopSnapshotImpl = options.saveStopSnapshotImpl ?? saveStopSnapshot;
|
|
2729
3086
|
let visualizer = null;
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
throw new Error("Background mining is already active. Run `cogcoin mine stop` first.");
|
|
2734
|
-
}
|
|
2735
|
-
const setupReady = await ensureBuiltInSetupIfNeeded({
|
|
3087
|
+
const setupReady = options.builtInSetupEnsured === true
|
|
3088
|
+
? true
|
|
3089
|
+
: await ensureBuiltInMiningSetupIfNeeded({
|
|
2736
3090
|
provider,
|
|
2737
3091
|
prompter: options.prompter,
|
|
2738
3092
|
paths,
|
|
2739
3093
|
});
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
3094
|
+
if (!setupReady) {
|
|
3095
|
+
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
3096
|
+
}
|
|
3097
|
+
const controlLock = await acquireMiningStartControlLock({
|
|
3098
|
+
paths,
|
|
3099
|
+
purpose: "mine-foreground",
|
|
3100
|
+
takeoverReason: "mine-foreground-replace",
|
|
3101
|
+
requestMiningPreemption,
|
|
3102
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3103
|
+
sleepImpl: options.sleepImpl,
|
|
3104
|
+
});
|
|
3105
|
+
const abortController = new AbortController();
|
|
3106
|
+
const abortListener = () => {
|
|
3107
|
+
abortController.abort();
|
|
3108
|
+
};
|
|
3109
|
+
const handleSigint = () => abortController.abort();
|
|
3110
|
+
const handleSigterm = () => abortController.abort();
|
|
3111
|
+
try {
|
|
3112
|
+
await takeOverMiningRuntime({
|
|
3113
|
+
paths,
|
|
3114
|
+
reason: "mine-foreground-replace",
|
|
3115
|
+
requestMiningPreemption,
|
|
3116
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3117
|
+
sleepImpl: options.sleepImpl,
|
|
3118
|
+
});
|
|
2743
3119
|
visualizer = new MiningFollowVisualizer({
|
|
2744
3120
|
progressOutput: options.progressOutput ?? "auto",
|
|
2745
3121
|
stream: options.stderr,
|
|
2746
3122
|
});
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
process.on("SIGINT", () => abortController.abort());
|
|
2752
|
-
process.on("SIGTERM", () => abortController.abort());
|
|
2753
|
-
await runMiningLoop({
|
|
3123
|
+
options.signal?.addEventListener("abort", abortListener, { once: true });
|
|
3124
|
+
process.on("SIGINT", handleSigint);
|
|
3125
|
+
process.on("SIGTERM", handleSigterm);
|
|
3126
|
+
await runMiningLoopImpl({
|
|
2754
3127
|
dataDir: options.dataDir,
|
|
2755
3128
|
databasePath: options.databasePath,
|
|
2756
3129
|
provider,
|
|
@@ -2766,7 +3139,7 @@ export async function runForegroundMining(options) {
|
|
|
2766
3139
|
stdout: options.stdout,
|
|
2767
3140
|
visualizer,
|
|
2768
3141
|
});
|
|
2769
|
-
await
|
|
3142
|
+
await saveStopSnapshotImpl({
|
|
2770
3143
|
dataDir: options.dataDir,
|
|
2771
3144
|
databasePath: options.databasePath,
|
|
2772
3145
|
provider,
|
|
@@ -2778,6 +3151,9 @@ export async function runForegroundMining(options) {
|
|
|
2778
3151
|
});
|
|
2779
3152
|
}
|
|
2780
3153
|
finally {
|
|
3154
|
+
options.signal?.removeEventListener("abort", abortListener);
|
|
3155
|
+
process.off("SIGINT", handleSigint);
|
|
3156
|
+
process.off("SIGTERM", handleSigterm);
|
|
2781
3157
|
visualizer?.close();
|
|
2782
3158
|
await controlLock.release();
|
|
2783
3159
|
}
|
|
@@ -2785,33 +3161,50 @@ export async function runForegroundMining(options) {
|
|
|
2785
3161
|
export async function startBackgroundMining(options) {
|
|
2786
3162
|
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
2787
3163
|
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
2788
|
-
const
|
|
2789
|
-
|
|
2790
|
-
|
|
3164
|
+
const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
|
|
3165
|
+
const spawnWorkerProcess = options.spawnWorkerProcess ?? spawn;
|
|
3166
|
+
const waitForBackgroundHealthyImpl = options.waitForBackgroundHealthyImpl ?? waitForBackgroundHealthy;
|
|
3167
|
+
const setupReady = options.builtInSetupEnsured === true
|
|
3168
|
+
? true
|
|
3169
|
+
: await ensureBuiltInMiningSetupIfNeeded({
|
|
3170
|
+
provider,
|
|
3171
|
+
prompter: options.prompter,
|
|
3172
|
+
paths,
|
|
3173
|
+
});
|
|
3174
|
+
if (!setupReady) {
|
|
3175
|
+
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
3176
|
+
}
|
|
3177
|
+
let controlLock;
|
|
2791
3178
|
try {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
3179
|
+
controlLock = await acquireMiningStartControlLock({
|
|
3180
|
+
paths,
|
|
3181
|
+
purpose: "mine-start",
|
|
3182
|
+
takeoverReason: "mine-start-replace",
|
|
3183
|
+
requestMiningPreemption,
|
|
3184
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3185
|
+
sleepImpl: options.sleepImpl,
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
catch (error) {
|
|
3189
|
+
if (error instanceof FileLockBusyError && error.existingMetadata?.processId === process.pid) {
|
|
2796
3190
|
return {
|
|
2797
3191
|
started: false,
|
|
2798
|
-
snapshot:
|
|
3192
|
+
snapshot: await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null),
|
|
2799
3193
|
};
|
|
2800
3194
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
provider,
|
|
2806
|
-
prompter: options.prompter,
|
|
3195
|
+
throw error;
|
|
3196
|
+
}
|
|
3197
|
+
try {
|
|
3198
|
+
await takeOverMiningRuntime({
|
|
2807
3199
|
paths,
|
|
3200
|
+
reason: "mine-start-replace",
|
|
3201
|
+
requestMiningPreemption,
|
|
3202
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3203
|
+
sleepImpl: options.sleepImpl,
|
|
2808
3204
|
});
|
|
2809
|
-
if (!setupReady) {
|
|
2810
|
-
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
2811
|
-
}
|
|
2812
3205
|
const runId = randomBytes(16).toString("hex");
|
|
2813
3206
|
const workerMainPath = fileURLToPath(new URL("./worker-main.js", import.meta.url));
|
|
2814
|
-
const child =
|
|
3207
|
+
const child = spawnWorkerProcess(process.execPath, [
|
|
2815
3208
|
workerMainPath,
|
|
2816
3209
|
`--data-dir=${options.dataDir}`,
|
|
2817
3210
|
`--database-path=${options.databasePath}`,
|
|
@@ -2821,7 +3214,7 @@ export async function startBackgroundMining(options) {
|
|
|
2821
3214
|
stdio: "ignore",
|
|
2822
3215
|
});
|
|
2823
3216
|
child.unref();
|
|
2824
|
-
const snapshot = await
|
|
3217
|
+
const snapshot = await waitForBackgroundHealthyImpl(paths);
|
|
2825
3218
|
return {
|
|
2826
3219
|
started: true,
|
|
2827
3220
|
snapshot,
|
|
@@ -2955,12 +3348,21 @@ export async function runBackgroundMiningWorker(options) {
|
|
|
2955
3348
|
export async function handleDetectedMiningRuntimeResumeForTesting(options) {
|
|
2956
3349
|
await handleDetectedMiningRuntimeResume(options);
|
|
2957
3350
|
}
|
|
3351
|
+
export async function takeOverMiningRuntimeForTesting(options) {
|
|
3352
|
+
return await takeOverMiningRuntime(options);
|
|
3353
|
+
}
|
|
2958
3354
|
export async function performMiningCycleForTesting(options) {
|
|
2959
3355
|
await performMiningCycle({
|
|
2960
3356
|
...options,
|
|
2961
3357
|
loopState: options.loopState ?? createMiningLoopState(),
|
|
2962
3358
|
});
|
|
2963
3359
|
}
|
|
3360
|
+
export function buildPrePublishStatusOverridesForTesting(options) {
|
|
3361
|
+
return buildPrePublishStatusOverrides(options);
|
|
3362
|
+
}
|
|
3363
|
+
export function buildStatusSnapshotForTesting(view, overrides = {}) {
|
|
3364
|
+
return buildStatusSnapshot(view, overrides);
|
|
3365
|
+
}
|
|
2964
3366
|
export function shouldKeepCurrentTipLivePublishForTesting(options) {
|
|
2965
3367
|
return livePublishTargetsCandidateTip(options);
|
|
2966
3368
|
}
|