@cogcoin/client 0.5.14 → 1.0.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 (172) hide show
  1. package/README.md +80 -25
  2. package/dist/app-paths.d.ts +5 -6
  3. package/dist/app-paths.js +8 -16
  4. package/dist/art/balance.txt +10 -0
  5. package/dist/art/welcome.txt +16 -0
  6. package/dist/bitcoind/bootstrap/controller.d.ts +1 -0
  7. package/dist/bitcoind/bootstrap/controller.js +53 -1
  8. package/dist/bitcoind/client/follow-block-times.d.ts +1 -0
  9. package/dist/bitcoind/client/follow-block-times.js +1 -1
  10. package/dist/bitcoind/client/internal-types.d.ts +7 -3
  11. package/dist/bitcoind/client/managed-client.d.ts +4 -2
  12. package/dist/bitcoind/client/managed-client.js +14 -0
  13. package/dist/bitcoind/client/sync-engine.js +72 -11
  14. package/dist/bitcoind/hash-order.d.ts +4 -0
  15. package/dist/bitcoind/hash-order.js +13 -0
  16. package/dist/bitcoind/indexer-daemon-main.js +11 -3
  17. package/dist/bitcoind/normalize.js +3 -2
  18. package/dist/bitcoind/processing-start-height.d.ts +5 -0
  19. package/dist/bitcoind/processing-start-height.js +7 -0
  20. package/dist/bitcoind/progress/constants.d.ts +4 -0
  21. package/dist/bitcoind/progress/constants.js +4 -0
  22. package/dist/bitcoind/progress/controller.d.ts +2 -1
  23. package/dist/bitcoind/progress/controller.js +3 -3
  24. package/dist/bitcoind/progress/follow-scene.d.ts +6 -2
  25. package/dist/bitcoind/progress/follow-scene.js +29 -6
  26. package/dist/bitcoind/progress/formatting.d.ts +1 -0
  27. package/dist/bitcoind/progress/formatting.js +6 -0
  28. package/dist/bitcoind/progress/train-scene.js +37 -18
  29. package/dist/bitcoind/progress/tty-renderer.d.ts +6 -1
  30. package/dist/bitcoind/progress/tty-renderer.js +8 -4
  31. package/dist/bitcoind/rpc.d.ts +2 -1
  32. package/dist/bitcoind/rpc.js +3 -0
  33. package/dist/bitcoind/types.d.ts +16 -0
  34. package/dist/bytes.d.ts +1 -0
  35. package/dist/bytes.js +3 -0
  36. package/dist/cli/art.d.ts +2 -0
  37. package/dist/cli/art.js +37 -0
  38. package/dist/cli/commands/client-admin.d.ts +2 -0
  39. package/dist/cli/commands/client-admin.js +91 -0
  40. package/dist/cli/commands/follow.js +0 -2
  41. package/dist/cli/commands/mining-admin.js +6 -47
  42. package/dist/cli/commands/mining-read.js +11 -50
  43. package/dist/cli/commands/mining-runtime.js +38 -3
  44. package/dist/cli/commands/service-runtime.js +0 -2
  45. package/dist/cli/commands/status.js +8 -2
  46. package/dist/cli/commands/sync.js +51 -4
  47. package/dist/cli/commands/wallet-admin.js +142 -136
  48. package/dist/cli/commands/wallet-mutation.js +91 -79
  49. package/dist/cli/commands/wallet-read.js +15 -18
  50. package/dist/cli/context.js +4 -14
  51. package/dist/cli/mining-format.d.ts +0 -1
  52. package/dist/cli/mining-format.js +5 -37
  53. package/dist/cli/mining-json.d.ts +0 -18
  54. package/dist/cli/mining-json.js +0 -35
  55. package/dist/cli/mutation-command-groups.d.ts +1 -2
  56. package/dist/cli/mutation-command-groups.js +0 -5
  57. package/dist/cli/mutation-json.d.ts +24 -145
  58. package/dist/cli/mutation-json.js +30 -136
  59. package/dist/cli/mutation-resolved-json.d.ts +0 -7
  60. package/dist/cli/mutation-resolved-json.js +4 -10
  61. package/dist/cli/mutation-success.d.ts +2 -0
  62. package/dist/cli/mutation-success.js +11 -1
  63. package/dist/cli/mutation-text-format.js +1 -3
  64. package/dist/cli/output.d.ts +1 -1
  65. package/dist/cli/output.js +254 -231
  66. package/dist/cli/parse.d.ts +1 -1
  67. package/dist/cli/parse.js +93 -122
  68. package/dist/cli/preview-json.d.ts +17 -120
  69. package/dist/cli/preview-json.js +14 -97
  70. package/dist/cli/prompt.js +8 -13
  71. package/dist/cli/read-json.d.ts +15 -37
  72. package/dist/cli/read-json.js +44 -140
  73. package/dist/cli/runner.js +10 -13
  74. package/dist/cli/types.d.ts +8 -17
  75. package/dist/cli/types.js +0 -2
  76. package/dist/cli/wallet-format.d.ts +1 -0
  77. package/dist/cli/wallet-format.js +205 -144
  78. package/dist/cli/workflow-hints.d.ts +3 -3
  79. package/dist/cli/workflow-hints.js +11 -8
  80. package/dist/client/default-client.d.ts +3 -1
  81. package/dist/client/default-client.js +45 -2
  82. package/dist/client/factory.js +1 -1
  83. package/dist/client/initialization.js +23 -0
  84. package/dist/client/persistence.js +5 -5
  85. package/dist/client/store-adapter.js +1 -0
  86. package/dist/sqlite/checkpoints.d.ts +1 -0
  87. package/dist/sqlite/checkpoints.js +7 -0
  88. package/dist/sqlite/store.js +14 -1
  89. package/dist/types.d.ts +1 -0
  90. package/dist/wallet/coin-control.d.ts +41 -11
  91. package/dist/wallet/coin-control.js +100 -357
  92. package/dist/wallet/descriptor-normalization.d.ts +1 -3
  93. package/dist/wallet/descriptor-normalization.js +0 -16
  94. package/dist/wallet/lifecycle.d.ts +7 -99
  95. package/dist/wallet/lifecycle.js +513 -968
  96. package/dist/wallet/managed-core-wallet.d.ts +13 -0
  97. package/dist/wallet/managed-core-wallet.js +20 -0
  98. package/dist/wallet/mining/constants.d.ts +5 -12
  99. package/dist/wallet/mining/constants.js +5 -12
  100. package/dist/wallet/mining/control.d.ts +1 -13
  101. package/dist/wallet/mining/control.js +45 -349
  102. package/dist/wallet/mining/index.d.ts +3 -4
  103. package/dist/wallet/mining/index.js +1 -2
  104. package/dist/wallet/mining/runner.d.ts +179 -6
  105. package/dist/wallet/mining/runner.js +891 -501
  106. package/dist/wallet/mining/runtime-artifacts.js +23 -3
  107. package/dist/wallet/mining/sentence-protocol.d.ts +44 -0
  108. package/dist/wallet/mining/sentence-protocol.js +123 -0
  109. package/dist/wallet/mining/sentences.d.ts +4 -8
  110. package/dist/wallet/mining/sentences.js +3 -52
  111. package/dist/wallet/mining/state.d.ts +11 -6
  112. package/dist/wallet/mining/state.js +7 -6
  113. package/dist/wallet/mining/types.d.ts +2 -30
  114. package/dist/wallet/mining/visualizer.d.ts +31 -3
  115. package/dist/wallet/mining/visualizer.js +135 -13
  116. package/dist/wallet/read/context.d.ts +0 -2
  117. package/dist/wallet/read/context.js +119 -140
  118. package/dist/wallet/read/filter.js +2 -11
  119. package/dist/wallet/read/index.d.ts +1 -1
  120. package/dist/wallet/read/project.js +24 -77
  121. package/dist/wallet/read/types.d.ts +10 -25
  122. package/dist/wallet/reset.d.ts +0 -1
  123. package/dist/wallet/reset.js +60 -138
  124. package/dist/wallet/root-resolution.d.ts +1 -5
  125. package/dist/wallet/root-resolution.js +0 -18
  126. package/dist/wallet/runtime.d.ts +0 -6
  127. package/dist/wallet/runtime.js +0 -8
  128. package/dist/wallet/state/client-password-agent.js +208 -0
  129. package/dist/wallet/state/client-password.d.ts +65 -0
  130. package/dist/wallet/state/client-password.js +952 -0
  131. package/dist/wallet/state/crypto.d.ts +1 -20
  132. package/dist/wallet/state/crypto.js +0 -63
  133. package/dist/wallet/state/provider.d.ts +23 -11
  134. package/dist/wallet/state/provider.js +248 -290
  135. package/dist/wallet/state/storage.d.ts +2 -2
  136. package/dist/wallet/state/storage.js +48 -16
  137. package/dist/wallet/tx/anchor.d.ts +3 -28
  138. package/dist/wallet/tx/anchor.js +349 -1240
  139. package/dist/wallet/tx/bitcoin-transfer.d.ts +35 -0
  140. package/dist/wallet/tx/bitcoin-transfer.js +200 -0
  141. package/dist/wallet/tx/cog.d.ts +5 -1
  142. package/dist/wallet/tx/cog.js +149 -185
  143. package/dist/wallet/tx/common.d.ts +74 -10
  144. package/dist/wallet/tx/common.js +315 -138
  145. package/dist/wallet/tx/domain-admin.d.ts +3 -1
  146. package/dist/wallet/tx/domain-admin.js +61 -99
  147. package/dist/wallet/tx/domain-market.d.ts +5 -1
  148. package/dist/wallet/tx/domain-market.js +221 -228
  149. package/dist/wallet/tx/field.d.ts +4 -10
  150. package/dist/wallet/tx/field.js +84 -914
  151. package/dist/wallet/tx/identity-selector.d.ts +9 -3
  152. package/dist/wallet/tx/identity-selector.js +17 -35
  153. package/dist/wallet/tx/index.d.ts +3 -1
  154. package/dist/wallet/tx/index.js +2 -1
  155. package/dist/wallet/tx/register.d.ts +3 -1
  156. package/dist/wallet/tx/register.js +62 -220
  157. package/dist/wallet/tx/reputation.d.ts +3 -1
  158. package/dist/wallet/tx/reputation.js +58 -95
  159. package/dist/wallet/types.d.ts +8 -122
  160. package/package.json +5 -5
  161. package/dist/wallet/archive.d.ts +0 -4
  162. package/dist/wallet/archive.js +0 -41
  163. package/dist/wallet/mining/hook-protocol.d.ts +0 -47
  164. package/dist/wallet/mining/hook-protocol.js +0 -161
  165. package/dist/wallet/mining/hook-runner.js +0 -52
  166. package/dist/wallet/mining/hooks.d.ts +0 -38
  167. package/dist/wallet/mining/hooks.js +0 -520
  168. package/dist/wallet/state/explicit-lock.d.ts +0 -4
  169. package/dist/wallet/state/explicit-lock.js +0 -19
  170. package/dist/wallet/state/session.d.ts +0 -12
  171. package/dist/wallet/state/session.js +0 -23
  172. /package/dist/wallet/{mining/hook-runner.d.ts → state/client-password-agent.d.ts} +0 -0
@@ -1,29 +1,29 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { fileURLToPath } from "node:url";
4
- import { lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
4
+ import { getBalance, getBlockWinners, lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
5
5
  import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
6
6
  import { probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
7
+ import { FOLLOW_VISIBLE_PRIOR_BLOCKS } from "../../bitcoind/client/follow-block-times.js";
7
8
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
8
9
  import { createRpcClient } from "../../bitcoind/node.js";
9
10
  import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
10
11
  import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
11
- import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, saveWalletStatePreservingUnlock, } from "../tx/common.js";
12
+ import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
12
13
  import { acquireFileLock } from "../fs/lock.js";
13
- import { loadOrAutoUnlockWalletState } from "../lifecycle.js";
14
14
  import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
15
15
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
16
16
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
17
17
  import { serializeMine } from "../cogop/index.js";
18
18
  import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
19
19
  import { loadClientConfig } from "./config.js";
20
- import { MINING_HOOK_COOLDOWN_MS, MINING_HOOK_FAILURE_THRESHOLD, 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";
20
+ 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
21
  import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
22
22
  import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
23
- import { clearMiningFamilyState, miningFamilyMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
24
- import { createGenerateSentencesHookLimits } from "./hook-protocol.js";
23
+ import { clearMiningPublishState, miningPublishIsInMempool, miningPublishMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
24
+ import { createMiningSentenceRequestLimits } from "./sentence-protocol.js";
25
25
  import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
26
- import { MiningFollowVisualizer } from "./visualizer.js";
26
+ import { createEmptyMiningFollowVisualizerState, MiningFollowVisualizer, } from "./visualizer.js";
27
27
  const BEST_BLOCK_POLL_INTERVAL_MS = 500;
28
28
  const BACKGROUND_START_TIMEOUT_MS = 15_000;
29
29
  class MiningSuspendDetectedError extends Error {
@@ -33,6 +33,14 @@ class MiningSuspendDetectedError extends Error {
33
33
  this.detectedAtUnixMs = detectedAtUnixMs;
34
34
  }
35
35
  }
36
+ class MiningPublishRejectedError extends Error {
37
+ revertedState;
38
+ constructor(message, revertedState) {
39
+ super(message);
40
+ this.name = "MiningPublishRejectedError";
41
+ this.revertedState = revertedState;
42
+ }
43
+ }
36
44
  const miningGateCache = new Map();
37
45
  function createMiningSuspendDetector(monotonicNow = performance.now()) {
38
46
  return {
@@ -116,11 +124,7 @@ function cloneMiningState(state) {
116
124
  };
117
125
  }
118
126
  function hasBlockingMutation(state) {
119
- return state.proactiveFamilies.some((family) => family.status === "draft"
120
- || family.status === "broadcasting"
121
- || family.status === "broadcast-unknown"
122
- || family.status === "live"
123
- || family.status === "repair-required") || (state.pendingMutations ?? []).some((mutation) => mutation.status === "draft"
127
+ return (state.pendingMutations ?? []).some((mutation) => mutation.status === "draft"
124
128
  || mutation.status === "broadcasting"
125
129
  || mutation.status === "broadcast-unknown"
126
130
  || mutation.status === "live"
@@ -174,6 +178,197 @@ function numberToSats(value) {
174
178
  function satsToBtc(value) {
175
179
  return Number(value) / 100_000_000;
176
180
  }
181
+ function compareLexicographically(left, right) {
182
+ const length = Math.min(left.length, right.length);
183
+ for (let index = 0; index < length; index += 1) {
184
+ if (left[index] !== right[index]) {
185
+ return left[index] < right[index] ? -1 : 1;
186
+ }
187
+ }
188
+ if (left.length === right.length) {
189
+ return 0;
190
+ }
191
+ return left.length < right.length ? -1 : 1;
192
+ }
193
+ function tieBreakHash(blendSeed, miningDomainId) {
194
+ return createHash("sha256")
195
+ .update(Buffer.from(blendSeed))
196
+ .update(uint32BigEndian(miningDomainId))
197
+ .digest();
198
+ }
199
+ function createMiningLoopState() {
200
+ return {
201
+ attemptedTipKey: null,
202
+ currentTipKey: null,
203
+ selectedCandidateTipKey: null,
204
+ selectedCandidate: null,
205
+ ui: createEmptyMiningFollowVisualizerState(),
206
+ waitingNote: null,
207
+ };
208
+ }
209
+ export function createMiningLoopStateForTesting() {
210
+ return createMiningLoopState();
211
+ }
212
+ function buildMiningTipKey(bestBlockHash, targetBlockHeight) {
213
+ if (bestBlockHash === null || targetBlockHeight === null) {
214
+ return null;
215
+ }
216
+ return `${bestBlockHash}:${targetBlockHeight}`;
217
+ }
218
+ function resetMiningUiForTip(loopState, targetBlockHeight) {
219
+ const preservedTxid = loopState.ui.latestTxid;
220
+ loopState.ui = {
221
+ ...createEmptyMiningFollowVisualizerState(),
222
+ latestTxid: preservedTxid,
223
+ };
224
+ loopState.selectedCandidateTipKey = null;
225
+ loopState.selectedCandidate = null;
226
+ loopState.waitingNote = null;
227
+ }
228
+ export function resetMiningUiForTipForTesting(loopState, targetBlockHeight) {
229
+ resetMiningUiForTip(loopState, targetBlockHeight);
230
+ }
231
+ function fallbackSettledWinnerDomainName(domainId) {
232
+ return `domain-${domainId}`;
233
+ }
234
+ function resolveCurrentMinedBlockBoard(options) {
235
+ const settledBlockHeight = options.nodeBestHeight ?? options.snapshotTipHeight ?? null;
236
+ if (settledBlockHeight === null) {
237
+ return {
238
+ settledBlockHeight,
239
+ settledBoardEntries: [],
240
+ };
241
+ }
242
+ if (options.snapshotState === null || options.snapshotState === undefined) {
243
+ return {
244
+ settledBlockHeight,
245
+ settledBoardEntries: [],
246
+ };
247
+ }
248
+ if (options.nodeBestHeight !== null && (options.snapshotTipHeight ?? -1) < options.nodeBestHeight) {
249
+ return {
250
+ settledBlockHeight,
251
+ settledBoardEntries: [],
252
+ };
253
+ }
254
+ const settledBoardEntries = (getBlockWinners(options.snapshotState, settledBlockHeight) ?? [])
255
+ .slice()
256
+ .sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
257
+ .slice(0, 5)
258
+ .map((winner) => ({
259
+ rank: winner.rank,
260
+ domainName: lookupDomainById(options.snapshotState, winner.domainId)?.name ?? fallbackSettledWinnerDomainName(winner.domainId),
261
+ sentence: winner.sentenceText ?? "[unavailable]",
262
+ }));
263
+ return {
264
+ settledBlockHeight,
265
+ settledBoardEntries,
266
+ };
267
+ }
268
+ export function resolveSettledBoardForTesting(options) {
269
+ return resolveCurrentMinedBlockBoard(options);
270
+ }
271
+ function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight, nodeBestHeight) {
272
+ const settledBoard = resolveCurrentMinedBlockBoard({
273
+ snapshotState,
274
+ snapshotTipHeight,
275
+ nodeBestHeight,
276
+ });
277
+ loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
278
+ loopState.ui.settledBoardEntries = settledBoard.settledBoardEntries;
279
+ }
280
+ function setMiningUiCandidate(loopState, candidate) {
281
+ loopState.ui.latestSentence = candidate.sentence;
282
+ loopState.ui.provisionalRequiredWords = [...candidate.bip39Words];
283
+ loopState.ui.provisionalEntry = {
284
+ domainName: candidate.domainName,
285
+ sentence: candidate.sentence,
286
+ };
287
+ }
288
+ function getSelectedCandidateForTip(loopState, tipKey) {
289
+ if (tipKey === null || loopState.selectedCandidateTipKey !== tipKey) {
290
+ return null;
291
+ }
292
+ return loopState.selectedCandidate;
293
+ }
294
+ export function getSelectedCandidateForTipForTesting(loopState, tipKey) {
295
+ return getSelectedCandidateForTip(loopState, tipKey);
296
+ }
297
+ function cacheSelectedCandidateForTip(loopState, tipKey, candidate) {
298
+ loopState.selectedCandidateTipKey = tipKey;
299
+ loopState.selectedCandidate = candidate;
300
+ setMiningUiCandidate(loopState, candidate);
301
+ }
302
+ export function cacheSelectedCandidateForTipForTesting(loopState, tipKey, candidate) {
303
+ cacheSelectedCandidateForTip(loopState, tipKey, candidate);
304
+ }
305
+ function clearSelectedCandidate(loopState) {
306
+ loopState.selectedCandidateTipKey = null;
307
+ loopState.selectedCandidate = null;
308
+ }
309
+ async function resolveFundingDisplaySats(state, rpc) {
310
+ const utxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
311
+ return utxos.reduce((sum, entry) => {
312
+ if (entry.scriptPubKey !== state.funding.scriptPubKeyHex
313
+ || entry.spendable === false) {
314
+ return sum;
315
+ }
316
+ return sum + numberToSats(entry.amount);
317
+ }, 0n);
318
+ }
319
+ export async function resolveFundingDisplaySatsForTesting(state, rpc) {
320
+ return resolveFundingDisplaySats(state, rpc);
321
+ }
322
+ async function loadMiningVisibleFollowBlockTimes(options) {
323
+ if (options.indexedTipHeight === null || options.indexedTipHashHex === null) {
324
+ return {};
325
+ }
326
+ const blockTimesByHeight = {};
327
+ let currentHeight = options.indexedTipHeight;
328
+ let currentHashHex = options.indexedTipHashHex;
329
+ for (let offset = 0; offset <= FOLLOW_VISIBLE_PRIOR_BLOCKS; offset += 1) {
330
+ if (currentHeight < 0 || currentHashHex === null) {
331
+ break;
332
+ }
333
+ const block = await options.rpc.getBlock(currentHashHex);
334
+ if (typeof block.time === "number") {
335
+ blockTimesByHeight[currentHeight] = block.time;
336
+ }
337
+ currentHashHex = block.previousblockhash ?? null;
338
+ currentHeight -= 1;
339
+ }
340
+ return blockTimesByHeight;
341
+ }
342
+ export async function loadMiningVisibleFollowBlockTimesForTesting(options) {
343
+ return loadMiningVisibleFollowBlockTimes(options);
344
+ }
345
+ function syncMiningVisualizerBalances(loopState, readContext, balanceSats) {
346
+ loopState.ui.balanceCogtoshi = readContext.snapshot === null
347
+ ? null
348
+ : getBalance(readContext.snapshot.state, readContext.localState.state.funding.scriptPubKeyHex);
349
+ loopState.ui.balanceSats = balanceSats;
350
+ }
351
+ function syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight) {
352
+ loopState.ui.visibleBlockTimesByHeight = { ...blockTimesByHeight };
353
+ }
354
+ export function syncMiningVisualizerBlockTimesForTesting(loopState, blockTimesByHeight) {
355
+ syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight);
356
+ }
357
+ function findRecentMiningWin(snapshotState, txid, targetBlockHeight) {
358
+ if (snapshotState === null || snapshotState === undefined || txid === null || targetBlockHeight === null) {
359
+ return null;
360
+ }
361
+ const winners = getBlockWinners(snapshotState, targetBlockHeight) ?? [];
362
+ const winner = winners.find((entry) => entry.txidHex === txid) ?? null;
363
+ if (winner === null) {
364
+ return null;
365
+ }
366
+ return {
367
+ rank: winner.rank,
368
+ rewardCogtoshi: winner.rewardCogtoshi,
369
+ blockHeight: winner.height,
370
+ };
371
+ }
177
372
  function computeIntentFingerprint(state, candidate) {
178
373
  return createHash("sha256")
179
374
  .update([
@@ -465,7 +660,7 @@ function buildStatusSnapshot(view, overrides = {}) {
465
660
  lastCompetitivenessGateAtUnixMs: overrides.lastCompetitivenessGateAtUnixMs ?? view.runtime.lastCompetitivenessGateAtUnixMs,
466
661
  lastError: overrides.lastError ?? view.runtime.lastError,
467
662
  note: overrides.note ?? view.runtime.note,
468
- liveMiningFamilyInMempool: overrides.liveMiningFamilyInMempool ?? view.runtime.liveMiningFamilyInMempool,
663
+ livePublishInMempool: overrides.livePublishInMempool ?? view.runtime.livePublishInMempool,
469
664
  updatedAtUnixMs: Date.now(),
470
665
  };
471
666
  }
@@ -481,7 +676,7 @@ async function refreshAndSaveStatus(options) {
481
676
  });
482
677
  const snapshot = buildStatusSnapshot(view, options.overrides);
483
678
  await saveMiningRuntimeStatus(options.paths.miningStatusPath, snapshot);
484
- options.visualizer?.update(snapshot);
679
+ options.visualizer?.update(snapshot, options.visualizerState);
485
680
  return snapshot;
486
681
  }
487
682
  async function appendEvent(paths, event) {
@@ -576,7 +771,9 @@ function createMiningPlan(options) {
576
771
  && entry.confirmations >= 1
577
772
  && entry.spendable !== false
578
773
  && entry.safe !== false
579
- && !(entry.txid === options.conflictOutpoint.txid && entry.vout === options.conflictOutpoint.vout));
774
+ && !(options.conflictOutpoint !== null
775
+ && entry.txid === options.conflictOutpoint.txid
776
+ && entry.vout === options.conflictOutpoint.vout));
580
777
  const opReturnData = serializeMine(options.candidate.domainId, options.candidate.referencedBlockHashInternal, options.candidate.encodedSentenceBytes).opReturnData;
581
778
  const expectedOpReturnScriptHex = Buffer.concat([
582
779
  Buffer.from([0x6a, opReturnData.length]),
@@ -584,19 +781,11 @@ function createMiningPlan(options) {
584
781
  ]).toString("hex");
585
782
  return {
586
783
  sender: options.candidate.sender,
587
- fixedInputs: [
588
- options.candidate.anchorOutpoint,
589
- options.conflictOutpoint,
590
- ],
591
- outputs: [
592
- { data: Buffer.from(opReturnData).toString("hex") },
593
- { [options.candidate.sender.address]: satsToBtc(BigInt(options.state.anchorValueSats)) },
594
- ],
784
+ fixedInputs: options.conflictOutpoint === null ? [] : [options.conflictOutpoint],
785
+ outputs: [{ data: Buffer.from(opReturnData).toString("hex") }],
595
786
  changeAddress: options.state.funding.address,
596
- changePosition: 2,
787
+ changePosition: 1,
597
788
  expectedOpReturnScriptHex,
598
- expectedAnchorScriptHex: options.candidate.sender.scriptPubKeyHex,
599
- expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
600
789
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
601
790
  eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => walletMutationOutpointKey({ txid: entry.txid, vout: entry.vout }))),
602
791
  expectedConflictOutpoint: options.conflictOutpoint,
@@ -606,34 +795,18 @@ function createMiningPlan(options) {
606
795
  function validateMiningDraft(decoded, funded, plan) {
607
796
  const inputs = decoded.tx.vin;
608
797
  const outputs = decoded.tx.vout;
609
- if (inputs.length < 2) {
798
+ if (inputs.length === 0) {
610
799
  throw new Error("wallet_mining_missing_inputs");
611
800
  }
612
801
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_mining_missing_inputs");
613
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
614
- throw new Error("wallet_mining_sender_input_mismatch");
615
- }
616
- if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex
617
- || inputs[1]?.txid !== plan.expectedConflictOutpoint.txid
618
- || inputs[1].vout !== plan.expectedConflictOutpoint.vout) {
802
+ if (plan.expectedConflictOutpoint !== null
803
+ && (inputs[0]?.txid !== plan.expectedConflictOutpoint.txid
804
+ || inputs[0]?.vout !== plan.expectedConflictOutpoint.vout)) {
619
805
  throw new Error("wallet_mining_conflict_input_mismatch");
620
806
  }
621
- assertFundingInputsAfterFixedPrefix({
622
- inputs,
623
- fixedInputs: plan.fixedInputs,
624
- allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
625
- eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
626
- errorCode: "wallet_mining_unexpected_funding_input",
627
- });
628
807
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
629
808
  throw new Error("wallet_mining_opreturn_mismatch");
630
809
  }
631
- if (outputs[1]?.scriptPubKey?.hex !== plan.expectedAnchorScriptHex) {
632
- throw new Error("wallet_mining_anchor_output_mismatch");
633
- }
634
- if (numberToSats(outputs[1]?.value ?? 0) !== plan.expectedAnchorValueSats) {
635
- throw new Error("wallet_mining_anchor_value_mismatch");
636
- }
637
810
  if (funded.changepos !== -1 && (funded.changepos !== plan.changePosition || outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex)) {
638
811
  throw new Error("wallet_mining_change_output_mismatch");
639
812
  }
@@ -650,6 +823,12 @@ async function buildMiningTransaction(options) {
650
823
  feeRate: options.plan.feeRateSatVb,
651
824
  });
652
825
  }
826
+ export function createMiningPlanForTesting(options) {
827
+ return createMiningPlan(options);
828
+ }
829
+ export function validateMiningDraftForTesting(decoded, funded, plan) {
830
+ validateMiningDraft(decoded, funded, plan);
831
+ }
653
832
  function resolveEligibleAnchoredRoots(context) {
654
833
  const state = context.localState.state;
655
834
  const model = context.model;
@@ -662,15 +841,11 @@ function resolveEligibleAnchoredRoots(context) {
662
841
  if (!isMineableWalletDomain(context, domain)) {
663
842
  continue;
664
843
  }
665
- const localRecord = state.domains.find((entry) => entry.name === domain.name);
666
- const ownerIdentity = model.identities.find((identity) => identity.index === domain.ownerLocalIndex);
667
844
  const domainId = domain.domainId;
668
845
  if (domainId === null
669
846
  || domainId === undefined
670
- || localRecord?.currentCanonicalAnchorOutpoint === null
671
- || localRecord?.currentCanonicalAnchorOutpoint === undefined
672
- || ownerIdentity?.address == null
673
- || ownerIdentity.readOnly) {
847
+ || domain.ownerAddress == null
848
+ || domain.ownerScriptPubKeyHex !== model.walletScriptPubKeyHex) {
674
849
  continue;
675
850
  }
676
851
  const chainDomain = lookupDomain(snapshot.state, domain.name);
@@ -680,60 +855,47 @@ function resolveEligibleAnchoredRoots(context) {
680
855
  domains.push({
681
856
  domainId,
682
857
  domainName: domain.name,
683
- localIndex: ownerIdentity.index,
858
+ localIndex: 0,
684
859
  sender: {
685
- localIndex: ownerIdentity.index,
686
- scriptPubKeyHex: ownerIdentity.scriptPubKeyHex,
687
- address: ownerIdentity.address,
688
- },
689
- anchorOutpoint: {
690
- txid: localRecord.currentCanonicalAnchorOutpoint.txid,
691
- vout: localRecord.currentCanonicalAnchorOutpoint.vout,
860
+ localIndex: 0,
861
+ scriptPubKeyHex: model.walletScriptPubKeyHex,
862
+ address: domain.ownerAddress,
692
863
  },
693
864
  });
694
865
  }
695
866
  return domains.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName));
696
867
  }
697
- async function persistCustomHookRuntimeOutcome(options) {
698
- const hookState = options.readContext.localState.state.hookClientState.mining;
699
- if (hookState.mode !== "custom") {
700
- return false;
701
- }
702
- if (options.success) {
703
- if ((hookState.consecutiveFailureCount ?? 0) === 0 && hookState.cooldownUntilUnixMs === null) {
704
- return false;
705
- }
706
- options.readContext.localState.state.hookClientState.mining = {
707
- ...hookState,
708
- consecutiveFailureCount: 0,
709
- cooldownUntilUnixMs: null,
710
- };
711
- await saveWalletStatePreservingUnlock({
712
- state: options.readContext.localState.state,
713
- provider: options.provider,
714
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
715
- nowUnixMs: options.nowUnixMs,
716
- paths: options.paths,
717
- });
718
- return false;
868
+ function refreshMiningCandidateFromCurrentState(context, candidate) {
869
+ const refreshed = resolveEligibleAnchoredRoots(context).find((domain) => domain.domainId === candidate.domainId);
870
+ if (refreshed === undefined) {
871
+ return null;
719
872
  }
720
- const consecutiveFailureCount = (hookState.consecutiveFailureCount ?? 0) + 1;
721
- const cooldownUntilUnixMs = consecutiveFailureCount >= MINING_HOOK_FAILURE_THRESHOLD
722
- ? options.nowUnixMs + MINING_HOOK_COOLDOWN_MS
723
- : null;
724
- options.readContext.localState.state.hookClientState.mining = {
725
- ...hookState,
726
- consecutiveFailureCount,
727
- cooldownUntilUnixMs,
873
+ return {
874
+ ...candidate,
875
+ domainName: refreshed.domainName,
876
+ localIndex: refreshed.localIndex,
877
+ sender: refreshed.sender,
728
878
  };
729
- await saveWalletStatePreservingUnlock({
730
- state: options.readContext.localState.state,
731
- provider: options.provider,
732
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
733
- nowUnixMs: options.nowUnixMs,
734
- paths: options.paths,
735
- });
736
- return cooldownUntilUnixMs !== null && cooldownUntilUnixMs > options.nowUnixMs;
879
+ }
880
+ export function refreshMiningCandidateFromCurrentStateForTesting(context, candidate) {
881
+ return refreshMiningCandidateFromCurrentState(context, candidate);
882
+ }
883
+ function resolveMiningConflictOutpoint(options) {
884
+ const normalizedMiningState = normalizeMiningStateRecord(options.state.miningState);
885
+ if (miningPublishIsInMempool(normalizedMiningState) && normalizedMiningState.sharedMiningConflictOutpoint !== null) {
886
+ return { ...normalizedMiningState.sharedMiningConflictOutpoint };
887
+ }
888
+ void options.allUtxos;
889
+ return null;
890
+ }
891
+ export function resolveMiningConflictOutpointForTesting(options) {
892
+ return resolveMiningConflictOutpoint(options);
893
+ }
894
+ function createStaleMiningCandidateWaitingNote() {
895
+ return "Mining candidate changed before broadcast: the selected root domain is no longer locally mineable. Skipping this tip and waiting for the next block.";
896
+ }
897
+ function createRetryableMiningPublishWaitingNote() {
898
+ return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
737
899
  }
738
900
  async function generateCandidatesForDomains(options) {
739
901
  const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
@@ -791,7 +953,7 @@ async function generateCandidatesForDomains(options) {
791
953
  referencedBlockHashDisplay: bestBlockHash,
792
954
  generatedAtUnixMs: Date.now(),
793
955
  extraPrompt: null,
794
- limits: createGenerateSentencesHookLimits(),
956
+ limits: createMiningSentenceRequestLimits(),
795
957
  rootDomains: rootDomains.map((domain) => ({
796
958
  domainId: domain.domainId,
797
959
  domainName: domain.domainName,
@@ -803,7 +965,6 @@ async function generateCandidatesForDomains(options) {
803
965
  generated = await generateMiningSentences(generationRequest, {
804
966
  paths: options.paths,
805
967
  provider: options.provider,
806
- hookState: options.readContext.localState.state.hookClientState.mining,
807
968
  signal: abortController.signal,
808
969
  fetchImpl: options.fetchImpl,
809
970
  });
@@ -833,15 +994,6 @@ async function generateCandidatesForDomains(options) {
833
994
  dataDir: options.readContext.dataDir,
834
995
  truthKey: options.indexerTruthKey,
835
996
  });
836
- if (generated.hookMode === "custom") {
837
- await persistCustomHookRuntimeOutcome({
838
- readContext: options.readContext,
839
- provider: options.provider,
840
- paths: options.paths,
841
- nowUnixMs: Date.now(),
842
- success: true,
843
- });
844
- }
845
997
  const sentencesByDomain = new Map();
846
998
  for (const candidate of generated.candidates) {
847
999
  const existing = sentencesByDomain.get(candidate.domainId) ?? [];
@@ -864,7 +1016,6 @@ async function generateCandidatesForDomains(options) {
864
1016
  domainName: domain.domainName,
865
1017
  localIndex: domain.localIndex,
866
1018
  sender: domain.sender,
867
- anchorOutpoint: domain.anchorOutpoint,
868
1019
  sentence: best.sentence,
869
1020
  encodedSentenceBytes: best.encodedSentenceBytes,
870
1021
  bip39WordIndices: [...best.bip39WordIndices],
@@ -914,6 +1065,46 @@ async function chooseBestLocalCandidate(candidates) {
914
1065
  }
915
1066
  return candidates.find((candidate) => candidate.domainId === winner.miningDomainId) ?? null;
916
1067
  }
1068
+ function isBetterVisibleCompetitor(candidate, current) {
1069
+ if (current === undefined) {
1070
+ return true;
1071
+ }
1072
+ if (candidate.canonicalBlend !== current.canonicalBlend) {
1073
+ return candidate.canonicalBlend > current.canonicalBlend;
1074
+ }
1075
+ if (candidate.effectiveFeeRate !== current.effectiveFeeRate) {
1076
+ return candidate.effectiveFeeRate > current.effectiveFeeRate;
1077
+ }
1078
+ return candidate.txid.localeCompare(current.txid) < 0;
1079
+ }
1080
+ function rankMiningSentenceEntries(entries, blendSeed) {
1081
+ return entries
1082
+ .map((entry) => ({
1083
+ ...entry,
1084
+ tieBreak: tieBreakHash(blendSeed, entry.domainId),
1085
+ }))
1086
+ .sort((left, right) => {
1087
+ if (left.canonicalBlend !== right.canonicalBlend) {
1088
+ return left.canonicalBlend > right.canonicalBlend ? -1 : 1;
1089
+ }
1090
+ const tieBreakOrder = compareLexicographically(left.tieBreak, right.tieBreak);
1091
+ if (tieBreakOrder !== 0) {
1092
+ return tieBreakOrder;
1093
+ }
1094
+ return left.txIndex - right.txIndex;
1095
+ })
1096
+ .map((entry, index) => ({
1097
+ ...entry,
1098
+ rank: index + 1,
1099
+ }));
1100
+ }
1101
+ function toSentenceBoardEntries(entries) {
1102
+ return entries.slice(0, 5).map((entry) => ({
1103
+ rank: entry.rank,
1104
+ domainName: entry.domainName,
1105
+ sentence: entry.sentence,
1106
+ }));
1107
+ }
917
1108
  async function runCompetitivenessGate(options) {
918
1109
  const createDecision = (overrides) => ({
919
1110
  allowed: overrides.allowed ?? false,
@@ -924,10 +1115,11 @@ async function runCompetitivenessGate(options) {
924
1115
  competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? false,
925
1116
  mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? null,
926
1117
  lastMempoolSequence: overrides.lastMempoolSequence ?? null,
1118
+ visibleBoardEntries: overrides.visibleBoardEntries ?? [],
1119
+ candidateRank: overrides.candidateRank ?? null,
927
1120
  });
928
1121
  const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
929
1122
  const indexerTruthKey = getIndexerTruthKey(options.readContext);
930
- const localFeeTarget = DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB;
931
1123
  const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
932
1124
  const localAssayTupleKey = [
933
1125
  options.candidate.domainId,
@@ -959,7 +1151,6 @@ async function runCompetitivenessGate(options) {
959
1151
  && cachedTruthMatches
960
1152
  && cachedReferencedBlockMatches
961
1153
  && cached.localAssayTupleKey === localAssayTupleKey
962
- && cached.currentFeeTargetSatVb === localFeeTarget
963
1154
  && cached.excludedTxidsKey === excludedTxids.join(",")
964
1155
  && cached.mempoolSequence === mempoolSequence) {
965
1156
  return {
@@ -1005,7 +1196,7 @@ async function runCompetitivenessGate(options) {
1005
1196
  const entries = new Map();
1006
1197
  for (const txid of visibleTxids) {
1007
1198
  const context = txContexts.get(txid);
1008
- if (context === undefined || context.effectiveFeeRate < localFeeTarget || context.payload === null || context.senderScriptHex === null) {
1199
+ if (context === undefined || context.payload === null || context.senderScriptHex === null) {
1009
1200
  continue;
1010
1201
  }
1011
1202
  const decoded = decodeMinePayload(context.payload);
@@ -1031,7 +1222,6 @@ async function runCompetitivenessGate(options) {
1031
1222
  indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
1032
1223
  referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1033
1224
  localAssayTupleKey,
1034
- currentFeeTargetSatVb: localFeeTarget,
1035
1225
  excludedTxidsKey: excludedTxids.join(","),
1036
1226
  mempoolSequence,
1037
1227
  txids: [...visibleTxids],
@@ -1052,26 +1242,46 @@ async function runCompetitivenessGate(options) {
1052
1242
  txid,
1053
1243
  effectiveFeeRate: context.effectiveFeeRate,
1054
1244
  domainId: decoded.domainId,
1245
+ domainName: overlayDomain.name,
1246
+ sentence: Buffer.from(decoded.sentenceBytes).toString("utf8"),
1055
1247
  senderScriptHex: context.senderScriptHex,
1056
1248
  encodedSentenceBytesHex: Buffer.from(scored.encodedSentenceBytes).toString("hex"),
1057
1249
  bip39WordIndices: [...scored.bip39WordIndices],
1058
1250
  canonicalBlend: scored.canonicalBlend,
1059
1251
  });
1060
1252
  }
1061
- const sameDomainCompetitors = [...entries.values()].filter((entry) => entry.domainId === options.candidate.domainId);
1253
+ const blendSeed = deriveBlendSeed(options.candidate.referencedBlockHashInternal);
1254
+ const visibleBestByDomain = new Map();
1255
+ for (const entry of entries.values()) {
1256
+ const current = visibleBestByDomain.get(entry.domainId);
1257
+ if (isBetterVisibleCompetitor(entry, current)) {
1258
+ visibleBestByDomain.set(entry.domainId, entry);
1259
+ }
1260
+ }
1261
+ const visibleRankedEntries = rankMiningSentenceEntries([...visibleBestByDomain.values()]
1262
+ .sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
1263
+ .map((entry, index) => ({
1264
+ domainId: entry.domainId,
1265
+ domainName: entry.domainName,
1266
+ sentence: entry.sentence,
1267
+ canonicalBlend: entry.canonicalBlend,
1268
+ senderScriptHex: entry.senderScriptHex,
1269
+ encodedSentenceBytesHex: entry.encodedSentenceBytesHex,
1270
+ bip39WordIndices: entry.bip39WordIndices,
1271
+ txid: entry.txid,
1272
+ txIndex: index,
1273
+ })), blendSeed);
1274
+ const sameDomainCompetitors = [...visibleBestByDomain.values()].filter((entry) => entry.domainId === options.candidate.domainId);
1062
1275
  const sameDomainCompetitorSuppressed = sameDomainCompetitors.some((competitor) => competitor.canonicalBlend > options.candidate.canonicalBlend
1063
1276
  || competitor.canonicalBlend === options.candidate.canonicalBlend);
1064
1277
  let decision;
1065
1278
  const otherDomainBest = new Map();
1066
- for (const entry of entries.values()) {
1279
+ for (const entry of visibleBestByDomain.values()) {
1067
1280
  if (entry.domainId === options.candidate.domainId) {
1068
1281
  continue;
1069
1282
  }
1070
1283
  const best = otherDomainBest.get(entry.domainId);
1071
- if (best === undefined
1072
- || entry.canonicalBlend > best.canonicalBlend
1073
- || (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate > best.effectiveFeeRate)
1074
- || (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate === best.effectiveFeeRate && entry.txid.localeCompare(best.txid) < 0)) {
1284
+ if (isBetterVisibleCompetitor(entry, best)) {
1075
1285
  otherDomainBest.set(entry.domainId, entry);
1076
1286
  }
1077
1287
  }
@@ -1085,38 +1295,41 @@ async function runCompetitivenessGate(options) {
1085
1295
  competitivenessGateIndeterminate: false,
1086
1296
  mempoolSequenceCacheStatus: "refreshed",
1087
1297
  lastMempoolSequence: mempoolSequence,
1298
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1088
1299
  });
1089
1300
  }
1090
1301
  else {
1091
1302
  try {
1092
- const submissions = [
1303
+ const candidateRankedEntries = rankMiningSentenceEntries([
1093
1304
  {
1094
- miningDomainId: options.candidate.domainId,
1095
- rawSentenceBytes: options.candidate.encodedSentenceBytes,
1096
- recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
1305
+ domainId: options.candidate.domainId,
1306
+ domainName: options.candidate.domainName,
1307
+ sentence: options.candidate.sentence,
1308
+ canonicalBlend: options.candidate.canonicalBlend,
1309
+ senderScriptHex: options.candidate.sender.scriptPubKeyHex,
1310
+ encodedSentenceBytesHex: Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
1097
1311
  bip39WordIndices: options.candidate.bip39WordIndices,
1312
+ txid: null,
1098
1313
  txIndex: 0,
1099
1314
  },
1100
1315
  ...[...otherDomainBest.values()]
1101
1316
  .sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
1102
1317
  .map((entry, index) => ({
1103
- miningDomainId: entry.domainId,
1104
- rawSentenceBytes: Buffer.from(entry.encodedSentenceBytesHex, "hex"),
1105
- recipientScriptPubKey: Buffer.from(entry.senderScriptHex, "hex"),
1318
+ domainId: entry.domainId,
1319
+ domainName: entry.domainName,
1320
+ sentence: entry.sentence,
1321
+ canonicalBlend: entry.canonicalBlend,
1322
+ senderScriptHex: entry.senderScriptHex,
1323
+ encodedSentenceBytesHex: entry.encodedSentenceBytesHex,
1106
1324
  bip39WordIndices: entry.bip39WordIndices,
1325
+ txid: entry.txid,
1107
1326
  txIndex: index + 1,
1108
1327
  })),
1109
- ];
1110
- const winners = await settleBlock({
1111
- blendSeed: deriveBlendSeed(options.candidate.referencedBlockHashInternal),
1112
- blockRewardCogtoshi: 100n,
1113
- submissions,
1114
- });
1115
- const localWinner = winners.find((winner) => winner.miningDomainId === options.candidate.domainId);
1116
- const higherRankedCompetitorDomainCount = localWinner === undefined
1117
- ? Math.max(0, winners.length - 1)
1118
- : Math.max(0, localWinner.rank - 1);
1119
- if (higherRankedCompetitorDomainCount >= 5) {
1328
+ ], blendSeed);
1329
+ const localEntry = candidateRankedEntries.find((entry) => entry.txid === null) ?? null;
1330
+ const candidateRank = localEntry?.rank ?? null;
1331
+ const higherRankedCompetitorDomainCount = candidateRank === null ? 0 : Math.max(0, candidateRank - 1);
1332
+ if (candidateRank !== null && candidateRank > 5) {
1120
1333
  decision = createDecision({
1121
1334
  allowed: false,
1122
1335
  decision: "suppressed-top5-mempool",
@@ -1126,11 +1339,13 @@ async function runCompetitivenessGate(options) {
1126
1339
  competitivenessGateIndeterminate: false,
1127
1340
  mempoolSequenceCacheStatus: "refreshed",
1128
1341
  lastMempoolSequence: mempoolSequence,
1342
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1343
+ candidateRank,
1129
1344
  });
1130
1345
  }
1131
1346
  else {
1132
1347
  decision = createDecision({
1133
- allowed: true,
1348
+ allowed: candidateRank !== null,
1134
1349
  decision: "publish",
1135
1350
  sameDomainCompetitorSuppressed: false,
1136
1351
  higherRankedCompetitorDomainCount,
@@ -1138,6 +1353,8 @@ async function runCompetitivenessGate(options) {
1138
1353
  competitivenessGateIndeterminate: false,
1139
1354
  mempoolSequenceCacheStatus: "refreshed",
1140
1355
  lastMempoolSequence: mempoolSequence,
1356
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1357
+ candidateRank,
1141
1358
  });
1142
1359
  }
1143
1360
  }
@@ -1151,6 +1368,7 @@ async function runCompetitivenessGate(options) {
1151
1368
  competitivenessGateIndeterminate: true,
1152
1369
  mempoolSequenceCacheStatus: "refreshed",
1153
1370
  lastMempoolSequence: mempoolSequence,
1371
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1154
1372
  });
1155
1373
  }
1156
1374
  }
@@ -1159,7 +1377,6 @@ async function runCompetitivenessGate(options) {
1159
1377
  indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
1160
1378
  referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1161
1379
  localAssayTupleKey,
1162
- currentFeeTargetSatVb: localFeeTarget,
1163
1380
  excludedTxidsKey: excludedTxids.join(","),
1164
1381
  mempoolSequence,
1165
1382
  txids: [...visibleTxids],
@@ -1168,74 +1385,14 @@ async function runCompetitivenessGate(options) {
1168
1385
  });
1169
1386
  return decision;
1170
1387
  }
1171
- function candidateOutranksLive(options) {
1172
- const liveState = normalizeMiningStateRecord(options.liveState);
1173
- const nextSentenceHex = Buffer.from(options.candidate.encodedSentenceBytes).toString("hex");
1174
- if (liveState.currentEncodedSentenceBytesHex === null) {
1175
- return true;
1176
- }
1177
- if (liveState.currentDomainId === options.candidate.domainId) {
1178
- if (liveState.currentEncodedSentenceBytesHex === nextSentenceHex) {
1179
- return false;
1180
- }
1181
- const currentScore = liveState.currentScore === null ? null : BigInt(liveState.currentScore);
1182
- return currentScore === null || options.candidate.canonicalBlend > currentScore;
1183
- }
1184
- return true;
1185
- }
1186
- function candidateMatchesLiveFamily(options) {
1388
+ function livePublishTargetsCandidateTip(options) {
1187
1389
  const liveState = normalizeMiningStateRecord(options.liveState);
1188
- return liveState.currentDomainId === options.candidate.domainId
1189
- && liveState.currentEncodedSentenceBytesHex === Buffer.from(options.candidate.encodedSentenceBytes).toString("hex")
1190
- && liveState.currentSenderScriptPubKeyHex === options.candidate.sender.scriptPubKeyHex
1390
+ return liveState.currentTxid !== null
1391
+ && liveState.currentPublishState === "in-mempool"
1392
+ && liveState.livePublishInMempool === true
1191
1393
  && liveState.currentReferencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay
1192
1394
  && liveState.currentBlockTargetHeight === options.candidate.targetBlockHeight;
1193
1395
  }
1194
- function candidateNeedsFeeMaintenance(options) {
1195
- const liveState = normalizeMiningStateRecord(options.liveState);
1196
- return candidateMatchesLiveFamily(options)
1197
- && liveState.currentTxid !== null
1198
- && liveState.currentFeeRateSatVb !== null
1199
- && liveState.currentPublishState === "in-mempool"
1200
- && liveState.liveMiningFamilyInMempool === true;
1201
- }
1202
- async function candidateWinsAgainstLive(options) {
1203
- const liveState = normalizeMiningStateRecord(options.liveState);
1204
- if (liveState.currentDomainId === null || liveState.currentEncodedSentenceBytesHex === null) {
1205
- return true;
1206
- }
1207
- if (liveState.currentDomainId === options.candidate.domainId) {
1208
- return candidateOutranksLive(options);
1209
- }
1210
- if (liveState.currentBip39WordIndices === null || liveState.currentSenderScriptPubKeyHex === null || liveState.currentBlendSeedHex === null) {
1211
- return true;
1212
- }
1213
- const settled = await settleBlock({
1214
- blendSeed: Buffer.from(liveState.currentBlendSeedHex, "hex"),
1215
- blockRewardCogtoshi: 100n,
1216
- submissions: [
1217
- {
1218
- miningDomainId: liveState.currentDomainId,
1219
- rawSentenceBytes: Buffer.from(liveState.currentEncodedSentenceBytesHex, "hex"),
1220
- recipientScriptPubKey: Buffer.from(liveState.currentSenderScriptPubKeyHex, "hex"),
1221
- bip39WordIndices: liveState.currentBip39WordIndices,
1222
- txIndex: 0,
1223
- },
1224
- {
1225
- miningDomainId: options.candidate.domainId,
1226
- rawSentenceBytes: options.candidate.encodedSentenceBytes,
1227
- recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
1228
- bip39WordIndices: options.candidate.bip39WordIndices,
1229
- txIndex: 1,
1230
- },
1231
- ],
1232
- });
1233
- const incumbent = settled.find((entry) => entry.miningDomainId === liveState.currentDomainId);
1234
- const challenger = settled.find((entry) => entry.miningDomainId === options.candidate.domainId);
1235
- return challenger !== undefined
1236
- && incumbent !== undefined
1237
- && challenger.rank < incumbent.rank;
1238
- }
1239
1396
  function miningCandidateIsCurrent(options) {
1240
1397
  return options.state.currentReferencedBlockHashDisplay !== null
1241
1398
  && options.nodeBestHash !== null
@@ -1250,14 +1407,17 @@ async function reconcileLiveMiningState(options) {
1250
1407
  miningState: normalizeMiningStateRecord(options.state.miningState),
1251
1408
  };
1252
1409
  const currentTxid = state.miningState.currentTxid;
1253
- if (currentTxid === null || !miningFamilyMayStillExist(state.miningState)) {
1410
+ if (currentTxid === null || !miningPublishMayStillExist(state.miningState)) {
1254
1411
  await reconcilePersistentPolicyLocks({
1255
1412
  rpc: options.rpc,
1256
1413
  walletName: state.managedCoreWallet.walletName,
1257
1414
  state,
1258
1415
  fixedInputs: [],
1259
1416
  });
1260
- return state;
1417
+ return {
1418
+ state,
1419
+ recentWin: null,
1420
+ };
1261
1421
  }
1262
1422
  const walletName = state.managedCoreWallet.walletName;
1263
1423
  const [mempoolVerbose, walletTx] = await Promise.all([
@@ -1269,10 +1429,11 @@ async function reconcileLiveMiningState(options) {
1269
1429
  ]);
1270
1430
  const inMempool = mempoolVerbose.txids.includes(currentTxid);
1271
1431
  if (walletTx !== null && walletTx.confirmations > 0) {
1432
+ const recentWin = findRecentMiningWin(options.snapshotState ?? null, currentTxid, state.miningState.currentBlockTargetHeight);
1272
1433
  state = {
1273
1434
  ...state,
1274
1435
  miningState: {
1275
- ...clearMiningFamilyState(state.miningState),
1436
+ ...clearMiningPublishState(state.miningState),
1276
1437
  currentPublishDecision: "tx-confirmed-while-down",
1277
1438
  },
1278
1439
  };
@@ -1282,7 +1443,10 @@ async function reconcileLiveMiningState(options) {
1282
1443
  state,
1283
1444
  fixedInputs: [],
1284
1445
  });
1285
- return state;
1446
+ return {
1447
+ state,
1448
+ recentWin,
1449
+ };
1286
1450
  }
1287
1451
  if (inMempool) {
1288
1452
  const stale = !miningCandidateIsCurrent({
@@ -1291,7 +1455,7 @@ async function reconcileLiveMiningState(options) {
1291
1455
  nodeBestHeight: options.nodeBestHeight,
1292
1456
  });
1293
1457
  state = defaultMiningStatePatch(state, {
1294
- liveMiningFamilyInMempool: true,
1458
+ livePublishInMempool: true,
1295
1459
  currentPublishState: "in-mempool",
1296
1460
  state: stale
1297
1461
  ? "paused-stale"
@@ -1303,7 +1467,7 @@ async function reconcileLiveMiningState(options) {
1303
1467
  : state.miningState.runMode === "stopped"
1304
1468
  ? "user-stopped"
1305
1469
  : null,
1306
- currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-family",
1470
+ currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-publish",
1307
1471
  });
1308
1472
  await reconcilePersistentPolicyLocks({
1309
1473
  rpc: options.rpc,
@@ -1311,7 +1475,10 @@ async function reconcileLiveMiningState(options) {
1311
1475
  state,
1312
1476
  fixedInputs: [],
1313
1477
  });
1314
- return state;
1478
+ return {
1479
+ state,
1480
+ recentWin: null,
1481
+ };
1315
1482
  }
1316
1483
  if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
1317
1484
  state = defaultMiningStatePatch(state, {
@@ -1319,7 +1486,7 @@ async function reconcileLiveMiningState(options) {
1319
1486
  pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
1320
1487
  ? "broadcast-unknown-conflict"
1321
1488
  : "wallet-conflict-observed",
1322
- liveMiningFamilyInMempool: false,
1489
+ livePublishInMempool: false,
1323
1490
  currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
1324
1491
  ? "repair-required-broadcast-conflict"
1325
1492
  : "repair-required-wallet-conflict",
@@ -1330,13 +1497,16 @@ async function reconcileLiveMiningState(options) {
1330
1497
  state,
1331
1498
  fixedInputs: [],
1332
1499
  });
1333
- return state;
1500
+ return {
1501
+ state,
1502
+ recentWin: null,
1503
+ };
1334
1504
  }
1335
1505
  state = defaultMiningStatePatch(state, {
1336
- ...clearMiningFamilyState(state.miningState),
1506
+ ...clearMiningPublishState(state.miningState),
1337
1507
  currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
1338
1508
  ? "broadcast-unknown-not-seen"
1339
- : "live-family-not-seen",
1509
+ : "live-publish-not-seen",
1340
1510
  });
1341
1511
  await reconcilePersistentPolicyLocks({
1342
1512
  rpc: options.rpc,
@@ -1344,9 +1514,12 @@ async function reconcileLiveMiningState(options) {
1344
1514
  state,
1345
1515
  fixedInputs: [],
1346
1516
  });
1347
- return state;
1517
+ return {
1518
+ state,
1519
+ recentWin: null,
1520
+ };
1348
1521
  }
1349
- async function publishCandidate(options) {
1522
+ async function publishCandidateOnce(options) {
1350
1523
  const service = await options.attachService({
1351
1524
  dataDir: options.dataDir,
1352
1525
  chain: "main",
@@ -1354,48 +1527,35 @@ async function publishCandidate(options) {
1354
1527
  walletRootId: options.readContext.localState.state.walletRootId,
1355
1528
  });
1356
1529
  const rpc = options.rpcFactory(service.rpc);
1357
- let state = await reconcileLiveMiningState({
1530
+ let state = (await reconcileLiveMiningState({
1358
1531
  state: options.readContext.localState.state,
1359
1532
  rpc,
1360
1533
  nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
1361
1534
  nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
1362
- });
1535
+ snapshotState: options.readContext.snapshot.state,
1536
+ })).state;
1363
1537
  const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
1364
- const fundingConflict = state.miningState.sharedMiningConflictOutpoint
1365
- ?? allUtxos.find((entry) => entry.scriptPubKey === state.funding.scriptPubKeyHex
1366
- && entry.confirmations >= 1
1367
- && entry.spendable !== false
1368
- && entry.safe !== false
1369
- && !(entry.txid === options.candidate.anchorOutpoint.txid && entry.vout === options.candidate.anchorOutpoint.vout));
1370
- if (fundingConflict === undefined || fundingConflict === null) {
1371
- throw new Error("wallet_mining_missing_conflict_utxo");
1372
- }
1373
- const conflictOutpoint = "txid" in fundingConflict
1374
- ? { txid: fundingConflict.txid, vout: fundingConflict.vout }
1375
- : fundingConflict;
1538
+ const conflictOutpoint = resolveMiningConflictOutpoint({
1539
+ state,
1540
+ allUtxos,
1541
+ });
1376
1542
  const priorMiningState = cloneMiningState(state.miningState);
1377
- const nextFeeRate = state.miningState.currentFeeRateSatVb === null
1378
- ? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB
1379
- : state.miningState.currentFeeRateSatVb + 1;
1380
- const shouldFeeBump = candidateNeedsFeeMaintenance({
1543
+ if (livePublishTargetsCandidateTip({
1381
1544
  liveState: state.miningState,
1382
1545
  candidate: options.candidate,
1383
- });
1384
- if (state.miningState.currentPublishState === "in-mempool"
1385
- && state.miningState.liveMiningFamilyInMempool === true
1386
- && !shouldFeeBump
1387
- && !await candidateWinsAgainstLive({
1388
- liveState: state.miningState,
1389
- candidate: options.candidate,
1390
- })) {
1546
+ })) {
1391
1547
  return {
1392
1548
  state: defaultMiningStatePatch(state, {
1393
- currentPublishDecision: "kept-live-family",
1549
+ currentPublishDecision: "kept-live-publish",
1394
1550
  }),
1395
1551
  txid: state.miningState.currentTxid,
1396
- decision: "kept-live-family",
1552
+ decision: "kept-live-publish",
1397
1553
  };
1398
1554
  }
1555
+ const feeSelection = await resolveWalletMutationFeeSelection({
1556
+ rpc,
1557
+ });
1558
+ const nextFeeRate = feeSelection.feeRateSatVb;
1399
1559
  const plan = createMiningPlan({
1400
1560
  state,
1401
1561
  candidate: options.candidate,
@@ -1430,18 +1590,14 @@ async function publishCandidate(options) {
1430
1590
  currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1431
1591
  currentIntentFingerprintHex: intentFingerprintHex,
1432
1592
  sharedMiningConflictOutpoint: conflictOutpoint,
1433
- liveMiningFamilyInMempool: null,
1593
+ livePublishInMempool: null,
1434
1594
  currentPublishDecision: priorMiningState.currentTxid === null
1435
1595
  ? "publishing"
1436
- : shouldFeeBump
1437
- ? "fee-bump"
1438
- : "replacing",
1596
+ : "replacing",
1439
1597
  });
1440
1598
  await saveWalletStatePreservingUnlock({
1441
1599
  state,
1442
1600
  provider: options.provider,
1443
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1444
- nowUnixMs: Date.now(),
1445
1601
  paths: options.paths,
1446
1602
  });
1447
1603
  try {
@@ -1451,13 +1607,11 @@ async function publishCandidate(options) {
1451
1607
  if (isAlreadyAcceptedError(error)) {
1452
1608
  state = defaultMiningStatePatch(state, {
1453
1609
  currentPublishState: "in-mempool",
1454
- liveMiningFamilyInMempool: true,
1610
+ livePublishInMempool: true,
1455
1611
  });
1456
1612
  await saveWalletStatePreservingUnlock({
1457
1613
  state,
1458
1614
  provider: options.provider,
1459
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1460
- nowUnixMs: Date.now(),
1461
1615
  paths: options.paths,
1462
1616
  });
1463
1617
  await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replacing" ? "tx-replaced" : "tx-broadcast", `Mining transaction ${built.txid} is already accepted by the local node.`, {
@@ -1474,11 +1628,9 @@ async function publishCandidate(options) {
1474
1628
  return {
1475
1629
  state,
1476
1630
  txid: built.txid,
1477
- decision: state.miningState.currentPublishDecision === "fee-bump"
1478
- ? "fee-bump"
1479
- : state.miningState.currentPublishDecision === "replacing"
1480
- ? "replaced"
1481
- : "broadcast",
1631
+ decision: state.miningState.currentPublishDecision === "replacing"
1632
+ ? "replaced"
1633
+ : "broadcast",
1482
1634
  };
1483
1635
  }
1484
1636
  if (isBroadcastUnknownError(error)) {
@@ -1489,8 +1641,6 @@ async function publishCandidate(options) {
1489
1641
  await saveWalletStatePreservingUnlock({
1490
1642
  state,
1491
1643
  provider: options.provider,
1492
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1493
- nowUnixMs: Date.now(),
1494
1644
  paths: options.paths,
1495
1645
  });
1496
1646
  await appendEvent(options.paths, createEvent("error", `Mining broadcast became uncertain for ${built.txid}.`, {
@@ -1512,7 +1662,16 @@ async function publishCandidate(options) {
1512
1662
  decision: "broadcast-unknown",
1513
1663
  };
1514
1664
  }
1515
- throw error;
1665
+ state = {
1666
+ ...state,
1667
+ miningState: cloneMiningState(priorMiningState),
1668
+ };
1669
+ await saveWalletStatePreservingUnlock({
1670
+ state,
1671
+ provider: options.provider,
1672
+ paths: options.paths,
1673
+ });
1674
+ throw new MiningPublishRejectedError(error instanceof Error ? error.message : String(error), state);
1516
1675
  }
1517
1676
  const absoluteFeeSats = numberToSats(built.funded.fee);
1518
1677
  const replacementCount = priorMiningState.currentTxid === null
@@ -1520,12 +1679,10 @@ async function publishCandidate(options) {
1520
1679
  : priorMiningState.replacementCount + 1;
1521
1680
  state = defaultMiningStatePatch(state, {
1522
1681
  currentPublishState: "in-mempool",
1523
- liveMiningFamilyInMempool: true,
1524
- currentPublishDecision: state.miningState.currentPublishDecision === "fee-bump"
1525
- ? "fee-bump"
1526
- : state.miningState.currentPublishDecision === "replacing"
1527
- ? "replaced"
1528
- : "broadcast",
1682
+ livePublishInMempool: true,
1683
+ currentPublishDecision: state.miningState.currentPublishDecision === "replacing"
1684
+ ? "replaced"
1685
+ : "broadcast",
1529
1686
  replacementCount,
1530
1687
  currentAbsoluteFeeSats: Number(absoluteFeeSats),
1531
1688
  currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
@@ -1535,19 +1692,13 @@ async function publishCandidate(options) {
1535
1692
  await saveWalletStatePreservingUnlock({
1536
1693
  state,
1537
1694
  provider: options.provider,
1538
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1539
- nowUnixMs: Date.now(),
1540
1695
  paths: options.paths,
1541
1696
  });
1542
1697
  await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replaced"
1543
1698
  ? "tx-replaced"
1544
- : state.miningState.currentPublishDecision === "fee-bump"
1545
- ? "tx-fee-bump"
1546
- : "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
1699
+ : "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
1547
1700
  ? "Replaced"
1548
- : state.miningState.currentPublishDecision === "fee-bump"
1549
- ? "Fee-bumped"
1550
- : "Broadcast"} mining transaction ${built.txid}.`, {
1701
+ : "Broadcast"} mining transaction ${built.txid}.`, {
1551
1702
  runId: options.runId,
1552
1703
  targetBlockHeight: options.candidate.targetBlockHeight,
1553
1704
  referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
@@ -1561,21 +1712,106 @@ async function publishCandidate(options) {
1561
1712
  return {
1562
1713
  state,
1563
1714
  txid: built.txid,
1564
- decision: state.miningState.currentPublishDecision === "fee-bump"
1565
- ? "fee-bump"
1566
- : state.miningState.currentPublishDecision === "replaced"
1567
- ? "replaced"
1568
- : "broadcast",
1715
+ decision: state.miningState.currentPublishDecision === "replaced"
1716
+ ? "replaced"
1717
+ : "broadcast",
1569
1718
  };
1570
1719
  }
1571
- async function ensureBuiltInSetupIfNeeded(options) {
1572
- const unlocked = await loadOrAutoUnlockWalletState({
1573
- provider: options.provider,
1720
+ async function publishCandidate(options) {
1721
+ const publishAttempt = options.publishAttempt ?? publishCandidateOnce;
1722
+ const appendEventFn = options.appendEventFn ?? appendEvent;
1723
+ const createStaleCandidateSkipResult = async (state) => {
1724
+ const note = createStaleMiningCandidateWaitingNote();
1725
+ await appendEventFn(options.paths, createEvent("publish-skipped-stale-candidate", "Skipped mining publish for the current tip because the selected root domain is no longer locally mineable.", {
1726
+ level: "warn",
1727
+ runId: options.runId,
1728
+ targetBlockHeight: options.candidate.targetBlockHeight,
1729
+ referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1730
+ domainId: options.candidate.domainId,
1731
+ domainName: options.candidate.domainName,
1732
+ score: options.candidate.canonicalBlend.toString(),
1733
+ reason: "candidate-unavailable",
1734
+ }));
1735
+ return {
1736
+ state,
1737
+ txid: null,
1738
+ decision: "publish-skipped-stale-candidate",
1739
+ note,
1740
+ skipped: true,
1741
+ candidate: null,
1742
+ };
1743
+ };
1744
+ const lockedReadContext = await options.openReadContext({
1745
+ dataDir: options.dataDir,
1746
+ databasePath: options.databasePath,
1747
+ secretProvider: options.provider,
1748
+ walletControlLockHeld: true,
1574
1749
  paths: options.paths,
1575
1750
  });
1576
- if (unlocked?.state.hookClientState.mining.mode === "custom") {
1577
- return true;
1751
+ try {
1752
+ if (lockedReadContext.localState.availability !== "ready"
1753
+ || lockedReadContext.localState.state === null
1754
+ || lockedReadContext.snapshot === null
1755
+ || lockedReadContext.model === null) {
1756
+ return await createStaleCandidateSkipResult(options.fallbackState);
1757
+ }
1758
+ const readyReadContext = lockedReadContext;
1759
+ const refreshedCandidate = refreshMiningCandidateFromCurrentState(readyReadContext, options.candidate);
1760
+ if (refreshedCandidate === null) {
1761
+ return await createStaleCandidateSkipResult(readyReadContext.localState.state);
1762
+ }
1763
+ try {
1764
+ const published = await publishAttempt({
1765
+ readContext: readyReadContext,
1766
+ candidate: refreshedCandidate,
1767
+ dataDir: options.dataDir,
1768
+ provider: options.provider,
1769
+ paths: options.paths,
1770
+ attachService: options.attachService,
1771
+ rpcFactory: options.rpcFactory,
1772
+ runId: options.runId,
1773
+ });
1774
+ return {
1775
+ ...published,
1776
+ candidate: refreshedCandidate,
1777
+ };
1778
+ }
1779
+ catch (error) {
1780
+ if (error instanceof Error && error.message === "wallet_mining_mempool_rejected_missing-inputs") {
1781
+ const note = createRetryableMiningPublishWaitingNote();
1782
+ const revertedState = error instanceof MiningPublishRejectedError
1783
+ ? error.revertedState
1784
+ : readyReadContext.localState.state;
1785
+ await appendEventFn(options.paths, createEvent("publish-retry-pending", "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.", {
1786
+ level: "warn",
1787
+ runId: options.runId,
1788
+ targetBlockHeight: refreshedCandidate.targetBlockHeight,
1789
+ referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
1790
+ domainId: refreshedCandidate.domainId,
1791
+ domainName: refreshedCandidate.domainName,
1792
+ score: refreshedCandidate.canonicalBlend.toString(),
1793
+ reason: "missing-inputs",
1794
+ }));
1795
+ return {
1796
+ state: revertedState,
1797
+ txid: null,
1798
+ decision: "publish-retry-pending",
1799
+ note,
1800
+ retryable: true,
1801
+ candidate: refreshedCandidate,
1802
+ };
1803
+ }
1804
+ throw error;
1805
+ }
1578
1806
  }
1807
+ finally {
1808
+ await lockedReadContext.close();
1809
+ }
1810
+ }
1811
+ export async function publishCandidateForTesting(options) {
1812
+ return await publishCandidate(options);
1813
+ }
1814
+ async function ensureBuiltInSetupIfNeeded(options) {
1579
1815
  const config = await loadClientConfig({
1580
1816
  path: options.paths.clientConfigPath,
1581
1817
  provider: options.provider,
@@ -1614,7 +1850,7 @@ async function performMiningCycle(options) {
1614
1850
  backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
1615
1851
  },
1616
1852
  });
1617
- if (readContext.localState.availability !== "ready" || readContext.localState.state === null || readContext.localState.unlockUntilUnixMs === null) {
1853
+ if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
1618
1854
  await refreshAndSaveStatus({
1619
1855
  paths: options.paths,
1620
1856
  provider: options.provider,
@@ -1622,9 +1858,10 @@ async function performMiningCycle(options) {
1622
1858
  overrides: {
1623
1859
  runMode: options.runMode,
1624
1860
  currentPhase: "waiting",
1625
- note: "Wallet must stay unlocked for mining to continue.",
1861
+ note: "Wallet state must be locally available for mining to continue.",
1626
1862
  },
1627
1863
  visualizer: options.visualizer,
1864
+ visualizerState: options.loopState.ui,
1628
1865
  });
1629
1866
  return;
1630
1867
  }
@@ -1636,20 +1873,20 @@ async function performMiningCycle(options) {
1636
1873
  });
1637
1874
  checkpointMiningSuspendDetector(options.suspendDetector);
1638
1875
  const rpc = options.rpcFactory(service.rpc);
1639
- const reconciledState = await reconcileLiveMiningState({
1876
+ const reconciliation = await reconcileLiveMiningState({
1640
1877
  state: readContext.localState.state,
1641
1878
  rpc,
1642
1879
  nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
1643
1880
  nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
1881
+ snapshotState: readContext.snapshot?.state ?? null,
1644
1882
  });
1883
+ const reconciledState = reconciliation.state;
1645
1884
  checkpointMiningSuspendDetector(options.suspendDetector);
1646
1885
  let effectiveReadContext = readContext;
1647
1886
  if (JSON.stringify(reconciledState.miningState) !== JSON.stringify(readContext.localState.state.miningState)) {
1648
1887
  await saveWalletStatePreservingUnlock({
1649
1888
  state: reconciledState,
1650
1889
  provider: options.provider,
1651
- unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1652
- nowUnixMs: Date.now(),
1653
1890
  paths: options.paths,
1654
1891
  });
1655
1892
  effectiveReadContext = {
@@ -1657,11 +1894,23 @@ async function performMiningCycle(options) {
1657
1894
  localState: {
1658
1895
  ...readContext.localState,
1659
1896
  availability: "ready",
1660
- unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1661
1897
  state: reconciledState,
1662
1898
  },
1663
1899
  };
1664
1900
  }
1901
+ if (reconciliation.recentWin !== null) {
1902
+ options.loopState.ui.recentWin = reconciliation.recentWin;
1903
+ }
1904
+ if (effectiveReadContext.localState.state.miningState.currentTxid !== null) {
1905
+ options.loopState.ui.latestTxid = effectiveReadContext.localState.state.miningState.currentTxid;
1906
+ }
1907
+ const indexedTip = effectiveReadContext.snapshot?.tip ?? effectiveReadContext.indexer.snapshotTip ?? null;
1908
+ const visibleBlockTimes = await loadMiningVisibleFollowBlockTimes({
1909
+ rpc,
1910
+ indexedTipHeight: indexedTip?.height ?? null,
1911
+ indexedTipHashHex: indexedTip?.blockHashHex ?? null,
1912
+ }).catch(() => ({}));
1913
+ syncMiningVisualizerBlockTimes(options.loopState, visibleBlockTimes);
1665
1914
  if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
1666
1915
  await refreshAndSaveStatus({
1667
1916
  paths: options.paths,
@@ -1670,9 +1919,10 @@ async function performMiningCycle(options) {
1670
1919
  overrides: {
1671
1920
  runMode: options.runMode,
1672
1921
  currentPhase: "waiting",
1673
- note: "Mining is blocked until the current mining family is repaired or reconciled.",
1922
+ note: "Mining is blocked until the current mining publish is repaired or reconciled.",
1674
1923
  },
1675
1924
  visualizer: options.visualizer,
1925
+ visualizerState: options.loopState.ui,
1676
1926
  });
1677
1927
  return;
1678
1928
  }
@@ -1684,8 +1934,6 @@ async function performMiningCycle(options) {
1684
1934
  await saveWalletStatePreservingUnlock({
1685
1935
  state: nextState,
1686
1936
  provider: options.provider,
1687
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1688
- nowUnixMs: Date.now(),
1689
1937
  paths: options.paths,
1690
1938
  });
1691
1939
  effectiveReadContext = {
@@ -1693,7 +1941,6 @@ async function performMiningCycle(options) {
1693
1941
  localState: {
1694
1942
  ...effectiveReadContext.localState,
1695
1943
  availability: "ready",
1696
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1697
1944
  state: nextState,
1698
1945
  },
1699
1946
  };
@@ -1704,16 +1951,17 @@ async function performMiningCycle(options) {
1704
1951
  overrides: {
1705
1952
  runMode: options.runMode,
1706
1953
  currentPhase: "waiting",
1707
- note: "Mining is paused while another wallet mutation family is active.",
1954
+ note: "Mining is paused while another wallet mutation is active.",
1708
1955
  },
1709
1956
  visualizer: options.visualizer,
1957
+ visualizerState: options.loopState.ui,
1710
1958
  });
1711
1959
  return;
1712
1960
  }
1713
1961
  const preemptionRequest = await readMiningPreemptionRequest(options.paths);
1714
1962
  if (preemptionRequest !== null) {
1715
1963
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
1716
- state: effectiveReadContext.localState.state.miningState.liveMiningFamilyInMempool
1964
+ state: effectiveReadContext.localState.state.miningState.livePublishInMempool
1717
1965
  && effectiveReadContext.localState.state.miningState.state === "paused-stale"
1718
1966
  ? "paused-stale"
1719
1967
  : "paused",
@@ -1722,8 +1970,6 @@ async function performMiningCycle(options) {
1722
1970
  await saveWalletStatePreservingUnlock({
1723
1971
  state: nextState,
1724
1972
  provider: options.provider,
1725
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1726
- nowUnixMs: Date.now(),
1727
1973
  paths: options.paths,
1728
1974
  });
1729
1975
  await refreshAndSaveStatus({
@@ -1742,6 +1988,7 @@ async function performMiningCycle(options) {
1742
1988
  note: "Mining is paused while another wallet command is preempting sentence generation.",
1743
1989
  },
1744
1990
  visualizer: options.visualizer,
1991
+ visualizerState: options.loopState.ui,
1745
1992
  });
1746
1993
  return;
1747
1994
  }
@@ -1768,6 +2015,7 @@ async function performMiningCycle(options) {
1768
2015
  note: "Mining is waiting for the local Bitcoin node to become publishable.",
1769
2016
  },
1770
2017
  visualizer: options.visualizer,
2018
+ visualizerState: options.loopState.ui,
1771
2019
  });
1772
2020
  return;
1773
2021
  }
@@ -1786,10 +2034,22 @@ async function performMiningCycle(options) {
1786
2034
  : "Mining is waiting for the local Bitcoin node to become publishable.",
1787
2035
  },
1788
2036
  visualizer: options.visualizer,
2037
+ visualizerState: options.loopState.ui,
1789
2038
  });
1790
2039
  return;
1791
2040
  }
1792
2041
  const targetBlockHeight = (effectiveReadContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
2042
+ const tipKey = buildMiningTipKey(effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null, targetBlockHeight);
2043
+ if (tipKey !== options.loopState.currentTipKey) {
2044
+ options.loopState.currentTipKey = tipKey;
2045
+ resetMiningUiForTip(options.loopState, targetBlockHeight);
2046
+ if (reconciliation.recentWin !== null) {
2047
+ options.loopState.ui.recentWin = reconciliation.recentWin;
2048
+ }
2049
+ }
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);
1793
2053
  if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
1794
2054
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
1795
2055
  state: "paused",
@@ -1798,8 +2058,6 @@ async function performMiningCycle(options) {
1798
2058
  await saveWalletStatePreservingUnlock({
1799
2059
  state: nextState,
1800
2060
  provider: options.provider,
1801
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1802
- nowUnixMs: Date.now(),
1803
2061
  paths: options.paths,
1804
2062
  });
1805
2063
  await refreshAndSaveStatus({
@@ -1819,6 +2077,7 @@ async function performMiningCycle(options) {
1819
2077
  note: "Mining is disabled because the target block reward is zero.",
1820
2078
  },
1821
2079
  visualizer: options.visualizer,
2080
+ visualizerState: options.loopState.ui,
1822
2081
  });
1823
2082
  await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
1824
2083
  targetBlockHeight,
@@ -1827,18 +2086,18 @@ async function performMiningCycle(options) {
1827
2086
  }));
1828
2087
  return;
1829
2088
  }
1830
- const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
1831
- if (domains.length === 0) {
2089
+ if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
1832
2090
  await refreshAndSaveStatus({
1833
2091
  paths: options.paths,
1834
2092
  provider: options.provider,
1835
2093
  readContext: effectiveReadContext,
1836
2094
  overrides: {
1837
2095
  runMode: options.runMode,
1838
- currentPhase: "idle",
1839
- note: "No locally controlled anchored root domains are currently eligible to mine.",
2096
+ currentPhase: "waiting",
2097
+ note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
1840
2098
  },
1841
2099
  visualizer: options.visualizer,
2100
+ visualizerState: options.loopState.ui,
1842
2101
  });
1843
2102
  return;
1844
2103
  }
@@ -1866,215 +2125,264 @@ async function performMiningCycle(options) {
1866
2125
  return false;
1867
2126
  }
1868
2127
  };
1869
- await refreshAndSaveStatus({
1870
- paths: options.paths,
1871
- provider: options.provider,
1872
- readContext: effectiveReadContext,
1873
- overrides: {
1874
- runMode: options.runMode,
1875
- currentPhase: "generating",
1876
- note: "Generating mining sentences for eligible root domains.",
1877
- },
1878
- visualizer: options.visualizer,
1879
- });
1880
- await appendEvent(options.paths, createEvent("hook-request-start", "Started mining sentence generation.", {
1881
- targetBlockHeight,
1882
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1883
- runId: options.backgroundWorkerRunId,
1884
- }));
1885
- let candidates;
1886
- try {
1887
- candidates = await generateCandidatesForDomains({
1888
- rpc,
1889
- readContext: effectiveReadContext,
1890
- domains,
1891
- provider: options.provider,
1892
- paths: options.paths,
1893
- indexerTruthKey,
1894
- runId: options.backgroundWorkerRunId,
1895
- fetchImpl: options.fetchImpl,
1896
- });
1897
- checkpointMiningSuspendDetector(options.suspendDetector);
1898
- }
1899
- catch (error) {
1900
- if (error instanceof MiningProviderRequestError) {
2128
+ let selectedCandidate = getSelectedCandidateForTip(options.loopState, tipKey);
2129
+ let gateSnapshot = {
2130
+ higherRankedCompetitorDomainCount: 0,
2131
+ dedupedCompetitorDomainCount: 0,
2132
+ mempoolSequenceCacheStatus: null,
2133
+ lastMempoolSequence: null,
2134
+ };
2135
+ if (selectedCandidate === null) {
2136
+ const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
2137
+ if (domains.length === 0) {
1901
2138
  await refreshAndSaveStatus({
1902
2139
  paths: options.paths,
1903
2140
  provider: options.provider,
1904
2141
  readContext: effectiveReadContext,
1905
2142
  overrides: {
1906
2143
  runMode: options.runMode,
1907
- currentPhase: "waiting-provider",
1908
- providerState: error.providerState,
1909
- lastError: error.message,
1910
- note: "Mining is waiting for the sentence provider to recover.",
2144
+ currentPhase: "idle",
2145
+ note: "No locally controlled anchored root domains are currently eligible to mine.",
1911
2146
  },
1912
2147
  visualizer: options.visualizer,
2148
+ visualizerState: options.loopState.ui,
1913
2149
  });
1914
- await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
1915
- level: "warn",
1916
- targetBlockHeight,
1917
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1918
- runId: options.backgroundWorkerRunId,
1919
- }));
1920
2150
  return;
1921
2151
  }
1922
- if (error instanceof Error && error.message === "mining_generation_stale_tip") {
1923
- await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
1924
- level: "warn",
1925
- targetBlockHeight,
1926
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1927
- runId: options.backgroundWorkerRunId,
1928
- }));
1929
- return;
1930
- }
1931
- if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
1932
- clearMiningGateCache(walletRootId);
1933
- await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during sentence generation; restarting on the next tick.", {
1934
- level: "warn",
1935
- targetBlockHeight,
1936
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1937
- runId: options.backgroundWorkerRunId,
1938
- }));
1939
- return;
1940
- }
1941
- if (error instanceof Error && error.message === "mining_generation_preempted") {
1942
- await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
1943
- level: "warn",
1944
- targetBlockHeight,
1945
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1946
- runId: options.backgroundWorkerRunId,
1947
- }));
1948
- return;
1949
- }
1950
- const hookCooldownActive = await persistCustomHookRuntimeOutcome({
1951
- readContext: effectiveReadContext,
1952
- provider: options.provider,
1953
- paths: options.paths,
1954
- nowUnixMs: Date.now(),
1955
- success: false,
1956
- });
1957
- const failureMessage = error instanceof Error ? error.message : String(error);
1958
- await refreshAndSaveStatus({
1959
- paths: options.paths,
1960
- provider: options.provider,
1961
- readContext: effectiveReadContext,
1962
- overrides: {
1963
- runMode: options.runMode,
1964
- currentPhase: "waiting-provider",
1965
- providerState: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
1966
- ? "hook-error"
1967
- : undefined,
1968
- lastError: failureMessage,
1969
- note: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
1970
- ? (hookCooldownActive
1971
- ? "Custom mining hook launch is paused during the post-failure cooldown window."
1972
- : "Custom mining hook failed during sentence generation. Fix it or rerun `cogcoin hooks enable mining`.")
1973
- : "Mining sentence generation failed for the current tip.",
1974
- },
1975
- visualizer: options.visualizer,
1976
- });
1977
- await appendEvent(options.paths, createEvent("hook-request-failed", failureMessage, {
1978
- level: "error",
1979
- targetBlockHeight,
1980
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1981
- runId: options.backgroundWorkerRunId,
1982
- }));
1983
- return;
1984
- }
1985
- await refreshAndSaveStatus({
1986
- paths: options.paths,
1987
- provider: options.provider,
1988
- readContext: effectiveReadContext,
1989
- overrides: {
1990
- runMode: options.runMode,
1991
- currentPhase: "scoring",
1992
- note: "Scoring mining candidates for the current tip.",
1993
- },
1994
- visualizer: options.visualizer,
1995
- });
1996
- const best = await chooseBestLocalCandidate(candidates);
1997
- if (best === null) {
1998
2152
  await refreshAndSaveStatus({
1999
2153
  paths: options.paths,
2000
2154
  provider: options.provider,
2001
2155
  readContext: effectiveReadContext,
2002
2156
  overrides: {
2003
2157
  runMode: options.runMode,
2004
- currentPhase: "idle",
2005
- currentPublishDecision: "publish-skipped-no-candidate",
2006
- note: "No publishable mining candidate passed scoring gates for the current tip.",
2158
+ currentPhase: "generating",
2159
+ note: "Generating mining sentences for eligible root domains.",
2007
2160
  },
2008
2161
  visualizer: options.visualizer,
2162
+ visualizerState: options.loopState.ui,
2009
2163
  });
2010
- await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
2164
+ await appendEvent(options.paths, createEvent("sentence-generation-start", "Started mining sentence generation.", {
2011
2165
  targetBlockHeight,
2012
2166
  referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2013
2167
  runId: options.backgroundWorkerRunId,
2014
2168
  }));
2015
- return;
2016
- }
2017
- if (!await ensureCurrentIndexerTruthOrRestart()) {
2018
- return;
2019
- }
2020
- writeStdout(options.stdout, `Selected ${best.domainName}: ${best.sentence} (${best.canonicalBlend.toString()})`);
2021
- await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
2022
- targetBlockHeight: best.targetBlockHeight,
2023
- referencedBlockHashDisplay: best.referencedBlockHashDisplay,
2024
- domainId: best.domainId,
2025
- domainName: best.domainName,
2026
- score: best.canonicalBlend.toString(),
2027
- runId: options.backgroundWorkerRunId,
2028
- }));
2029
- const gate = await runCompetitivenessGate({
2030
- rpc,
2031
- readContext: effectiveReadContext,
2032
- candidate: best,
2033
- currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
2034
- });
2035
- checkpointMiningSuspendDetector(options.suspendDetector);
2036
- if (!gate.allowed) {
2169
+ let candidates;
2170
+ try {
2171
+ candidates = await generateCandidatesForDomains({
2172
+ rpc,
2173
+ readContext: effectiveReadContext,
2174
+ domains,
2175
+ provider: options.provider,
2176
+ paths: options.paths,
2177
+ indexerTruthKey,
2178
+ runId: options.backgroundWorkerRunId,
2179
+ fetchImpl: options.fetchImpl,
2180
+ });
2181
+ checkpointMiningSuspendDetector(options.suspendDetector);
2182
+ }
2183
+ catch (error) {
2184
+ if (error instanceof MiningProviderRequestError) {
2185
+ if (tipKey !== null) {
2186
+ options.loopState.attemptedTipKey = tipKey;
2187
+ options.loopState.waitingNote = "Mining is waiting for the sentence provider to recover.";
2188
+ }
2189
+ await refreshAndSaveStatus({
2190
+ paths: options.paths,
2191
+ provider: options.provider,
2192
+ readContext: effectiveReadContext,
2193
+ overrides: {
2194
+ runMode: options.runMode,
2195
+ currentPhase: "waiting-provider",
2196
+ providerState: error.providerState,
2197
+ lastError: error.message,
2198
+ note: "Mining is waiting for the sentence provider to recover.",
2199
+ },
2200
+ visualizer: options.visualizer,
2201
+ visualizerState: options.loopState.ui,
2202
+ });
2203
+ await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
2204
+ level: "warn",
2205
+ targetBlockHeight,
2206
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2207
+ runId: options.backgroundWorkerRunId,
2208
+ }));
2209
+ return;
2210
+ }
2211
+ if (error instanceof Error && error.message === "mining_generation_stale_tip") {
2212
+ await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
2213
+ level: "warn",
2214
+ targetBlockHeight,
2215
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2216
+ runId: options.backgroundWorkerRunId,
2217
+ }));
2218
+ return;
2219
+ }
2220
+ if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
2221
+ clearMiningGateCache(walletRootId);
2222
+ await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
2223
+ level: "warn",
2224
+ targetBlockHeight,
2225
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2226
+ runId: options.backgroundWorkerRunId,
2227
+ }));
2228
+ return;
2229
+ }
2230
+ if (error instanceof Error && error.message === "mining_generation_preempted") {
2231
+ await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
2232
+ level: "warn",
2233
+ targetBlockHeight,
2234
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2235
+ runId: options.backgroundWorkerRunId,
2236
+ }));
2237
+ return;
2238
+ }
2239
+ const failureMessage = error instanceof Error ? error.message : String(error);
2240
+ if (tipKey !== null) {
2241
+ options.loopState.attemptedTipKey = tipKey;
2242
+ options.loopState.waitingNote = "Mining sentence generation failed for the current tip.";
2243
+ }
2244
+ await refreshAndSaveStatus({
2245
+ paths: options.paths,
2246
+ provider: options.provider,
2247
+ readContext: effectiveReadContext,
2248
+ overrides: {
2249
+ runMode: options.runMode,
2250
+ currentPhase: "waiting-provider",
2251
+ providerState: "unavailable",
2252
+ lastError: failureMessage,
2253
+ note: "Mining sentence generation failed for the current tip.",
2254
+ },
2255
+ visualizer: options.visualizer,
2256
+ visualizerState: options.loopState.ui,
2257
+ });
2258
+ await appendEvent(options.paths, createEvent("sentence-generation-failed", failureMessage, {
2259
+ level: "error",
2260
+ targetBlockHeight,
2261
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2262
+ runId: options.backgroundWorkerRunId,
2263
+ }));
2264
+ return;
2265
+ }
2037
2266
  await refreshAndSaveStatus({
2038
2267
  paths: options.paths,
2039
2268
  provider: options.provider,
2040
2269
  readContext: effectiveReadContext,
2041
2270
  overrides: {
2042
2271
  runMode: options.runMode,
2043
- currentPhase: "waiting",
2044
- currentPublishDecision: gate.decision,
2045
- sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
2046
- higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2047
- dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2048
- competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
2049
- mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2050
- lastMempoolSequence: gate.lastMempoolSequence,
2051
- lastCompetitivenessGateAtUnixMs: Date.now(),
2052
- note: gate.decision === "suppressed-same-domain-mempool"
2053
- ? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
2054
- : gate.decision === "suppressed-top5-mempool"
2055
- ? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2056
- : "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.",
2272
+ currentPhase: "scoring",
2273
+ note: "Scoring mining candidates for the current tip.",
2057
2274
  },
2058
2275
  visualizer: options.visualizer,
2276
+ visualizerState: options.loopState.ui,
2059
2277
  });
2060
- await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
2061
- ? "publish-skipped-same-domain-mempool"
2062
- : gate.decision === "suppressed-top5-mempool"
2063
- ? "publish-skipped-top5-mempool"
2064
- : "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
2065
- ? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
2066
- : gate.decision === "suppressed-top5-mempool"
2067
- ? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2068
- : "Skipped publish because the competitiveness gate could not be evaluated safely.", {
2278
+ const best = await chooseBestLocalCandidate(candidates);
2279
+ if (best === null) {
2280
+ if (tipKey !== null) {
2281
+ options.loopState.attemptedTipKey = tipKey;
2282
+ options.loopState.waitingNote = "No publishable mining candidate passed scoring gates for the current tip.";
2283
+ }
2284
+ clearSelectedCandidate(options.loopState);
2285
+ await refreshAndSaveStatus({
2286
+ paths: options.paths,
2287
+ provider: options.provider,
2288
+ readContext: effectiveReadContext,
2289
+ overrides: {
2290
+ runMode: options.runMode,
2291
+ currentPhase: "idle",
2292
+ currentPublishDecision: "publish-skipped-no-candidate",
2293
+ note: "No publishable mining candidate passed scoring gates for the current tip.",
2294
+ },
2295
+ visualizer: options.visualizer,
2296
+ visualizerState: options.loopState.ui,
2297
+ });
2298
+ await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
2299
+ targetBlockHeight,
2300
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2301
+ runId: options.backgroundWorkerRunId,
2302
+ }));
2303
+ return;
2304
+ }
2305
+ if (!await ensureCurrentIndexerTruthOrRestart()) {
2306
+ return;
2307
+ }
2308
+ options.loopState.ui.recentWin = null;
2309
+ cacheSelectedCandidateForTip(options.loopState, tipKey, best);
2310
+ selectedCandidate = best;
2311
+ await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
2069
2312
  targetBlockHeight: best.targetBlockHeight,
2070
2313
  referencedBlockHashDisplay: best.referencedBlockHashDisplay,
2071
2314
  domainId: best.domainId,
2072
2315
  domainName: best.domainName,
2073
2316
  score: best.canonicalBlend.toString(),
2074
2317
  runId: options.backgroundWorkerRunId,
2075
- reason: gate.decision,
2076
2318
  }));
2077
- return;
2319
+ const gate = await runCompetitivenessGate({
2320
+ rpc,
2321
+ readContext: effectiveReadContext,
2322
+ candidate: best,
2323
+ currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
2324
+ });
2325
+ checkpointMiningSuspendDetector(options.suspendDetector);
2326
+ gateSnapshot = {
2327
+ higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2328
+ dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2329
+ mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2330
+ lastMempoolSequence: gate.lastMempoolSequence,
2331
+ };
2332
+ if (!gate.allowed) {
2333
+ if (tipKey !== null) {
2334
+ options.loopState.attemptedTipKey = tipKey;
2335
+ }
2336
+ clearSelectedCandidate(options.loopState);
2337
+ setMiningUiCandidate(options.loopState, best);
2338
+ options.loopState.waitingNote = gate.decision === "suppressed-same-domain-mempool"
2339
+ ? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
2340
+ : gate.decision === "suppressed-top5-mempool"
2341
+ ? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2342
+ : "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.";
2343
+ await refreshAndSaveStatus({
2344
+ paths: options.paths,
2345
+ provider: options.provider,
2346
+ readContext: effectiveReadContext,
2347
+ overrides: {
2348
+ runMode: options.runMode,
2349
+ currentPhase: "waiting",
2350
+ currentPublishDecision: gate.decision,
2351
+ sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
2352
+ higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2353
+ dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2354
+ competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
2355
+ mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2356
+ lastMempoolSequence: gate.lastMempoolSequence,
2357
+ lastCompetitivenessGateAtUnixMs: Date.now(),
2358
+ note: options.loopState.waitingNote,
2359
+ },
2360
+ visualizer: options.visualizer,
2361
+ visualizerState: options.loopState.ui,
2362
+ });
2363
+ await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
2364
+ ? "publish-skipped-same-domain-mempool"
2365
+ : gate.decision === "suppressed-top5-mempool"
2366
+ ? "publish-skipped-top5-mempool"
2367
+ : "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
2368
+ ? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
2369
+ : gate.decision === "suppressed-top5-mempool"
2370
+ ? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2371
+ : "Skipped publish because the competitiveness gate could not be evaluated safely.", {
2372
+ targetBlockHeight: best.targetBlockHeight,
2373
+ referencedBlockHashDisplay: best.referencedBlockHashDisplay,
2374
+ domainId: best.domainId,
2375
+ domainName: best.domainName,
2376
+ score: best.canonicalBlend.toString(),
2377
+ runId: options.backgroundWorkerRunId,
2378
+ reason: gate.decision,
2379
+ }));
2380
+ return;
2381
+ }
2382
+ }
2383
+ else {
2384
+ options.loopState.ui.recentWin = null;
2385
+ setMiningUiCandidate(options.loopState, selectedCandidate);
2078
2386
  }
2079
2387
  if (!await ensureCurrentIndexerTruthOrRestart()) {
2080
2388
  return;
@@ -2093,6 +2401,7 @@ async function performMiningCycle(options) {
2093
2401
  : "Replacing the live mining transaction for the current tip.",
2094
2402
  },
2095
2403
  visualizer: options.visualizer,
2404
+ visualizerState: options.loopState.ui,
2096
2405
  });
2097
2406
  const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
2098
2407
  purpose: "wallet-mine",
@@ -2105,16 +2414,98 @@ async function performMiningCycle(options) {
2105
2414
  }
2106
2415
  checkpointMiningSuspendDetector(options.suspendDetector);
2107
2416
  const published = await publishCandidate({
2108
- readContext: effectiveReadContext,
2109
- candidate: best,
2110
2417
  dataDir: options.dataDir,
2418
+ databasePath: options.databasePath,
2111
2419
  provider: options.provider,
2112
2420
  paths: options.paths,
2421
+ fallbackState: effectiveReadContext.localState.state,
2422
+ openReadContext: options.openReadContext,
2113
2423
  attachService: options.attachService,
2114
2424
  rpcFactory: options.rpcFactory,
2425
+ candidate: selectedCandidate,
2115
2426
  runId: options.backgroundWorkerRunId,
2116
2427
  });
2117
2428
  checkpointMiningSuspendDetector(options.suspendDetector);
2429
+ if (tipKey !== null && published.retryable !== true) {
2430
+ options.loopState.attemptedTipKey = tipKey;
2431
+ }
2432
+ if (published.retryable === true) {
2433
+ cacheSelectedCandidateForTip(options.loopState, tipKey, published.candidate);
2434
+ options.loopState.waitingNote = published.note;
2435
+ await refreshAndSaveStatus({
2436
+ paths: options.paths,
2437
+ provider: options.provider,
2438
+ readContext: {
2439
+ ...effectiveReadContext,
2440
+ localState: {
2441
+ ...effectiveReadContext.localState,
2442
+ state: published.state,
2443
+ },
2444
+ },
2445
+ overrides: {
2446
+ runMode: options.runMode,
2447
+ currentPhase: "waiting",
2448
+ currentPublishDecision: published.decision,
2449
+ sameDomainCompetitorSuppressed: false,
2450
+ higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
2451
+ dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
2452
+ competitivenessGateIndeterminate: false,
2453
+ mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2454
+ lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2455
+ lastCompetitivenessGateAtUnixMs: Date.now(),
2456
+ note: published.note,
2457
+ livePublishInMempool: published.state.miningState.livePublishInMempool,
2458
+ },
2459
+ visualizer: options.visualizer,
2460
+ visualizerState: options.loopState.ui,
2461
+ });
2462
+ return;
2463
+ }
2464
+ if (published.skipped === true) {
2465
+ clearSelectedCandidate(options.loopState);
2466
+ setMiningUiCandidate(options.loopState, selectedCandidate);
2467
+ options.loopState.waitingNote = published.note;
2468
+ await refreshAndSaveStatus({
2469
+ paths: options.paths,
2470
+ provider: options.provider,
2471
+ readContext: {
2472
+ ...effectiveReadContext,
2473
+ localState: {
2474
+ ...effectiveReadContext.localState,
2475
+ state: published.state,
2476
+ },
2477
+ },
2478
+ overrides: {
2479
+ runMode: options.runMode,
2480
+ currentPhase: "waiting",
2481
+ currentPublishDecision: published.decision,
2482
+ sameDomainCompetitorSuppressed: false,
2483
+ higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
2484
+ dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
2485
+ competitivenessGateIndeterminate: false,
2486
+ mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2487
+ lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2488
+ lastCompetitivenessGateAtUnixMs: Date.now(),
2489
+ note: published.note,
2490
+ livePublishInMempool: published.state.miningState.livePublishInMempool,
2491
+ },
2492
+ visualizer: options.visualizer,
2493
+ visualizerState: options.loopState.ui,
2494
+ });
2495
+ return;
2496
+ }
2497
+ clearSelectedCandidate(options.loopState);
2498
+ if (published.txid !== null) {
2499
+ options.loopState.ui.latestTxid = published.txid;
2500
+ }
2501
+ setMiningUiCandidate(options.loopState, published.candidate);
2502
+ options.loopState.waitingNote = published.decision === "kept-live-publish"
2503
+ ? "Existing live mining publish already covers this block attempt. Waiting for the next block."
2504
+ : published.txid === null
2505
+ ? "Mining candidate was evaluated but the existing live publish stayed in place."
2506
+ : `Mining candidate ${published.decision === "replaced"
2507
+ ? "replaced"
2508
+ : "broadcast"} as ${published.txid}. Waiting for the next block.`;
2118
2509
  await refreshAndSaveStatus({
2119
2510
  paths: options.paths,
2120
2511
  provider: options.provider,
@@ -2127,25 +2518,20 @@ async function performMiningCycle(options) {
2127
2518
  },
2128
2519
  overrides: {
2129
2520
  runMode: options.runMode,
2130
- currentPhase: "publishing",
2521
+ currentPhase: "waiting",
2131
2522
  currentPublishDecision: published.decision,
2132
2523
  sameDomainCompetitorSuppressed: false,
2133
- higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2134
- dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2524
+ higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
2525
+ dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
2135
2526
  competitivenessGateIndeterminate: false,
2136
- mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2137
- lastMempoolSequence: gate.lastMempoolSequence,
2527
+ mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2528
+ lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2138
2529
  lastCompetitivenessGateAtUnixMs: Date.now(),
2139
- note: published.txid === null
2140
- ? "Mining candidate was evaluated but the existing live family stayed in place."
2141
- : `Mining candidate ${published.decision === "replaced"
2142
- ? "replaced"
2143
- : published.decision === "fee-bump"
2144
- ? "fee-bumped"
2145
- : "broadcast"} as ${published.txid}.`,
2146
- liveMiningFamilyInMempool: published.state.miningState.liveMiningFamilyInMempool,
2530
+ note: options.loopState.waitingNote,
2531
+ livePublishInMempool: published.state.miningState.livePublishInMempool,
2147
2532
  },
2148
2533
  visualizer: options.visualizer,
2534
+ visualizerState: options.loopState.ui,
2149
2535
  });
2150
2536
  }
2151
2537
  finally {
@@ -2189,7 +2575,7 @@ async function saveStopSnapshot(options) {
2189
2575
  });
2190
2576
  try {
2191
2577
  let localState = readContext.localState;
2192
- if (localState.availability === "ready" && localState.state !== null && localState.unlockUntilUnixMs !== null) {
2578
+ if (localState.availability === "ready" && localState.state !== null) {
2193
2579
  const service = await attachOrStartManagedBitcoindService({
2194
2580
  dataDir: options.dataDir,
2195
2581
  chain: "main",
@@ -2198,22 +2584,23 @@ async function saveStopSnapshot(options) {
2198
2584
  }).catch(() => null);
2199
2585
  if (service !== null) {
2200
2586
  const rpc = createRpcClient(service.rpc);
2201
- const reconciledState = await reconcileLiveMiningState({
2587
+ const reconciledState = (await reconcileLiveMiningState({
2202
2588
  state: localState.state,
2203
2589
  rpc,
2204
2590
  nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
2205
2591
  nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
2206
- });
2592
+ snapshotState: readContext.snapshot?.state ?? null,
2593
+ })).state;
2207
2594
  const stopState = defaultMiningStatePatch(reconciledState, {
2208
2595
  runMode: "stopped",
2209
- state: reconciledState.miningState.liveMiningFamilyInMempool
2596
+ state: reconciledState.miningState.livePublishInMempool
2210
2597
  ? reconciledState.miningState.state === "paused-stale"
2211
2598
  ? "paused-stale"
2212
2599
  : "paused"
2213
2600
  : reconciledState.miningState.state === "repair-required"
2214
2601
  ? "repair-required"
2215
2602
  : "idle",
2216
- pauseReason: reconciledState.miningState.liveMiningFamilyInMempool
2603
+ pauseReason: reconciledState.miningState.livePublishInMempool
2217
2604
  ? reconciledState.miningState.state === "paused-stale"
2218
2605
  ? "stale-block-context"
2219
2606
  : "user-stopped"
@@ -2224,8 +2611,6 @@ async function saveStopSnapshot(options) {
2224
2611
  await saveWalletStatePreservingUnlock({
2225
2612
  state: stopState,
2226
2613
  provider: options.provider,
2227
- unlockUntilUnixMs: localState.unlockUntilUnixMs,
2228
- nowUnixMs: Date.now(),
2229
2614
  paths: options.paths,
2230
2615
  });
2231
2616
  localState = {
@@ -2270,6 +2655,7 @@ async function attemptSaveMempool(rpc, paths, runId) {
2270
2655
  }
2271
2656
  async function runMiningLoop(options) {
2272
2657
  const suspendDetector = createMiningSuspendDetector();
2658
+ const loopState = createMiningLoopState();
2273
2659
  await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
2274
2660
  runId: options.backgroundWorkerRunId,
2275
2661
  }));
@@ -2298,6 +2684,7 @@ async function runMiningLoop(options) {
2298
2684
  await performMiningCycle({
2299
2685
  ...options,
2300
2686
  suspendDetector,
2687
+ loopState,
2301
2688
  });
2302
2689
  await sleep(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
2303
2690
  }
@@ -2487,7 +2874,7 @@ export async function stopBackgroundMining(options) {
2487
2874
  runMode: "background",
2488
2875
  backgroundWorkerPid: snapshot.backgroundWorkerPid,
2489
2876
  backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
2490
- note: snapshot.liveMiningFamilyInMempool
2877
+ note: snapshot.livePublishInMempool
2491
2878
  ? "Background mining stopped. The last mining transaction may still confirm from mempool."
2492
2879
  : "Background mining stopped.",
2493
2880
  });
@@ -2569,8 +2956,11 @@ export async function handleDetectedMiningRuntimeResumeForTesting(options) {
2569
2956
  await handleDetectedMiningRuntimeResume(options);
2570
2957
  }
2571
2958
  export async function performMiningCycleForTesting(options) {
2572
- await performMiningCycle(options);
2959
+ await performMiningCycle({
2960
+ ...options,
2961
+ loopState: options.loopState ?? createMiningLoopState(),
2962
+ });
2573
2963
  }
2574
- export function shouldTreatCandidateAsFeeBumpForTesting(options) {
2575
- return candidateNeedsFeeMaintenance(options);
2964
+ export function shouldKeepCurrentTipLivePublishForTesting(options) {
2965
+ return livePublishTargetsCandidateTip(options);
2576
2966
  }