@cogcoin/client 0.5.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 (289) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/dist/app-paths.d.ts +38 -0
  4. package/dist/app-paths.js +121 -0
  5. package/dist/art/banner.txt +13 -0
  6. package/dist/art/scroll.txt +13 -0
  7. package/dist/art/train-car.txt +6 -0
  8. package/dist/art/train-smoke.txt +6 -0
  9. package/dist/art/train.txt +6 -0
  10. package/dist/bitcoind/bootstrap/chainstate.d.ts +4 -0
  11. package/dist/bitcoind/bootstrap/chainstate.js +13 -0
  12. package/dist/bitcoind/bootstrap/constants.d.ts +7 -0
  13. package/dist/bitcoind/bootstrap/constants.js +12 -0
  14. package/dist/bitcoind/bootstrap/controller.d.ts +29 -0
  15. package/dist/bitcoind/bootstrap/controller.js +101 -0
  16. package/dist/bitcoind/bootstrap/download.d.ts +2 -0
  17. package/dist/bitcoind/bootstrap/download.js +196 -0
  18. package/dist/bitcoind/bootstrap/headers.d.ts +13 -0
  19. package/dist/bitcoind/bootstrap/headers.js +61 -0
  20. package/dist/bitcoind/bootstrap/paths.d.ts +4 -0
  21. package/dist/bitcoind/bootstrap/paths.js +15 -0
  22. package/dist/bitcoind/bootstrap/snapshot-file.d.ts +7 -0
  23. package/dist/bitcoind/bootstrap/snapshot-file.js +42 -0
  24. package/dist/bitcoind/bootstrap/state.d.ts +40 -0
  25. package/dist/bitcoind/bootstrap/state.js +70 -0
  26. package/dist/bitcoind/bootstrap/types.d.ts +28 -0
  27. package/dist/bitcoind/bootstrap/types.js +1 -0
  28. package/dist/bitcoind/bootstrap.d.ts +8 -0
  29. package/dist/bitcoind/bootstrap.js +7 -0
  30. package/dist/bitcoind/client/factory.d.ts +3 -0
  31. package/dist/bitcoind/client/factory.js +57 -0
  32. package/dist/bitcoind/client/follow-block-times.d.ts +8 -0
  33. package/dist/bitcoind/client/follow-block-times.js +25 -0
  34. package/dist/bitcoind/client/follow-loop.d.ts +10 -0
  35. package/dist/bitcoind/client/follow-loop.js +57 -0
  36. package/dist/bitcoind/client/internal-types.d.ts +63 -0
  37. package/dist/bitcoind/client/internal-types.js +18 -0
  38. package/dist/bitcoind/client/managed-client.d.ts +20 -0
  39. package/dist/bitcoind/client/managed-client.js +197 -0
  40. package/dist/bitcoind/client/rate-tracker.d.ts +2 -0
  41. package/dist/bitcoind/client/rate-tracker.js +24 -0
  42. package/dist/bitcoind/client/sync-engine.d.ts +3 -0
  43. package/dist/bitcoind/client/sync-engine.js +143 -0
  44. package/dist/bitcoind/client.d.ts +1 -0
  45. package/dist/bitcoind/client.js +1 -0
  46. package/dist/bitcoind/errors.d.ts +1 -0
  47. package/dist/bitcoind/errors.js +49 -0
  48. package/dist/bitcoind/index.d.ts +2 -0
  49. package/dist/bitcoind/index.js +1 -0
  50. package/dist/bitcoind/indexer-daemon-main.d.ts +1 -0
  51. package/dist/bitcoind/indexer-daemon-main.js +472 -0
  52. package/dist/bitcoind/indexer-daemon.d.ts +107 -0
  53. package/dist/bitcoind/indexer-daemon.js +391 -0
  54. package/dist/bitcoind/node.d.ts +8 -0
  55. package/dist/bitcoind/node.js +219 -0
  56. package/dist/bitcoind/normalize.d.ts +3 -0
  57. package/dist/bitcoind/normalize.js +47 -0
  58. package/dist/bitcoind/progress/assets.d.ts +10 -0
  59. package/dist/bitcoind/progress/assets.js +90 -0
  60. package/dist/bitcoind/progress/constants.d.ts +48 -0
  61. package/dist/bitcoind/progress/constants.js +53 -0
  62. package/dist/bitcoind/progress/controller.d.ts +28 -0
  63. package/dist/bitcoind/progress/controller.js +188 -0
  64. package/dist/bitcoind/progress/follow-scene.d.ts +40 -0
  65. package/dist/bitcoind/progress/follow-scene.js +367 -0
  66. package/dist/bitcoind/progress/formatting.d.ts +23 -0
  67. package/dist/bitcoind/progress/formatting.js +227 -0
  68. package/dist/bitcoind/progress/quote-scene.d.ts +4 -0
  69. package/dist/bitcoind/progress/quote-scene.js +137 -0
  70. package/dist/bitcoind/progress/train-scene.d.ts +9 -0
  71. package/dist/bitcoind/progress/train-scene.js +92 -0
  72. package/dist/bitcoind/progress/tty-renderer.d.ts +18 -0
  73. package/dist/bitcoind/progress/tty-renderer.js +150 -0
  74. package/dist/bitcoind/progress.d.ts +7 -0
  75. package/dist/bitcoind/progress.js +7 -0
  76. package/dist/bitcoind/quotes.d.ts +24 -0
  77. package/dist/bitcoind/quotes.js +195 -0
  78. package/dist/bitcoind/rpc.d.ts +71 -0
  79. package/dist/bitcoind/rpc.js +322 -0
  80. package/dist/bitcoind/service-paths.d.ts +19 -0
  81. package/dist/bitcoind/service-paths.js +49 -0
  82. package/dist/bitcoind/service.d.ts +40 -0
  83. package/dist/bitcoind/service.js +735 -0
  84. package/dist/bitcoind/testing.d.ts +9 -0
  85. package/dist/bitcoind/testing.js +9 -0
  86. package/dist/bitcoind/types.d.ts +396 -0
  87. package/dist/bitcoind/types.js +3 -0
  88. package/dist/bytes.d.ts +9 -0
  89. package/dist/bytes.js +36 -0
  90. package/dist/cli/commands/follow.d.ts +2 -0
  91. package/dist/cli/commands/follow.js +43 -0
  92. package/dist/cli/commands/mining-admin.d.ts +2 -0
  93. package/dist/cli/commands/mining-admin.js +92 -0
  94. package/dist/cli/commands/mining-read.d.ts +2 -0
  95. package/dist/cli/commands/mining-read.js +173 -0
  96. package/dist/cli/commands/mining-runtime.d.ts +2 -0
  97. package/dist/cli/commands/mining-runtime.js +108 -0
  98. package/dist/cli/commands/status.d.ts +2 -0
  99. package/dist/cli/commands/status.js +31 -0
  100. package/dist/cli/commands/sync.d.ts +2 -0
  101. package/dist/cli/commands/sync.js +52 -0
  102. package/dist/cli/commands/wallet-admin.d.ts +2 -0
  103. package/dist/cli/commands/wallet-admin.js +175 -0
  104. package/dist/cli/commands/wallet-mutation.d.ts +2 -0
  105. package/dist/cli/commands/wallet-mutation.js +681 -0
  106. package/dist/cli/commands/wallet-read.d.ts +2 -0
  107. package/dist/cli/commands/wallet-read.js +265 -0
  108. package/dist/cli/context.d.ts +3 -0
  109. package/dist/cli/context.js +75 -0
  110. package/dist/cli/io.d.ts +3 -0
  111. package/dist/cli/io.js +12 -0
  112. package/dist/cli/mining-format.d.ts +5 -0
  113. package/dist/cli/mining-format.js +156 -0
  114. package/dist/cli/mining-json.d.ts +49 -0
  115. package/dist/cli/mining-json.js +89 -0
  116. package/dist/cli/mutation-command-groups.d.ts +15 -0
  117. package/dist/cli/mutation-command-groups.js +71 -0
  118. package/dist/cli/mutation-json.d.ts +430 -0
  119. package/dist/cli/mutation-json.js +311 -0
  120. package/dist/cli/mutation-resolved-json.d.ts +124 -0
  121. package/dist/cli/mutation-resolved-json.js +129 -0
  122. package/dist/cli/mutation-success.d.ts +20 -0
  123. package/dist/cli/mutation-success.js +47 -0
  124. package/dist/cli/mutation-text-format.d.ts +22 -0
  125. package/dist/cli/mutation-text-format.js +171 -0
  126. package/dist/cli/mutation-text-write.d.ts +13 -0
  127. package/dist/cli/mutation-text-write.js +16 -0
  128. package/dist/cli/output.d.ts +185 -0
  129. package/dist/cli/output.js +1085 -0
  130. package/dist/cli/parse.d.ts +3 -0
  131. package/dist/cli/parse.js +971 -0
  132. package/dist/cli/preview-json.d.ts +416 -0
  133. package/dist/cli/preview-json.js +293 -0
  134. package/dist/cli/prompt.d.ts +3 -0
  135. package/dist/cli/prompt.js +33 -0
  136. package/dist/cli/read-json.d.ts +187 -0
  137. package/dist/cli/read-json.js +675 -0
  138. package/dist/cli/runner.d.ts +2 -0
  139. package/dist/cli/runner.js +129 -0
  140. package/dist/cli/signals.d.ts +3 -0
  141. package/dist/cli/signals.js +63 -0
  142. package/dist/cli/status-format.d.ts +2 -0
  143. package/dist/cli/status-format.js +48 -0
  144. package/dist/cli/types.d.ts +148 -0
  145. package/dist/cli/types.js +2 -0
  146. package/dist/cli/wallet-format.d.ts +29 -0
  147. package/dist/cli/wallet-format.js +637 -0
  148. package/dist/cli/workflow-hints.d.ts +13 -0
  149. package/dist/cli/workflow-hints.js +94 -0
  150. package/dist/cli-runner.d.ts +3 -0
  151. package/dist/cli-runner.js +3 -0
  152. package/dist/cli.d.ts +2 -0
  153. package/dist/cli.js +6 -0
  154. package/dist/client/default-client.d.ts +11 -0
  155. package/dist/client/default-client.js +118 -0
  156. package/dist/client/factory.d.ts +2 -0
  157. package/dist/client/factory.js +15 -0
  158. package/dist/client/initialization.d.ts +6 -0
  159. package/dist/client/initialization.js +30 -0
  160. package/dist/client/persistence.d.ts +5 -0
  161. package/dist/client/persistence.js +28 -0
  162. package/dist/client/store-adapter.d.ts +3 -0
  163. package/dist/client/store-adapter.js +20 -0
  164. package/dist/client.d.ts +2 -0
  165. package/dist/client.js +2 -0
  166. package/dist/index.d.ts +2 -0
  167. package/dist/index.js +1 -0
  168. package/dist/passive-status.d.ts +36 -0
  169. package/dist/passive-status.js +100 -0
  170. package/dist/sqlite/better-sqlite3.d.ts +26 -0
  171. package/dist/sqlite/better-sqlite3.js +4 -0
  172. package/dist/sqlite/checkpoints.d.ts +11 -0
  173. package/dist/sqlite/checkpoints.js +27 -0
  174. package/dist/sqlite/driver.d.ts +17 -0
  175. package/dist/sqlite/driver.js +98 -0
  176. package/dist/sqlite/index.d.ts +4 -0
  177. package/dist/sqlite/index.js +9 -0
  178. package/dist/sqlite/migrate.d.ts +2 -0
  179. package/dist/sqlite/migrate.js +37 -0
  180. package/dist/sqlite/store.d.ts +3 -0
  181. package/dist/sqlite/store.js +122 -0
  182. package/dist/sqlite/tip-meta.d.ts +26 -0
  183. package/dist/sqlite/tip-meta.js +97 -0
  184. package/dist/sqlite/types.d.ts +10 -0
  185. package/dist/sqlite/types.js +1 -0
  186. package/dist/types.d.ts +55 -0
  187. package/dist/types.js +1 -0
  188. package/dist/wallet/archive.d.ts +4 -0
  189. package/dist/wallet/archive.js +39 -0
  190. package/dist/wallet/cogop/constants.d.ts +32 -0
  191. package/dist/wallet/cogop/constants.js +32 -0
  192. package/dist/wallet/cogop/index.d.ts +32 -0
  193. package/dist/wallet/cogop/index.js +213 -0
  194. package/dist/wallet/cogop/numeric.d.ts +3 -0
  195. package/dist/wallet/cogop/numeric.js +24 -0
  196. package/dist/wallet/cogop/scriptpubkey.d.ts +2 -0
  197. package/dist/wallet/cogop/scriptpubkey.js +13 -0
  198. package/dist/wallet/cogop/validate-name.d.ts +2 -0
  199. package/dist/wallet/cogop/validate-name.js +18 -0
  200. package/dist/wallet/fs/atomic.d.ts +6 -0
  201. package/dist/wallet/fs/atomic.js +46 -0
  202. package/dist/wallet/fs/lock.d.ts +19 -0
  203. package/dist/wallet/fs/lock.js +61 -0
  204. package/dist/wallet/fs/status-file.d.ts +1 -0
  205. package/dist/wallet/fs/status-file.js +4 -0
  206. package/dist/wallet/lifecycle.d.ts +193 -0
  207. package/dist/wallet/lifecycle.js +1475 -0
  208. package/dist/wallet/material.d.ts +45 -0
  209. package/dist/wallet/material.js +118 -0
  210. package/dist/wallet/mining/config.d.ts +18 -0
  211. package/dist/wallet/mining/config.js +44 -0
  212. package/dist/wallet/mining/constants.d.ts +24 -0
  213. package/dist/wallet/mining/constants.js +24 -0
  214. package/dist/wallet/mining/control.d.ts +53 -0
  215. package/dist/wallet/mining/control.js +758 -0
  216. package/dist/wallet/mining/coordination.d.ts +40 -0
  217. package/dist/wallet/mining/coordination.js +121 -0
  218. package/dist/wallet/mining/hook-protocol.d.ts +47 -0
  219. package/dist/wallet/mining/hook-protocol.js +161 -0
  220. package/dist/wallet/mining/hook-runner.d.ts +1 -0
  221. package/dist/wallet/mining/hook-runner.js +52 -0
  222. package/dist/wallet/mining/hooks.d.ts +38 -0
  223. package/dist/wallet/mining/hooks.js +520 -0
  224. package/dist/wallet/mining/index.d.ts +8 -0
  225. package/dist/wallet/mining/index.js +6 -0
  226. package/dist/wallet/mining/runner.d.ts +155 -0
  227. package/dist/wallet/mining/runner.js +2574 -0
  228. package/dist/wallet/mining/runtime-artifacts.d.ts +17 -0
  229. package/dist/wallet/mining/runtime-artifacts.js +166 -0
  230. package/dist/wallet/mining/sentences.d.ts +23 -0
  231. package/dist/wallet/mining/sentences.js +281 -0
  232. package/dist/wallet/mining/state.d.ts +9 -0
  233. package/dist/wallet/mining/state.js +75 -0
  234. package/dist/wallet/mining/types.d.ts +141 -0
  235. package/dist/wallet/mining/types.js +1 -0
  236. package/dist/wallet/mining/visualizer.d.ts +19 -0
  237. package/dist/wallet/mining/visualizer.js +134 -0
  238. package/dist/wallet/mining/worker-main.d.ts +1 -0
  239. package/dist/wallet/mining/worker-main.js +17 -0
  240. package/dist/wallet/read/context.d.ts +20 -0
  241. package/dist/wallet/read/context.js +532 -0
  242. package/dist/wallet/read/filter.d.ts +9 -0
  243. package/dist/wallet/read/filter.js +42 -0
  244. package/dist/wallet/read/index.d.ts +4 -0
  245. package/dist/wallet/read/index.js +3 -0
  246. package/dist/wallet/read/project.d.ts +11 -0
  247. package/dist/wallet/read/project.js +300 -0
  248. package/dist/wallet/read/types.d.ts +144 -0
  249. package/dist/wallet/read/types.js +1 -0
  250. package/dist/wallet/runtime.d.ts +26 -0
  251. package/dist/wallet/runtime.js +28 -0
  252. package/dist/wallet/state/crypto.d.ts +31 -0
  253. package/dist/wallet/state/crypto.js +127 -0
  254. package/dist/wallet/state/provider.d.ts +37 -0
  255. package/dist/wallet/state/provider.js +312 -0
  256. package/dist/wallet/state/session.d.ts +12 -0
  257. package/dist/wallet/state/session.js +23 -0
  258. package/dist/wallet/state/storage.d.ts +19 -0
  259. package/dist/wallet/state/storage.js +55 -0
  260. package/dist/wallet/tx/anchor.d.ts +40 -0
  261. package/dist/wallet/tx/anchor.js +1210 -0
  262. package/dist/wallet/tx/cog.d.ts +92 -0
  263. package/dist/wallet/tx/cog.js +1055 -0
  264. package/dist/wallet/tx/common.d.ts +89 -0
  265. package/dist/wallet/tx/common.js +156 -0
  266. package/dist/wallet/tx/confirm.d.ts +15 -0
  267. package/dist/wallet/tx/confirm.js +24 -0
  268. package/dist/wallet/tx/domain-admin.d.ts +105 -0
  269. package/dist/wallet/tx/domain-admin.js +869 -0
  270. package/dist/wallet/tx/domain-market.d.ts +112 -0
  271. package/dist/wallet/tx/domain-market.js +1365 -0
  272. package/dist/wallet/tx/field.d.ts +101 -0
  273. package/dist/wallet/tx/field.js +1853 -0
  274. package/dist/wallet/tx/identity-selector.d.ts +12 -0
  275. package/dist/wallet/tx/identity-selector.js +52 -0
  276. package/dist/wallet/tx/index.d.ts +7 -0
  277. package/dist/wallet/tx/index.js +7 -0
  278. package/dist/wallet/tx/journal.d.ts +5 -0
  279. package/dist/wallet/tx/journal.js +31 -0
  280. package/dist/wallet/tx/register.d.ts +68 -0
  281. package/dist/wallet/tx/register.js +952 -0
  282. package/dist/wallet/tx/reputation.d.ts +72 -0
  283. package/dist/wallet/tx/reputation.js +693 -0
  284. package/dist/wallet/tx/targets.d.ts +7 -0
  285. package/dist/wallet/tx/targets.js +122 -0
  286. package/dist/wallet/types.d.ts +249 -0
  287. package/dist/wallet/types.js +1 -0
  288. package/dist/writing_quotes.json +1654 -0
  289. package/package.json +78 -0
@@ -0,0 +1,2574 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
5
+ import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
6
+ import { probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
7
+ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
8
+ import { createRpcClient } from "../../bitcoind/node.js";
9
+ import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
10
+ import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
11
+ import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, saveWalletStatePreservingUnlock, } from "../tx/common.js";
12
+ import { acquireFileLock } from "../fs/lock.js";
13
+ import { loadUnlockedWalletState } from "../lifecycle.js";
14
+ import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
15
+ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
16
+ import { createDefaultWalletSecretProvider, } from "../state/provider.js";
17
+ import { serializeMine } from "../cogop/index.js";
18
+ import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
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";
21
+ import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
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";
25
+ import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
26
+ import { MiningFollowVisualizer } from "./visualizer.js";
27
+ const BEST_BLOCK_POLL_INTERVAL_MS = 500;
28
+ const BACKGROUND_START_TIMEOUT_MS = 15_000;
29
+ class MiningSuspendDetectedError extends Error {
30
+ detectedAtUnixMs;
31
+ constructor(detectedAtUnixMs) {
32
+ super("mining_runtime_resumed");
33
+ this.detectedAtUnixMs = detectedAtUnixMs;
34
+ }
35
+ }
36
+ const miningGateCache = new Map();
37
+ function createMiningSuspendDetector(monotonicNow = performance.now()) {
38
+ return {
39
+ lastMonotonicMs: monotonicNow,
40
+ };
41
+ }
42
+ function checkpointMiningSuspendDetector(detector, monotonicNow = performance.now()) {
43
+ if (detector === undefined) {
44
+ return;
45
+ }
46
+ const gapMs = monotonicNow - detector.lastMonotonicMs;
47
+ detector.lastMonotonicMs = monotonicNow;
48
+ if (gapMs > MINING_SUSPEND_GAP_THRESHOLD_MS) {
49
+ throw new MiningSuspendDetectedError(Date.now());
50
+ }
51
+ }
52
+ function clearMiningGateCache(walletRootId) {
53
+ if (walletRootId === null || walletRootId === undefined) {
54
+ miningGateCache.clear();
55
+ return;
56
+ }
57
+ miningGateCache.delete(walletRootId);
58
+ }
59
+ function sleep(ms, signal) {
60
+ return new Promise((resolve) => {
61
+ const timer = setTimeout(resolve, ms);
62
+ signal?.addEventListener("abort", () => {
63
+ clearTimeout(timer);
64
+ resolve();
65
+ }, { once: true });
66
+ });
67
+ }
68
+ async function isProcessAlive(pid) {
69
+ if (pid === null) {
70
+ return false;
71
+ }
72
+ try {
73
+ process.kill(pid, 0);
74
+ return true;
75
+ }
76
+ catch (error) {
77
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+ }
83
+ function writeStdout(stream, line) {
84
+ if (stream === undefined) {
85
+ return;
86
+ }
87
+ stream.write(`${line}\n`);
88
+ }
89
+ function createEvent(kind, message, options = {}) {
90
+ return {
91
+ schemaVersion: 1,
92
+ timestampUnixMs: options.timestampUnixMs ?? Date.now(),
93
+ level: options.level ?? "info",
94
+ kind,
95
+ message,
96
+ targetBlockHeight: options.targetBlockHeight ?? null,
97
+ referencedBlockHashDisplay: options.referencedBlockHashDisplay ?? null,
98
+ domainId: options.domainId ?? null,
99
+ domainName: options.domainName ?? null,
100
+ txid: options.txid ?? null,
101
+ feeRateSatVb: options.feeRateSatVb ?? null,
102
+ feeSats: options.feeSats ?? null,
103
+ score: options.score ?? null,
104
+ reason: options.reason ?? null,
105
+ runId: options.runId ?? null,
106
+ };
107
+ }
108
+ function cloneMiningState(state) {
109
+ const normalized = normalizeMiningStateRecord(state);
110
+ return {
111
+ ...normalized,
112
+ currentBip39WordIndices: normalized.currentBip39WordIndices === null ? null : [...normalized.currentBip39WordIndices],
113
+ sharedMiningConflictOutpoint: normalized.sharedMiningConflictOutpoint === null
114
+ ? null
115
+ : { ...normalized.sharedMiningConflictOutpoint },
116
+ };
117
+ }
118
+ 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"
124
+ || mutation.status === "broadcasting"
125
+ || mutation.status === "broadcast-unknown"
126
+ || mutation.status === "live"
127
+ || mutation.status === "repair-required");
128
+ }
129
+ function rootDomain(name) {
130
+ return !name.includes("-");
131
+ }
132
+ function uint32BigEndian(value) {
133
+ const buffer = Buffer.alloc(4);
134
+ buffer.writeUInt32BE(value >>> 0, 0);
135
+ return buffer;
136
+ }
137
+ function getBlockRewardCogtoshi(height) {
138
+ const halvingEra = Math.floor(height / 210_000);
139
+ if (halvingEra >= 33) {
140
+ return 0n;
141
+ }
142
+ return 5000000000n >> BigInt(halvingEra);
143
+ }
144
+ function deriveMiningWordIndices(referencedBlockhash, miningDomainId) {
145
+ const seed = createHash("sha256")
146
+ .update(Buffer.from(referencedBlockhash))
147
+ .update(uint32BigEndian(miningDomainId))
148
+ .digest();
149
+ const indices = [];
150
+ for (let index = 0; index < 5; index += 1) {
151
+ const chunkOffset = index * 4;
152
+ let wordIndex = seed.readUInt32BE(chunkOffset) % 2048;
153
+ while (indices.includes(wordIndex)) {
154
+ wordIndex = (wordIndex + 1) % 2048;
155
+ }
156
+ indices.push(wordIndex);
157
+ }
158
+ return indices;
159
+ }
160
+ function outpointKey(outpoint) {
161
+ return outpoint === null ? null : `${outpoint.txid}:${outpoint.vout}`;
162
+ }
163
+ function numberToSats(value) {
164
+ const text = typeof value === "number" ? value.toFixed(8) : value;
165
+ const match = /^(-?)(\d+)(?:\.(\d{0,8}))?$/.exec(text.trim());
166
+ if (match == null) {
167
+ throw new Error(`mining_invalid_amount_${text}`);
168
+ }
169
+ const sign = match[1] === "-" ? -1n : 1n;
170
+ const whole = BigInt(match[2] ?? "0");
171
+ const fraction = BigInt((match[3] ?? "").padEnd(8, "0"));
172
+ return sign * ((whole * 100000000n) + fraction);
173
+ }
174
+ function satsToBtc(value) {
175
+ return Number(value) / 100_000_000;
176
+ }
177
+ function computeIntentFingerprint(state, candidate) {
178
+ return createHash("sha256")
179
+ .update([
180
+ "mine",
181
+ state.walletRootId,
182
+ candidate.domainId,
183
+ candidate.referencedBlockHashDisplay,
184
+ Buffer.from(candidate.encodedSentenceBytes).toString("hex"),
185
+ ].join("\n"))
186
+ .digest("hex");
187
+ }
188
+ function defaultMiningStatePatch(state, patch) {
189
+ return {
190
+ ...state,
191
+ miningState: {
192
+ ...cloneMiningState(state.miningState),
193
+ ...patch,
194
+ currentPublishState: normalizeMiningPublishState(patch.currentPublishState ?? state.miningState.currentPublishState),
195
+ },
196
+ };
197
+ }
198
+ function decodeMinePayload(payload) {
199
+ if (payload.length < 68 || Buffer.from(payload.subarray(0, 3)).toString("utf8") !== "COG" || payload[3] !== 0x01) {
200
+ return null;
201
+ }
202
+ return {
203
+ domainId: Buffer.from(payload).readUInt32BE(4),
204
+ referencedBlockPrefixHex: Buffer.from(payload.subarray(8, 12)).toString("hex"),
205
+ sentenceBytes: payload.subarray(12, 72),
206
+ };
207
+ }
208
+ function bytesToHex(value) {
209
+ return value == null ? null : Buffer.from(value).toString("hex");
210
+ }
211
+ function readU32BE(bytes, offset) {
212
+ if ((offset + 4) > bytes.length) {
213
+ return null;
214
+ }
215
+ return Buffer.from(bytes.subarray(offset, offset + 4)).readUInt32BE(0);
216
+ }
217
+ function readLenPrefixedScriptHex(bytes, offset) {
218
+ const length = bytes[offset];
219
+ if (length === undefined || (offset + 1 + length) > bytes.length) {
220
+ return null;
221
+ }
222
+ return {
223
+ scriptHex: Buffer.from(bytes.subarray(offset + 1, offset + 1 + length)).toString("hex"),
224
+ nextOffset: offset + 1 + length,
225
+ };
226
+ }
227
+ function parseSupportedAncestorOperation(context) {
228
+ const payload = context.payload;
229
+ if (payload === null) {
230
+ return null;
231
+ }
232
+ if (payload.length < 4
233
+ || payload[0] !== COG_PREFIX[0]
234
+ || payload[1] !== COG_PREFIX[1]
235
+ || payload[2] !== COG_PREFIX[2]) {
236
+ return null;
237
+ }
238
+ const opcode = payload[3];
239
+ if (opcode === COG_OPCODES.DOMAIN_REG) {
240
+ const nameLength = payload[4];
241
+ if (nameLength === undefined || (5 + nameLength) !== payload.length) {
242
+ return "unsupported";
243
+ }
244
+ return {
245
+ kind: "domain-reg",
246
+ name: Buffer.from(payload.subarray(5, 5 + nameLength)).toString("utf8"),
247
+ senderScriptHex: context.senderScriptHex,
248
+ };
249
+ }
250
+ if (opcode === COG_OPCODES.DOMAIN_TRANSFER) {
251
+ const domainId = readU32BE(payload, 4);
252
+ const recipient = domainId === null ? null : readLenPrefixedScriptHex(payload, 8);
253
+ if (domainId === null || recipient === null || recipient.nextOffset !== payload.length) {
254
+ return "unsupported";
255
+ }
256
+ return {
257
+ kind: "domain-transfer",
258
+ domainId,
259
+ recipientScriptHex: recipient.scriptHex,
260
+ senderScriptHex: context.senderScriptHex,
261
+ };
262
+ }
263
+ if (opcode === COG_OPCODES.DOMAIN_ANCHOR) {
264
+ const domainId = readU32BE(payload, 4);
265
+ if (domainId === null) {
266
+ return "unsupported";
267
+ }
268
+ return {
269
+ kind: "domain-anchor",
270
+ domainId,
271
+ senderScriptHex: context.senderScriptHex,
272
+ };
273
+ }
274
+ if (opcode === COG_OPCODES.SET_DELEGATE || opcode === COG_OPCODES.SET_MINER) {
275
+ const domainId = readU32BE(payload, 4);
276
+ if (domainId === null) {
277
+ return "unsupported";
278
+ }
279
+ if (payload.length === 8) {
280
+ return opcode === COG_OPCODES.SET_DELEGATE
281
+ ? { kind: "set-delegate", domainId, delegateScriptHex: null }
282
+ : { kind: "set-miner", domainId, minerScriptHex: null };
283
+ }
284
+ const target = readLenPrefixedScriptHex(payload, 8);
285
+ if (target === null || target.nextOffset !== payload.length) {
286
+ return "unsupported";
287
+ }
288
+ return opcode === COG_OPCODES.SET_DELEGATE
289
+ ? { kind: "set-delegate", domainId, delegateScriptHex: target.scriptHex }
290
+ : { kind: "set-miner", domainId, minerScriptHex: target.scriptHex };
291
+ }
292
+ return "unsupported";
293
+ }
294
+ function getAncestorTxids(context, txContexts) {
295
+ return context.rawTransaction.vin
296
+ .map((vin) => vin.txid ?? null)
297
+ .filter((txid) => txid !== null && txContexts.has(txid));
298
+ }
299
+ function topologicallyOrderAncestorContexts(options) {
300
+ const visited = new Set();
301
+ const visiting = new Set();
302
+ const ordered = [];
303
+ const visit = (txid) => {
304
+ if (visited.has(txid)) {
305
+ return true;
306
+ }
307
+ if (visiting.has(txid)) {
308
+ return false;
309
+ }
310
+ const context = options.txContexts.get(txid);
311
+ if (context === undefined) {
312
+ return true;
313
+ }
314
+ visiting.add(txid);
315
+ for (const parentTxid of getAncestorTxids(context, options.txContexts)) {
316
+ if (!visit(parentTxid)) {
317
+ return false;
318
+ }
319
+ }
320
+ visiting.delete(txid);
321
+ visited.add(txid);
322
+ ordered.push(context);
323
+ return true;
324
+ };
325
+ const root = options.txContexts.get(options.txid);
326
+ if (root === undefined) {
327
+ return [];
328
+ }
329
+ for (const parentTxid of getAncestorTxids(root, options.txContexts)) {
330
+ if (!visit(parentTxid)) {
331
+ return null;
332
+ }
333
+ }
334
+ return ordered;
335
+ }
336
+ function cloneOverlayDomainFromConfirmed(readContext, domainId) {
337
+ const domain = lookupDomainById(readContext.snapshot.state, domainId);
338
+ if (domain === null) {
339
+ return null;
340
+ }
341
+ return {
342
+ domainId,
343
+ name: domain.name,
344
+ anchored: domain.anchored,
345
+ ownerScriptHex: bytesToHex(domain.ownerScriptPubKey),
346
+ delegateScriptHex: bytesToHex(domain.delegate),
347
+ minerScriptHex: bytesToHex(domain.miner),
348
+ };
349
+ }
350
+ function applySupportedAncestorOperation(options) {
351
+ const ensureDomain = (domainId) => {
352
+ const existing = options.overlay.get(domainId);
353
+ if (existing !== undefined) {
354
+ return existing;
355
+ }
356
+ const confirmed = cloneOverlayDomainFromConfirmed(options.readContext, domainId);
357
+ if (confirmed === null) {
358
+ return null;
359
+ }
360
+ options.overlay.set(domainId, confirmed);
361
+ return confirmed;
362
+ };
363
+ if (options.operation.kind === "domain-reg") {
364
+ if (!rootDomain(options.operation.name)) {
365
+ return { nextDomainId: options.nextDomainId, indeterminate: true };
366
+ }
367
+ if (lookupDomain(options.readContext.snapshot.state, options.operation.name) !== null) {
368
+ return { nextDomainId: options.nextDomainId, indeterminate: true };
369
+ }
370
+ options.overlay.set(options.nextDomainId, {
371
+ domainId: options.nextDomainId,
372
+ name: options.operation.name,
373
+ anchored: false,
374
+ ownerScriptHex: options.operation.senderScriptHex,
375
+ delegateScriptHex: null,
376
+ minerScriptHex: null,
377
+ });
378
+ return {
379
+ nextDomainId: options.nextDomainId + 1,
380
+ indeterminate: false,
381
+ };
382
+ }
383
+ const domain = ensureDomain(options.operation.domainId);
384
+ if (domain === null) {
385
+ return { nextDomainId: options.nextDomainId, indeterminate: true };
386
+ }
387
+ if (options.operation.kind === "domain-transfer") {
388
+ domain.ownerScriptHex = options.operation.recipientScriptHex;
389
+ options.overlay.set(domain.domainId, domain);
390
+ return { nextDomainId: options.nextDomainId, indeterminate: false };
391
+ }
392
+ if (options.operation.kind === "domain-anchor") {
393
+ domain.anchored = true;
394
+ if (options.operation.senderScriptHex !== null) {
395
+ domain.ownerScriptHex = options.operation.senderScriptHex;
396
+ }
397
+ options.overlay.set(domain.domainId, domain);
398
+ return { nextDomainId: options.nextDomainId, indeterminate: false };
399
+ }
400
+ if (options.operation.kind === "set-delegate") {
401
+ domain.delegateScriptHex = options.operation.delegateScriptHex;
402
+ options.overlay.set(domain.domainId, domain);
403
+ return { nextDomainId: options.nextDomainId, indeterminate: false };
404
+ }
405
+ domain.minerScriptHex = options.operation.minerScriptHex;
406
+ options.overlay.set(domain.domainId, domain);
407
+ return { nextDomainId: options.nextDomainId, indeterminate: false };
408
+ }
409
+ async function resolveOverlayAuthorizedMiningDomain(options) {
410
+ const orderedAncestors = topologicallyOrderAncestorContexts({
411
+ txid: options.txid,
412
+ txContexts: options.txContexts,
413
+ });
414
+ if (orderedAncestors === null) {
415
+ return "indeterminate";
416
+ }
417
+ const overlay = new Map();
418
+ let nextDomainId = options.readContext.snapshot.state.consensus.nextDomainId;
419
+ for (const ancestor of orderedAncestors) {
420
+ const parsed = parseSupportedAncestorOperation(ancestor);
421
+ if (parsed === "unsupported") {
422
+ return "indeterminate";
423
+ }
424
+ if (parsed === null) {
425
+ continue;
426
+ }
427
+ const applied = applySupportedAncestorOperation({
428
+ readContext: options.readContext,
429
+ overlay,
430
+ nextDomainId,
431
+ operation: parsed,
432
+ });
433
+ nextDomainId = applied.nextDomainId;
434
+ if (applied.indeterminate) {
435
+ return "indeterminate";
436
+ }
437
+ }
438
+ const domain = overlay.get(options.domainId) ?? cloneOverlayDomainFromConfirmed(options.readContext, options.domainId);
439
+ if (domain === null || domain.name === null || !rootDomain(domain.name) || !domain.anchored) {
440
+ return null;
441
+ }
442
+ const authorized = domain.ownerScriptHex === options.senderScriptHex
443
+ || domain.delegateScriptHex === options.senderScriptHex
444
+ || domain.minerScriptHex === options.senderScriptHex;
445
+ return authorized ? domain : null;
446
+ }
447
+ function buildStatusSnapshot(view, overrides = {}) {
448
+ return {
449
+ ...view.runtime,
450
+ runMode: overrides.runMode ?? view.runtime.runMode,
451
+ backgroundWorkerPid: overrides.backgroundWorkerPid ?? view.runtime.backgroundWorkerPid,
452
+ backgroundWorkerRunId: overrides.backgroundWorkerRunId ?? view.runtime.backgroundWorkerRunId,
453
+ backgroundWorkerHeartbeatAtUnixMs: overrides.backgroundWorkerHeartbeatAtUnixMs ?? view.runtime.backgroundWorkerHeartbeatAtUnixMs,
454
+ currentPhase: overrides.currentPhase ?? view.runtime.currentPhase,
455
+ lastSuspendDetectedAtUnixMs: overrides.lastSuspendDetectedAtUnixMs ?? view.runtime.lastSuspendDetectedAtUnixMs,
456
+ providerState: overrides.providerState ?? view.runtime.providerState,
457
+ corePublishState: overrides.corePublishState ?? view.runtime.corePublishState,
458
+ currentPublishDecision: overrides.currentPublishDecision ?? view.runtime.currentPublishDecision,
459
+ sameDomainCompetitorSuppressed: overrides.sameDomainCompetitorSuppressed ?? view.runtime.sameDomainCompetitorSuppressed,
460
+ higherRankedCompetitorDomainCount: overrides.higherRankedCompetitorDomainCount ?? view.runtime.higherRankedCompetitorDomainCount,
461
+ dedupedCompetitorDomainCount: overrides.dedupedCompetitorDomainCount ?? view.runtime.dedupedCompetitorDomainCount,
462
+ competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? view.runtime.competitivenessGateIndeterminate,
463
+ mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? view.runtime.mempoolSequenceCacheStatus,
464
+ lastMempoolSequence: overrides.lastMempoolSequence ?? view.runtime.lastMempoolSequence,
465
+ lastCompetitivenessGateAtUnixMs: overrides.lastCompetitivenessGateAtUnixMs ?? view.runtime.lastCompetitivenessGateAtUnixMs,
466
+ lastError: overrides.lastError ?? view.runtime.lastError,
467
+ note: overrides.note ?? view.runtime.note,
468
+ liveMiningFamilyInMempool: overrides.liveMiningFamilyInMempool ?? view.runtime.liveMiningFamilyInMempool,
469
+ updatedAtUnixMs: Date.now(),
470
+ };
471
+ }
472
+ async function refreshAndSaveStatus(options) {
473
+ const view = await inspectMiningControlPlane({
474
+ provider: options.provider,
475
+ localState: options.readContext.localState,
476
+ bitcoind: options.readContext.bitcoind,
477
+ nodeStatus: options.readContext.nodeStatus,
478
+ nodeHealth: options.readContext.nodeHealth,
479
+ indexer: options.readContext.indexer,
480
+ paths: options.paths,
481
+ });
482
+ const snapshot = buildStatusSnapshot(view, options.overrides);
483
+ await saveMiningRuntimeStatus(options.paths.miningStatusPath, snapshot);
484
+ options.visualizer?.update(snapshot);
485
+ return snapshot;
486
+ }
487
+ async function appendEvent(paths, event) {
488
+ await appendMiningEvent(paths.miningEventsPath, event);
489
+ }
490
+ async function handleDetectedMiningRuntimeResume(options) {
491
+ const readContext = await options.openReadContext({
492
+ dataDir: options.dataDir,
493
+ databasePath: options.databasePath,
494
+ secretProvider: options.provider,
495
+ paths: options.paths,
496
+ });
497
+ try {
498
+ clearMiningGateCache(readContext.localState.walletRootId);
499
+ await refreshAndSaveStatus({
500
+ paths: options.paths,
501
+ provider: options.provider,
502
+ readContext,
503
+ overrides: {
504
+ runMode: options.runMode,
505
+ backgroundWorkerPid: options.backgroundWorkerPid,
506
+ backgroundWorkerRunId: options.backgroundWorkerRunId,
507
+ backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
508
+ currentPhase: "resuming",
509
+ lastSuspendDetectedAtUnixMs: options.detectedAtUnixMs,
510
+ note: "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.",
511
+ },
512
+ visualizer: options.visualizer,
513
+ });
514
+ }
515
+ finally {
516
+ await readContext.close();
517
+ }
518
+ await appendEvent(options.paths, createEvent("system-resumed", "Detected a large local runtime gap, discarded stale in-flight mining work, and resumed health checks from scratch.", {
519
+ level: "warn",
520
+ runId: options.backgroundWorkerRunId,
521
+ timestampUnixMs: options.detectedAtUnixMs,
522
+ }));
523
+ }
524
+ function getIndexerTruthKey(readContext) {
525
+ if (readContext.snapshot.daemonInstanceId == null
526
+ || readContext.snapshot.snapshotSeq == null) {
527
+ return null;
528
+ }
529
+ return {
530
+ walletRootId: readContext.localState.state.walletRootId,
531
+ daemonInstanceId: readContext.snapshot.daemonInstanceId,
532
+ snapshotSeq: readContext.snapshot.snapshotSeq,
533
+ };
534
+ }
535
+ async function indexerTruthIsCurrent(options) {
536
+ if (options.truthKey === null) {
537
+ return false;
538
+ }
539
+ const probe = await probeIndexerDaemon({
540
+ dataDir: options.dataDir,
541
+ walletRootId: options.truthKey.walletRootId,
542
+ });
543
+ try {
544
+ return probe.compatibility === "compatible"
545
+ && probe.status !== null
546
+ && probe.status.state === "synced"
547
+ && probe.status.daemonInstanceId === options.truthKey.daemonInstanceId
548
+ && probe.status.snapshotSeq === options.truthKey.snapshotSeq;
549
+ }
550
+ finally {
551
+ await probe.client?.close().catch(() => undefined);
552
+ }
553
+ }
554
+ async function ensureIndexerTruthIsCurrent(options) {
555
+ if (!await indexerTruthIsCurrent(options)) {
556
+ throw new Error("mining_generation_stale_indexer_truth");
557
+ }
558
+ }
559
+ function determineCorePublishState(info) {
560
+ if (info.network.networkactive === false) {
561
+ return "network-inactive";
562
+ }
563
+ if ((info.network.connections_out ?? 0) <= 0) {
564
+ return "no-outbound-peers";
565
+ }
566
+ if (info.blockchain.initialblockdownload === true) {
567
+ return "ibd";
568
+ }
569
+ if (info.mempool.loaded === false) {
570
+ return "mempool-loading";
571
+ }
572
+ return "healthy";
573
+ }
574
+ function createMiningPlan(options) {
575
+ const fundingUtxos = options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
576
+ && entry.confirmations >= 1
577
+ && entry.spendable !== false
578
+ && entry.safe !== false
579
+ && !(entry.txid === options.conflictOutpoint.txid && entry.vout === options.conflictOutpoint.vout));
580
+ const opReturnData = serializeMine(options.candidate.domainId, options.candidate.referencedBlockHashInternal, options.candidate.encodedSentenceBytes).opReturnData;
581
+ const expectedOpReturnScriptHex = Buffer.concat([
582
+ Buffer.from([0x6a, opReturnData.length]),
583
+ Buffer.from(opReturnData),
584
+ ]).toString("hex");
585
+ return {
586
+ sender: options.candidate.sender,
587
+ inputs: [
588
+ options.candidate.anchorOutpoint,
589
+ options.conflictOutpoint,
590
+ ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
591
+ ],
592
+ outputs: [
593
+ { data: Buffer.from(opReturnData).toString("hex") },
594
+ { [options.candidate.sender.address]: satsToBtc(BigInt(options.state.anchorValueSats)) },
595
+ ],
596
+ changeAddress: options.state.funding.address,
597
+ changePosition: 2,
598
+ expectedOpReturnScriptHex,
599
+ expectedAnchorScriptHex: options.candidate.sender.scriptPubKeyHex,
600
+ expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
601
+ allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
602
+ expectedConflictOutpoint: options.conflictOutpoint,
603
+ feeRateSatVb: options.feeRateSatVb,
604
+ };
605
+ }
606
+ function validateMiningDraft(decoded, funded, plan) {
607
+ const inputs = decoded.tx.vin;
608
+ const outputs = decoded.tx.vout;
609
+ if (inputs.length < 2) {
610
+ throw new Error("wallet_mining_missing_inputs");
611
+ }
612
+ if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
613
+ throw new Error("wallet_mining_sender_input_mismatch");
614
+ }
615
+ if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
616
+ throw new Error("wallet_mining_conflict_input_mismatch");
617
+ }
618
+ for (let index = 2; index < inputs.length; index += 1) {
619
+ if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
620
+ throw new Error("wallet_mining_unexpected_funding_input");
621
+ }
622
+ }
623
+ if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
624
+ throw new Error("wallet_mining_opreturn_mismatch");
625
+ }
626
+ if (outputs[1]?.scriptPubKey?.hex !== plan.expectedAnchorScriptHex) {
627
+ throw new Error("wallet_mining_anchor_output_mismatch");
628
+ }
629
+ if (numberToSats(outputs[1]?.value ?? 0) !== plan.expectedAnchorValueSats) {
630
+ throw new Error("wallet_mining_anchor_value_mismatch");
631
+ }
632
+ if (funded.changepos !== -1 && (funded.changepos !== plan.changePosition || outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex)) {
633
+ throw new Error("wallet_mining_change_output_mismatch");
634
+ }
635
+ }
636
+ async function buildMiningTransaction(options) {
637
+ return buildWalletMutationTransaction({
638
+ rpc: options.rpc,
639
+ walletName: options.walletName,
640
+ plan: options.plan,
641
+ validateFundedDraft: validateMiningDraft,
642
+ finalizeErrorCode: "wallet_mining_finalize_failed",
643
+ mempoolRejectPrefix: "wallet_mining_mempool_rejected",
644
+ feeRate: options.plan.feeRateSatVb,
645
+ builderOptions: {
646
+ addInputs: true,
647
+ includeUnsafe: true,
648
+ minConf: 0,
649
+ lockUnspents: true,
650
+ },
651
+ });
652
+ }
653
+ function resolveEligibleAnchoredRoots(context) {
654
+ const state = context.localState.state;
655
+ const model = context.model;
656
+ const snapshot = context.snapshot;
657
+ if (state === null || model === null || snapshot === null) {
658
+ return [];
659
+ }
660
+ const domains = [];
661
+ for (const domain of model.domains) {
662
+ if (!isMineableWalletDomain(context, domain)) {
663
+ continue;
664
+ }
665
+ const localRecord = state.domains.find((entry) => entry.name === domain.name);
666
+ const ownerIdentity = model.identities.find((identity) => identity.index === domain.ownerLocalIndex);
667
+ const domainId = domain.domainId;
668
+ if (domainId === null
669
+ || domainId === undefined
670
+ || localRecord?.currentCanonicalAnchorOutpoint === null
671
+ || localRecord?.currentCanonicalAnchorOutpoint === undefined
672
+ || ownerIdentity?.address == null
673
+ || ownerIdentity.readOnly) {
674
+ continue;
675
+ }
676
+ const chainDomain = lookupDomain(snapshot.state, domain.name);
677
+ if (chainDomain === null || !chainDomain.anchored) {
678
+ continue;
679
+ }
680
+ domains.push({
681
+ domainId,
682
+ domainName: domain.name,
683
+ localIndex: ownerIdentity.index,
684
+ sender: {
685
+ localIndex: ownerIdentity.index,
686
+ scriptPubKeyHex: ownerIdentity.scriptPubKeyHex,
687
+ address: ownerIdentity.address,
688
+ },
689
+ anchorOutpoint: {
690
+ txid: localRecord.currentCanonicalAnchorOutpoint.txid,
691
+ vout: localRecord.currentCanonicalAnchorOutpoint.vout,
692
+ },
693
+ });
694
+ }
695
+ return domains.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName));
696
+ }
697
+ async function persistCustomHookRuntimeOutcome(options) {
698
+ const hookState = options.readContext.localState.state.hookClientState.mining;
699
+ if (hookState.mode !== "custom") {
700
+ return false;
701
+ }
702
+ if (options.success) {
703
+ if ((hookState.consecutiveFailureCount ?? 0) === 0 && hookState.cooldownUntilUnixMs === null) {
704
+ return false;
705
+ }
706
+ options.readContext.localState.state.hookClientState.mining = {
707
+ ...hookState,
708
+ consecutiveFailureCount: 0,
709
+ cooldownUntilUnixMs: null,
710
+ };
711
+ await saveWalletStatePreservingUnlock({
712
+ state: options.readContext.localState.state,
713
+ provider: options.provider,
714
+ unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
715
+ nowUnixMs: options.nowUnixMs,
716
+ paths: options.paths,
717
+ });
718
+ return false;
719
+ }
720
+ const consecutiveFailureCount = (hookState.consecutiveFailureCount ?? 0) + 1;
721
+ const cooldownUntilUnixMs = consecutiveFailureCount >= MINING_HOOK_FAILURE_THRESHOLD
722
+ ? options.nowUnixMs + MINING_HOOK_COOLDOWN_MS
723
+ : null;
724
+ options.readContext.localState.state.hookClientState.mining = {
725
+ ...hookState,
726
+ consecutiveFailureCount,
727
+ cooldownUntilUnixMs,
728
+ };
729
+ await saveWalletStatePreservingUnlock({
730
+ state: options.readContext.localState.state,
731
+ provider: options.provider,
732
+ unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
733
+ nowUnixMs: options.nowUnixMs,
734
+ paths: options.paths,
735
+ });
736
+ return cooldownUntilUnixMs !== null && cooldownUntilUnixMs > options.nowUnixMs;
737
+ }
738
+ async function generateCandidatesForDomains(options) {
739
+ const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
740
+ if (bestBlockHash === null || bestBlockHash === undefined) {
741
+ return [];
742
+ }
743
+ const targetBlockHeight = (options.readContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
744
+ const referencedBlockHashInternal = Buffer.from(displayToInternalBlockhash(bestBlockHash), "hex");
745
+ const rootDomains = options.domains.map((domain) => ({
746
+ ...domain,
747
+ requiredWords: getWords(domain.domainId, referencedBlockHashInternal),
748
+ }));
749
+ const abortController = new AbortController();
750
+ let stale = false;
751
+ let staleIndexerTruth = false;
752
+ let preempted = false;
753
+ const timer = setInterval(async () => {
754
+ try {
755
+ const [current, truthCurrent] = await Promise.all([
756
+ options.rpc.getBlockchainInfo(),
757
+ indexerTruthIsCurrent({
758
+ dataDir: options.readContext.dataDir,
759
+ truthKey: options.indexerTruthKey,
760
+ }),
761
+ ]);
762
+ if (current.bestblockhash !== bestBlockHash) {
763
+ stale = true;
764
+ abortController.abort();
765
+ return;
766
+ }
767
+ if (!truthCurrent) {
768
+ staleIndexerTruth = true;
769
+ abortController.abort();
770
+ return;
771
+ }
772
+ if (await isMiningGenerationAbortRequested(options.paths)) {
773
+ preempted = true;
774
+ abortController.abort();
775
+ }
776
+ }
777
+ catch {
778
+ // Ignore transient polling failures and let the main cycle degrade on the next tick.
779
+ }
780
+ }, BEST_BLOCK_POLL_INTERVAL_MS);
781
+ try {
782
+ await markMiningGenerationActive({
783
+ paths: options.paths,
784
+ runId: options.runId ?? null,
785
+ pid: process.pid ?? null,
786
+ });
787
+ const generationRequest = {
788
+ schemaVersion: 1,
789
+ requestId: `mining-${targetBlockHeight}-${randomBytes(8).toString("hex")}`,
790
+ targetBlockHeight,
791
+ referencedBlockHashDisplay: bestBlockHash,
792
+ generatedAtUnixMs: Date.now(),
793
+ extraPrompt: null,
794
+ limits: createGenerateSentencesHookLimits(),
795
+ rootDomains: rootDomains.map((domain) => ({
796
+ domainId: domain.domainId,
797
+ domainName: domain.domainName,
798
+ requiredWords: domain.requiredWords,
799
+ })),
800
+ };
801
+ let generated;
802
+ try {
803
+ generated = await generateMiningSentences(generationRequest, {
804
+ paths: options.paths,
805
+ provider: options.provider,
806
+ hookState: options.readContext.localState.state.hookClientState.mining,
807
+ signal: abortController.signal,
808
+ fetchImpl: options.fetchImpl,
809
+ });
810
+ }
811
+ catch (error) {
812
+ if (stale) {
813
+ throw new Error("mining_generation_stale_tip");
814
+ }
815
+ if (staleIndexerTruth) {
816
+ throw new Error("mining_generation_stale_indexer_truth");
817
+ }
818
+ if (preempted) {
819
+ throw new Error("mining_generation_preempted");
820
+ }
821
+ throw error;
822
+ }
823
+ if (stale) {
824
+ throw new Error("mining_generation_stale_tip");
825
+ }
826
+ if (staleIndexerTruth) {
827
+ throw new Error("mining_generation_stale_indexer_truth");
828
+ }
829
+ if (preempted) {
830
+ throw new Error("mining_generation_preempted");
831
+ }
832
+ await ensureIndexerTruthIsCurrent({
833
+ dataDir: options.readContext.dataDir,
834
+ truthKey: options.indexerTruthKey,
835
+ });
836
+ if (generated.hookMode === "custom") {
837
+ await persistCustomHookRuntimeOutcome({
838
+ readContext: options.readContext,
839
+ provider: options.provider,
840
+ paths: options.paths,
841
+ nowUnixMs: Date.now(),
842
+ success: true,
843
+ });
844
+ }
845
+ const sentencesByDomain = new Map();
846
+ for (const candidate of generated.candidates) {
847
+ const existing = sentencesByDomain.get(candidate.domainId) ?? [];
848
+ existing.push(candidate.sentence);
849
+ sentencesByDomain.set(candidate.domainId, existing);
850
+ }
851
+ const candidates = [];
852
+ for (const domain of rootDomains) {
853
+ const domainSentences = sentencesByDomain.get(domain.domainId) ?? [];
854
+ if (domainSentences.length === 0) {
855
+ continue;
856
+ }
857
+ const assayed = await assaySentences(domain.domainId, referencedBlockHashInternal, domainSentences);
858
+ const best = assayed.find((entry) => entry.gatesPass && entry.encodedSentenceBytes !== null && entry.rank === 1);
859
+ if (best === undefined || best.encodedSentenceBytes === null || best.canonicalBlend === null) {
860
+ continue;
861
+ }
862
+ candidates.push({
863
+ domainId: domain.domainId,
864
+ domainName: domain.domainName,
865
+ localIndex: domain.localIndex,
866
+ sender: domain.sender,
867
+ anchorOutpoint: domain.anchorOutpoint,
868
+ sentence: best.sentence,
869
+ encodedSentenceBytes: best.encodedSentenceBytes,
870
+ bip39WordIndices: [...best.bip39WordIndices],
871
+ bip39Words: best.bip39Words,
872
+ canonicalBlend: best.canonicalBlend,
873
+ referencedBlockHashDisplay: bestBlockHash,
874
+ referencedBlockHashInternal,
875
+ targetBlockHeight,
876
+ });
877
+ }
878
+ return candidates;
879
+ }
880
+ finally {
881
+ clearInterval(timer);
882
+ await markMiningGenerationInactive({
883
+ paths: options.paths,
884
+ runId: options.runId ?? null,
885
+ pid: process.pid ?? null,
886
+ }).catch(() => undefined);
887
+ }
888
+ }
889
+ async function chooseBestLocalCandidate(candidates) {
890
+ if (candidates.length === 0) {
891
+ return null;
892
+ }
893
+ if (candidates.length === 1) {
894
+ return candidates[0];
895
+ }
896
+ const blendSeed = deriveBlendSeed(candidates[0].referencedBlockHashInternal);
897
+ const winners = await settleBlock({
898
+ blendSeed,
899
+ blockRewardCogtoshi: 100n,
900
+ submissions: candidates
901
+ .slice()
902
+ .sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName))
903
+ .map((candidate, index) => ({
904
+ miningDomainId: candidate.domainId,
905
+ rawSentenceBytes: candidate.encodedSentenceBytes,
906
+ recipientScriptPubKey: Buffer.from(candidate.sender.scriptPubKeyHex, "hex"),
907
+ bip39WordIndices: candidate.bip39WordIndices,
908
+ txIndex: index,
909
+ })),
910
+ });
911
+ const winner = winners[0];
912
+ if (winner === undefined) {
913
+ return null;
914
+ }
915
+ return candidates.find((candidate) => candidate.domainId === winner.miningDomainId) ?? null;
916
+ }
917
+ async function runCompetitivenessGate(options) {
918
+ const createDecision = (overrides) => ({
919
+ allowed: overrides.allowed ?? false,
920
+ decision: overrides.decision ?? "indeterminate-mempool-gate",
921
+ sameDomainCompetitorSuppressed: overrides.sameDomainCompetitorSuppressed ?? false,
922
+ higherRankedCompetitorDomainCount: overrides.higherRankedCompetitorDomainCount ?? 0,
923
+ dedupedCompetitorDomainCount: overrides.dedupedCompetitorDomainCount ?? 0,
924
+ competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? false,
925
+ mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? null,
926
+ lastMempoolSequence: overrides.lastMempoolSequence ?? null,
927
+ });
928
+ const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
929
+ const indexerTruthKey = getIndexerTruthKey(options.readContext);
930
+ const localFeeTarget = DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB;
931
+ const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
932
+ const localAssayTupleKey = [
933
+ options.candidate.domainId,
934
+ Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
935
+ options.candidate.canonicalBlend.toString(),
936
+ options.candidate.sender.scriptPubKeyHex,
937
+ ].join(":");
938
+ let mempoolVerbose;
939
+ try {
940
+ mempoolVerbose = await options.rpc.getRawMempoolVerbose();
941
+ }
942
+ catch {
943
+ return createDecision({
944
+ competitivenessGateIndeterminate: true,
945
+ });
946
+ }
947
+ const mempoolSequence = String(mempoolVerbose.mempool_sequence);
948
+ const cached = miningGateCache.get(walletRootId);
949
+ const cachedTruthMatches = cached !== undefined
950
+ && indexerTruthKey !== null
951
+ && cached.indexerDaemonInstanceId === indexerTruthKey.daemonInstanceId
952
+ && cached.indexerSnapshotSeq === indexerTruthKey.snapshotSeq;
953
+ const cachedReferencedBlockMatches = cached !== undefined
954
+ && cached.referencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay;
955
+ if (cached !== undefined && (!cachedTruthMatches || !cachedReferencedBlockMatches)) {
956
+ clearMiningGateCache(walletRootId);
957
+ }
958
+ if (cached !== undefined
959
+ && cachedTruthMatches
960
+ && cachedReferencedBlockMatches
961
+ && cached.localAssayTupleKey === localAssayTupleKey
962
+ && cached.currentFeeTargetSatVb === localFeeTarget
963
+ && cached.excludedTxidsKey === excludedTxids.join(",")
964
+ && cached.mempoolSequence === mempoolSequence) {
965
+ return {
966
+ ...cached.decision,
967
+ mempoolSequenceCacheStatus: "reused",
968
+ };
969
+ }
970
+ const referencedPrefix = Buffer.from(options.candidate.referencedBlockHashInternal.subarray(0, 4)).toString("hex");
971
+ const visibleTxids = mempoolVerbose.txids.filter((txid) => !excludedTxids.includes(txid));
972
+ const txContexts = cachedTruthMatches && cachedReferencedBlockMatches
973
+ ? (cached?.txContexts ?? new Map())
974
+ : new Map();
975
+ for (const txid of [...txContexts.keys()]) {
976
+ if (!visibleTxids.includes(txid)) {
977
+ txContexts.delete(txid);
978
+ }
979
+ }
980
+ for (const txid of visibleTxids) {
981
+ if (txContexts.has(txid)) {
982
+ continue;
983
+ }
984
+ const [tx, mempoolEntry] = await Promise.all([
985
+ options.rpc.getRawTransaction(txid, true).catch(() => null),
986
+ options.rpc.getMempoolEntry(txid).catch(() => null),
987
+ ]);
988
+ if (tx === null || mempoolEntry === null) {
989
+ continue;
990
+ }
991
+ const effectiveFeeRate = Number([
992
+ mempoolEntry.vsize > 0 ? (numberToSats(mempoolEntry.fees.base) / BigInt(mempoolEntry.vsize)) : 0n,
993
+ (mempoolEntry.ancestorsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.ancestor) / BigInt(mempoolEntry.ancestorsize ?? 1)) : 0n,
994
+ (mempoolEntry.descendantsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.descendant) / BigInt(mempoolEntry.descendantsize ?? 1)) : 0n,
995
+ ].reduce((best, candidate) => (candidate > best ? candidate : best), 0n));
996
+ const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
997
+ txContexts.set(txid, {
998
+ txid,
999
+ effectiveFeeRate,
1000
+ senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
1001
+ rawTransaction: tx,
1002
+ payload: payloadHex === undefined ? null : extractOpReturnPayloadFromScriptHex(payloadHex),
1003
+ });
1004
+ }
1005
+ const entries = new Map();
1006
+ for (const txid of visibleTxids) {
1007
+ const context = txContexts.get(txid);
1008
+ if (context === undefined || context.effectiveFeeRate < localFeeTarget || context.payload === null || context.senderScriptHex === null) {
1009
+ continue;
1010
+ }
1011
+ const decoded = decodeMinePayload(context.payload);
1012
+ if (decoded === null || decoded.referencedBlockPrefixHex !== referencedPrefix) {
1013
+ continue;
1014
+ }
1015
+ const overlayDomain = await resolveOverlayAuthorizedMiningDomain({
1016
+ readContext: options.readContext,
1017
+ txid,
1018
+ txContexts,
1019
+ domainId: decoded.domainId,
1020
+ senderScriptHex: context.senderScriptHex,
1021
+ });
1022
+ if (overlayDomain === "indeterminate") {
1023
+ const decision = createDecision({
1024
+ competitivenessGateIndeterminate: true,
1025
+ decision: "indeterminate-mempool-gate",
1026
+ mempoolSequenceCacheStatus: "refreshed",
1027
+ lastMempoolSequence: mempoolSequence,
1028
+ });
1029
+ miningGateCache.set(walletRootId, {
1030
+ indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
1031
+ indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
1032
+ referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1033
+ localAssayTupleKey,
1034
+ currentFeeTargetSatVb: localFeeTarget,
1035
+ excludedTxidsKey: excludedTxids.join(","),
1036
+ mempoolSequence,
1037
+ txids: [...visibleTxids],
1038
+ txContexts,
1039
+ decision,
1040
+ });
1041
+ return decision;
1042
+ }
1043
+ if (overlayDomain === null || overlayDomain.name === null || !rootDomain(overlayDomain.name)) {
1044
+ continue;
1045
+ }
1046
+ const assayed = await assaySentences(decoded.domainId, options.candidate.referencedBlockHashInternal, [Buffer.from(decoded.sentenceBytes).toString("utf8")]).catch(() => []);
1047
+ const scored = assayed[0];
1048
+ if (scored === undefined || !scored.gatesPass || scored.encodedSentenceBytes === null || scored.canonicalBlend === null) {
1049
+ continue;
1050
+ }
1051
+ entries.set(txid, {
1052
+ txid,
1053
+ effectiveFeeRate: context.effectiveFeeRate,
1054
+ domainId: decoded.domainId,
1055
+ senderScriptHex: context.senderScriptHex,
1056
+ encodedSentenceBytesHex: Buffer.from(scored.encodedSentenceBytes).toString("hex"),
1057
+ bip39WordIndices: [...scored.bip39WordIndices],
1058
+ canonicalBlend: scored.canonicalBlend,
1059
+ });
1060
+ }
1061
+ const sameDomainCompetitors = [...entries.values()].filter((entry) => entry.domainId === options.candidate.domainId);
1062
+ const sameDomainCompetitorSuppressed = sameDomainCompetitors.some((competitor) => competitor.canonicalBlend > options.candidate.canonicalBlend
1063
+ || competitor.canonicalBlend === options.candidate.canonicalBlend);
1064
+ let decision;
1065
+ const otherDomainBest = new Map();
1066
+ for (const entry of entries.values()) {
1067
+ if (entry.domainId === options.candidate.domainId) {
1068
+ continue;
1069
+ }
1070
+ const best = otherDomainBest.get(entry.domainId);
1071
+ if (best === undefined
1072
+ || entry.canonicalBlend > best.canonicalBlend
1073
+ || (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate > best.effectiveFeeRate)
1074
+ || (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate === best.effectiveFeeRate && entry.txid.localeCompare(best.txid) < 0)) {
1075
+ otherDomainBest.set(entry.domainId, entry);
1076
+ }
1077
+ }
1078
+ if (sameDomainCompetitorSuppressed) {
1079
+ decision = createDecision({
1080
+ allowed: false,
1081
+ decision: "suppressed-same-domain-mempool",
1082
+ sameDomainCompetitorSuppressed: true,
1083
+ higherRankedCompetitorDomainCount: 1,
1084
+ dedupedCompetitorDomainCount: otherDomainBest.size,
1085
+ competitivenessGateIndeterminate: false,
1086
+ mempoolSequenceCacheStatus: "refreshed",
1087
+ lastMempoolSequence: mempoolSequence,
1088
+ });
1089
+ }
1090
+ else {
1091
+ try {
1092
+ const submissions = [
1093
+ {
1094
+ miningDomainId: options.candidate.domainId,
1095
+ rawSentenceBytes: options.candidate.encodedSentenceBytes,
1096
+ recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
1097
+ bip39WordIndices: options.candidate.bip39WordIndices,
1098
+ txIndex: 0,
1099
+ },
1100
+ ...[...otherDomainBest.values()]
1101
+ .sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
1102
+ .map((entry, index) => ({
1103
+ miningDomainId: entry.domainId,
1104
+ rawSentenceBytes: Buffer.from(entry.encodedSentenceBytesHex, "hex"),
1105
+ recipientScriptPubKey: Buffer.from(entry.senderScriptHex, "hex"),
1106
+ bip39WordIndices: entry.bip39WordIndices,
1107
+ txIndex: index + 1,
1108
+ })),
1109
+ ];
1110
+ const winners = await settleBlock({
1111
+ blendSeed: deriveBlendSeed(options.candidate.referencedBlockHashInternal),
1112
+ blockRewardCogtoshi: 100n,
1113
+ submissions,
1114
+ });
1115
+ const localWinner = winners.find((winner) => winner.miningDomainId === options.candidate.domainId);
1116
+ const higherRankedCompetitorDomainCount = localWinner === undefined
1117
+ ? Math.max(0, winners.length - 1)
1118
+ : Math.max(0, localWinner.rank - 1);
1119
+ if (higherRankedCompetitorDomainCount >= 5) {
1120
+ decision = createDecision({
1121
+ allowed: false,
1122
+ decision: "suppressed-top5-mempool",
1123
+ sameDomainCompetitorSuppressed: false,
1124
+ higherRankedCompetitorDomainCount,
1125
+ dedupedCompetitorDomainCount: otherDomainBest.size,
1126
+ competitivenessGateIndeterminate: false,
1127
+ mempoolSequenceCacheStatus: "refreshed",
1128
+ lastMempoolSequence: mempoolSequence,
1129
+ });
1130
+ }
1131
+ else {
1132
+ decision = createDecision({
1133
+ allowed: true,
1134
+ decision: "publish",
1135
+ sameDomainCompetitorSuppressed: false,
1136
+ higherRankedCompetitorDomainCount,
1137
+ dedupedCompetitorDomainCount: otherDomainBest.size,
1138
+ competitivenessGateIndeterminate: false,
1139
+ mempoolSequenceCacheStatus: "refreshed",
1140
+ lastMempoolSequence: mempoolSequence,
1141
+ });
1142
+ }
1143
+ }
1144
+ catch {
1145
+ decision = createDecision({
1146
+ allowed: false,
1147
+ decision: "indeterminate-mempool-gate",
1148
+ sameDomainCompetitorSuppressed: false,
1149
+ higherRankedCompetitorDomainCount: 0,
1150
+ dedupedCompetitorDomainCount: otherDomainBest.size,
1151
+ competitivenessGateIndeterminate: true,
1152
+ mempoolSequenceCacheStatus: "refreshed",
1153
+ lastMempoolSequence: mempoolSequence,
1154
+ });
1155
+ }
1156
+ }
1157
+ miningGateCache.set(walletRootId, {
1158
+ indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
1159
+ indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
1160
+ referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1161
+ localAssayTupleKey,
1162
+ currentFeeTargetSatVb: localFeeTarget,
1163
+ excludedTxidsKey: excludedTxids.join(","),
1164
+ mempoolSequence,
1165
+ txids: [...visibleTxids],
1166
+ txContexts,
1167
+ decision,
1168
+ });
1169
+ return decision;
1170
+ }
1171
+ function candidateOutranksLive(options) {
1172
+ const liveState = normalizeMiningStateRecord(options.liveState);
1173
+ const nextSentenceHex = Buffer.from(options.candidate.encodedSentenceBytes).toString("hex");
1174
+ if (liveState.currentEncodedSentenceBytesHex === null) {
1175
+ return true;
1176
+ }
1177
+ if (liveState.currentDomainId === options.candidate.domainId) {
1178
+ if (liveState.currentEncodedSentenceBytesHex === nextSentenceHex) {
1179
+ return false;
1180
+ }
1181
+ const currentScore = liveState.currentScore === null ? null : BigInt(liveState.currentScore);
1182
+ return currentScore === null || options.candidate.canonicalBlend > currentScore;
1183
+ }
1184
+ return true;
1185
+ }
1186
+ function candidateMatchesLiveFamily(options) {
1187
+ const liveState = normalizeMiningStateRecord(options.liveState);
1188
+ return liveState.currentDomainId === options.candidate.domainId
1189
+ && liveState.currentEncodedSentenceBytesHex === Buffer.from(options.candidate.encodedSentenceBytes).toString("hex")
1190
+ && liveState.currentSenderScriptPubKeyHex === options.candidate.sender.scriptPubKeyHex
1191
+ && liveState.currentReferencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay
1192
+ && liveState.currentBlockTargetHeight === options.candidate.targetBlockHeight;
1193
+ }
1194
+ function candidateNeedsFeeMaintenance(options) {
1195
+ const liveState = normalizeMiningStateRecord(options.liveState);
1196
+ return candidateMatchesLiveFamily(options)
1197
+ && liveState.currentTxid !== null
1198
+ && liveState.currentFeeRateSatVb !== null
1199
+ && liveState.currentPublishState === "in-mempool"
1200
+ && liveState.liveMiningFamilyInMempool === true;
1201
+ }
1202
+ async function candidateWinsAgainstLive(options) {
1203
+ const liveState = normalizeMiningStateRecord(options.liveState);
1204
+ if (liveState.currentDomainId === null || liveState.currentEncodedSentenceBytesHex === null) {
1205
+ return true;
1206
+ }
1207
+ if (liveState.currentDomainId === options.candidate.domainId) {
1208
+ return candidateOutranksLive(options);
1209
+ }
1210
+ if (liveState.currentBip39WordIndices === null || liveState.currentSenderScriptPubKeyHex === null || liveState.currentBlendSeedHex === null) {
1211
+ return true;
1212
+ }
1213
+ const settled = await settleBlock({
1214
+ blendSeed: Buffer.from(liveState.currentBlendSeedHex, "hex"),
1215
+ blockRewardCogtoshi: 100n,
1216
+ submissions: [
1217
+ {
1218
+ miningDomainId: liveState.currentDomainId,
1219
+ rawSentenceBytes: Buffer.from(liveState.currentEncodedSentenceBytesHex, "hex"),
1220
+ recipientScriptPubKey: Buffer.from(liveState.currentSenderScriptPubKeyHex, "hex"),
1221
+ bip39WordIndices: liveState.currentBip39WordIndices,
1222
+ txIndex: 0,
1223
+ },
1224
+ {
1225
+ miningDomainId: options.candidate.domainId,
1226
+ rawSentenceBytes: options.candidate.encodedSentenceBytes,
1227
+ recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
1228
+ bip39WordIndices: options.candidate.bip39WordIndices,
1229
+ txIndex: 1,
1230
+ },
1231
+ ],
1232
+ });
1233
+ const incumbent = settled.find((entry) => entry.miningDomainId === liveState.currentDomainId);
1234
+ const challenger = settled.find((entry) => entry.miningDomainId === options.candidate.domainId);
1235
+ return challenger !== undefined
1236
+ && incumbent !== undefined
1237
+ && challenger.rank < incumbent.rank;
1238
+ }
1239
+ function miningCandidateIsCurrent(options) {
1240
+ return options.state.currentReferencedBlockHashDisplay !== null
1241
+ && options.nodeBestHash !== null
1242
+ && options.state.currentReferencedBlockHashDisplay === options.nodeBestHash
1243
+ && options.state.currentBlockTargetHeight !== null
1244
+ && options.nodeBestHeight !== null
1245
+ && options.state.currentBlockTargetHeight === (options.nodeBestHeight + 1);
1246
+ }
1247
+ async function rebuildPersistentAnchorLocks(options) {
1248
+ const walletName = options.state.managedCoreWallet.walletName;
1249
+ const [locked, spendable] = await Promise.all([
1250
+ options.rpc.listLockUnspent(walletName).catch(() => []),
1251
+ options.rpc.listUnspent(walletName, 0).catch(() => []),
1252
+ ]);
1253
+ const spendableKeys = new Set(spendable.map((entry) => `${entry.txid}:${entry.vout}`));
1254
+ const expected = options.state.domains
1255
+ .map((domain) => domain.currentCanonicalAnchorOutpoint)
1256
+ .filter((outpoint) => outpoint !== null)
1257
+ .map((outpoint) => ({ txid: outpoint.txid, vout: outpoint.vout }))
1258
+ .filter((outpoint) => spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
1259
+ const expectedKeys = new Set(expected.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
1260
+ const lockedKeys = new Set(locked.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
1261
+ const staleLocked = locked.filter((outpoint) => !expectedKeys.has(`${outpoint.txid}:${outpoint.vout}`)
1262
+ || !spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
1263
+ const missingLocked = expected.filter((outpoint) => !lockedKeys.has(`${outpoint.txid}:${outpoint.vout}`));
1264
+ if (staleLocked.length > 0) {
1265
+ await options.rpc.lockUnspent(walletName, true, staleLocked).catch(() => undefined);
1266
+ }
1267
+ if (missingLocked.length > 0) {
1268
+ await options.rpc.lockUnspent(walletName, false, missingLocked).catch(() => undefined);
1269
+ }
1270
+ }
1271
+ async function reconcileLiveMiningState(options) {
1272
+ let state = {
1273
+ ...options.state,
1274
+ miningState: normalizeMiningStateRecord(options.state.miningState),
1275
+ };
1276
+ const currentTxid = state.miningState.currentTxid;
1277
+ if (currentTxid === null || !miningFamilyMayStillExist(state.miningState)) {
1278
+ await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
1279
+ return state;
1280
+ }
1281
+ const walletName = state.managedCoreWallet.walletName;
1282
+ const [mempoolVerbose, walletTx] = await Promise.all([
1283
+ options.rpc.getRawMempoolVerbose().catch(() => ({
1284
+ txids: [],
1285
+ mempool_sequence: "unknown",
1286
+ })),
1287
+ options.rpc.getTransaction(walletName, currentTxid).catch(() => null),
1288
+ ]);
1289
+ const inMempool = mempoolVerbose.txids.includes(currentTxid);
1290
+ if (walletTx !== null && walletTx.confirmations > 0) {
1291
+ state = {
1292
+ ...state,
1293
+ miningState: {
1294
+ ...clearMiningFamilyState(state.miningState),
1295
+ currentPublishDecision: "tx-confirmed-while-down",
1296
+ },
1297
+ };
1298
+ await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
1299
+ return state;
1300
+ }
1301
+ if (inMempool) {
1302
+ const stale = !miningCandidateIsCurrent({
1303
+ state: state.miningState,
1304
+ nodeBestHash: options.nodeBestHash,
1305
+ nodeBestHeight: options.nodeBestHeight,
1306
+ });
1307
+ state = defaultMiningStatePatch(state, {
1308
+ liveMiningFamilyInMempool: true,
1309
+ currentPublishState: "in-mempool",
1310
+ state: stale
1311
+ ? "paused-stale"
1312
+ : state.miningState.runMode === "stopped"
1313
+ ? "paused"
1314
+ : "live",
1315
+ pauseReason: stale
1316
+ ? "stale-block-context"
1317
+ : state.miningState.runMode === "stopped"
1318
+ ? "user-stopped"
1319
+ : null,
1320
+ currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-family",
1321
+ });
1322
+ await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
1323
+ return state;
1324
+ }
1325
+ if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
1326
+ state = defaultMiningStatePatch(state, {
1327
+ state: "repair-required",
1328
+ pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
1329
+ ? "broadcast-unknown-conflict"
1330
+ : "wallet-conflict-observed",
1331
+ liveMiningFamilyInMempool: false,
1332
+ currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
1333
+ ? "repair-required-broadcast-conflict"
1334
+ : "repair-required-wallet-conflict",
1335
+ });
1336
+ await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
1337
+ return state;
1338
+ }
1339
+ state = defaultMiningStatePatch(state, {
1340
+ ...clearMiningFamilyState(state.miningState),
1341
+ currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
1342
+ ? "broadcast-unknown-not-seen"
1343
+ : "live-family-not-seen",
1344
+ });
1345
+ await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
1346
+ return state;
1347
+ }
1348
+ async function publishCandidate(options) {
1349
+ const service = await options.attachService({
1350
+ dataDir: options.dataDir,
1351
+ chain: "main",
1352
+ startHeight: 0,
1353
+ walletRootId: options.readContext.localState.state.walletRootId,
1354
+ });
1355
+ const rpc = options.rpcFactory(service.rpc);
1356
+ let state = await reconcileLiveMiningState({
1357
+ state: options.readContext.localState.state,
1358
+ rpc,
1359
+ nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
1360
+ nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
1361
+ });
1362
+ const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
1363
+ const fundingConflict = state.miningState.sharedMiningConflictOutpoint
1364
+ ?? allUtxos.find((entry) => entry.scriptPubKey === state.funding.scriptPubKeyHex
1365
+ && entry.confirmations >= 1
1366
+ && entry.spendable !== false
1367
+ && entry.safe !== false
1368
+ && !(entry.txid === options.candidate.anchorOutpoint.txid && entry.vout === options.candidate.anchorOutpoint.vout));
1369
+ if (fundingConflict === undefined || fundingConflict === null) {
1370
+ throw new Error("wallet_mining_missing_conflict_utxo");
1371
+ }
1372
+ const conflictOutpoint = "txid" in fundingConflict
1373
+ ? { txid: fundingConflict.txid, vout: fundingConflict.vout }
1374
+ : fundingConflict;
1375
+ const priorMiningState = cloneMiningState(state.miningState);
1376
+ const nextFeeRate = state.miningState.currentFeeRateSatVb === null
1377
+ ? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB
1378
+ : state.miningState.currentFeeRateSatVb + 1;
1379
+ const shouldFeeBump = candidateNeedsFeeMaintenance({
1380
+ liveState: state.miningState,
1381
+ candidate: options.candidate,
1382
+ });
1383
+ if (state.miningState.currentPublishState === "in-mempool"
1384
+ && state.miningState.liveMiningFamilyInMempool === true
1385
+ && !shouldFeeBump
1386
+ && !await candidateWinsAgainstLive({
1387
+ liveState: state.miningState,
1388
+ candidate: options.candidate,
1389
+ })) {
1390
+ return {
1391
+ state: defaultMiningStatePatch(state, {
1392
+ currentPublishDecision: "kept-live-family",
1393
+ }),
1394
+ txid: state.miningState.currentTxid,
1395
+ decision: "kept-live-family",
1396
+ };
1397
+ }
1398
+ const plan = createMiningPlan({
1399
+ state,
1400
+ candidate: options.candidate,
1401
+ conflictOutpoint,
1402
+ allUtxos,
1403
+ feeRateSatVb: nextFeeRate,
1404
+ });
1405
+ const built = await buildMiningTransaction({
1406
+ rpc,
1407
+ walletName: state.managedCoreWallet.walletName,
1408
+ plan,
1409
+ });
1410
+ const intentFingerprintHex = computeIntentFingerprint(state, options.candidate);
1411
+ state = defaultMiningStatePatch(state, {
1412
+ state: "live",
1413
+ currentPublishState: "broadcasting",
1414
+ currentDomain: options.candidate.domainName,
1415
+ currentDomainId: options.candidate.domainId,
1416
+ currentDomainIndex: options.candidate.localIndex,
1417
+ currentSenderScriptPubKeyHex: options.candidate.sender.scriptPubKeyHex,
1418
+ currentTxid: built.txid,
1419
+ currentWtxid: built.wtxid,
1420
+ currentFeeRateSatVb: nextFeeRate,
1421
+ currentAbsoluteFeeSats: numberToSats(built.funded.fee).toString() === "0" ? 0 : Number(numberToSats(built.funded.fee)),
1422
+ currentScore: options.candidate.canonicalBlend.toString(),
1423
+ currentSentence: options.candidate.sentence,
1424
+ currentEncodedSentenceBytesHex: Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
1425
+ currentBip39WordIndices: [...options.candidate.bip39WordIndices],
1426
+ currentBlendSeedHex: Buffer.from(deriveBlendSeed(options.candidate.referencedBlockHashInternal)).toString("hex"),
1427
+ currentBlockTargetHeight: options.candidate.targetBlockHeight,
1428
+ currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1429
+ currentIntentFingerprintHex: intentFingerprintHex,
1430
+ sharedMiningConflictOutpoint: conflictOutpoint,
1431
+ liveMiningFamilyInMempool: null,
1432
+ currentPublishDecision: priorMiningState.currentTxid === null
1433
+ ? "publishing"
1434
+ : shouldFeeBump
1435
+ ? "fee-bump"
1436
+ : "replacing",
1437
+ });
1438
+ await saveWalletStatePreservingUnlock({
1439
+ state,
1440
+ provider: options.provider,
1441
+ unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1442
+ nowUnixMs: Date.now(),
1443
+ paths: options.paths,
1444
+ });
1445
+ try {
1446
+ await rpc.sendRawTransaction(built.rawHex);
1447
+ }
1448
+ catch (error) {
1449
+ if (isAlreadyAcceptedError(error)) {
1450
+ state = defaultMiningStatePatch(state, {
1451
+ currentPublishState: "in-mempool",
1452
+ liveMiningFamilyInMempool: true,
1453
+ });
1454
+ await saveWalletStatePreservingUnlock({
1455
+ state,
1456
+ provider: options.provider,
1457
+ unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1458
+ nowUnixMs: Date.now(),
1459
+ paths: options.paths,
1460
+ });
1461
+ await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replacing" ? "tx-replaced" : "tx-broadcast", `Mining transaction ${built.txid} is already accepted by the local node.`, {
1462
+ runId: options.runId,
1463
+ targetBlockHeight: options.candidate.targetBlockHeight,
1464
+ referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1465
+ domainId: options.candidate.domainId,
1466
+ domainName: options.candidate.domainName,
1467
+ txid: built.txid,
1468
+ feeRateSatVb: nextFeeRate,
1469
+ feeSats: numberToSats(built.funded.fee).toString(),
1470
+ score: options.candidate.canonicalBlend.toString(),
1471
+ }));
1472
+ return {
1473
+ state,
1474
+ txid: built.txid,
1475
+ decision: state.miningState.currentPublishDecision === "fee-bump"
1476
+ ? "fee-bump"
1477
+ : state.miningState.currentPublishDecision === "replacing"
1478
+ ? "replaced"
1479
+ : "broadcast",
1480
+ };
1481
+ }
1482
+ if (isBroadcastUnknownError(error)) {
1483
+ state = defaultMiningStatePatch(state, {
1484
+ currentPublishState: "broadcast-unknown",
1485
+ currentPublishDecision: "broadcast-unknown",
1486
+ });
1487
+ await saveWalletStatePreservingUnlock({
1488
+ state,
1489
+ provider: options.provider,
1490
+ unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1491
+ nowUnixMs: Date.now(),
1492
+ paths: options.paths,
1493
+ });
1494
+ await appendEvent(options.paths, createEvent("error", `Mining broadcast became uncertain for ${built.txid}.`, {
1495
+ level: "warn",
1496
+ runId: options.runId,
1497
+ targetBlockHeight: options.candidate.targetBlockHeight,
1498
+ referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1499
+ domainId: options.candidate.domainId,
1500
+ domainName: options.candidate.domainName,
1501
+ txid: built.txid,
1502
+ feeRateSatVb: nextFeeRate,
1503
+ feeSats: numberToSats(built.funded.fee).toString(),
1504
+ score: options.candidate.canonicalBlend.toString(),
1505
+ reason: "broadcast-unknown",
1506
+ }));
1507
+ return {
1508
+ state,
1509
+ txid: built.txid,
1510
+ decision: "broadcast-unknown",
1511
+ };
1512
+ }
1513
+ throw error;
1514
+ }
1515
+ const absoluteFeeSats = numberToSats(built.funded.fee);
1516
+ const replacementCount = priorMiningState.currentTxid === null
1517
+ ? priorMiningState.replacementCount
1518
+ : priorMiningState.replacementCount + 1;
1519
+ state = defaultMiningStatePatch(state, {
1520
+ currentPublishState: "in-mempool",
1521
+ liveMiningFamilyInMempool: true,
1522
+ currentPublishDecision: state.miningState.currentPublishDecision === "fee-bump"
1523
+ ? "fee-bump"
1524
+ : state.miningState.currentPublishDecision === "replacing"
1525
+ ? "replaced"
1526
+ : "broadcast",
1527
+ replacementCount,
1528
+ currentAbsoluteFeeSats: Number(absoluteFeeSats),
1529
+ currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
1530
+ sessionFeeSpentSats: (BigInt(state.miningState.sessionFeeSpentSats) + absoluteFeeSats).toString(),
1531
+ lifetimeFeeSpentSats: (BigInt(state.miningState.lifetimeFeeSpentSats) + absoluteFeeSats).toString(),
1532
+ });
1533
+ await saveWalletStatePreservingUnlock({
1534
+ state,
1535
+ provider: options.provider,
1536
+ unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
1537
+ nowUnixMs: Date.now(),
1538
+ paths: options.paths,
1539
+ });
1540
+ await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replaced"
1541
+ ? "tx-replaced"
1542
+ : state.miningState.currentPublishDecision === "fee-bump"
1543
+ ? "tx-fee-bump"
1544
+ : "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
1545
+ ? "Replaced"
1546
+ : state.miningState.currentPublishDecision === "fee-bump"
1547
+ ? "Fee-bumped"
1548
+ : "Broadcast"} mining transaction ${built.txid}.`, {
1549
+ runId: options.runId,
1550
+ targetBlockHeight: options.candidate.targetBlockHeight,
1551
+ referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1552
+ domainId: options.candidate.domainId,
1553
+ domainName: options.candidate.domainName,
1554
+ txid: built.txid,
1555
+ feeRateSatVb: nextFeeRate,
1556
+ feeSats: absoluteFeeSats.toString(),
1557
+ score: options.candidate.canonicalBlend.toString(),
1558
+ }));
1559
+ return {
1560
+ state,
1561
+ txid: built.txid,
1562
+ decision: state.miningState.currentPublishDecision === "fee-bump"
1563
+ ? "fee-bump"
1564
+ : state.miningState.currentPublishDecision === "replaced"
1565
+ ? "replaced"
1566
+ : "broadcast",
1567
+ };
1568
+ }
1569
+ async function ensureBuiltInSetupIfNeeded(options) {
1570
+ const unlocked = await loadUnlockedWalletState({
1571
+ provider: options.provider,
1572
+ paths: options.paths,
1573
+ });
1574
+ if (unlocked?.state.hookClientState.mining.mode === "custom") {
1575
+ return true;
1576
+ }
1577
+ const config = await loadClientConfig({
1578
+ path: options.paths.clientConfigPath,
1579
+ provider: options.provider,
1580
+ }).catch(() => null);
1581
+ if (config?.mining.builtIn !== null) {
1582
+ return true;
1583
+ }
1584
+ if (options.prompter.isInteractive === false) {
1585
+ return false;
1586
+ }
1587
+ await setupBuiltInMining({
1588
+ provider: options.provider,
1589
+ prompter: options.prompter,
1590
+ paths: options.paths,
1591
+ });
1592
+ return true;
1593
+ }
1594
+ async function performMiningCycle(options) {
1595
+ let readContext = await options.openReadContext({
1596
+ dataDir: options.dataDir,
1597
+ databasePath: options.databasePath,
1598
+ secretProvider: options.provider,
1599
+ paths: options.paths,
1600
+ });
1601
+ let readContextClosed = false;
1602
+ try {
1603
+ checkpointMiningSuspendDetector(options.suspendDetector);
1604
+ await refreshAndSaveStatus({
1605
+ paths: options.paths,
1606
+ provider: options.provider,
1607
+ readContext,
1608
+ overrides: {
1609
+ runMode: options.runMode,
1610
+ backgroundWorkerPid: options.backgroundWorkerPid,
1611
+ backgroundWorkerRunId: options.backgroundWorkerRunId,
1612
+ backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
1613
+ },
1614
+ });
1615
+ if (readContext.localState.availability !== "ready" || readContext.localState.state === null || readContext.localState.unlockUntilUnixMs === null) {
1616
+ await refreshAndSaveStatus({
1617
+ paths: options.paths,
1618
+ provider: options.provider,
1619
+ readContext,
1620
+ overrides: {
1621
+ runMode: options.runMode,
1622
+ currentPhase: "waiting",
1623
+ note: "Wallet must stay unlocked for mining to continue.",
1624
+ },
1625
+ visualizer: options.visualizer,
1626
+ });
1627
+ return;
1628
+ }
1629
+ const service = await options.attachService({
1630
+ dataDir: options.dataDir,
1631
+ chain: "main",
1632
+ startHeight: 0,
1633
+ walletRootId: readContext.localState.state.walletRootId,
1634
+ });
1635
+ checkpointMiningSuspendDetector(options.suspendDetector);
1636
+ const rpc = options.rpcFactory(service.rpc);
1637
+ const reconciledState = await reconcileLiveMiningState({
1638
+ state: readContext.localState.state,
1639
+ rpc,
1640
+ nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
1641
+ nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
1642
+ });
1643
+ checkpointMiningSuspendDetector(options.suspendDetector);
1644
+ let effectiveReadContext = readContext;
1645
+ if (JSON.stringify(reconciledState.miningState) !== JSON.stringify(readContext.localState.state.miningState)) {
1646
+ await saveWalletStatePreservingUnlock({
1647
+ state: reconciledState,
1648
+ provider: options.provider,
1649
+ unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1650
+ nowUnixMs: Date.now(),
1651
+ paths: options.paths,
1652
+ });
1653
+ effectiveReadContext = {
1654
+ ...readContext,
1655
+ localState: {
1656
+ ...readContext.localState,
1657
+ availability: "ready",
1658
+ unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1659
+ state: reconciledState,
1660
+ },
1661
+ };
1662
+ }
1663
+ if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
1664
+ await refreshAndSaveStatus({
1665
+ paths: options.paths,
1666
+ provider: options.provider,
1667
+ readContext: effectiveReadContext,
1668
+ overrides: {
1669
+ runMode: options.runMode,
1670
+ currentPhase: "waiting",
1671
+ note: "Mining is blocked until the current mining family is repaired or reconciled.",
1672
+ },
1673
+ visualizer: options.visualizer,
1674
+ });
1675
+ return;
1676
+ }
1677
+ if (hasBlockingMutation(effectiveReadContext.localState.state)) {
1678
+ const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
1679
+ state: "paused",
1680
+ pauseReason: "wallet-busy",
1681
+ });
1682
+ await saveWalletStatePreservingUnlock({
1683
+ state: nextState,
1684
+ provider: options.provider,
1685
+ unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1686
+ nowUnixMs: Date.now(),
1687
+ paths: options.paths,
1688
+ });
1689
+ effectiveReadContext = {
1690
+ ...effectiveReadContext,
1691
+ localState: {
1692
+ ...effectiveReadContext.localState,
1693
+ availability: "ready",
1694
+ unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1695
+ state: nextState,
1696
+ },
1697
+ };
1698
+ await refreshAndSaveStatus({
1699
+ paths: options.paths,
1700
+ provider: options.provider,
1701
+ readContext: effectiveReadContext,
1702
+ overrides: {
1703
+ runMode: options.runMode,
1704
+ currentPhase: "waiting",
1705
+ note: "Mining is paused while another wallet mutation family is active.",
1706
+ },
1707
+ visualizer: options.visualizer,
1708
+ });
1709
+ return;
1710
+ }
1711
+ const preemptionRequest = await readMiningPreemptionRequest(options.paths);
1712
+ if (preemptionRequest !== null) {
1713
+ const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
1714
+ state: effectiveReadContext.localState.state.miningState.liveMiningFamilyInMempool
1715
+ && effectiveReadContext.localState.state.miningState.state === "paused-stale"
1716
+ ? "paused-stale"
1717
+ : "paused",
1718
+ pauseReason: preemptionRequest.reason,
1719
+ });
1720
+ await saveWalletStatePreservingUnlock({
1721
+ state: nextState,
1722
+ provider: options.provider,
1723
+ unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1724
+ nowUnixMs: Date.now(),
1725
+ paths: options.paths,
1726
+ });
1727
+ await refreshAndSaveStatus({
1728
+ paths: options.paths,
1729
+ provider: options.provider,
1730
+ readContext: {
1731
+ ...effectiveReadContext,
1732
+ localState: {
1733
+ ...effectiveReadContext.localState,
1734
+ state: nextState,
1735
+ },
1736
+ },
1737
+ overrides: {
1738
+ runMode: options.runMode,
1739
+ currentPhase: "waiting",
1740
+ note: "Mining is paused while another wallet command is preempting sentence generation.",
1741
+ },
1742
+ visualizer: options.visualizer,
1743
+ });
1744
+ return;
1745
+ }
1746
+ const [blockchainInfo, networkInfo, mempoolInfo] = await Promise.all([
1747
+ rpc.getBlockchainInfo(),
1748
+ rpc.getNetworkInfo(),
1749
+ rpc.getMempoolInfo(),
1750
+ ]);
1751
+ checkpointMiningSuspendDetector(options.suspendDetector);
1752
+ const corePublishState = determineCorePublishState({
1753
+ blockchain: blockchainInfo,
1754
+ network: networkInfo,
1755
+ mempool: mempoolInfo,
1756
+ });
1757
+ if (corePublishState !== "healthy") {
1758
+ await refreshAndSaveStatus({
1759
+ paths: options.paths,
1760
+ provider: options.provider,
1761
+ readContext: effectiveReadContext,
1762
+ overrides: {
1763
+ runMode: options.runMode,
1764
+ currentPhase: "waiting-bitcoin-network",
1765
+ corePublishState,
1766
+ note: "Mining is waiting for the local Bitcoin node to become publishable.",
1767
+ },
1768
+ visualizer: options.visualizer,
1769
+ });
1770
+ return;
1771
+ }
1772
+ if (effectiveReadContext.indexer.health !== "synced" || effectiveReadContext.nodeHealth !== "synced") {
1773
+ await refreshAndSaveStatus({
1774
+ paths: options.paths,
1775
+ provider: options.provider,
1776
+ readContext: effectiveReadContext,
1777
+ overrides: {
1778
+ runMode: options.runMode,
1779
+ currentPhase: effectiveReadContext.indexer.health !== "synced"
1780
+ ? "waiting-indexer"
1781
+ : "waiting-bitcoin-network",
1782
+ note: effectiveReadContext.indexer.health !== "synced"
1783
+ ? "Mining is waiting for Bitcoin Core and the indexer to align."
1784
+ : "Mining is waiting for the local Bitcoin node to become publishable.",
1785
+ },
1786
+ visualizer: options.visualizer,
1787
+ });
1788
+ return;
1789
+ }
1790
+ const targetBlockHeight = (effectiveReadContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
1791
+ if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
1792
+ const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
1793
+ state: "paused",
1794
+ pauseReason: "zero-reward",
1795
+ });
1796
+ await saveWalletStatePreservingUnlock({
1797
+ state: nextState,
1798
+ provider: options.provider,
1799
+ unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
1800
+ nowUnixMs: Date.now(),
1801
+ paths: options.paths,
1802
+ });
1803
+ await refreshAndSaveStatus({
1804
+ paths: options.paths,
1805
+ provider: options.provider,
1806
+ readContext: {
1807
+ ...effectiveReadContext,
1808
+ localState: {
1809
+ ...effectiveReadContext.localState,
1810
+ state: nextState,
1811
+ },
1812
+ },
1813
+ overrides: {
1814
+ runMode: options.runMode,
1815
+ currentPhase: "idle",
1816
+ currentPublishDecision: "publish-skipped-zero-reward",
1817
+ note: "Mining is disabled because the target block reward is zero.",
1818
+ },
1819
+ visualizer: options.visualizer,
1820
+ });
1821
+ await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
1822
+ targetBlockHeight,
1823
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1824
+ runId: options.backgroundWorkerRunId,
1825
+ }));
1826
+ return;
1827
+ }
1828
+ const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
1829
+ if (domains.length === 0) {
1830
+ await refreshAndSaveStatus({
1831
+ paths: options.paths,
1832
+ provider: options.provider,
1833
+ readContext: effectiveReadContext,
1834
+ overrides: {
1835
+ runMode: options.runMode,
1836
+ currentPhase: "idle",
1837
+ note: "No locally controlled anchored root domains are currently eligible to mine.",
1838
+ },
1839
+ visualizer: options.visualizer,
1840
+ });
1841
+ return;
1842
+ }
1843
+ const indexerTruthKey = getIndexerTruthKey(effectiveReadContext);
1844
+ const walletRootId = effectiveReadContext.localState.walletRootId;
1845
+ const ensureCurrentIndexerTruthOrRestart = async () => {
1846
+ try {
1847
+ await ensureIndexerTruthIsCurrent({
1848
+ dataDir: effectiveReadContext.dataDir,
1849
+ truthKey: indexerTruthKey,
1850
+ });
1851
+ return true;
1852
+ }
1853
+ catch (error) {
1854
+ if (!(error instanceof Error) || error.message !== "mining_generation_stale_indexer_truth") {
1855
+ throw error;
1856
+ }
1857
+ clearMiningGateCache(walletRootId);
1858
+ await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
1859
+ level: "warn",
1860
+ targetBlockHeight,
1861
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1862
+ runId: options.backgroundWorkerRunId,
1863
+ }));
1864
+ return false;
1865
+ }
1866
+ };
1867
+ await refreshAndSaveStatus({
1868
+ paths: options.paths,
1869
+ provider: options.provider,
1870
+ readContext: effectiveReadContext,
1871
+ overrides: {
1872
+ runMode: options.runMode,
1873
+ currentPhase: "generating",
1874
+ note: "Generating mining sentences for eligible root domains.",
1875
+ },
1876
+ visualizer: options.visualizer,
1877
+ });
1878
+ await appendEvent(options.paths, createEvent("hook-request-start", "Started mining sentence generation.", {
1879
+ targetBlockHeight,
1880
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1881
+ runId: options.backgroundWorkerRunId,
1882
+ }));
1883
+ let candidates;
1884
+ try {
1885
+ candidates = await generateCandidatesForDomains({
1886
+ rpc,
1887
+ readContext: effectiveReadContext,
1888
+ domains,
1889
+ provider: options.provider,
1890
+ paths: options.paths,
1891
+ indexerTruthKey,
1892
+ runId: options.backgroundWorkerRunId,
1893
+ fetchImpl: options.fetchImpl,
1894
+ });
1895
+ checkpointMiningSuspendDetector(options.suspendDetector);
1896
+ }
1897
+ catch (error) {
1898
+ if (error instanceof MiningProviderRequestError) {
1899
+ await refreshAndSaveStatus({
1900
+ paths: options.paths,
1901
+ provider: options.provider,
1902
+ readContext: effectiveReadContext,
1903
+ overrides: {
1904
+ runMode: options.runMode,
1905
+ currentPhase: "waiting-provider",
1906
+ providerState: error.providerState,
1907
+ lastError: error.message,
1908
+ note: "Mining is waiting for the sentence provider to recover.",
1909
+ },
1910
+ visualizer: options.visualizer,
1911
+ });
1912
+ await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
1913
+ level: "warn",
1914
+ targetBlockHeight,
1915
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1916
+ runId: options.backgroundWorkerRunId,
1917
+ }));
1918
+ return;
1919
+ }
1920
+ if (error instanceof Error && error.message === "mining_generation_stale_tip") {
1921
+ await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
1922
+ level: "warn",
1923
+ targetBlockHeight,
1924
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1925
+ runId: options.backgroundWorkerRunId,
1926
+ }));
1927
+ return;
1928
+ }
1929
+ if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
1930
+ clearMiningGateCache(walletRootId);
1931
+ await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during sentence generation; restarting on the next tick.", {
1932
+ level: "warn",
1933
+ targetBlockHeight,
1934
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1935
+ runId: options.backgroundWorkerRunId,
1936
+ }));
1937
+ return;
1938
+ }
1939
+ if (error instanceof Error && error.message === "mining_generation_preempted") {
1940
+ await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
1941
+ level: "warn",
1942
+ targetBlockHeight,
1943
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1944
+ runId: options.backgroundWorkerRunId,
1945
+ }));
1946
+ return;
1947
+ }
1948
+ const hookCooldownActive = await persistCustomHookRuntimeOutcome({
1949
+ readContext: effectiveReadContext,
1950
+ provider: options.provider,
1951
+ paths: options.paths,
1952
+ nowUnixMs: Date.now(),
1953
+ success: false,
1954
+ });
1955
+ const failureMessage = error instanceof Error ? error.message : String(error);
1956
+ await refreshAndSaveStatus({
1957
+ paths: options.paths,
1958
+ provider: options.provider,
1959
+ readContext: effectiveReadContext,
1960
+ overrides: {
1961
+ runMode: options.runMode,
1962
+ currentPhase: "waiting-provider",
1963
+ providerState: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
1964
+ ? "hook-error"
1965
+ : undefined,
1966
+ lastError: failureMessage,
1967
+ note: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
1968
+ ? (hookCooldownActive
1969
+ ? "Custom mining hook launch is paused during the post-failure cooldown window."
1970
+ : "Custom mining hook failed during sentence generation. Fix it or rerun `cogcoin hooks enable mining`.")
1971
+ : "Mining sentence generation failed for the current tip.",
1972
+ },
1973
+ visualizer: options.visualizer,
1974
+ });
1975
+ await appendEvent(options.paths, createEvent("hook-request-failed", failureMessage, {
1976
+ level: "error",
1977
+ targetBlockHeight,
1978
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
1979
+ runId: options.backgroundWorkerRunId,
1980
+ }));
1981
+ return;
1982
+ }
1983
+ await refreshAndSaveStatus({
1984
+ paths: options.paths,
1985
+ provider: options.provider,
1986
+ readContext: effectiveReadContext,
1987
+ overrides: {
1988
+ runMode: options.runMode,
1989
+ currentPhase: "scoring",
1990
+ note: "Scoring mining candidates for the current tip.",
1991
+ },
1992
+ visualizer: options.visualizer,
1993
+ });
1994
+ const best = await chooseBestLocalCandidate(candidates);
1995
+ if (best === null) {
1996
+ await refreshAndSaveStatus({
1997
+ paths: options.paths,
1998
+ provider: options.provider,
1999
+ readContext: effectiveReadContext,
2000
+ overrides: {
2001
+ runMode: options.runMode,
2002
+ currentPhase: "idle",
2003
+ currentPublishDecision: "publish-skipped-no-candidate",
2004
+ note: "No publishable mining candidate passed scoring gates for the current tip.",
2005
+ },
2006
+ visualizer: options.visualizer,
2007
+ });
2008
+ await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
2009
+ targetBlockHeight,
2010
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2011
+ runId: options.backgroundWorkerRunId,
2012
+ }));
2013
+ return;
2014
+ }
2015
+ if (!await ensureCurrentIndexerTruthOrRestart()) {
2016
+ return;
2017
+ }
2018
+ writeStdout(options.stdout, `Selected ${best.domainName}: ${best.sentence} (${best.canonicalBlend.toString()})`);
2019
+ await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
2020
+ targetBlockHeight: best.targetBlockHeight,
2021
+ referencedBlockHashDisplay: best.referencedBlockHashDisplay,
2022
+ domainId: best.domainId,
2023
+ domainName: best.domainName,
2024
+ score: best.canonicalBlend.toString(),
2025
+ runId: options.backgroundWorkerRunId,
2026
+ }));
2027
+ const gate = await runCompetitivenessGate({
2028
+ rpc,
2029
+ readContext: effectiveReadContext,
2030
+ candidate: best,
2031
+ currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
2032
+ });
2033
+ checkpointMiningSuspendDetector(options.suspendDetector);
2034
+ if (!gate.allowed) {
2035
+ await refreshAndSaveStatus({
2036
+ paths: options.paths,
2037
+ provider: options.provider,
2038
+ readContext: effectiveReadContext,
2039
+ overrides: {
2040
+ runMode: options.runMode,
2041
+ currentPhase: "waiting",
2042
+ currentPublishDecision: gate.decision,
2043
+ sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
2044
+ higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2045
+ dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2046
+ competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
2047
+ mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2048
+ lastMempoolSequence: gate.lastMempoolSequence,
2049
+ lastCompetitivenessGateAtUnixMs: Date.now(),
2050
+ note: gate.decision === "suppressed-same-domain-mempool"
2051
+ ? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
2052
+ : gate.decision === "suppressed-top5-mempool"
2053
+ ? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2054
+ : "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.",
2055
+ },
2056
+ visualizer: options.visualizer,
2057
+ });
2058
+ await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
2059
+ ? "publish-skipped-same-domain-mempool"
2060
+ : gate.decision === "suppressed-top5-mempool"
2061
+ ? "publish-skipped-top5-mempool"
2062
+ : "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
2063
+ ? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
2064
+ : gate.decision === "suppressed-top5-mempool"
2065
+ ? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
2066
+ : "Skipped publish because the competitiveness gate could not be evaluated safely.", {
2067
+ targetBlockHeight: best.targetBlockHeight,
2068
+ referencedBlockHashDisplay: best.referencedBlockHashDisplay,
2069
+ domainId: best.domainId,
2070
+ domainName: best.domainName,
2071
+ score: best.canonicalBlend.toString(),
2072
+ runId: options.backgroundWorkerRunId,
2073
+ reason: gate.decision,
2074
+ }));
2075
+ return;
2076
+ }
2077
+ if (!await ensureCurrentIndexerTruthOrRestart()) {
2078
+ return;
2079
+ }
2080
+ await refreshAndSaveStatus({
2081
+ paths: options.paths,
2082
+ provider: options.provider,
2083
+ readContext: effectiveReadContext,
2084
+ overrides: {
2085
+ runMode: options.runMode,
2086
+ currentPhase: effectiveReadContext.localState.state.miningState.currentTxid === null
2087
+ ? "publishing"
2088
+ : "replacing",
2089
+ note: effectiveReadContext.localState.state.miningState.currentTxid === null
2090
+ ? "Broadcasting the best mining candidate for the current tip."
2091
+ : "Replacing the live mining transaction for the current tip.",
2092
+ },
2093
+ visualizer: options.visualizer,
2094
+ });
2095
+ const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
2096
+ purpose: "wallet-mine",
2097
+ walletRootId: effectiveReadContext.localState.state.walletRootId,
2098
+ });
2099
+ checkpointMiningSuspendDetector(options.suspendDetector);
2100
+ try {
2101
+ if (!await ensureCurrentIndexerTruthOrRestart()) {
2102
+ return;
2103
+ }
2104
+ checkpointMiningSuspendDetector(options.suspendDetector);
2105
+ const published = await publishCandidate({
2106
+ readContext: effectiveReadContext,
2107
+ candidate: best,
2108
+ dataDir: options.dataDir,
2109
+ provider: options.provider,
2110
+ paths: options.paths,
2111
+ attachService: options.attachService,
2112
+ rpcFactory: options.rpcFactory,
2113
+ runId: options.backgroundWorkerRunId,
2114
+ });
2115
+ checkpointMiningSuspendDetector(options.suspendDetector);
2116
+ await refreshAndSaveStatus({
2117
+ paths: options.paths,
2118
+ provider: options.provider,
2119
+ readContext: {
2120
+ ...effectiveReadContext,
2121
+ localState: {
2122
+ ...effectiveReadContext.localState,
2123
+ state: published.state,
2124
+ },
2125
+ },
2126
+ overrides: {
2127
+ runMode: options.runMode,
2128
+ currentPhase: "publishing",
2129
+ currentPublishDecision: published.decision,
2130
+ sameDomainCompetitorSuppressed: false,
2131
+ higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2132
+ dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
2133
+ competitivenessGateIndeterminate: false,
2134
+ mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
2135
+ lastMempoolSequence: gate.lastMempoolSequence,
2136
+ lastCompetitivenessGateAtUnixMs: Date.now(),
2137
+ note: published.txid === null
2138
+ ? "Mining candidate was evaluated but the existing live family stayed in place."
2139
+ : `Mining candidate ${published.decision === "replaced"
2140
+ ? "replaced"
2141
+ : published.decision === "fee-bump"
2142
+ ? "fee-bumped"
2143
+ : "broadcast"} as ${published.txid}.`,
2144
+ liveMiningFamilyInMempool: published.state.miningState.liveMiningFamilyInMempool,
2145
+ },
2146
+ visualizer: options.visualizer,
2147
+ });
2148
+ }
2149
+ finally {
2150
+ await publishLock.release();
2151
+ }
2152
+ }
2153
+ catch (error) {
2154
+ if (error instanceof MiningSuspendDetectedError) {
2155
+ if (readContext !== null && !readContextClosed) {
2156
+ await readContext.close();
2157
+ readContextClosed = true;
2158
+ }
2159
+ await handleDetectedMiningRuntimeResume({
2160
+ dataDir: options.dataDir,
2161
+ databasePath: options.databasePath,
2162
+ provider: options.provider,
2163
+ paths: options.paths,
2164
+ runMode: options.runMode,
2165
+ backgroundWorkerPid: options.backgroundWorkerPid,
2166
+ backgroundWorkerRunId: options.backgroundWorkerRunId,
2167
+ detectedAtUnixMs: error.detectedAtUnixMs,
2168
+ openReadContext: options.openReadContext,
2169
+ visualizer: options.visualizer,
2170
+ });
2171
+ return;
2172
+ }
2173
+ throw error;
2174
+ }
2175
+ finally {
2176
+ if (readContext !== null && !readContextClosed) {
2177
+ await readContext.close();
2178
+ }
2179
+ }
2180
+ }
2181
+ async function saveStopSnapshot(options) {
2182
+ const readContext = await openWalletReadContext({
2183
+ dataDir: options.dataDir,
2184
+ databasePath: options.databasePath,
2185
+ secretProvider: options.provider,
2186
+ paths: options.paths,
2187
+ });
2188
+ try {
2189
+ let localState = readContext.localState;
2190
+ if (localState.availability === "ready" && localState.state !== null && localState.unlockUntilUnixMs !== null) {
2191
+ const service = await attachOrStartManagedBitcoindService({
2192
+ dataDir: options.dataDir,
2193
+ chain: "main",
2194
+ startHeight: 0,
2195
+ walletRootId: localState.state.walletRootId,
2196
+ }).catch(() => null);
2197
+ if (service !== null) {
2198
+ const rpc = createRpcClient(service.rpc);
2199
+ const reconciledState = await reconcileLiveMiningState({
2200
+ state: localState.state,
2201
+ rpc,
2202
+ nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
2203
+ nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
2204
+ });
2205
+ const stopState = defaultMiningStatePatch(reconciledState, {
2206
+ runMode: "stopped",
2207
+ state: reconciledState.miningState.liveMiningFamilyInMempool
2208
+ ? reconciledState.miningState.state === "paused-stale"
2209
+ ? "paused-stale"
2210
+ : "paused"
2211
+ : reconciledState.miningState.state === "repair-required"
2212
+ ? "repair-required"
2213
+ : "idle",
2214
+ pauseReason: reconciledState.miningState.liveMiningFamilyInMempool
2215
+ ? reconciledState.miningState.state === "paused-stale"
2216
+ ? "stale-block-context"
2217
+ : "user-stopped"
2218
+ : reconciledState.miningState.state === "repair-required"
2219
+ ? reconciledState.miningState.pauseReason
2220
+ : null,
2221
+ });
2222
+ await saveWalletStatePreservingUnlock({
2223
+ state: stopState,
2224
+ provider: options.provider,
2225
+ unlockUntilUnixMs: localState.unlockUntilUnixMs,
2226
+ nowUnixMs: Date.now(),
2227
+ paths: options.paths,
2228
+ });
2229
+ localState = {
2230
+ ...localState,
2231
+ state: stopState,
2232
+ };
2233
+ }
2234
+ }
2235
+ await refreshAndSaveStatus({
2236
+ paths: options.paths,
2237
+ provider: options.provider,
2238
+ readContext: {
2239
+ ...readContext,
2240
+ localState,
2241
+ },
2242
+ overrides: {
2243
+ runMode: "stopped",
2244
+ backgroundWorkerPid: options.runMode === "background" ? null : options.backgroundWorkerPid,
2245
+ backgroundWorkerRunId: options.runMode === "background" ? null : options.backgroundWorkerRunId,
2246
+ backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? null : Date.now(),
2247
+ currentPhase: "idle",
2248
+ note: options.note,
2249
+ },
2250
+ });
2251
+ }
2252
+ finally {
2253
+ await readContext.close();
2254
+ }
2255
+ }
2256
+ async function attemptSaveMempool(rpc, paths, runId) {
2257
+ try {
2258
+ await rpc.saveMempool?.();
2259
+ }
2260
+ catch {
2261
+ // ignore
2262
+ }
2263
+ finally {
2264
+ await appendEvent(paths, createEvent("savemempool-attempted", "Attempted to persist the local mempool before stopping mining.", {
2265
+ runId,
2266
+ }));
2267
+ }
2268
+ }
2269
+ async function runMiningLoop(options) {
2270
+ const suspendDetector = createMiningSuspendDetector();
2271
+ await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
2272
+ runId: options.backgroundWorkerRunId,
2273
+ }));
2274
+ while (!options.signal?.aborted) {
2275
+ try {
2276
+ checkpointMiningSuspendDetector(suspendDetector);
2277
+ }
2278
+ catch (error) {
2279
+ if (!(error instanceof MiningSuspendDetectedError)) {
2280
+ throw error;
2281
+ }
2282
+ await handleDetectedMiningRuntimeResume({
2283
+ dataDir: options.dataDir,
2284
+ databasePath: options.databasePath,
2285
+ provider: options.provider,
2286
+ paths: options.paths,
2287
+ runMode: options.runMode,
2288
+ backgroundWorkerPid: options.backgroundWorkerPid,
2289
+ backgroundWorkerRunId: options.backgroundWorkerRunId,
2290
+ detectedAtUnixMs: error.detectedAtUnixMs,
2291
+ openReadContext: options.openReadContext,
2292
+ visualizer: options.visualizer,
2293
+ });
2294
+ continue;
2295
+ }
2296
+ await performMiningCycle({
2297
+ ...options,
2298
+ suspendDetector,
2299
+ });
2300
+ await sleep(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
2301
+ }
2302
+ const service = await options.attachService({
2303
+ dataDir: options.dataDir,
2304
+ chain: "main",
2305
+ startHeight: 0,
2306
+ walletRootId: undefined,
2307
+ }).catch(() => null);
2308
+ if (service !== null) {
2309
+ await attemptSaveMempool(options.rpcFactory(service.rpc), options.paths, options.backgroundWorkerRunId);
2310
+ }
2311
+ await appendEvent(options.paths, createEvent("runtime-stop", `Stopped ${options.runMode} mining runtime.`, {
2312
+ runId: options.backgroundWorkerRunId,
2313
+ }));
2314
+ }
2315
+ async function waitForBackgroundHealthy(paths) {
2316
+ const deadline = Date.now() + BACKGROUND_START_TIMEOUT_MS;
2317
+ while (Date.now() < deadline) {
2318
+ const snapshot = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
2319
+ if (snapshot !== null
2320
+ && snapshot.runMode === "background"
2321
+ && snapshot.backgroundWorkerHealth === "healthy") {
2322
+ return snapshot;
2323
+ }
2324
+ await sleep(250);
2325
+ }
2326
+ return loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
2327
+ }
2328
+ export async function runForegroundMining(options) {
2329
+ if (!options.prompter.isInteractive) {
2330
+ throw new Error("mine_requires_tty");
2331
+ }
2332
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
2333
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
2334
+ const openReadContext = options.openReadContext ?? openWalletReadContext;
2335
+ const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
2336
+ const rpcFactory = options.rpcFactory ?? createRpcClient;
2337
+ const controlLock = await acquireFileLock(paths.miningControlLockPath, {
2338
+ purpose: "mine-foreground",
2339
+ });
2340
+ let visualizer = null;
2341
+ try {
2342
+ const existing = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
2343
+ if (existing?.runMode === "background") {
2344
+ throw new Error("Background mining is already active. Run `cogcoin mine stop` first.");
2345
+ }
2346
+ const setupReady = await ensureBuiltInSetupIfNeeded({
2347
+ provider,
2348
+ prompter: options.prompter,
2349
+ paths,
2350
+ });
2351
+ if (!setupReady) {
2352
+ throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
2353
+ }
2354
+ visualizer = new MiningFollowVisualizer({
2355
+ progressOutput: options.progressOutput ?? "auto",
2356
+ stream: options.stderr,
2357
+ });
2358
+ const abortController = new AbortController();
2359
+ options.signal?.addEventListener("abort", () => {
2360
+ abortController.abort();
2361
+ }, { once: true });
2362
+ process.on("SIGINT", () => abortController.abort());
2363
+ process.on("SIGTERM", () => abortController.abort());
2364
+ await runMiningLoop({
2365
+ dataDir: options.dataDir,
2366
+ databasePath: options.databasePath,
2367
+ provider,
2368
+ paths,
2369
+ runMode: "foreground",
2370
+ backgroundWorkerPid: null,
2371
+ backgroundWorkerRunId: null,
2372
+ signal: abortController.signal,
2373
+ fetchImpl: options.fetchImpl,
2374
+ openReadContext,
2375
+ attachService,
2376
+ rpcFactory,
2377
+ stdout: options.stdout,
2378
+ visualizer,
2379
+ });
2380
+ await saveStopSnapshot({
2381
+ dataDir: options.dataDir,
2382
+ databasePath: options.databasePath,
2383
+ provider,
2384
+ paths,
2385
+ runMode: "foreground",
2386
+ backgroundWorkerPid: null,
2387
+ backgroundWorkerRunId: null,
2388
+ note: "Foreground mining stopped cleanly.",
2389
+ });
2390
+ }
2391
+ finally {
2392
+ visualizer?.close();
2393
+ await controlLock.release();
2394
+ }
2395
+ }
2396
+ export async function startBackgroundMining(options) {
2397
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
2398
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
2399
+ const controlLock = await acquireFileLock(paths.miningControlLockPath, {
2400
+ purpose: "mine-start",
2401
+ });
2402
+ try {
2403
+ const existing = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
2404
+ if (existing?.runMode === "background"
2405
+ && existing.backgroundWorkerPid !== null
2406
+ && await isProcessAlive(existing.backgroundWorkerPid)) {
2407
+ return {
2408
+ started: false,
2409
+ snapshot: existing,
2410
+ };
2411
+ }
2412
+ if (existing?.runMode === "foreground") {
2413
+ throw new Error("Foreground mining is already active. Interrupt that process directly.");
2414
+ }
2415
+ const setupReady = await ensureBuiltInSetupIfNeeded({
2416
+ provider,
2417
+ prompter: options.prompter,
2418
+ paths,
2419
+ });
2420
+ if (!setupReady) {
2421
+ throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
2422
+ }
2423
+ const runId = randomBytes(16).toString("hex");
2424
+ const workerMainPath = fileURLToPath(new URL("./worker-main.js", import.meta.url));
2425
+ const child = spawn(process.execPath, [
2426
+ workerMainPath,
2427
+ `--data-dir=${options.dataDir}`,
2428
+ `--database-path=${options.databasePath}`,
2429
+ `--run-id=${runId}`,
2430
+ ], {
2431
+ detached: true,
2432
+ stdio: "ignore",
2433
+ });
2434
+ child.unref();
2435
+ const snapshot = await waitForBackgroundHealthy(paths);
2436
+ return {
2437
+ started: true,
2438
+ snapshot,
2439
+ };
2440
+ }
2441
+ finally {
2442
+ await controlLock.release();
2443
+ }
2444
+ }
2445
+ export async function stopBackgroundMining(options) {
2446
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
2447
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
2448
+ const controlLock = await acquireFileLock(paths.miningControlLockPath, {
2449
+ purpose: "mine-stop",
2450
+ });
2451
+ try {
2452
+ const snapshot = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
2453
+ if (snapshot === null || snapshot.runMode !== "background" || snapshot.backgroundWorkerPid === null) {
2454
+ return snapshot;
2455
+ }
2456
+ const preemption = await requestMiningGenerationPreemption({
2457
+ paths,
2458
+ reason: "mine-stop",
2459
+ timeoutMs: Math.min(MINING_SHUTDOWN_GRACE_MS, 15_000),
2460
+ }).catch(() => null);
2461
+ process.kill(snapshot.backgroundWorkerPid, "SIGTERM");
2462
+ const deadline = Date.now() + MINING_SHUTDOWN_GRACE_MS;
2463
+ while (Date.now() < deadline) {
2464
+ try {
2465
+ process.kill(snapshot.backgroundWorkerPid, 0);
2466
+ await sleep(250);
2467
+ }
2468
+ catch (error) {
2469
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
2470
+ break;
2471
+ }
2472
+ }
2473
+ }
2474
+ try {
2475
+ process.kill(snapshot.backgroundWorkerPid, "SIGKILL");
2476
+ }
2477
+ catch {
2478
+ // ignore
2479
+ }
2480
+ await saveStopSnapshot({
2481
+ dataDir: options.dataDir,
2482
+ databasePath: options.databasePath,
2483
+ provider,
2484
+ paths,
2485
+ runMode: "background",
2486
+ backgroundWorkerPid: snapshot.backgroundWorkerPid,
2487
+ backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
2488
+ note: snapshot.liveMiningFamilyInMempool
2489
+ ? "Background mining stopped. The last mining transaction may still confirm from mempool."
2490
+ : "Background mining stopped.",
2491
+ });
2492
+ await preemption?.release().catch(() => undefined);
2493
+ return loadMiningRuntimeStatus(paths.miningStatusPath);
2494
+ }
2495
+ finally {
2496
+ await controlLock.release();
2497
+ }
2498
+ }
2499
+ export async function runBackgroundMiningWorker(options) {
2500
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
2501
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
2502
+ const openReadContext = options.openReadContext ?? openWalletReadContext;
2503
+ const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
2504
+ const rpcFactory = options.rpcFactory ?? createRpcClient;
2505
+ const abortController = new AbortController();
2506
+ process.on("SIGINT", () => abortController.abort());
2507
+ process.on("SIGTERM", () => abortController.abort());
2508
+ const initialContext = await openReadContext({
2509
+ dataDir: options.dataDir,
2510
+ databasePath: options.databasePath,
2511
+ secretProvider: provider,
2512
+ paths,
2513
+ });
2514
+ try {
2515
+ const initialView = await inspectMiningControlPlane({
2516
+ provider,
2517
+ localState: initialContext.localState,
2518
+ bitcoind: initialContext.bitcoind,
2519
+ nodeStatus: initialContext.nodeStatus,
2520
+ nodeHealth: initialContext.nodeHealth,
2521
+ indexer: initialContext.indexer,
2522
+ paths,
2523
+ });
2524
+ await saveMiningRuntimeStatus(paths.miningStatusPath, {
2525
+ ...initialView.runtime,
2526
+ walletRootId: initialContext.localState.walletRootId,
2527
+ workerApiVersion: MINING_WORKER_API_VERSION,
2528
+ workerBinaryVersion: process.version,
2529
+ workerBuildId: options.runId,
2530
+ runMode: "background",
2531
+ backgroundWorkerPid: process.pid,
2532
+ backgroundWorkerRunId: options.runId,
2533
+ backgroundWorkerHeartbeatAtUnixMs: Date.now(),
2534
+ currentPhase: "idle",
2535
+ updatedAtUnixMs: Date.now(),
2536
+ });
2537
+ }
2538
+ finally {
2539
+ await initialContext.close();
2540
+ }
2541
+ await runMiningLoop({
2542
+ dataDir: options.dataDir,
2543
+ databasePath: options.databasePath,
2544
+ provider,
2545
+ paths,
2546
+ runMode: "background",
2547
+ backgroundWorkerPid: process.pid,
2548
+ backgroundWorkerRunId: options.runId,
2549
+ signal: abortController.signal,
2550
+ fetchImpl: options.fetchImpl,
2551
+ openReadContext,
2552
+ attachService,
2553
+ rpcFactory,
2554
+ });
2555
+ await saveStopSnapshot({
2556
+ dataDir: options.dataDir,
2557
+ databasePath: options.databasePath,
2558
+ provider,
2559
+ paths,
2560
+ runMode: "background",
2561
+ backgroundWorkerPid: process.pid,
2562
+ backgroundWorkerRunId: options.runId,
2563
+ note: "Background mining worker stopped cleanly.",
2564
+ });
2565
+ }
2566
+ export async function handleDetectedMiningRuntimeResumeForTesting(options) {
2567
+ await handleDetectedMiningRuntimeResume(options);
2568
+ }
2569
+ export async function performMiningCycleForTesting(options) {
2570
+ await performMiningCycle(options);
2571
+ }
2572
+ export function shouldTreatCandidateAsFeeBumpForTesting(options) {
2573
+ return candidateNeedsFeeMaintenance(options);
2574
+ }