@cogcoin/client 0.5.15 → 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 +6 -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 -12
  91. package/dist/wallet/coin-control.js +100 -428
  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 +116 -13
  105. package/dist/wallet/mining/runner.js +885 -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 -1250
  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 +61 -8
  144. package/dist/wallet/tx/common.js +266 -146
  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 +83 -924
  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, getDecodedInputScriptPubKeyHex, 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 (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
614
- throw new Error("wallet_mining_sender_input_mismatch");
615
- }
616
- if (getDecodedInputScriptPubKeyHex(decoded, 1) !== 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
- decoded,
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
  }
@@ -668,15 +841,11 @@ function resolveEligibleAnchoredRoots(context) {
668
841
  if (!isMineableWalletDomain(context, domain)) {
669
842
  continue;
670
843
  }
671
- const localRecord = state.domains.find((entry) => entry.name === domain.name);
672
- const ownerIdentity = model.identities.find((identity) => identity.index === domain.ownerLocalIndex);
673
844
  const domainId = domain.domainId;
674
845
  if (domainId === null
675
846
  || domainId === undefined
676
- || localRecord?.currentCanonicalAnchorOutpoint === null
677
- || localRecord?.currentCanonicalAnchorOutpoint === undefined
678
- || ownerIdentity?.address == null
679
- || ownerIdentity.readOnly) {
847
+ || domain.ownerAddress == null
848
+ || domain.ownerScriptPubKeyHex !== model.walletScriptPubKeyHex) {
680
849
  continue;
681
850
  }
682
851
  const chainDomain = lookupDomain(snapshot.state, domain.name);
@@ -686,60 +855,47 @@ function resolveEligibleAnchoredRoots(context) {
686
855
  domains.push({
687
856
  domainId,
688
857
  domainName: domain.name,
689
- localIndex: ownerIdentity.index,
858
+ localIndex: 0,
690
859
  sender: {
691
- localIndex: ownerIdentity.index,
692
- scriptPubKeyHex: ownerIdentity.scriptPubKeyHex,
693
- address: ownerIdentity.address,
694
- },
695
- anchorOutpoint: {
696
- txid: localRecord.currentCanonicalAnchorOutpoint.txid,
697
- vout: localRecord.currentCanonicalAnchorOutpoint.vout,
860
+ localIndex: 0,
861
+ scriptPubKeyHex: model.walletScriptPubKeyHex,
862
+ address: domain.ownerAddress,
698
863
  },
699
864
  });
700
865
  }
701
866
  return domains.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName));
702
867
  }
703
- async function persistCustomHookRuntimeOutcome(options) {
704
- const hookState = options.readContext.localState.state.hookClientState.mining;
705
- if (hookState.mode !== "custom") {
706
- return false;
707
- }
708
- if (options.success) {
709
- if ((hookState.consecutiveFailureCount ?? 0) === 0 && hookState.cooldownUntilUnixMs === null) {
710
- return false;
711
- }
712
- options.readContext.localState.state.hookClientState.mining = {
713
- ...hookState,
714
- consecutiveFailureCount: 0,
715
- cooldownUntilUnixMs: null,
716
- };
717
- await saveWalletStatePreservingUnlock({
718
- state: options.readContext.localState.state,
719
- provider: options.provider,
720
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
721
- nowUnixMs: options.nowUnixMs,
722
- paths: options.paths,
723
- });
724
- 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;
725
872
  }
726
- const consecutiveFailureCount = (hookState.consecutiveFailureCount ?? 0) + 1;
727
- const cooldownUntilUnixMs = consecutiveFailureCount >= MINING_HOOK_FAILURE_THRESHOLD
728
- ? options.nowUnixMs + MINING_HOOK_COOLDOWN_MS
729
- : null;
730
- options.readContext.localState.state.hookClientState.mining = {
731
- ...hookState,
732
- consecutiveFailureCount,
733
- cooldownUntilUnixMs,
873
+ return {
874
+ ...candidate,
875
+ domainName: refreshed.domainName,
876
+ localIndex: refreshed.localIndex,
877
+ sender: refreshed.sender,
734
878
  };
735
- await saveWalletStatePreservingUnlock({
736
- state: options.readContext.localState.state,
737
- provider: options.provider,
738
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
739
- nowUnixMs: options.nowUnixMs,
740
- paths: options.paths,
741
- });
742
- 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.";
743
899
  }
744
900
  async function generateCandidatesForDomains(options) {
745
901
  const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
@@ -797,7 +953,7 @@ async function generateCandidatesForDomains(options) {
797
953
  referencedBlockHashDisplay: bestBlockHash,
798
954
  generatedAtUnixMs: Date.now(),
799
955
  extraPrompt: null,
800
- limits: createGenerateSentencesHookLimits(),
956
+ limits: createMiningSentenceRequestLimits(),
801
957
  rootDomains: rootDomains.map((domain) => ({
802
958
  domainId: domain.domainId,
803
959
  domainName: domain.domainName,
@@ -809,7 +965,6 @@ async function generateCandidatesForDomains(options) {
809
965
  generated = await generateMiningSentences(generationRequest, {
810
966
  paths: options.paths,
811
967
  provider: options.provider,
812
- hookState: options.readContext.localState.state.hookClientState.mining,
813
968
  signal: abortController.signal,
814
969
  fetchImpl: options.fetchImpl,
815
970
  });
@@ -839,15 +994,6 @@ async function generateCandidatesForDomains(options) {
839
994
  dataDir: options.readContext.dataDir,
840
995
  truthKey: options.indexerTruthKey,
841
996
  });
842
- if (generated.hookMode === "custom") {
843
- await persistCustomHookRuntimeOutcome({
844
- readContext: options.readContext,
845
- provider: options.provider,
846
- paths: options.paths,
847
- nowUnixMs: Date.now(),
848
- success: true,
849
- });
850
- }
851
997
  const sentencesByDomain = new Map();
852
998
  for (const candidate of generated.candidates) {
853
999
  const existing = sentencesByDomain.get(candidate.domainId) ?? [];
@@ -870,7 +1016,6 @@ async function generateCandidatesForDomains(options) {
870
1016
  domainName: domain.domainName,
871
1017
  localIndex: domain.localIndex,
872
1018
  sender: domain.sender,
873
- anchorOutpoint: domain.anchorOutpoint,
874
1019
  sentence: best.sentence,
875
1020
  encodedSentenceBytes: best.encodedSentenceBytes,
876
1021
  bip39WordIndices: [...best.bip39WordIndices],
@@ -920,6 +1065,46 @@ async function chooseBestLocalCandidate(candidates) {
920
1065
  }
921
1066
  return candidates.find((candidate) => candidate.domainId === winner.miningDomainId) ?? null;
922
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
+ }
923
1108
  async function runCompetitivenessGate(options) {
924
1109
  const createDecision = (overrides) => ({
925
1110
  allowed: overrides.allowed ?? false,
@@ -930,10 +1115,11 @@ async function runCompetitivenessGate(options) {
930
1115
  competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? false,
931
1116
  mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? null,
932
1117
  lastMempoolSequence: overrides.lastMempoolSequence ?? null,
1118
+ visibleBoardEntries: overrides.visibleBoardEntries ?? [],
1119
+ candidateRank: overrides.candidateRank ?? null,
933
1120
  });
934
1121
  const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
935
1122
  const indexerTruthKey = getIndexerTruthKey(options.readContext);
936
- const localFeeTarget = DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB;
937
1123
  const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
938
1124
  const localAssayTupleKey = [
939
1125
  options.candidate.domainId,
@@ -965,7 +1151,6 @@ async function runCompetitivenessGate(options) {
965
1151
  && cachedTruthMatches
966
1152
  && cachedReferencedBlockMatches
967
1153
  && cached.localAssayTupleKey === localAssayTupleKey
968
- && cached.currentFeeTargetSatVb === localFeeTarget
969
1154
  && cached.excludedTxidsKey === excludedTxids.join(",")
970
1155
  && cached.mempoolSequence === mempoolSequence) {
971
1156
  return {
@@ -1011,7 +1196,7 @@ async function runCompetitivenessGate(options) {
1011
1196
  const entries = new Map();
1012
1197
  for (const txid of visibleTxids) {
1013
1198
  const context = txContexts.get(txid);
1014
- if (context === undefined || context.effectiveFeeRate < localFeeTarget || context.payload === null || context.senderScriptHex === null) {
1199
+ if (context === undefined || context.payload === null || context.senderScriptHex === null) {
1015
1200
  continue;
1016
1201
  }
1017
1202
  const decoded = decodeMinePayload(context.payload);
@@ -1037,7 +1222,6 @@ async function runCompetitivenessGate(options) {
1037
1222
  indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
1038
1223
  referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1039
1224
  localAssayTupleKey,
1040
- currentFeeTargetSatVb: localFeeTarget,
1041
1225
  excludedTxidsKey: excludedTxids.join(","),
1042
1226
  mempoolSequence,
1043
1227
  txids: [...visibleTxids],
@@ -1058,26 +1242,46 @@ async function runCompetitivenessGate(options) {
1058
1242
  txid,
1059
1243
  effectiveFeeRate: context.effectiveFeeRate,
1060
1244
  domainId: decoded.domainId,
1245
+ domainName: overlayDomain.name,
1246
+ sentence: Buffer.from(decoded.sentenceBytes).toString("utf8"),
1061
1247
  senderScriptHex: context.senderScriptHex,
1062
1248
  encodedSentenceBytesHex: Buffer.from(scored.encodedSentenceBytes).toString("hex"),
1063
1249
  bip39WordIndices: [...scored.bip39WordIndices],
1064
1250
  canonicalBlend: scored.canonicalBlend,
1065
1251
  });
1066
1252
  }
1067
- 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);
1068
1275
  const sameDomainCompetitorSuppressed = sameDomainCompetitors.some((competitor) => competitor.canonicalBlend > options.candidate.canonicalBlend
1069
1276
  || competitor.canonicalBlend === options.candidate.canonicalBlend);
1070
1277
  let decision;
1071
1278
  const otherDomainBest = new Map();
1072
- for (const entry of entries.values()) {
1279
+ for (const entry of visibleBestByDomain.values()) {
1073
1280
  if (entry.domainId === options.candidate.domainId) {
1074
1281
  continue;
1075
1282
  }
1076
1283
  const best = otherDomainBest.get(entry.domainId);
1077
- if (best === undefined
1078
- || entry.canonicalBlend > best.canonicalBlend
1079
- || (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate > best.effectiveFeeRate)
1080
- || (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate === best.effectiveFeeRate && entry.txid.localeCompare(best.txid) < 0)) {
1284
+ if (isBetterVisibleCompetitor(entry, best)) {
1081
1285
  otherDomainBest.set(entry.domainId, entry);
1082
1286
  }
1083
1287
  }
@@ -1091,38 +1295,41 @@ async function runCompetitivenessGate(options) {
1091
1295
  competitivenessGateIndeterminate: false,
1092
1296
  mempoolSequenceCacheStatus: "refreshed",
1093
1297
  lastMempoolSequence: mempoolSequence,
1298
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1094
1299
  });
1095
1300
  }
1096
1301
  else {
1097
1302
  try {
1098
- const submissions = [
1303
+ const candidateRankedEntries = rankMiningSentenceEntries([
1099
1304
  {
1100
- miningDomainId: options.candidate.domainId,
1101
- rawSentenceBytes: options.candidate.encodedSentenceBytes,
1102
- 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"),
1103
1311
  bip39WordIndices: options.candidate.bip39WordIndices,
1312
+ txid: null,
1104
1313
  txIndex: 0,
1105
1314
  },
1106
1315
  ...[...otherDomainBest.values()]
1107
1316
  .sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
1108
1317
  .map((entry, index) => ({
1109
- miningDomainId: entry.domainId,
1110
- rawSentenceBytes: Buffer.from(entry.encodedSentenceBytesHex, "hex"),
1111
- 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,
1112
1324
  bip39WordIndices: entry.bip39WordIndices,
1325
+ txid: entry.txid,
1113
1326
  txIndex: index + 1,
1114
1327
  })),
1115
- ];
1116
- const winners = await settleBlock({
1117
- blendSeed: deriveBlendSeed(options.candidate.referencedBlockHashInternal),
1118
- blockRewardCogtoshi: 100n,
1119
- submissions,
1120
- });
1121
- const localWinner = winners.find((winner) => winner.miningDomainId === options.candidate.domainId);
1122
- const higherRankedCompetitorDomainCount = localWinner === undefined
1123
- ? Math.max(0, winners.length - 1)
1124
- : Math.max(0, localWinner.rank - 1);
1125
- 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) {
1126
1333
  decision = createDecision({
1127
1334
  allowed: false,
1128
1335
  decision: "suppressed-top5-mempool",
@@ -1132,11 +1339,13 @@ async function runCompetitivenessGate(options) {
1132
1339
  competitivenessGateIndeterminate: false,
1133
1340
  mempoolSequenceCacheStatus: "refreshed",
1134
1341
  lastMempoolSequence: mempoolSequence,
1342
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1343
+ candidateRank,
1135
1344
  });
1136
1345
  }
1137
1346
  else {
1138
1347
  decision = createDecision({
1139
- allowed: true,
1348
+ allowed: candidateRank !== null,
1140
1349
  decision: "publish",
1141
1350
  sameDomainCompetitorSuppressed: false,
1142
1351
  higherRankedCompetitorDomainCount,
@@ -1144,6 +1353,8 @@ async function runCompetitivenessGate(options) {
1144
1353
  competitivenessGateIndeterminate: false,
1145
1354
  mempoolSequenceCacheStatus: "refreshed",
1146
1355
  lastMempoolSequence: mempoolSequence,
1356
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1357
+ candidateRank,
1147
1358
  });
1148
1359
  }
1149
1360
  }
@@ -1157,6 +1368,7 @@ async function runCompetitivenessGate(options) {
1157
1368
  competitivenessGateIndeterminate: true,
1158
1369
  mempoolSequenceCacheStatus: "refreshed",
1159
1370
  lastMempoolSequence: mempoolSequence,
1371
+ visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
1160
1372
  });
1161
1373
  }
1162
1374
  }
@@ -1165,7 +1377,6 @@ async function runCompetitivenessGate(options) {
1165
1377
  indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
1166
1378
  referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1167
1379
  localAssayTupleKey,
1168
- currentFeeTargetSatVb: localFeeTarget,
1169
1380
  excludedTxidsKey: excludedTxids.join(","),
1170
1381
  mempoolSequence,
1171
1382
  txids: [...visibleTxids],
@@ -1174,74 +1385,14 @@ async function runCompetitivenessGate(options) {
1174
1385
  });
1175
1386
  return decision;
1176
1387
  }
1177
- function candidateOutranksLive(options) {
1388
+ function livePublishTargetsCandidateTip(options) {
1178
1389
  const liveState = normalizeMiningStateRecord(options.liveState);
1179
- const nextSentenceHex = Buffer.from(options.candidate.encodedSentenceBytes).toString("hex");
1180
- if (liveState.currentEncodedSentenceBytesHex === null) {
1181
- return true;
1182
- }
1183
- if (liveState.currentDomainId === options.candidate.domainId) {
1184
- if (liveState.currentEncodedSentenceBytesHex === nextSentenceHex) {
1185
- return false;
1186
- }
1187
- const currentScore = liveState.currentScore === null ? null : BigInt(liveState.currentScore);
1188
- return currentScore === null || options.candidate.canonicalBlend > currentScore;
1189
- }
1190
- return true;
1191
- }
1192
- function candidateMatchesLiveFamily(options) {
1193
- const liveState = normalizeMiningStateRecord(options.liveState);
1194
- return liveState.currentDomainId === options.candidate.domainId
1195
- && liveState.currentEncodedSentenceBytesHex === Buffer.from(options.candidate.encodedSentenceBytes).toString("hex")
1196
- && liveState.currentSenderScriptPubKeyHex === options.candidate.sender.scriptPubKeyHex
1390
+ return liveState.currentTxid !== null
1391
+ && liveState.currentPublishState === "in-mempool"
1392
+ && liveState.livePublishInMempool === true
1197
1393
  && liveState.currentReferencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay
1198
1394
  && liveState.currentBlockTargetHeight === options.candidate.targetBlockHeight;
1199
1395
  }
1200
- function candidateNeedsFeeMaintenance(options) {
1201
- const liveState = normalizeMiningStateRecord(options.liveState);
1202
- return candidateMatchesLiveFamily(options)
1203
- && liveState.currentTxid !== null
1204
- && liveState.currentFeeRateSatVb !== null
1205
- && liveState.currentPublishState === "in-mempool"
1206
- && liveState.liveMiningFamilyInMempool === true;
1207
- }
1208
- async function candidateWinsAgainstLive(options) {
1209
- const liveState = normalizeMiningStateRecord(options.liveState);
1210
- if (liveState.currentDomainId === null || liveState.currentEncodedSentenceBytesHex === null) {
1211
- return true;
1212
- }
1213
- if (liveState.currentDomainId === options.candidate.domainId) {
1214
- return candidateOutranksLive(options);
1215
- }
1216
- if (liveState.currentBip39WordIndices === null || liveState.currentSenderScriptPubKeyHex === null || liveState.currentBlendSeedHex === null) {
1217
- return true;
1218
- }
1219
- const settled = await settleBlock({
1220
- blendSeed: Buffer.from(liveState.currentBlendSeedHex, "hex"),
1221
- blockRewardCogtoshi: 100n,
1222
- submissions: [
1223
- {
1224
- miningDomainId: liveState.currentDomainId,
1225
- rawSentenceBytes: Buffer.from(liveState.currentEncodedSentenceBytesHex, "hex"),
1226
- recipientScriptPubKey: Buffer.from(liveState.currentSenderScriptPubKeyHex, "hex"),
1227
- bip39WordIndices: liveState.currentBip39WordIndices,
1228
- txIndex: 0,
1229
- },
1230
- {
1231
- miningDomainId: options.candidate.domainId,
1232
- rawSentenceBytes: options.candidate.encodedSentenceBytes,
1233
- recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
1234
- bip39WordIndices: options.candidate.bip39WordIndices,
1235
- txIndex: 1,
1236
- },
1237
- ],
1238
- });
1239
- const incumbent = settled.find((entry) => entry.miningDomainId === liveState.currentDomainId);
1240
- const challenger = settled.find((entry) => entry.miningDomainId === options.candidate.domainId);
1241
- return challenger !== undefined
1242
- && incumbent !== undefined
1243
- && challenger.rank < incumbent.rank;
1244
- }
1245
1396
  function miningCandidateIsCurrent(options) {
1246
1397
  return options.state.currentReferencedBlockHashDisplay !== null
1247
1398
  && options.nodeBestHash !== null
@@ -1256,14 +1407,17 @@ async function reconcileLiveMiningState(options) {
1256
1407
  miningState: normalizeMiningStateRecord(options.state.miningState),
1257
1408
  };
1258
1409
  const currentTxid = state.miningState.currentTxid;
1259
- if (currentTxid === null || !miningFamilyMayStillExist(state.miningState)) {
1410
+ if (currentTxid === null || !miningPublishMayStillExist(state.miningState)) {
1260
1411
  await reconcilePersistentPolicyLocks({
1261
1412
  rpc: options.rpc,
1262
1413
  walletName: state.managedCoreWallet.walletName,
1263
1414
  state,
1264
1415
  fixedInputs: [],
1265
1416
  });
1266
- return state;
1417
+ return {
1418
+ state,
1419
+ recentWin: null,
1420
+ };
1267
1421
  }
1268
1422
  const walletName = state.managedCoreWallet.walletName;
1269
1423
  const [mempoolVerbose, walletTx] = await Promise.all([
@@ -1275,10 +1429,11 @@ async function reconcileLiveMiningState(options) {
1275
1429
  ]);
1276
1430
  const inMempool = mempoolVerbose.txids.includes(currentTxid);
1277
1431
  if (walletTx !== null && walletTx.confirmations > 0) {
1432
+ const recentWin = findRecentMiningWin(options.snapshotState ?? null, currentTxid, state.miningState.currentBlockTargetHeight);
1278
1433
  state = {
1279
1434
  ...state,
1280
1435
  miningState: {
1281
- ...clearMiningFamilyState(state.miningState),
1436
+ ...clearMiningPublishState(state.miningState),
1282
1437
  currentPublishDecision: "tx-confirmed-while-down",
1283
1438
  },
1284
1439
  };
@@ -1288,7 +1443,10 @@ async function reconcileLiveMiningState(options) {
1288
1443
  state,
1289
1444
  fixedInputs: [],
1290
1445
  });
1291
- return state;
1446
+ return {
1447
+ state,
1448
+ recentWin,
1449
+ };
1292
1450
  }
1293
1451
  if (inMempool) {
1294
1452
  const stale = !miningCandidateIsCurrent({
@@ -1297,7 +1455,7 @@ async function reconcileLiveMiningState(options) {
1297
1455
  nodeBestHeight: options.nodeBestHeight,
1298
1456
  });
1299
1457
  state = defaultMiningStatePatch(state, {
1300
- liveMiningFamilyInMempool: true,
1458
+ livePublishInMempool: true,
1301
1459
  currentPublishState: "in-mempool",
1302
1460
  state: stale
1303
1461
  ? "paused-stale"
@@ -1309,7 +1467,7 @@ async function reconcileLiveMiningState(options) {
1309
1467
  : state.miningState.runMode === "stopped"
1310
1468
  ? "user-stopped"
1311
1469
  : null,
1312
- currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-family",
1470
+ currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-publish",
1313
1471
  });
1314
1472
  await reconcilePersistentPolicyLocks({
1315
1473
  rpc: options.rpc,
@@ -1317,7 +1475,10 @@ async function reconcileLiveMiningState(options) {
1317
1475
  state,
1318
1476
  fixedInputs: [],
1319
1477
  });
1320
- return state;
1478
+ return {
1479
+ state,
1480
+ recentWin: null,
1481
+ };
1321
1482
  }
1322
1483
  if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
1323
1484
  state = defaultMiningStatePatch(state, {
@@ -1325,7 +1486,7 @@ async function reconcileLiveMiningState(options) {
1325
1486
  pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
1326
1487
  ? "broadcast-unknown-conflict"
1327
1488
  : "wallet-conflict-observed",
1328
- liveMiningFamilyInMempool: false,
1489
+ livePublishInMempool: false,
1329
1490
  currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
1330
1491
  ? "repair-required-broadcast-conflict"
1331
1492
  : "repair-required-wallet-conflict",
@@ -1336,13 +1497,16 @@ async function reconcileLiveMiningState(options) {
1336
1497
  state,
1337
1498
  fixedInputs: [],
1338
1499
  });
1339
- return state;
1500
+ return {
1501
+ state,
1502
+ recentWin: null,
1503
+ };
1340
1504
  }
1341
1505
  state = defaultMiningStatePatch(state, {
1342
- ...clearMiningFamilyState(state.miningState),
1506
+ ...clearMiningPublishState(state.miningState),
1343
1507
  currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
1344
1508
  ? "broadcast-unknown-not-seen"
1345
- : "live-family-not-seen",
1509
+ : "live-publish-not-seen",
1346
1510
  });
1347
1511
  await reconcilePersistentPolicyLocks({
1348
1512
  rpc: options.rpc,
@@ -1350,9 +1514,12 @@ async function reconcileLiveMiningState(options) {
1350
1514
  state,
1351
1515
  fixedInputs: [],
1352
1516
  });
1353
- return state;
1517
+ return {
1518
+ state,
1519
+ recentWin: null,
1520
+ };
1354
1521
  }
1355
- async function publishCandidate(options) {
1522
+ async function publishCandidateOnce(options) {
1356
1523
  const service = await options.attachService({
1357
1524
  dataDir: options.dataDir,
1358
1525
  chain: "main",
@@ -1360,48 +1527,35 @@ async function publishCandidate(options) {
1360
1527
  walletRootId: options.readContext.localState.state.walletRootId,
1361
1528
  });
1362
1529
  const rpc = options.rpcFactory(service.rpc);
1363
- let state = await reconcileLiveMiningState({
1530
+ let state = (await reconcileLiveMiningState({
1364
1531
  state: options.readContext.localState.state,
1365
1532
  rpc,
1366
1533
  nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
1367
1534
  nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
1368
- });
1535
+ snapshotState: options.readContext.snapshot.state,
1536
+ })).state;
1369
1537
  const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
1370
- const fundingConflict = state.miningState.sharedMiningConflictOutpoint
1371
- ?? allUtxos.find((entry) => entry.scriptPubKey === state.funding.scriptPubKeyHex
1372
- && entry.confirmations >= 1
1373
- && entry.spendable !== false
1374
- && entry.safe !== false
1375
- && !(entry.txid === options.candidate.anchorOutpoint.txid && entry.vout === options.candidate.anchorOutpoint.vout));
1376
- if (fundingConflict === undefined || fundingConflict === null) {
1377
- throw new Error("wallet_mining_missing_conflict_utxo");
1378
- }
1379
- const conflictOutpoint = "txid" in fundingConflict
1380
- ? { txid: fundingConflict.txid, vout: fundingConflict.vout }
1381
- : fundingConflict;
1538
+ const conflictOutpoint = resolveMiningConflictOutpoint({
1539
+ state,
1540
+ allUtxos,
1541
+ });
1382
1542
  const priorMiningState = cloneMiningState(state.miningState);
1383
- const nextFeeRate = state.miningState.currentFeeRateSatVb === null
1384
- ? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB
1385
- : state.miningState.currentFeeRateSatVb + 1;
1386
- const shouldFeeBump = candidateNeedsFeeMaintenance({
1543
+ if (livePublishTargetsCandidateTip({
1387
1544
  liveState: state.miningState,
1388
1545
  candidate: options.candidate,
1389
- });
1390
- if (state.miningState.currentPublishState === "in-mempool"
1391
- && state.miningState.liveMiningFamilyInMempool === true
1392
- && !shouldFeeBump
1393
- && !await candidateWinsAgainstLive({
1394
- liveState: state.miningState,
1395
- candidate: options.candidate,
1396
- })) {
1546
+ })) {
1397
1547
  return {
1398
1548
  state: defaultMiningStatePatch(state, {
1399
- currentPublishDecision: "kept-live-family",
1549
+ currentPublishDecision: "kept-live-publish",
1400
1550
  }),
1401
1551
  txid: state.miningState.currentTxid,
1402
- decision: "kept-live-family",
1552
+ decision: "kept-live-publish",
1403
1553
  };
1404
1554
  }
1555
+ const feeSelection = await resolveWalletMutationFeeSelection({
1556
+ rpc,
1557
+ });
1558
+ const nextFeeRate = feeSelection.feeRateSatVb;
1405
1559
  const plan = createMiningPlan({
1406
1560
  state,
1407
1561
  candidate: options.candidate,
@@ -1436,18 +1590,14 @@ async function publishCandidate(options) {
1436
1590
  currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1437
1591
  currentIntentFingerprintHex: intentFingerprintHex,
1438
1592
  sharedMiningConflictOutpoint: conflictOutpoint,
1439
- liveMiningFamilyInMempool: null,
1593
+ livePublishInMempool: null,
1440
1594
  currentPublishDecision: priorMiningState.currentTxid === null
1441
1595
  ? "publishing"
1442
- : shouldFeeBump
1443
- ? "fee-bump"
1444
- : "replacing",
1596
+ : "replacing",
1445
1597
  });
1446
1598
  await saveWalletStatePreservingUnlock({
1447
1599
  state,
1448
1600
  provider: options.provider,
1449
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1450
- nowUnixMs: Date.now(),
1451
1601
  paths: options.paths,
1452
1602
  });
1453
1603
  try {
@@ -1457,13 +1607,11 @@ async function publishCandidate(options) {
1457
1607
  if (isAlreadyAcceptedError(error)) {
1458
1608
  state = defaultMiningStatePatch(state, {
1459
1609
  currentPublishState: "in-mempool",
1460
- liveMiningFamilyInMempool: true,
1610
+ livePublishInMempool: true,
1461
1611
  });
1462
1612
  await saveWalletStatePreservingUnlock({
1463
1613
  state,
1464
1614
  provider: options.provider,
1465
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1466
- nowUnixMs: Date.now(),
1467
1615
  paths: options.paths,
1468
1616
  });
1469
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.`, {
@@ -1480,11 +1628,9 @@ async function publishCandidate(options) {
1480
1628
  return {
1481
1629
  state,
1482
1630
  txid: built.txid,
1483
- decision: state.miningState.currentPublishDecision === "fee-bump"
1484
- ? "fee-bump"
1485
- : state.miningState.currentPublishDecision === "replacing"
1486
- ? "replaced"
1487
- : "broadcast",
1631
+ decision: state.miningState.currentPublishDecision === "replacing"
1632
+ ? "replaced"
1633
+ : "broadcast",
1488
1634
  };
1489
1635
  }
1490
1636
  if (isBroadcastUnknownError(error)) {
@@ -1495,8 +1641,6 @@ async function publishCandidate(options) {
1495
1641
  await saveWalletStatePreservingUnlock({
1496
1642
  state,
1497
1643
  provider: options.provider,
1498
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1499
- nowUnixMs: Date.now(),
1500
1644
  paths: options.paths,
1501
1645
  });
1502
1646
  await appendEvent(options.paths, createEvent("error", `Mining broadcast became uncertain for ${built.txid}.`, {
@@ -1518,7 +1662,16 @@ async function publishCandidate(options) {
1518
1662
  decision: "broadcast-unknown",
1519
1663
  };
1520
1664
  }
1521
- 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);
1522
1675
  }
1523
1676
  const absoluteFeeSats = numberToSats(built.funded.fee);
1524
1677
  const replacementCount = priorMiningState.currentTxid === null
@@ -1526,12 +1679,10 @@ async function publishCandidate(options) {
1526
1679
  : priorMiningState.replacementCount + 1;
1527
1680
  state = defaultMiningStatePatch(state, {
1528
1681
  currentPublishState: "in-mempool",
1529
- liveMiningFamilyInMempool: true,
1530
- currentPublishDecision: state.miningState.currentPublishDecision === "fee-bump"
1531
- ? "fee-bump"
1532
- : state.miningState.currentPublishDecision === "replacing"
1533
- ? "replaced"
1534
- : "broadcast",
1682
+ livePublishInMempool: true,
1683
+ currentPublishDecision: state.miningState.currentPublishDecision === "replacing"
1684
+ ? "replaced"
1685
+ : "broadcast",
1535
1686
  replacementCount,
1536
1687
  currentAbsoluteFeeSats: Number(absoluteFeeSats),
1537
1688
  currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
@@ -1541,19 +1692,13 @@ async function publishCandidate(options) {
1541
1692
  await saveWalletStatePreservingUnlock({
1542
1693
  state,
1543
1694
  provider: options.provider,
1544
- unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1545
- nowUnixMs: Date.now(),
1546
1695
  paths: options.paths,
1547
1696
  });
1548
1697
  await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replaced"
1549
1698
  ? "tx-replaced"
1550
- : state.miningState.currentPublishDecision === "fee-bump"
1551
- ? "tx-fee-bump"
1552
- : "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
1699
+ : "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
1553
1700
  ? "Replaced"
1554
- : state.miningState.currentPublishDecision === "fee-bump"
1555
- ? "Fee-bumped"
1556
- : "Broadcast"} mining transaction ${built.txid}.`, {
1701
+ : "Broadcast"} mining transaction ${built.txid}.`, {
1557
1702
  runId: options.runId,
1558
1703
  targetBlockHeight: options.candidate.targetBlockHeight,
1559
1704
  referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
@@ -1567,21 +1712,106 @@ async function publishCandidate(options) {
1567
1712
  return {
1568
1713
  state,
1569
1714
  txid: built.txid,
1570
- decision: state.miningState.currentPublishDecision === "fee-bump"
1571
- ? "fee-bump"
1572
- : state.miningState.currentPublishDecision === "replaced"
1573
- ? "replaced"
1574
- : "broadcast",
1715
+ decision: state.miningState.currentPublishDecision === "replaced"
1716
+ ? "replaced"
1717
+ : "broadcast",
1575
1718
  };
1576
1719
  }
1577
- async function ensureBuiltInSetupIfNeeded(options) {
1578
- const unlocked = await loadOrAutoUnlockWalletState({
1579
- 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,
1580
1749
  paths: options.paths,
1581
1750
  });
1582
- if (unlocked?.state.hookClientState.mining.mode === "custom") {
1583
- 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
+ }
1806
+ }
1807
+ finally {
1808
+ await lockedReadContext.close();
1584
1809
  }
1810
+ }
1811
+ export async function publishCandidateForTesting(options) {
1812
+ return await publishCandidate(options);
1813
+ }
1814
+ async function ensureBuiltInSetupIfNeeded(options) {
1585
1815
  const config = await loadClientConfig({
1586
1816
  path: options.paths.clientConfigPath,
1587
1817
  provider: options.provider,
@@ -1620,7 +1850,7 @@ async function performMiningCycle(options) {
1620
1850
  backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
1621
1851
  },
1622
1852
  });
1623
- if (readContext.localState.availability !== "ready" || readContext.localState.state === null || readContext.localState.unlockUntilUnixMs === null) {
1853
+ if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
1624
1854
  await refreshAndSaveStatus({
1625
1855
  paths: options.paths,
1626
1856
  provider: options.provider,
@@ -1628,9 +1858,10 @@ async function performMiningCycle(options) {
1628
1858
  overrides: {
1629
1859
  runMode: options.runMode,
1630
1860
  currentPhase: "waiting",
1631
- note: "Wallet must stay unlocked for mining to continue.",
1861
+ note: "Wallet state must be locally available for mining to continue.",
1632
1862
  },
1633
1863
  visualizer: options.visualizer,
1864
+ visualizerState: options.loopState.ui,
1634
1865
  });
1635
1866
  return;
1636
1867
  }
@@ -1642,20 +1873,20 @@ async function performMiningCycle(options) {
1642
1873
  });
1643
1874
  checkpointMiningSuspendDetector(options.suspendDetector);
1644
1875
  const rpc = options.rpcFactory(service.rpc);
1645
- const reconciledState = await reconcileLiveMiningState({
1876
+ const reconciliation = await reconcileLiveMiningState({
1646
1877
  state: readContext.localState.state,
1647
1878
  rpc,
1648
1879
  nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
1649
1880
  nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
1881
+ snapshotState: readContext.snapshot?.state ?? null,
1650
1882
  });
1883
+ const reconciledState = reconciliation.state;
1651
1884
  checkpointMiningSuspendDetector(options.suspendDetector);
1652
1885
  let effectiveReadContext = readContext;
1653
1886
  if (JSON.stringify(reconciledState.miningState) !== JSON.stringify(readContext.localState.state.miningState)) {
1654
1887
  await saveWalletStatePreservingUnlock({
1655
1888
  state: reconciledState,
1656
1889
  provider: options.provider,
1657
- unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1658
- nowUnixMs: Date.now(),
1659
1890
  paths: options.paths,
1660
1891
  });
1661
1892
  effectiveReadContext = {
@@ -1663,11 +1894,23 @@ async function performMiningCycle(options) {
1663
1894
  localState: {
1664
1895
  ...readContext.localState,
1665
1896
  availability: "ready",
1666
- unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1667
1897
  state: reconciledState,
1668
1898
  },
1669
1899
  };
1670
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);
1671
1914
  if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
1672
1915
  await refreshAndSaveStatus({
1673
1916
  paths: options.paths,
@@ -1676,9 +1919,10 @@ async function performMiningCycle(options) {
1676
1919
  overrides: {
1677
1920
  runMode: options.runMode,
1678
1921
  currentPhase: "waiting",
1679
- 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.",
1680
1923
  },
1681
1924
  visualizer: options.visualizer,
1925
+ visualizerState: options.loopState.ui,
1682
1926
  });
1683
1927
  return;
1684
1928
  }
@@ -1690,8 +1934,6 @@ async function performMiningCycle(options) {
1690
1934
  await saveWalletStatePreservingUnlock({
1691
1935
  state: nextState,
1692
1936
  provider: options.provider,
1693
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1694
- nowUnixMs: Date.now(),
1695
1937
  paths: options.paths,
1696
1938
  });
1697
1939
  effectiveReadContext = {
@@ -1699,7 +1941,6 @@ async function performMiningCycle(options) {
1699
1941
  localState: {
1700
1942
  ...effectiveReadContext.localState,
1701
1943
  availability: "ready",
1702
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1703
1944
  state: nextState,
1704
1945
  },
1705
1946
  };
@@ -1710,16 +1951,17 @@ async function performMiningCycle(options) {
1710
1951
  overrides: {
1711
1952
  runMode: options.runMode,
1712
1953
  currentPhase: "waiting",
1713
- note: "Mining is paused while another wallet mutation family is active.",
1954
+ note: "Mining is paused while another wallet mutation is active.",
1714
1955
  },
1715
1956
  visualizer: options.visualizer,
1957
+ visualizerState: options.loopState.ui,
1716
1958
  });
1717
1959
  return;
1718
1960
  }
1719
1961
  const preemptionRequest = await readMiningPreemptionRequest(options.paths);
1720
1962
  if (preemptionRequest !== null) {
1721
1963
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
1722
- state: effectiveReadContext.localState.state.miningState.liveMiningFamilyInMempool
1964
+ state: effectiveReadContext.localState.state.miningState.livePublishInMempool
1723
1965
  && effectiveReadContext.localState.state.miningState.state === "paused-stale"
1724
1966
  ? "paused-stale"
1725
1967
  : "paused",
@@ -1728,8 +1970,6 @@ async function performMiningCycle(options) {
1728
1970
  await saveWalletStatePreservingUnlock({
1729
1971
  state: nextState,
1730
1972
  provider: options.provider,
1731
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1732
- nowUnixMs: Date.now(),
1733
1973
  paths: options.paths,
1734
1974
  });
1735
1975
  await refreshAndSaveStatus({
@@ -1748,6 +1988,7 @@ async function performMiningCycle(options) {
1748
1988
  note: "Mining is paused while another wallet command is preempting sentence generation.",
1749
1989
  },
1750
1990
  visualizer: options.visualizer,
1991
+ visualizerState: options.loopState.ui,
1751
1992
  });
1752
1993
  return;
1753
1994
  }
@@ -1774,6 +2015,7 @@ async function performMiningCycle(options) {
1774
2015
  note: "Mining is waiting for the local Bitcoin node to become publishable.",
1775
2016
  },
1776
2017
  visualizer: options.visualizer,
2018
+ visualizerState: options.loopState.ui,
1777
2019
  });
1778
2020
  return;
1779
2021
  }
@@ -1792,10 +2034,22 @@ async function performMiningCycle(options) {
1792
2034
  : "Mining is waiting for the local Bitcoin node to become publishable.",
1793
2035
  },
1794
2036
  visualizer: options.visualizer,
2037
+ visualizerState: options.loopState.ui,
1795
2038
  });
1796
2039
  return;
1797
2040
  }
1798
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);
1799
2053
  if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
1800
2054
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
1801
2055
  state: "paused",
@@ -1804,8 +2058,6 @@ async function performMiningCycle(options) {
1804
2058
  await saveWalletStatePreservingUnlock({
1805
2059
  state: nextState,
1806
2060
  provider: options.provider,
1807
- unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1808
- nowUnixMs: Date.now(),
1809
2061
  paths: options.paths,
1810
2062
  });
1811
2063
  await refreshAndSaveStatus({
@@ -1825,6 +2077,7 @@ async function performMiningCycle(options) {
1825
2077
  note: "Mining is disabled because the target block reward is zero.",
1826
2078
  },
1827
2079
  visualizer: options.visualizer,
2080
+ visualizerState: options.loopState.ui,
1828
2081
  });
1829
2082
  await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
1830
2083
  targetBlockHeight,
@@ -1833,18 +2086,18 @@ async function performMiningCycle(options) {
1833
2086
  }));
1834
2087
  return;
1835
2088
  }
1836
- const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
1837
- if (domains.length === 0) {
2089
+ if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
1838
2090
  await refreshAndSaveStatus({
1839
2091
  paths: options.paths,
1840
2092
  provider: options.provider,
1841
2093
  readContext: effectiveReadContext,
1842
2094
  overrides: {
1843
2095
  runMode: options.runMode,
1844
- currentPhase: "idle",
1845
- 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.",
1846
2098
  },
1847
2099
  visualizer: options.visualizer,
2100
+ visualizerState: options.loopState.ui,
1848
2101
  });
1849
2102
  return;
1850
2103
  }
@@ -1872,215 +2125,264 @@ async function performMiningCycle(options) {
1872
2125
  return false;
1873
2126
  }
1874
2127
  };
1875
- await refreshAndSaveStatus({
1876
- paths: options.paths,
1877
- provider: options.provider,
1878
- readContext: effectiveReadContext,
1879
- overrides: {
1880
- runMode: options.runMode,
1881
- currentPhase: "generating",
1882
- note: "Generating mining sentences for eligible root domains.",
1883
- },
1884
- visualizer: options.visualizer,
1885
- });
1886
- await appendEvent(options.paths, createEvent("hook-request-start", "Started mining sentence generation.", {
1887
- targetBlockHeight,
1888
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1889
- runId: options.backgroundWorkerRunId,
1890
- }));
1891
- let candidates;
1892
- try {
1893
- candidates = await generateCandidatesForDomains({
1894
- rpc,
1895
- readContext: effectiveReadContext,
1896
- domains,
1897
- provider: options.provider,
1898
- paths: options.paths,
1899
- indexerTruthKey,
1900
- runId: options.backgroundWorkerRunId,
1901
- fetchImpl: options.fetchImpl,
1902
- });
1903
- checkpointMiningSuspendDetector(options.suspendDetector);
1904
- }
1905
- catch (error) {
1906
- 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) {
1907
2138
  await refreshAndSaveStatus({
1908
2139
  paths: options.paths,
1909
2140
  provider: options.provider,
1910
2141
  readContext: effectiveReadContext,
1911
2142
  overrides: {
1912
2143
  runMode: options.runMode,
1913
- currentPhase: "waiting-provider",
1914
- providerState: error.providerState,
1915
- lastError: error.message,
1916
- 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.",
1917
2146
  },
1918
2147
  visualizer: options.visualizer,
2148
+ visualizerState: options.loopState.ui,
1919
2149
  });
1920
- await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
1921
- level: "warn",
1922
- targetBlockHeight,
1923
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1924
- runId: options.backgroundWorkerRunId,
1925
- }));
1926
- return;
1927
- }
1928
- if (error instanceof Error && error.message === "mining_generation_stale_tip") {
1929
- await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
1930
- level: "warn",
1931
- targetBlockHeight,
1932
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1933
- runId: options.backgroundWorkerRunId,
1934
- }));
1935
- return;
1936
- }
1937
- if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
1938
- clearMiningGateCache(walletRootId);
1939
- await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during sentence generation; restarting on the next tick.", {
1940
- level: "warn",
1941
- targetBlockHeight,
1942
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1943
- runId: options.backgroundWorkerRunId,
1944
- }));
1945
- return;
1946
- }
1947
- if (error instanceof Error && error.message === "mining_generation_preempted") {
1948
- await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
1949
- level: "warn",
1950
- targetBlockHeight,
1951
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1952
- runId: options.backgroundWorkerRunId,
1953
- }));
1954
2150
  return;
1955
2151
  }
1956
- const hookCooldownActive = await persistCustomHookRuntimeOutcome({
1957
- readContext: effectiveReadContext,
1958
- provider: options.provider,
1959
- paths: options.paths,
1960
- nowUnixMs: Date.now(),
1961
- success: false,
1962
- });
1963
- const failureMessage = error instanceof Error ? error.message : String(error);
1964
- await refreshAndSaveStatus({
1965
- paths: options.paths,
1966
- provider: options.provider,
1967
- readContext: effectiveReadContext,
1968
- overrides: {
1969
- runMode: options.runMode,
1970
- currentPhase: "waiting-provider",
1971
- providerState: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
1972
- ? "hook-error"
1973
- : undefined,
1974
- lastError: failureMessage,
1975
- note: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
1976
- ? (hookCooldownActive
1977
- ? "Custom mining hook launch is paused during the post-failure cooldown window."
1978
- : "Custom mining hook failed during sentence generation. Fix it or rerun `cogcoin hooks enable mining`.")
1979
- : "Mining sentence generation failed for the current tip.",
1980
- },
1981
- visualizer: options.visualizer,
1982
- });
1983
- await appendEvent(options.paths, createEvent("hook-request-failed", failureMessage, {
1984
- level: "error",
1985
- targetBlockHeight,
1986
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1987
- runId: options.backgroundWorkerRunId,
1988
- }));
1989
- return;
1990
- }
1991
- await refreshAndSaveStatus({
1992
- paths: options.paths,
1993
- provider: options.provider,
1994
- readContext: effectiveReadContext,
1995
- overrides: {
1996
- runMode: options.runMode,
1997
- currentPhase: "scoring",
1998
- note: "Scoring mining candidates for the current tip.",
1999
- },
2000
- visualizer: options.visualizer,
2001
- });
2002
- const best = await chooseBestLocalCandidate(candidates);
2003
- if (best === null) {
2004
2152
  await refreshAndSaveStatus({
2005
2153
  paths: options.paths,
2006
2154
  provider: options.provider,
2007
2155
  readContext: effectiveReadContext,
2008
2156
  overrides: {
2009
2157
  runMode: options.runMode,
2010
- currentPhase: "idle",
2011
- currentPublishDecision: "publish-skipped-no-candidate",
2012
- note: "No publishable mining candidate passed scoring gates for the current tip.",
2158
+ currentPhase: "generating",
2159
+ note: "Generating mining sentences for eligible root domains.",
2013
2160
  },
2014
2161
  visualizer: options.visualizer,
2162
+ visualizerState: options.loopState.ui,
2015
2163
  });
2016
- 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.", {
2017
2165
  targetBlockHeight,
2018
2166
  referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2019
2167
  runId: options.backgroundWorkerRunId,
2020
2168
  }));
2021
- return;
2022
- }
2023
- if (!await ensureCurrentIndexerTruthOrRestart()) {
2024
- return;
2025
- }
2026
- writeStdout(options.stdout, `Selected ${best.domainName}: ${best.sentence} (${best.canonicalBlend.toString()})`);
2027
- await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
2028
- targetBlockHeight: best.targetBlockHeight,
2029
- referencedBlockHashDisplay: best.referencedBlockHashDisplay,
2030
- domainId: best.domainId,
2031
- domainName: best.domainName,
2032
- score: best.canonicalBlend.toString(),
2033
- runId: options.backgroundWorkerRunId,
2034
- }));
2035
- const gate = await runCompetitivenessGate({
2036
- rpc,
2037
- readContext: effectiveReadContext,
2038
- candidate: best,
2039
- currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
2040
- });
2041
- checkpointMiningSuspendDetector(options.suspendDetector);
2042
- 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
+ }
2043
2266
  await refreshAndSaveStatus({
2044
2267
  paths: options.paths,
2045
2268
  provider: options.provider,
2046
2269
  readContext: effectiveReadContext,
2047
2270
  overrides: {
2048
2271
  runMode: options.runMode,
2049
- currentPhase: "waiting",
2050
- currentPublishDecision: gate.decision,
2051
- sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
2052
- higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2053
- dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2054
- competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
2055
- mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2056
- lastMempoolSequence: gate.lastMempoolSequence,
2057
- lastCompetitivenessGateAtUnixMs: Date.now(),
2058
- note: gate.decision === "suppressed-same-domain-mempool"
2059
- ? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
2060
- : gate.decision === "suppressed-top5-mempool"
2061
- ? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2062
- : "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.",
2063
2274
  },
2064
2275
  visualizer: options.visualizer,
2276
+ visualizerState: options.loopState.ui,
2065
2277
  });
2066
- await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
2067
- ? "publish-skipped-same-domain-mempool"
2068
- : gate.decision === "suppressed-top5-mempool"
2069
- ? "publish-skipped-top5-mempool"
2070
- : "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
2071
- ? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
2072
- : gate.decision === "suppressed-top5-mempool"
2073
- ? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2074
- : "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()}.`, {
2075
2312
  targetBlockHeight: best.targetBlockHeight,
2076
2313
  referencedBlockHashDisplay: best.referencedBlockHashDisplay,
2077
2314
  domainId: best.domainId,
2078
2315
  domainName: best.domainName,
2079
2316
  score: best.canonicalBlend.toString(),
2080
2317
  runId: options.backgroundWorkerRunId,
2081
- reason: gate.decision,
2082
2318
  }));
2083
- 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);
2084
2386
  }
2085
2387
  if (!await ensureCurrentIndexerTruthOrRestart()) {
2086
2388
  return;
@@ -2099,6 +2401,7 @@ async function performMiningCycle(options) {
2099
2401
  : "Replacing the live mining transaction for the current tip.",
2100
2402
  },
2101
2403
  visualizer: options.visualizer,
2404
+ visualizerState: options.loopState.ui,
2102
2405
  });
2103
2406
  const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
2104
2407
  purpose: "wallet-mine",
@@ -2111,16 +2414,98 @@ async function performMiningCycle(options) {
2111
2414
  }
2112
2415
  checkpointMiningSuspendDetector(options.suspendDetector);
2113
2416
  const published = await publishCandidate({
2114
- readContext: effectiveReadContext,
2115
- candidate: best,
2116
2417
  dataDir: options.dataDir,
2418
+ databasePath: options.databasePath,
2117
2419
  provider: options.provider,
2118
2420
  paths: options.paths,
2421
+ fallbackState: effectiveReadContext.localState.state,
2422
+ openReadContext: options.openReadContext,
2119
2423
  attachService: options.attachService,
2120
2424
  rpcFactory: options.rpcFactory,
2425
+ candidate: selectedCandidate,
2121
2426
  runId: options.backgroundWorkerRunId,
2122
2427
  });
2123
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.`;
2124
2509
  await refreshAndSaveStatus({
2125
2510
  paths: options.paths,
2126
2511
  provider: options.provider,
@@ -2133,25 +2518,20 @@ async function performMiningCycle(options) {
2133
2518
  },
2134
2519
  overrides: {
2135
2520
  runMode: options.runMode,
2136
- currentPhase: "publishing",
2521
+ currentPhase: "waiting",
2137
2522
  currentPublishDecision: published.decision,
2138
2523
  sameDomainCompetitorSuppressed: false,
2139
- higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2140
- dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2524
+ higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
2525
+ dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
2141
2526
  competitivenessGateIndeterminate: false,
2142
- mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2143
- lastMempoolSequence: gate.lastMempoolSequence,
2527
+ mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2528
+ lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2144
2529
  lastCompetitivenessGateAtUnixMs: Date.now(),
2145
- note: published.txid === null
2146
- ? "Mining candidate was evaluated but the existing live family stayed in place."
2147
- : `Mining candidate ${published.decision === "replaced"
2148
- ? "replaced"
2149
- : published.decision === "fee-bump"
2150
- ? "fee-bumped"
2151
- : "broadcast"} as ${published.txid}.`,
2152
- liveMiningFamilyInMempool: published.state.miningState.liveMiningFamilyInMempool,
2530
+ note: options.loopState.waitingNote,
2531
+ livePublishInMempool: published.state.miningState.livePublishInMempool,
2153
2532
  },
2154
2533
  visualizer: options.visualizer,
2534
+ visualizerState: options.loopState.ui,
2155
2535
  });
2156
2536
  }
2157
2537
  finally {
@@ -2195,7 +2575,7 @@ async function saveStopSnapshot(options) {
2195
2575
  });
2196
2576
  try {
2197
2577
  let localState = readContext.localState;
2198
- if (localState.availability === "ready" && localState.state !== null && localState.unlockUntilUnixMs !== null) {
2578
+ if (localState.availability === "ready" && localState.state !== null) {
2199
2579
  const service = await attachOrStartManagedBitcoindService({
2200
2580
  dataDir: options.dataDir,
2201
2581
  chain: "main",
@@ -2204,22 +2584,23 @@ async function saveStopSnapshot(options) {
2204
2584
  }).catch(() => null);
2205
2585
  if (service !== null) {
2206
2586
  const rpc = createRpcClient(service.rpc);
2207
- const reconciledState = await reconcileLiveMiningState({
2587
+ const reconciledState = (await reconcileLiveMiningState({
2208
2588
  state: localState.state,
2209
2589
  rpc,
2210
2590
  nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
2211
2591
  nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
2212
- });
2592
+ snapshotState: readContext.snapshot?.state ?? null,
2593
+ })).state;
2213
2594
  const stopState = defaultMiningStatePatch(reconciledState, {
2214
2595
  runMode: "stopped",
2215
- state: reconciledState.miningState.liveMiningFamilyInMempool
2596
+ state: reconciledState.miningState.livePublishInMempool
2216
2597
  ? reconciledState.miningState.state === "paused-stale"
2217
2598
  ? "paused-stale"
2218
2599
  : "paused"
2219
2600
  : reconciledState.miningState.state === "repair-required"
2220
2601
  ? "repair-required"
2221
2602
  : "idle",
2222
- pauseReason: reconciledState.miningState.liveMiningFamilyInMempool
2603
+ pauseReason: reconciledState.miningState.livePublishInMempool
2223
2604
  ? reconciledState.miningState.state === "paused-stale"
2224
2605
  ? "stale-block-context"
2225
2606
  : "user-stopped"
@@ -2230,8 +2611,6 @@ async function saveStopSnapshot(options) {
2230
2611
  await saveWalletStatePreservingUnlock({
2231
2612
  state: stopState,
2232
2613
  provider: options.provider,
2233
- unlockUntilUnixMs: localState.unlockUntilUnixMs,
2234
- nowUnixMs: Date.now(),
2235
2614
  paths: options.paths,
2236
2615
  });
2237
2616
  localState = {
@@ -2276,6 +2655,7 @@ async function attemptSaveMempool(rpc, paths, runId) {
2276
2655
  }
2277
2656
  async function runMiningLoop(options) {
2278
2657
  const suspendDetector = createMiningSuspendDetector();
2658
+ const loopState = createMiningLoopState();
2279
2659
  await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
2280
2660
  runId: options.backgroundWorkerRunId,
2281
2661
  }));
@@ -2304,6 +2684,7 @@ async function runMiningLoop(options) {
2304
2684
  await performMiningCycle({
2305
2685
  ...options,
2306
2686
  suspendDetector,
2687
+ loopState,
2307
2688
  });
2308
2689
  await sleep(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
2309
2690
  }
@@ -2493,7 +2874,7 @@ export async function stopBackgroundMining(options) {
2493
2874
  runMode: "background",
2494
2875
  backgroundWorkerPid: snapshot.backgroundWorkerPid,
2495
2876
  backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
2496
- note: snapshot.liveMiningFamilyInMempool
2877
+ note: snapshot.livePublishInMempool
2497
2878
  ? "Background mining stopped. The last mining transaction may still confirm from mempool."
2498
2879
  : "Background mining stopped.",
2499
2880
  });
@@ -2575,8 +2956,11 @@ export async function handleDetectedMiningRuntimeResumeForTesting(options) {
2575
2956
  await handleDetectedMiningRuntimeResume(options);
2576
2957
  }
2577
2958
  export async function performMiningCycleForTesting(options) {
2578
- await performMiningCycle(options);
2959
+ await performMiningCycle({
2960
+ ...options,
2961
+ loopState: options.loopState ?? createMiningLoopState(),
2962
+ });
2579
2963
  }
2580
- export function shouldTreatCandidateAsFeeBumpForTesting(options) {
2581
- return candidateNeedsFeeMaintenance(options);
2964
+ export function shouldKeepCurrentTipLivePublishForTesting(options) {
2965
+ return livePublishTargetsCandidateTip(options);
2582
2966
  }