@cogcoin/client 1.0.2 → 1.1.0

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 (76) hide show
  1. package/README.md +3 -2
  2. package/dist/bitcoind/client/factory.d.ts +0 -8
  3. package/dist/bitcoind/client/factory.js +1 -59
  4. package/dist/bitcoind/client/managed-client.d.ts +1 -3
  5. package/dist/bitcoind/client/managed-client.js +3 -47
  6. package/dist/bitcoind/indexer-daemon-main.js +173 -28
  7. package/dist/bitcoind/indexer-daemon.d.ts +11 -3
  8. package/dist/bitcoind/indexer-daemon.js +123 -57
  9. package/dist/bitcoind/indexer-monitor.d.ts +12 -0
  10. package/dist/bitcoind/indexer-monitor.js +89 -0
  11. package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
  12. package/dist/bitcoind/progress/follow-scene.js +87 -4
  13. package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
  14. package/dist/bitcoind/progress/tty-renderer.js +2 -0
  15. package/dist/bitcoind/testing.d.ts +0 -1
  16. package/dist/bitcoind/testing.js +0 -1
  17. package/dist/bitcoind/types.d.ts +5 -2
  18. package/dist/cli/commands/follow.js +44 -49
  19. package/dist/cli/commands/mining-admin.js +56 -2
  20. package/dist/cli/commands/mining-read.js +43 -3
  21. package/dist/cli/commands/mining-runtime.js +91 -73
  22. package/dist/cli/commands/service-runtime.js +42 -2
  23. package/dist/cli/commands/status.js +3 -1
  24. package/dist/cli/commands/sync.js +50 -90
  25. package/dist/cli/commands/wallet-admin.js +21 -3
  26. package/dist/cli/commands/wallet-read.js +2 -0
  27. package/dist/cli/context.js +5 -1
  28. package/dist/cli/managed-indexer-observer.d.ts +33 -0
  29. package/dist/cli/managed-indexer-observer.js +163 -0
  30. package/dist/cli/mining-format.d.ts +3 -1
  31. package/dist/cli/mining-format.js +35 -0
  32. package/dist/cli/mining-json.d.ts +11 -1
  33. package/dist/cli/mining-json.js +9 -0
  34. package/dist/cli/output.js +24 -0
  35. package/dist/cli/parse.d.ts +1 -1
  36. package/dist/cli/parse.js +23 -0
  37. package/dist/cli/read-json.d.ts +13 -1
  38. package/dist/cli/read-json.js +31 -0
  39. package/dist/cli/runner.js +4 -2
  40. package/dist/cli/signals.d.ts +12 -0
  41. package/dist/cli/signals.js +31 -13
  42. package/dist/cli/types.d.ts +8 -4
  43. package/dist/cli/update-service.d.ts +2 -12
  44. package/dist/cli/update-service.js +2 -68
  45. package/dist/semver.d.ts +12 -0
  46. package/dist/semver.js +68 -0
  47. package/dist/wallet/lifecycle.js +0 -6
  48. package/dist/wallet/mining/config.js +54 -3
  49. package/dist/wallet/mining/control.d.ts +5 -2
  50. package/dist/wallet/mining/control.js +153 -34
  51. package/dist/wallet/mining/domain-prompts.d.ts +17 -0
  52. package/dist/wallet/mining/domain-prompts.js +130 -0
  53. package/dist/wallet/mining/index.d.ts +2 -1
  54. package/dist/wallet/mining/index.js +1 -0
  55. package/dist/wallet/mining/runner.d.ts +58 -2
  56. package/dist/wallet/mining/runner.js +553 -331
  57. package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
  58. package/dist/wallet/mining/sentences.js +7 -4
  59. package/dist/wallet/mining/types.d.ts +26 -0
  60. package/dist/wallet/mining/visualizer.d.ts +3 -0
  61. package/dist/wallet/mining/visualizer.js +106 -12
  62. package/dist/wallet/read/context.d.ts +1 -0
  63. package/dist/wallet/read/context.js +15 -10
  64. package/dist/wallet/reset.js +0 -1
  65. package/dist/wallet/state/client-password-agent.js +4 -1
  66. package/dist/wallet/state/client-password.js +15 -8
  67. package/dist/wallet/tx/anchor.js +0 -1
  68. package/dist/wallet/tx/bitcoin-transfer.js +0 -1
  69. package/dist/wallet/tx/cog.js +0 -3
  70. package/dist/wallet/tx/common.js +1 -1
  71. package/dist/wallet/tx/domain-admin.js +0 -1
  72. package/dist/wallet/tx/domain-market.js +0 -3
  73. package/dist/wallet/tx/field.js +0 -1
  74. package/dist/wallet/tx/register.js +0 -1
  75. package/dist/wallet/tx/reputation.js +0 -1
  76. package/package.json +1 -1
@@ -5,9 +5,11 @@ import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { getBalance, getBlockWinners, lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
7
7
  import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
8
+ import { wordlist as englishWordlist } from "@scure/bip39/wordlists/english.js";
8
9
  import { probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
10
+ import { isRetryableManagedRpcError } from "../../bitcoind/retryable-rpc.js";
9
11
  import { FOLLOW_VISIBLE_PRIOR_BLOCKS } from "../../bitcoind/client/follow-block-times.js";
10
- import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
12
+ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../../bitcoind/service.js";
11
13
  import { createRpcClient } from "../../bitcoind/node.js";
12
14
  import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
13
15
  import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
@@ -28,6 +30,35 @@ import { generateMiningSentences, MiningProviderRequestError } from "./sentences
28
30
  import { createEmptyMiningFollowVisualizerState, MiningFollowVisualizer, } from "./visualizer.js";
29
31
  const BEST_BLOCK_POLL_INTERVAL_MS = 500;
30
32
  const BACKGROUND_START_TIMEOUT_MS = 15_000;
33
+ const MINING_BITCOIN_RECOVERY_GRACE_MS = 15_000;
34
+ const MINING_BITCOIN_RECOVERY_RESTART_COOLDOWN_MS = 60_000;
35
+ const MINING_BITCOIN_RECOVERY_NOTE = "Mining lost contact with the local Bitcoin RPC service and is waiting for it to recover.";
36
+ function resolveBip39WordsFromIndices(indices) {
37
+ if (indices === null || indices === undefined) {
38
+ return [];
39
+ }
40
+ const words = [];
41
+ for (const index of indices) {
42
+ if (!Number.isInteger(index) || index < 0 || index >= englishWordlist.length) {
43
+ continue;
44
+ }
45
+ words.push(englishWordlist[index]);
46
+ }
47
+ return words;
48
+ }
49
+ function resolveSettledWinnerRequiredWords(options) {
50
+ const storedWords = resolveBip39WordsFromIndices(options.bip39WordIndices);
51
+ if (storedWords.length > 0) {
52
+ return storedWords;
53
+ }
54
+ if (options.snapshotTipPreviousHashHex === null
55
+ || options.snapshotTipPreviousHashHex === undefined
56
+ || !Number.isInteger(options.domainId)
57
+ || options.domainId <= 0) {
58
+ return [];
59
+ }
60
+ return resolveBip39WordsFromIndices(deriveMiningWordIndices(Buffer.from(displayToInternalBlockhash(options.snapshotTipPreviousHashHex), "hex"), options.domainId));
61
+ }
31
62
  function resolveSnapshotOverride(override, fallback) {
32
63
  return override === undefined ? fallback : override;
33
64
  }
@@ -442,11 +473,41 @@ function createMiningLoopState() {
442
473
  selectedCandidate: null,
443
474
  ui: createEmptyMiningFollowVisualizerState(),
444
475
  waitingNote: null,
476
+ bitcoinRecoveryFirstFailureAtUnixMs: null,
477
+ bitcoinRecoveryFirstUnreachableAtUnixMs: null,
478
+ bitcoinRecoveryLastRestartAttemptAtUnixMs: null,
479
+ bitcoinRecoveryServiceInstanceId: null,
480
+ bitcoinRecoveryProcessId: null,
481
+ reconnectSettledUntilUnixMs: null,
482
+ tipSettledUntilUnixMs: null,
445
483
  };
446
484
  }
447
485
  export function createMiningLoopStateForTesting() {
448
486
  return createMiningLoopState();
449
487
  }
488
+ function expireMiningSettleWindows(loopState, nowUnixMs) {
489
+ if (loopState.reconnectSettledUntilUnixMs !== null
490
+ && loopState.reconnectSettledUntilUnixMs <= nowUnixMs) {
491
+ loopState.reconnectSettledUntilUnixMs = null;
492
+ }
493
+ if (loopState.tipSettledUntilUnixMs !== null
494
+ && loopState.tipSettledUntilUnixMs <= nowUnixMs) {
495
+ loopState.tipSettledUntilUnixMs = null;
496
+ }
497
+ }
498
+ function setMiningReconnectSettleWindow(loopState, nowUnixMs) {
499
+ loopState.reconnectSettledUntilUnixMs = nowUnixMs + MINING_NETWORK_SETTLE_WINDOW_MS;
500
+ }
501
+ function setMiningTipSettleWindow(loopState, nowUnixMs) {
502
+ loopState.tipSettledUntilUnixMs = nowUnixMs + MINING_TIP_SETTLE_WINDOW_MS;
503
+ }
504
+ function buildMiningSettleWindowStatusOverrides(loopState, nowUnixMs) {
505
+ expireMiningSettleWindows(loopState, nowUnixMs);
506
+ return {
507
+ reconnectSettledUntilUnixMs: loopState.reconnectSettledUntilUnixMs,
508
+ tipSettledUntilUnixMs: loopState.tipSettledUntilUnixMs,
509
+ };
510
+ }
450
511
  function buildMiningTipKey(bestBlockHash, targetBlockHeight) {
451
512
  if (bestBlockHash === null || targetBlockHeight === null) {
452
513
  return null;
@@ -491,6 +552,11 @@ function resolveCurrentMinedBlockBoard(options) {
491
552
  rank: winner.rank,
492
553
  domainName: lookupDomainById(options.snapshotState, winner.domainId)?.name ?? fallbackSettledWinnerDomainName(winner.domainId),
493
554
  sentence: winner.sentenceText ?? "[unavailable]",
555
+ requiredWords: resolveSettledWinnerRequiredWords({
556
+ domainId: winner.domainId,
557
+ bip39WordIndices: winner.bip39WordIndices,
558
+ snapshotTipPreviousHashHex: options.snapshotTipPreviousHashHex,
559
+ }),
494
560
  }));
495
561
  return {
496
562
  settledBlockHeight,
@@ -498,12 +564,16 @@ function resolveCurrentMinedBlockBoard(options) {
498
564
  };
499
565
  }
500
566
  export function resolveSettledBoardForTesting(options) {
501
- return resolveCurrentMinedBlockBoard(options);
567
+ return resolveCurrentMinedBlockBoard({
568
+ ...options,
569
+ snapshotTipPreviousHashHex: options.snapshotTipPreviousHashHex ?? null,
570
+ });
502
571
  }
503
- function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight) {
572
+ function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight, snapshotTipPreviousHashHex) {
504
573
  const settledBoard = resolveCurrentMinedBlockBoard({
505
574
  snapshotState,
506
575
  snapshotTipHeight,
576
+ snapshotTipPreviousHashHex,
507
577
  nodeBestHeight: null,
508
578
  });
509
579
  loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
@@ -514,17 +584,20 @@ function syncMiningUiForCurrentTip(options) {
514
584
  ? null
515
585
  : options.nodeBestHeight + 1;
516
586
  const tipKey = buildMiningTipKey(options.nodeBestHash, targetBlockHeight);
517
- if (tipKey !== options.loopState.currentTipKey) {
587
+ const priorTipKey = options.loopState.currentTipKey;
588
+ const tipChanged = tipKey !== null && tipKey !== priorTipKey;
589
+ if (tipKey !== priorTipKey) {
518
590
  options.loopState.currentTipKey = tipKey;
519
591
  resetMiningUiForTip(options.loopState, targetBlockHeight);
520
592
  if (options.recentWin !== null) {
521
593
  options.loopState.ui.recentWin = options.recentWin;
522
594
  }
523
595
  }
524
- syncMiningUiSettledBoard(options.loopState, options.snapshotState, options.snapshotTipHeight);
596
+ syncMiningUiSettledBoard(options.loopState, options.snapshotState, options.snapshotTipHeight, options.snapshotTipPreviousHashHex);
525
597
  return {
526
598
  targetBlockHeight,
527
599
  tipKey,
600
+ tipChanged,
528
601
  };
529
602
  }
530
603
  function setMiningUiCandidate(loopState, candidate) {
@@ -556,6 +629,123 @@ function clearSelectedCandidate(loopState) {
556
629
  loopState.selectedCandidateTipKey = null;
557
630
  loopState.selectedCandidate = null;
558
631
  }
632
+ function clearMiningUiTransientCandidate(loopState) {
633
+ loopState.ui.provisionalRequiredWords = [];
634
+ loopState.ui.provisionalEntry = {
635
+ domainName: null,
636
+ sentence: null,
637
+ };
638
+ loopState.ui.latestSentence = null;
639
+ }
640
+ function discardMiningLoopTransientWork(loopState, walletRootId) {
641
+ clearMiningGateCache(walletRootId);
642
+ clearSelectedCandidate(loopState);
643
+ clearMiningUiTransientCandidate(loopState);
644
+ loopState.waitingNote = null;
645
+ }
646
+ function resolveMiningBitcoindRecoveryIdentity(value) {
647
+ const raw = (value ?? {});
648
+ return {
649
+ serviceInstanceId: raw.serviceInstanceId ?? null,
650
+ processId: raw.processId ?? raw.pid ?? null,
651
+ };
652
+ }
653
+ function miningBitcoindRecoveryIdentityMatches(left, right) {
654
+ if (left.serviceInstanceId !== null && right.serviceInstanceId !== null) {
655
+ return left.serviceInstanceId === right.serviceInstanceId;
656
+ }
657
+ if (left.processId !== null && right.processId !== null) {
658
+ return left.processId === right.processId;
659
+ }
660
+ return false;
661
+ }
662
+ function rememberMiningBitcoindRecoveryIdentity(loopState, value) {
663
+ const next = resolveMiningBitcoindRecoveryIdentity(value);
664
+ if (next.serviceInstanceId === null && next.processId === null) {
665
+ return false;
666
+ }
667
+ const previous = {
668
+ serviceInstanceId: loopState.bitcoinRecoveryServiceInstanceId,
669
+ processId: loopState.bitcoinRecoveryProcessId,
670
+ };
671
+ const changed = (previous.serviceInstanceId !== null
672
+ || previous.processId !== null) && !miningBitcoindRecoveryIdentityMatches(previous, next);
673
+ loopState.bitcoinRecoveryServiceInstanceId = next.serviceInstanceId ?? (next.processId !== null && previous.processId === next.processId
674
+ ? previous.serviceInstanceId
675
+ : null);
676
+ loopState.bitcoinRecoveryProcessId = next.processId ?? (next.serviceInstanceId !== null && previous.serviceInstanceId === next.serviceInstanceId
677
+ ? previous.processId
678
+ : null);
679
+ return changed;
680
+ }
681
+ function resetMiningBitcoindRecoveryState(loopState, value) {
682
+ const hadRecovery = loopState.bitcoinRecoveryFirstFailureAtUnixMs !== null;
683
+ loopState.bitcoinRecoveryFirstFailureAtUnixMs = null;
684
+ loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
685
+ loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs = null;
686
+ if (value !== undefined) {
687
+ rememberMiningBitcoindRecoveryIdentity(loopState, value);
688
+ }
689
+ return hadRecovery;
690
+ }
691
+ function isMiningBitcoindRecoveryPidAlive(pid) {
692
+ if (pid === null || pid === undefined || !Number.isInteger(pid) || pid <= 0) {
693
+ return false;
694
+ }
695
+ try {
696
+ process.kill(pid, 0);
697
+ return true;
698
+ }
699
+ catch (error) {
700
+ if (error instanceof Error && "code" in error && error.code === "EPERM") {
701
+ return true;
702
+ }
703
+ return false;
704
+ }
705
+ }
706
+ function describeRecoverableMiningBitcoindError(error) {
707
+ return error instanceof Error ? error.message : String(error);
708
+ }
709
+ function isRecoverableMiningBitcoindError(error) {
710
+ if (isRetryableManagedRpcError(error)) {
711
+ return true;
712
+ }
713
+ if (!(error instanceof Error)) {
714
+ return false;
715
+ }
716
+ if ("code" in error) {
717
+ const code = error.code;
718
+ if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET") {
719
+ return true;
720
+ }
721
+ }
722
+ return error.message === "managed_bitcoind_service_start_timeout"
723
+ || error.message === "bitcoind_cookie_timeout"
724
+ || error.message.includes("cookie file is unavailable")
725
+ || error.message.includes("cookie file could not be read")
726
+ || error.message.includes("ECONNREFUSED")
727
+ || error.message.includes("ECONNRESET")
728
+ || error.message.includes("socket hang up");
729
+ }
730
+ async function attachManagedBitcoindForRecovery(options) {
731
+ try {
732
+ const service = await options.attachService({
733
+ dataDir: options.dataDir,
734
+ chain: "main",
735
+ startHeight: 0,
736
+ walletRootId: options.walletRootId,
737
+ });
738
+ const serviceStatus = await service.refreshServiceStatus?.().catch(() => null);
739
+ rememberMiningBitcoindRecoveryIdentity(options.loopState, serviceStatus ?? { pid: service.pid });
740
+ return true;
741
+ }
742
+ catch (error) {
743
+ if (!isRecoverableMiningBitcoindError(error)) {
744
+ throw error;
745
+ }
746
+ return false;
747
+ }
748
+ }
559
749
  async function resolveFundingDisplaySats(state, rpc) {
560
750
  const utxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
561
751
  return utxos.reduce((sum, entry) => {
@@ -604,6 +794,7 @@ function createIndexedMiningFollowVisualizerState(readContext) {
604
794
  const settledBoard = resolveCurrentMinedBlockBoard({
605
795
  snapshotState: readContext.snapshot?.state ?? null,
606
796
  snapshotTipHeight: readContext.snapshot?.tip?.height ?? readContext.indexer.snapshotTip?.height ?? null,
797
+ snapshotTipPreviousHashHex: readContext.snapshot?.tip?.previousHashHex ?? readContext.indexer.snapshotTip?.previousHashHex ?? null,
607
798
  nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
608
799
  });
609
800
  uiState.settledBlockHeight = settledBoard.settledBlockHeight;
@@ -925,6 +1116,8 @@ function buildStatusSnapshot(view, overrides = {}) {
925
1116
  currentAbsoluteFeeSats: resolveSnapshotOverride(overrides.currentAbsoluteFeeSats, view.runtime.currentAbsoluteFeeSats),
926
1117
  currentBlockFeeSpentSats: resolveSnapshotOverride(overrides.currentBlockFeeSpentSats, view.runtime.currentBlockFeeSpentSats),
927
1118
  lastSuspendDetectedAtUnixMs: resolveSnapshotOverride(overrides.lastSuspendDetectedAtUnixMs, view.runtime.lastSuspendDetectedAtUnixMs),
1119
+ reconnectSettledUntilUnixMs: resolveSnapshotOverride(overrides.reconnectSettledUntilUnixMs, view.runtime.reconnectSettledUntilUnixMs),
1120
+ tipSettledUntilUnixMs: resolveSnapshotOverride(overrides.tipSettledUntilUnixMs, view.runtime.tipSettledUntilUnixMs),
928
1121
  providerState: resolveSnapshotOverride(overrides.providerState, view.runtime.providerState),
929
1122
  corePublishState: resolveSnapshotOverride(overrides.corePublishState, view.runtime.corePublishState),
930
1123
  currentPublishDecision: resolveSnapshotOverride(overrides.currentPublishDecision, view.runtime.currentPublishDecision),
@@ -990,6 +1183,93 @@ async function refreshAndSaveStatus(options) {
990
1183
  async function appendEvent(paths, event) {
991
1184
  await appendMiningEvent(paths.miningEventsPath, event);
992
1185
  }
1186
+ async function handleRecoverableMiningBitcoindFailure(options) {
1187
+ const failureMessage = describeRecoverableMiningBitcoindError(options.error);
1188
+ const walletRootId = options.readContext.localState.walletRootId ?? undefined;
1189
+ if (options.loopState.bitcoinRecoveryFirstFailureAtUnixMs === null) {
1190
+ options.loopState.bitcoinRecoveryFirstFailureAtUnixMs = options.nowUnixMs;
1191
+ }
1192
+ let restartedService = false;
1193
+ const probe = await options.probeService({
1194
+ dataDir: options.dataDir,
1195
+ chain: "main",
1196
+ startHeight: 0,
1197
+ walletRootId,
1198
+ }).catch((probeError) => {
1199
+ if (!isRecoverableMiningBitcoindError(probeError)) {
1200
+ throw probeError;
1201
+ }
1202
+ return null;
1203
+ });
1204
+ if (probe !== null) {
1205
+ if (probe.compatibility === "compatible") {
1206
+ rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
1207
+ options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
1208
+ }
1209
+ else if (probe.compatibility === "unreachable") {
1210
+ const identityChanged = rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
1211
+ const livePid = isMiningBitcoindRecoveryPidAlive(probe.status?.processId ?? null);
1212
+ if (identityChanged || options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs === null) {
1213
+ options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = options.nowUnixMs;
1214
+ }
1215
+ if (!livePid) {
1216
+ restartedService = await attachManagedBitcoindForRecovery({
1217
+ dataDir: options.dataDir,
1218
+ walletRootId,
1219
+ attachService: options.attachService,
1220
+ loopState: options.loopState,
1221
+ });
1222
+ }
1223
+ else {
1224
+ const graceElapsed = (options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs !== null
1225
+ && options.nowUnixMs - options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs
1226
+ >= MINING_BITCOIN_RECOVERY_GRACE_MS);
1227
+ const cooldownElapsed = (options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs === null
1228
+ || options.nowUnixMs - options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs
1229
+ >= MINING_BITCOIN_RECOVERY_RESTART_COOLDOWN_MS);
1230
+ if (graceElapsed && cooldownElapsed) {
1231
+ options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs = options.nowUnixMs;
1232
+ await options.stopService({
1233
+ dataDir: options.dataDir,
1234
+ walletRootId,
1235
+ }).catch((stopError) => {
1236
+ if (!isRecoverableMiningBitcoindError(stopError)) {
1237
+ throw stopError;
1238
+ }
1239
+ });
1240
+ await attachManagedBitcoindForRecovery({
1241
+ dataDir: options.dataDir,
1242
+ walletRootId,
1243
+ attachService: options.attachService,
1244
+ loopState: options.loopState,
1245
+ });
1246
+ restartedService = true;
1247
+ }
1248
+ }
1249
+ }
1250
+ else {
1251
+ throw new Error(probe.error ?? "managed_bitcoind_protocol_error");
1252
+ }
1253
+ }
1254
+ if (restartedService) {
1255
+ discardMiningLoopTransientWork(options.loopState, walletRootId);
1256
+ setMiningReconnectSettleWindow(options.loopState, options.nowUnixMs);
1257
+ }
1258
+ await refreshAndSaveStatus({
1259
+ paths: options.paths,
1260
+ provider: options.provider,
1261
+ readContext: options.readContext,
1262
+ overrides: {
1263
+ runMode: options.runMode,
1264
+ currentPhase: "waiting-bitcoin-network",
1265
+ lastError: failureMessage,
1266
+ note: MINING_BITCOIN_RECOVERY_NOTE,
1267
+ ...buildMiningSettleWindowStatusOverrides(options.loopState, options.nowUnixMs),
1268
+ },
1269
+ visualizer: options.visualizer,
1270
+ visualizerState: options.loopState.ui,
1271
+ });
1272
+ }
993
1273
  async function handleDetectedMiningRuntimeResume(options) {
994
1274
  const readContext = await options.openReadContext({
995
1275
  dataDir: options.dataDir,
@@ -999,6 +1279,7 @@ async function handleDetectedMiningRuntimeResume(options) {
999
1279
  });
1000
1280
  try {
1001
1281
  clearMiningGateCache(readContext.localState.walletRootId);
1282
+ setMiningReconnectSettleWindow(options.loopState, options.detectedAtUnixMs);
1002
1283
  await refreshAndSaveStatus({
1003
1284
  paths: options.paths,
1004
1285
  provider: options.provider,
@@ -1011,6 +1292,7 @@ async function handleDetectedMiningRuntimeResume(options) {
1011
1292
  currentPhase: "resuming",
1012
1293
  lastSuspendDetectedAtUnixMs: options.detectedAtUnixMs,
1013
1294
  note: "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.",
1295
+ ...buildMiningSettleWindowStatusOverrides(options.loopState, options.detectedAtUnixMs),
1014
1296
  },
1015
1297
  visualizer: options.visualizer,
1016
1298
  visualizerState: createIndexedMiningFollowVisualizerState(readContext),
@@ -1077,7 +1359,7 @@ function determineCorePublishState(info) {
1077
1359
  }
1078
1360
  function createMiningPlan(options) {
1079
1361
  const fundingUtxos = options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
1080
- && entry.confirmations >= 1
1362
+ && entry.confirmations >= MINING_FUNDING_MIN_CONF
1081
1363
  && entry.spendable !== false
1082
1364
  && entry.safe !== false
1083
1365
  && !(options.conflictOutpoint !== null
@@ -1130,6 +1412,7 @@ async function buildMiningTransaction(options) {
1130
1412
  finalizeErrorCode: "wallet_mining_finalize_failed",
1131
1413
  mempoolRejectPrefix: "wallet_mining_mempool_rejected",
1132
1414
  feeRate: options.plan.feeRateSatVb,
1415
+ availableFundingMinConf: MINING_FUNDING_MIN_CONF,
1133
1416
  });
1134
1417
  }
1135
1418
  export function createMiningPlanForTesting(options) {
@@ -1206,11 +1489,36 @@ function createStaleMiningCandidateWaitingNote() {
1206
1489
  function createRetryableMiningPublishWaitingNote() {
1207
1490
  return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
1208
1491
  }
1492
+ const MINING_FUNDING_MIN_CONF = 0;
1209
1493
  function createInsufficientFundsMiningPublishWaitingNote() {
1210
- return "Mining is waiting for enough confirmed safe BTC funding that Bitcoin Core can use for the next publish.";
1494
+ return "Mining is waiting for enough safe BTC funding that Bitcoin Core can use for the next publish.";
1211
1495
  }
1212
1496
  function createInsufficientFundsMiningPublishErrorMessage() {
1213
- return "Bitcoin Core could not fund the next mining publish with confirmed safe BTC.";
1497
+ return "Bitcoin Core could not fund the next mining publish with safe BTC.";
1498
+ }
1499
+ function buildMiningGenerationRequest(options) {
1500
+ return {
1501
+ schemaVersion: 1,
1502
+ requestId: options.requestId ?? `mining-${options.targetBlockHeight}-${randomBytes(8).toString("hex")}`,
1503
+ targetBlockHeight: options.targetBlockHeight,
1504
+ referencedBlockHashDisplay: options.referencedBlockHashDisplay,
1505
+ generatedAtUnixMs: options.generatedAtUnixMs ?? Date.now(),
1506
+ extraPrompt: options.extraPrompt,
1507
+ limits: createMiningSentenceRequestLimits(),
1508
+ rootDomains: options.domains.map((domain) => ({
1509
+ domainId: domain.domainId,
1510
+ domainName: domain.domainName,
1511
+ requiredWords: domain.requiredWords,
1512
+ extraPrompt: options.domainExtraPrompts[domain.domainName.toLowerCase()] ?? null,
1513
+ })),
1514
+ };
1515
+ }
1516
+ export function buildMiningGenerationRequestForTesting(options) {
1517
+ return buildMiningGenerationRequest({
1518
+ ...options,
1519
+ domainExtraPrompts: options.domainExtraPrompts ?? {},
1520
+ extraPrompt: options.extraPrompt ?? null,
1521
+ });
1214
1522
  }
1215
1523
  async function generateCandidatesForDomains(options) {
1216
1524
  const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
@@ -1223,6 +1531,10 @@ async function generateCandidatesForDomains(options) {
1223
1531
  ...domain,
1224
1532
  requiredWords: getWords(domain.domainId, referencedBlockHashInternal),
1225
1533
  }));
1534
+ const clientConfig = await loadClientConfig({
1535
+ path: options.paths.clientConfigPath,
1536
+ provider: options.provider,
1537
+ }).catch(() => null);
1226
1538
  const abortController = new AbortController();
1227
1539
  let stale = false;
1228
1540
  let staleIndexerTruth = false;
@@ -1261,20 +1573,13 @@ async function generateCandidatesForDomains(options) {
1261
1573
  runId: options.runId ?? null,
1262
1574
  pid: process.pid ?? null,
1263
1575
  });
1264
- const generationRequest = {
1265
- schemaVersion: 1,
1266
- requestId: `mining-${targetBlockHeight}-${randomBytes(8).toString("hex")}`,
1576
+ const generationRequest = buildMiningGenerationRequest({
1267
1577
  targetBlockHeight,
1268
1578
  referencedBlockHashDisplay: bestBlockHash,
1269
- generatedAtUnixMs: Date.now(),
1270
- extraPrompt: null,
1271
- limits: createMiningSentenceRequestLimits(),
1272
- rootDomains: rootDomains.map((domain) => ({
1273
- domainId: domain.domainId,
1274
- domainName: domain.domainName,
1275
- requiredWords: domain.requiredWords,
1276
- })),
1277
- };
1579
+ domains: rootDomains,
1580
+ domainExtraPrompts: clientConfig?.mining.domainExtraPrompts ?? {},
1581
+ extraPrompt: clientConfig?.mining.builtIn?.extraPrompt ?? null,
1582
+ });
1278
1583
  let generated;
1279
1584
  try {
1280
1585
  generated = await generateMiningSentences(generationRequest, {
@@ -1418,6 +1723,7 @@ function toSentenceBoardEntries(entries) {
1418
1723
  rank: entry.rank,
1419
1724
  domainName: entry.domainName,
1420
1725
  sentence: entry.sentence,
1726
+ requiredWords: resolveBip39WordsFromIndices(entry.bip39WordIndices),
1421
1727
  }));
1422
1728
  }
1423
1729
  async function runCompetitivenessGate(options) {
@@ -1839,7 +2145,6 @@ async function publishCandidateOnce(options) {
1839
2145
  dataDir: options.dataDir,
1840
2146
  chain: "main",
1841
2147
  startHeight: 0,
1842
- serviceLifetime: "ephemeral",
1843
2148
  walletRootId: options.readContext.localState.state.walletRootId,
1844
2149
  });
1845
2150
  const rpc = options.rpcFactory(service.rpc);
@@ -1850,7 +2155,7 @@ async function publishCandidateOnce(options) {
1850
2155
  nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
1851
2156
  snapshotState: options.readContext.snapshot.state,
1852
2157
  })).state;
1853
- const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
2158
+ const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, MINING_FUNDING_MIN_CONF);
1854
2159
  const conflictOutpoint = resolveMiningConflictOutpoint({
1855
2160
  state,
1856
2161
  allUtxos,
@@ -2120,7 +2425,7 @@ async function publishCandidate(options) {
2120
2425
  if (isInsufficientFundsError(error)) {
2121
2426
  const note = createInsufficientFundsMiningPublishWaitingNote();
2122
2427
  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.", {
2428
+ await appendEventFn(options.paths, createEvent("publish-paused-insufficient-funds", "Paused mining publish because Bitcoin Core could not fund the next mining transaction with safe BTC.", {
2124
2429
  level: "warn",
2125
2430
  runId: options.runId,
2126
2431
  targetBlockHeight: refreshedCandidate.targetBlockHeight,
@@ -2169,6 +2474,7 @@ export async function ensureBuiltInMiningSetupIfNeeded(options) {
2169
2474
  return true;
2170
2475
  }
2171
2476
  async function performMiningCycle(options) {
2477
+ const now = options.nowImpl ?? Date.now;
2172
2478
  let readContext = await options.openReadContext({
2173
2479
  dataDir: options.dataDir,
2174
2480
  databasePath: options.databasePath,
@@ -2177,30 +2483,39 @@ async function performMiningCycle(options) {
2177
2483
  });
2178
2484
  let readContextClosed = false;
2179
2485
  try {
2180
- checkpointMiningSuspendDetector(options.suspendDetector);
2181
- await refreshAndSaveStatus({
2182
- paths: options.paths,
2183
- provider: options.provider,
2184
- readContext,
2185
- overrides: {
2186
- runMode: options.runMode,
2187
- backgroundWorkerPid: options.backgroundWorkerPid,
2188
- backgroundWorkerRunId: options.backgroundWorkerRunId,
2189
- backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
2190
- },
2191
- });
2192
- if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
2193
- await refreshAndSaveStatus({
2486
+ let clearRecoveredBitcoindError = false;
2487
+ const saveCycleStatus = async (readContext, overrides, includeVisualizer = true) => {
2488
+ const statusNowUnixMs = now();
2489
+ const resolvedOverrides = clearRecoveredBitcoindError && overrides.lastError === undefined
2490
+ ? {
2491
+ ...overrides,
2492
+ lastError: null,
2493
+ }
2494
+ : overrides;
2495
+ return await refreshAndSaveStatus({
2194
2496
  paths: options.paths,
2195
2497
  provider: options.provider,
2196
2498
  readContext,
2197
2499
  overrides: {
2198
- runMode: options.runMode,
2199
- currentPhase: "waiting",
2200
- note: "Wallet state must be locally available for mining to continue.",
2500
+ ...buildMiningSettleWindowStatusOverrides(options.loopState, statusNowUnixMs),
2501
+ ...resolvedOverrides,
2201
2502
  },
2202
- visualizer: options.visualizer,
2203
- visualizerState: options.loopState.ui,
2503
+ visualizer: includeVisualizer ? options.visualizer : undefined,
2504
+ visualizerState: includeVisualizer ? options.loopState.ui : undefined,
2505
+ });
2506
+ };
2507
+ checkpointMiningSuspendDetector(options.suspendDetector);
2508
+ await saveCycleStatus(readContext, {
2509
+ runMode: options.runMode,
2510
+ backgroundWorkerPid: options.backgroundWorkerPid,
2511
+ backgroundWorkerRunId: options.backgroundWorkerRunId,
2512
+ backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? now() : null,
2513
+ }, false);
2514
+ if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
2515
+ await saveCycleStatus(readContext, {
2516
+ runMode: options.runMode,
2517
+ currentPhase: "waiting",
2518
+ note: "Wallet state must be locally available for mining to continue.",
2204
2519
  });
2205
2520
  return;
2206
2521
  }
@@ -2208,7 +2523,6 @@ async function performMiningCycle(options) {
2208
2523
  dataDir: options.dataDir,
2209
2524
  chain: "main",
2210
2525
  startHeight: 0,
2211
- serviceLifetime: "ephemeral",
2212
2526
  walletRootId: readContext.localState.state.walletRootId,
2213
2527
  });
2214
2528
  checkpointMiningSuspendDetector(options.suspendDetector);
@@ -2251,28 +2565,25 @@ async function performMiningCycle(options) {
2251
2565
  indexedTipHashHex: indexedTip?.blockHashHex ?? null,
2252
2566
  }).catch(() => ({}));
2253
2567
  syncMiningVisualizerBlockTimes(options.loopState, visibleBlockTimes);
2254
- const { targetBlockHeight, tipKey } = syncMiningUiForCurrentTip({
2568
+ const { targetBlockHeight, tipKey, tipChanged } = syncMiningUiForCurrentTip({
2255
2569
  loopState: options.loopState,
2256
2570
  snapshotState: effectiveReadContext.snapshot?.state ?? null,
2257
2571
  snapshotTipHeight: effectiveReadContext.snapshot?.tip?.height ?? effectiveReadContext.indexer.snapshotTip?.height ?? null,
2572
+ snapshotTipPreviousHashHex: effectiveReadContext.snapshot?.tip?.previousHashHex ?? effectiveReadContext.indexer.snapshotTip?.previousHashHex ?? null,
2258
2573
  nodeBestHeight: effectiveReadContext.nodeStatus?.nodeBestHeight ?? null,
2259
2574
  nodeBestHash: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2260
2575
  recentWin: reconciliation.recentWin,
2261
2576
  });
2577
+ if (tipChanged) {
2578
+ setMiningTipSettleWindow(options.loopState, now());
2579
+ }
2262
2580
  const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
2263
2581
  syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
2264
2582
  if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
2265
- await refreshAndSaveStatus({
2266
- paths: options.paths,
2267
- provider: options.provider,
2268
- readContext: effectiveReadContext,
2269
- overrides: {
2270
- runMode: options.runMode,
2271
- currentPhase: "waiting",
2272
- note: "Mining is blocked until the current mining publish is repaired or reconciled.",
2273
- },
2274
- visualizer: options.visualizer,
2275
- visualizerState: options.loopState.ui,
2583
+ await saveCycleStatus(effectiveReadContext, {
2584
+ runMode: options.runMode,
2585
+ currentPhase: "waiting",
2586
+ note: "Mining is blocked until the current mining publish is repaired or reconciled.",
2276
2587
  });
2277
2588
  return;
2278
2589
  }
@@ -2294,17 +2605,10 @@ async function performMiningCycle(options) {
2294
2605
  state: nextState,
2295
2606
  },
2296
2607
  };
2297
- await refreshAndSaveStatus({
2298
- paths: options.paths,
2299
- provider: options.provider,
2300
- readContext: effectiveReadContext,
2301
- overrides: {
2302
- runMode: options.runMode,
2303
- currentPhase: "waiting",
2304
- note: "Mining is paused while another wallet mutation is active.",
2305
- },
2306
- visualizer: options.visualizer,
2307
- visualizerState: options.loopState.ui,
2608
+ await saveCycleStatus(effectiveReadContext, {
2609
+ runMode: options.runMode,
2610
+ currentPhase: "waiting",
2611
+ note: "Mining is paused while another wallet mutation is active.",
2308
2612
  });
2309
2613
  return;
2310
2614
  }
@@ -2322,23 +2626,16 @@ async function performMiningCycle(options) {
2322
2626
  provider: options.provider,
2323
2627
  paths: options.paths,
2324
2628
  });
2325
- await refreshAndSaveStatus({
2326
- paths: options.paths,
2327
- provider: options.provider,
2328
- readContext: {
2329
- ...effectiveReadContext,
2330
- localState: {
2331
- ...effectiveReadContext.localState,
2332
- state: nextState,
2333
- },
2334
- },
2335
- overrides: {
2336
- runMode: options.runMode,
2337
- currentPhase: "waiting",
2338
- note: "Mining is paused while another wallet command is preempting sentence generation.",
2629
+ await saveCycleStatus({
2630
+ ...effectiveReadContext,
2631
+ localState: {
2632
+ ...effectiveReadContext.localState,
2633
+ state: nextState,
2339
2634
  },
2340
- visualizer: options.visualizer,
2341
- visualizerState: options.loopState.ui,
2635
+ }, {
2636
+ runMode: options.runMode,
2637
+ currentPhase: "waiting",
2638
+ note: "Mining is paused while another wallet command is preempting sentence generation.",
2342
2639
  });
2343
2640
  return;
2344
2641
  }
@@ -2353,53 +2650,33 @@ async function performMiningCycle(options) {
2353
2650
  network: networkInfo,
2354
2651
  mempool: mempoolInfo,
2355
2652
  });
2653
+ clearRecoveredBitcoindError = resetMiningBitcoindRecoveryState(options.loopState, effectiveReadContext.nodeStatus?.serviceStatus ?? { pid: service.pid });
2356
2654
  if (corePublishState !== "healthy") {
2357
- await refreshAndSaveStatus({
2358
- paths: options.paths,
2359
- provider: options.provider,
2360
- readContext: effectiveReadContext,
2361
- overrides: {
2362
- runMode: options.runMode,
2363
- currentPhase: "waiting-bitcoin-network",
2364
- corePublishState,
2365
- note: "Mining is waiting for the local Bitcoin node to become publishable.",
2366
- },
2367
- visualizer: options.visualizer,
2368
- visualizerState: options.loopState.ui,
2655
+ await saveCycleStatus(effectiveReadContext, {
2656
+ runMode: options.runMode,
2657
+ currentPhase: "waiting-bitcoin-network",
2658
+ corePublishState,
2659
+ note: "Mining is waiting for the local Bitcoin node to become publishable.",
2369
2660
  });
2370
2661
  return;
2371
2662
  }
2372
2663
  if (effectiveReadContext.indexer.health !== "synced" || effectiveReadContext.nodeHealth !== "synced") {
2373
- await refreshAndSaveStatus({
2374
- paths: options.paths,
2375
- provider: options.provider,
2376
- readContext: effectiveReadContext,
2377
- overrides: {
2378
- runMode: options.runMode,
2379
- currentPhase: effectiveReadContext.indexer.health !== "synced"
2380
- ? "waiting-indexer"
2381
- : "waiting-bitcoin-network",
2382
- note: effectiveReadContext.indexer.health !== "synced"
2383
- ? "Mining is waiting for Bitcoin Core and the indexer to align."
2384
- : "Mining is waiting for the local Bitcoin node to become publishable.",
2385
- },
2386
- visualizer: options.visualizer,
2387
- visualizerState: options.loopState.ui,
2664
+ await saveCycleStatus(effectiveReadContext, {
2665
+ runMode: options.runMode,
2666
+ currentPhase: effectiveReadContext.indexer.health !== "synced"
2667
+ ? "waiting-indexer"
2668
+ : "waiting-bitcoin-network",
2669
+ note: effectiveReadContext.indexer.health !== "synced"
2670
+ ? "Mining is waiting for Bitcoin Core and the indexer to align."
2671
+ : "Mining is waiting for the local Bitcoin node to become publishable.",
2388
2672
  });
2389
2673
  return;
2390
2674
  }
2391
2675
  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,
2676
+ await saveCycleStatus(effectiveReadContext, {
2677
+ runMode: options.runMode,
2678
+ currentPhase: "waiting-bitcoin-network",
2679
+ note: "Mining is waiting for the local Bitcoin node to become publishable.",
2403
2680
  });
2404
2681
  return;
2405
2682
  }
@@ -2413,24 +2690,17 @@ async function performMiningCycle(options) {
2413
2690
  provider: options.provider,
2414
2691
  paths: options.paths,
2415
2692
  });
2416
- await refreshAndSaveStatus({
2417
- paths: options.paths,
2418
- provider: options.provider,
2419
- readContext: {
2420
- ...effectiveReadContext,
2421
- localState: {
2422
- ...effectiveReadContext.localState,
2423
- state: nextState,
2424
- },
2425
- },
2426
- overrides: {
2427
- runMode: options.runMode,
2428
- currentPhase: "idle",
2429
- currentPublishDecision: "publish-skipped-zero-reward",
2430
- note: "Mining is disabled because the target block reward is zero.",
2693
+ await saveCycleStatus({
2694
+ ...effectiveReadContext,
2695
+ localState: {
2696
+ ...effectiveReadContext.localState,
2697
+ state: nextState,
2431
2698
  },
2432
- visualizer: options.visualizer,
2433
- visualizerState: options.loopState.ui,
2699
+ }, {
2700
+ runMode: options.runMode,
2701
+ currentPhase: "idle",
2702
+ currentPublishDecision: "publish-skipped-zero-reward",
2703
+ note: "Mining is disabled because the target block reward is zero.",
2434
2704
  });
2435
2705
  await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
2436
2706
  targetBlockHeight,
@@ -2440,17 +2710,10 @@ async function performMiningCycle(options) {
2440
2710
  return;
2441
2711
  }
2442
2712
  if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
2443
- await refreshAndSaveStatus({
2444
- paths: options.paths,
2445
- provider: options.provider,
2446
- readContext: effectiveReadContext,
2447
- overrides: {
2448
- runMode: options.runMode,
2449
- currentPhase: "waiting",
2450
- note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
2451
- },
2452
- visualizer: options.visualizer,
2453
- visualizerState: options.loopState.ui,
2713
+ await saveCycleStatus(effectiveReadContext, {
2714
+ runMode: options.runMode,
2715
+ currentPhase: "waiting",
2716
+ note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
2454
2717
  });
2455
2718
  return;
2456
2719
  }
@@ -2488,31 +2751,17 @@ async function performMiningCycle(options) {
2488
2751
  if (selectedCandidate === null) {
2489
2752
  const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
2490
2753
  if (domains.length === 0) {
2491
- await refreshAndSaveStatus({
2492
- paths: options.paths,
2493
- provider: options.provider,
2494
- readContext: effectiveReadContext,
2495
- overrides: {
2496
- runMode: options.runMode,
2497
- currentPhase: "idle",
2498
- note: "No locally controlled anchored root domains are currently eligible to mine.",
2499
- },
2500
- visualizer: options.visualizer,
2501
- visualizerState: options.loopState.ui,
2754
+ await saveCycleStatus(effectiveReadContext, {
2755
+ runMode: options.runMode,
2756
+ currentPhase: "idle",
2757
+ note: "No locally controlled anchored root domains are currently eligible to mine.",
2502
2758
  });
2503
2759
  return;
2504
2760
  }
2505
- await refreshAndSaveStatus({
2506
- paths: options.paths,
2507
- provider: options.provider,
2508
- readContext: effectiveReadContext,
2509
- overrides: {
2510
- runMode: options.runMode,
2511
- currentPhase: "generating",
2512
- note: "Generating mining sentences for eligible root domains.",
2513
- },
2514
- visualizer: options.visualizer,
2515
- visualizerState: options.loopState.ui,
2761
+ await saveCycleStatus(effectiveReadContext, {
2762
+ runMode: options.runMode,
2763
+ currentPhase: "generating",
2764
+ note: "Generating mining sentences for eligible root domains.",
2516
2765
  });
2517
2766
  await appendEvent(options.paths, createEvent("sentence-generation-start", "Started mining sentence generation.", {
2518
2767
  targetBlockHeight,
@@ -2539,19 +2788,12 @@ async function performMiningCycle(options) {
2539
2788
  options.loopState.attemptedTipKey = tipKey;
2540
2789
  options.loopState.waitingNote = "Mining is waiting for the sentence provider to recover.";
2541
2790
  }
2542
- await refreshAndSaveStatus({
2543
- paths: options.paths,
2544
- provider: options.provider,
2545
- readContext: effectiveReadContext,
2546
- overrides: {
2547
- runMode: options.runMode,
2548
- currentPhase: "waiting-provider",
2549
- providerState: error.providerState,
2550
- lastError: error.message,
2551
- note: "Mining is waiting for the sentence provider to recover.",
2552
- },
2553
- visualizer: options.visualizer,
2554
- visualizerState: options.loopState.ui,
2791
+ await saveCycleStatus(effectiveReadContext, {
2792
+ runMode: options.runMode,
2793
+ currentPhase: "waiting-provider",
2794
+ providerState: error.providerState,
2795
+ lastError: error.message,
2796
+ note: "Mining is waiting for the sentence provider to recover.",
2555
2797
  });
2556
2798
  await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
2557
2799
  level: "warn",
@@ -2594,19 +2836,12 @@ async function performMiningCycle(options) {
2594
2836
  options.loopState.attemptedTipKey = tipKey;
2595
2837
  options.loopState.waitingNote = "Mining sentence generation failed for the current tip.";
2596
2838
  }
2597
- await refreshAndSaveStatus({
2598
- paths: options.paths,
2599
- provider: options.provider,
2600
- readContext: effectiveReadContext,
2601
- overrides: {
2602
- runMode: options.runMode,
2603
- currentPhase: "waiting-provider",
2604
- providerState: "unavailable",
2605
- lastError: failureMessage,
2606
- note: "Mining sentence generation failed for the current tip.",
2607
- },
2608
- visualizer: options.visualizer,
2609
- visualizerState: options.loopState.ui,
2839
+ await saveCycleStatus(effectiveReadContext, {
2840
+ runMode: options.runMode,
2841
+ currentPhase: "waiting-provider",
2842
+ providerState: "unavailable",
2843
+ lastError: failureMessage,
2844
+ note: "Mining sentence generation failed for the current tip.",
2610
2845
  });
2611
2846
  await appendEvent(options.paths, createEvent("sentence-generation-failed", failureMessage, {
2612
2847
  level: "error",
@@ -2616,17 +2851,10 @@ async function performMiningCycle(options) {
2616
2851
  }));
2617
2852
  return;
2618
2853
  }
2619
- await refreshAndSaveStatus({
2620
- paths: options.paths,
2621
- provider: options.provider,
2622
- readContext: effectiveReadContext,
2623
- overrides: {
2624
- runMode: options.runMode,
2625
- currentPhase: "scoring",
2626
- note: "Scoring mining candidates for the current tip.",
2627
- },
2628
- visualizer: options.visualizer,
2629
- visualizerState: options.loopState.ui,
2854
+ await saveCycleStatus(effectiveReadContext, {
2855
+ runMode: options.runMode,
2856
+ currentPhase: "scoring",
2857
+ note: "Scoring mining candidates for the current tip.",
2630
2858
  });
2631
2859
  const best = await chooseBestLocalCandidate(candidates);
2632
2860
  if (best === null) {
@@ -2635,18 +2863,11 @@ async function performMiningCycle(options) {
2635
2863
  options.loopState.waitingNote = "No publishable mining candidate passed scoring gates for the current tip.";
2636
2864
  }
2637
2865
  clearSelectedCandidate(options.loopState);
2638
- await refreshAndSaveStatus({
2639
- paths: options.paths,
2640
- provider: options.provider,
2641
- readContext: effectiveReadContext,
2642
- overrides: {
2643
- runMode: options.runMode,
2644
- currentPhase: "idle",
2645
- currentPublishDecision: "publish-skipped-no-candidate",
2646
- note: "No publishable mining candidate passed scoring gates for the current tip.",
2647
- },
2648
- visualizer: options.visualizer,
2649
- visualizerState: options.loopState.ui,
2866
+ await saveCycleStatus(effectiveReadContext, {
2867
+ runMode: options.runMode,
2868
+ currentPhase: "idle",
2869
+ currentPublishDecision: "publish-skipped-no-candidate",
2870
+ note: "No publishable mining candidate passed scoring gates for the current tip.",
2650
2871
  });
2651
2872
  await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
2652
2873
  targetBlockHeight,
@@ -2693,25 +2914,18 @@ async function performMiningCycle(options) {
2693
2914
  : gate.decision === "suppressed-top5-mempool"
2694
2915
  ? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2695
2916
  : "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.";
2696
- await refreshAndSaveStatus({
2697
- paths: options.paths,
2698
- provider: options.provider,
2699
- readContext: effectiveReadContext,
2700
- overrides: {
2701
- runMode: options.runMode,
2702
- currentPhase: "waiting",
2703
- currentPublishDecision: gate.decision,
2704
- sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
2705
- higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2706
- dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2707
- competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
2708
- mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2709
- lastMempoolSequence: gate.lastMempoolSequence,
2710
- lastCompetitivenessGateAtUnixMs: Date.now(),
2711
- note: options.loopState.waitingNote,
2712
- },
2713
- visualizer: options.visualizer,
2714
- visualizerState: options.loopState.ui,
2917
+ await saveCycleStatus(effectiveReadContext, {
2918
+ runMode: options.runMode,
2919
+ currentPhase: "waiting",
2920
+ currentPublishDecision: gate.decision,
2921
+ sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
2922
+ higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2923
+ dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2924
+ competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
2925
+ mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2926
+ lastMempoolSequence: gate.lastMempoolSequence,
2927
+ lastCompetitivenessGateAtUnixMs: now(),
2928
+ note: options.loopState.waitingNote,
2715
2929
  });
2716
2930
  await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
2717
2931
  ? "publish-skipped-same-domain-mempool"
@@ -2740,19 +2954,12 @@ async function performMiningCycle(options) {
2740
2954
  if (!await ensureCurrentIndexerTruthOrRestart()) {
2741
2955
  return;
2742
2956
  }
2743
- await refreshAndSaveStatus({
2744
- paths: options.paths,
2745
- provider: options.provider,
2746
- readContext: effectiveReadContext,
2747
- overrides: {
2748
- runMode: options.runMode,
2749
- ...buildPrePublishStatusOverrides({
2750
- state: effectiveReadContext.localState.state,
2751
- candidate: selectedCandidate,
2752
- }),
2753
- },
2754
- visualizer: options.visualizer,
2755
- visualizerState: options.loopState.ui,
2957
+ await saveCycleStatus(effectiveReadContext, {
2958
+ runMode: options.runMode,
2959
+ ...buildPrePublishStatusOverrides({
2960
+ state: effectiveReadContext.localState.state,
2961
+ candidate: selectedCandidate,
2962
+ }),
2756
2963
  });
2757
2964
  const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
2758
2965
  purpose: "wallet-mine",
@@ -2783,32 +2990,25 @@ async function performMiningCycle(options) {
2783
2990
  if (published.retryable === true) {
2784
2991
  cacheSelectedCandidateForTip(options.loopState, tipKey, published.candidate);
2785
2992
  options.loopState.waitingNote = published.note;
2786
- await refreshAndSaveStatus({
2787
- paths: options.paths,
2788
- provider: options.provider,
2789
- readContext: {
2790
- ...effectiveReadContext,
2791
- localState: {
2792
- ...effectiveReadContext.localState,
2793
- state: published.state,
2794
- },
2795
- },
2796
- overrides: {
2797
- runMode: options.runMode,
2798
- currentPhase: "waiting",
2799
- currentPublishDecision: published.decision,
2800
- sameDomainCompetitorSuppressed: false,
2801
- higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
2802
- dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
2803
- competitivenessGateIndeterminate: false,
2804
- mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2805
- lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2806
- lastCompetitivenessGateAtUnixMs: Date.now(),
2807
- note: published.note,
2808
- livePublishInMempool: published.state.miningState.livePublishInMempool,
2993
+ await saveCycleStatus({
2994
+ ...effectiveReadContext,
2995
+ localState: {
2996
+ ...effectiveReadContext.localState,
2997
+ state: published.state,
2809
2998
  },
2810
- visualizer: options.visualizer,
2811
- visualizerState: options.loopState.ui,
2999
+ }, {
3000
+ runMode: options.runMode,
3001
+ currentPhase: "waiting",
3002
+ currentPublishDecision: published.decision,
3003
+ sameDomainCompetitorSuppressed: false,
3004
+ higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
3005
+ dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
3006
+ competitivenessGateIndeterminate: false,
3007
+ mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
3008
+ lastMempoolSequence: gateSnapshot.lastMempoolSequence,
3009
+ lastCompetitivenessGateAtUnixMs: now(),
3010
+ note: published.note,
3011
+ livePublishInMempool: published.state.miningState.livePublishInMempool,
2812
3012
  });
2813
3013
  return;
2814
3014
  }
@@ -2819,33 +3019,26 @@ async function performMiningCycle(options) {
2819
3019
  const lastError = published.decision === "publish-paused-insufficient-funds"
2820
3020
  ? published.lastError ?? createInsufficientFundsMiningPublishErrorMessage()
2821
3021
  : undefined;
2822
- await refreshAndSaveStatus({
2823
- paths: options.paths,
2824
- provider: options.provider,
2825
- readContext: {
2826
- ...effectiveReadContext,
2827
- localState: {
2828
- ...effectiveReadContext.localState,
2829
- state: published.state,
2830
- },
2831
- },
2832
- overrides: {
2833
- runMode: options.runMode,
2834
- currentPhase: "waiting",
2835
- currentPublishDecision: published.decision,
2836
- sameDomainCompetitorSuppressed: false,
2837
- higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
2838
- dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
2839
- competitivenessGateIndeterminate: false,
2840
- mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2841
- lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2842
- lastCompetitivenessGateAtUnixMs: Date.now(),
2843
- lastError,
2844
- note: published.note,
2845
- livePublishInMempool: published.state.miningState.livePublishInMempool,
3022
+ await saveCycleStatus({
3023
+ ...effectiveReadContext,
3024
+ localState: {
3025
+ ...effectiveReadContext.localState,
3026
+ state: published.state,
2846
3027
  },
2847
- visualizer: options.visualizer,
2848
- visualizerState: options.loopState.ui,
3028
+ }, {
3029
+ runMode: options.runMode,
3030
+ currentPhase: "waiting",
3031
+ currentPublishDecision: published.decision,
3032
+ sameDomainCompetitorSuppressed: false,
3033
+ higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
3034
+ dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
3035
+ competitivenessGateIndeterminate: false,
3036
+ mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
3037
+ lastMempoolSequence: gateSnapshot.lastMempoolSequence,
3038
+ lastCompetitivenessGateAtUnixMs: now(),
3039
+ lastError,
3040
+ note: published.note,
3041
+ livePublishInMempool: published.state.miningState.livePublishInMempool,
2849
3042
  });
2850
3043
  return;
2851
3044
  }
@@ -2861,32 +3054,25 @@ async function performMiningCycle(options) {
2861
3054
  : `Mining candidate ${published.decision === "replaced"
2862
3055
  ? "replaced"
2863
3056
  : "broadcast"} as ${published.txid}. Waiting for the next block.`;
2864
- await refreshAndSaveStatus({
2865
- paths: options.paths,
2866
- provider: options.provider,
2867
- readContext: {
2868
- ...effectiveReadContext,
2869
- localState: {
2870
- ...effectiveReadContext.localState,
2871
- state: published.state,
2872
- },
2873
- },
2874
- overrides: {
2875
- runMode: options.runMode,
2876
- currentPhase: "waiting",
2877
- currentPublishDecision: published.decision,
2878
- sameDomainCompetitorSuppressed: false,
2879
- higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
2880
- dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
2881
- competitivenessGateIndeterminate: false,
2882
- mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2883
- lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2884
- lastCompetitivenessGateAtUnixMs: Date.now(),
2885
- note: options.loopState.waitingNote,
2886
- livePublishInMempool: published.state.miningState.livePublishInMempool,
3057
+ await saveCycleStatus({
3058
+ ...effectiveReadContext,
3059
+ localState: {
3060
+ ...effectiveReadContext.localState,
3061
+ state: published.state,
2887
3062
  },
2888
- visualizer: options.visualizer,
2889
- visualizerState: options.loopState.ui,
3063
+ }, {
3064
+ runMode: options.runMode,
3065
+ currentPhase: "waiting",
3066
+ currentPublishDecision: published.decision,
3067
+ sameDomainCompetitorSuppressed: false,
3068
+ higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
3069
+ dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
3070
+ competitivenessGateIndeterminate: false,
3071
+ mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
3072
+ lastMempoolSequence: gateSnapshot.lastMempoolSequence,
3073
+ lastCompetitivenessGateAtUnixMs: now(),
3074
+ note: options.loopState.waitingNote,
3075
+ livePublishInMempool: published.state.miningState.livePublishInMempool,
2890
3076
  });
2891
3077
  }
2892
3078
  finally {
@@ -2895,6 +3081,7 @@ async function performMiningCycle(options) {
2895
3081
  }
2896
3082
  catch (error) {
2897
3083
  if (error instanceof MiningSuspendDetectedError) {
3084
+ discardMiningLoopTransientWork(options.loopState, readContext?.localState.walletRootId ?? undefined);
2898
3085
  if (readContext !== null && !readContextClosed) {
2899
3086
  await readContext.close();
2900
3087
  readContextClosed = true;
@@ -2910,6 +3097,24 @@ async function performMiningCycle(options) {
2910
3097
  detectedAtUnixMs: error.detectedAtUnixMs,
2911
3098
  openReadContext: options.openReadContext,
2912
3099
  visualizer: options.visualizer,
3100
+ loopState: options.loopState,
3101
+ });
3102
+ return;
3103
+ }
3104
+ if (readContext !== null && isRecoverableMiningBitcoindError(error)) {
3105
+ await handleRecoverableMiningBitcoindFailure({
3106
+ error,
3107
+ dataDir: options.dataDir,
3108
+ provider: options.provider,
3109
+ paths: options.paths,
3110
+ runMode: options.runMode,
3111
+ readContext,
3112
+ loopState: options.loopState,
3113
+ attachService: options.attachService,
3114
+ probeService: options.probeService,
3115
+ stopService: options.stopService,
3116
+ nowUnixMs: now(),
3117
+ visualizer: options.visualizer,
2913
3118
  });
2914
3119
  return;
2915
3120
  }
@@ -2935,7 +3140,6 @@ async function saveStopSnapshot(options) {
2935
3140
  dataDir: options.dataDir,
2936
3141
  chain: "main",
2937
3142
  startHeight: 0,
2938
- serviceLifetime: "ephemeral",
2939
3143
  walletRootId: localState.state.walletRootId,
2940
3144
  }).catch(() => null);
2941
3145
  if (service !== null) {
@@ -3012,6 +3216,9 @@ async function attemptSaveMempool(rpc, paths, runId) {
3012
3216
  async function runMiningLoop(options) {
3013
3217
  const suspendDetector = createMiningSuspendDetector();
3014
3218
  const loopState = createMiningLoopState();
3219
+ const probeService = options.probeService ?? probeManagedBitcoindService;
3220
+ const stopService = options.stopService ?? stopManagedBitcoindService;
3221
+ const sleepImpl = options.sleepImpl ?? sleep;
3015
3222
  await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
3016
3223
  runId: options.backgroundWorkerRunId,
3017
3224
  }));
@@ -3023,6 +3230,7 @@ async function runMiningLoop(options) {
3023
3230
  if (!(error instanceof MiningSuspendDetectedError)) {
3024
3231
  throw error;
3025
3232
  }
3233
+ discardMiningLoopTransientWork(loopState, null);
3026
3234
  await handleDetectedMiningRuntimeResume({
3027
3235
  dataDir: options.dataDir,
3028
3236
  databasePath: options.databasePath,
@@ -3034,6 +3242,7 @@ async function runMiningLoop(options) {
3034
3242
  detectedAtUnixMs: error.detectedAtUnixMs,
3035
3243
  openReadContext: options.openReadContext,
3036
3244
  visualizer: options.visualizer,
3245
+ loopState,
3037
3246
  });
3038
3247
  continue;
3039
3248
  }
@@ -3041,14 +3250,15 @@ async function runMiningLoop(options) {
3041
3250
  ...options,
3042
3251
  suspendDetector,
3043
3252
  loopState,
3253
+ probeService,
3254
+ stopService,
3044
3255
  });
3045
- await sleep(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
3256
+ await sleepImpl(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
3046
3257
  }
3047
3258
  const service = await options.attachService({
3048
3259
  dataDir: options.dataDir,
3049
3260
  chain: "main",
3050
3261
  startHeight: 0,
3051
- serviceLifetime: "ephemeral",
3052
3262
  walletRootId: undefined,
3053
3263
  }).catch(() => null);
3054
3264
  if (service !== null) {
@@ -3117,6 +3327,8 @@ export async function runForegroundMining(options) {
3117
3327
  sleepImpl: options.sleepImpl,
3118
3328
  });
3119
3329
  visualizer = new MiningFollowVisualizer({
3330
+ clientVersion: options.clientVersion,
3331
+ updateAvailable: options.updateAvailable,
3120
3332
  progressOutput: options.progressOutput ?? "auto",
3121
3333
  stream: options.stderr,
3122
3334
  });
@@ -3346,7 +3558,10 @@ export async function runBackgroundMiningWorker(options) {
3346
3558
  });
3347
3559
  }
3348
3560
  export async function handleDetectedMiningRuntimeResumeForTesting(options) {
3349
- await handleDetectedMiningRuntimeResume(options);
3561
+ await handleDetectedMiningRuntimeResume({
3562
+ ...options,
3563
+ loopState: options.loopState ?? createMiningLoopState(),
3564
+ });
3350
3565
  }
3351
3566
  export async function takeOverMiningRuntimeForTesting(options) {
3352
3567
  return await takeOverMiningRuntime(options);
@@ -3354,9 +3569,16 @@ export async function takeOverMiningRuntimeForTesting(options) {
3354
3569
  export async function performMiningCycleForTesting(options) {
3355
3570
  await performMiningCycle({
3356
3571
  ...options,
3572
+ probeService: options.probeService ?? probeManagedBitcoindService,
3573
+ stopService: options.stopService ?? stopManagedBitcoindService,
3357
3574
  loopState: options.loopState ?? createMiningLoopState(),
3358
3575
  });
3359
3576
  }
3577
+ export async function runMiningLoopForTesting(options) {
3578
+ await runMiningLoop({
3579
+ ...options,
3580
+ });
3581
+ }
3360
3582
  export function buildPrePublishStatusOverridesForTesting(options) {
3361
3583
  return buildPrePublishStatusOverrides(options);
3362
3584
  }