@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,1475 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { access, constants, mkdir, readFile, rename, rm } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { openClient } from "../client.js";
5
+ import { attachOrStartIndexerDaemon, probeIndexerDaemon, readSnapshotWithRetry, } from "../bitcoind/indexer-daemon.js";
6
+ import { attachOrStartManagedBitcoindService, createManagedWalletReplica, probeManagedBitcoindService, } from "../bitcoind/service.js";
7
+ import { resolveManagedServicePaths } from "../bitcoind/service-paths.js";
8
+ import { createRpcClient } from "../bitcoind/node.js";
9
+ import { openSqliteStore } from "../sqlite/index.js";
10
+ import { readPortableWalletArchive, writePortableWalletArchive } from "./archive.js";
11
+ import { acquireFileLock } from "./fs/lock.js";
12
+ import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, } from "./material.js";
13
+ import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
14
+ import { requestMiningGenerationPreemption } from "./mining/coordination.js";
15
+ import { loadClientConfig } from "./mining/config.js";
16
+ import { inspectMiningHookState } from "./mining/hooks.js";
17
+ import { loadMiningRuntimeStatus, saveMiningRuntimeStatus } from "./mining/runtime-artifacts.js";
18
+ import { normalizeMiningStateRecord } from "./mining/state.js";
19
+ import { clearUnlockSession, loadUnlockSession, saveUnlockSession } from "./state/session.js";
20
+ import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
21
+ import { loadWalletState, saveWalletState } from "./state/storage.js";
22
+ export const DEFAULT_UNLOCK_DURATION_MS = 15 * 60 * 1000;
23
+ function sanitizeWalletName(walletRootId) {
24
+ return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
25
+ }
26
+ function stripDescriptorChecksum(descriptor) {
27
+ return descriptor.replace(/#[A-Za-z0-9]+$/, "");
28
+ }
29
+ async function pathExists(path) {
30
+ try {
31
+ await access(path, constants.F_OK);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ function createInitialWalletState(options) {
39
+ return {
40
+ schemaVersion: 1,
41
+ stateRevision: 1,
42
+ lastWrittenAtUnixMs: options.nowUnixMs,
43
+ walletRootId: options.walletRootId,
44
+ network: "mainnet",
45
+ anchorValueSats: 2_000,
46
+ nextDedicatedIndex: 1,
47
+ fundingIndex: 0,
48
+ mnemonic: {
49
+ phrase: options.material.mnemonic.phrase,
50
+ language: options.material.mnemonic.language,
51
+ },
52
+ keys: {
53
+ masterFingerprintHex: options.material.keys.masterFingerprintHex,
54
+ accountPath: options.material.keys.accountPath,
55
+ accountXprv: options.material.keys.accountXprv,
56
+ accountXpub: options.material.keys.accountXpub,
57
+ },
58
+ descriptor: {
59
+ privateExternal: options.material.descriptor.privateExternal,
60
+ publicExternal: options.material.descriptor.publicExternal,
61
+ checksum: options.material.descriptor.checksum,
62
+ rangeEnd: options.material.descriptor.rangeEnd,
63
+ safetyMargin: options.material.descriptor.safetyMargin,
64
+ },
65
+ funding: {
66
+ address: options.material.funding.address,
67
+ scriptPubKeyHex: options.material.funding.scriptPubKeyHex,
68
+ },
69
+ walletBirthTime: Math.floor(options.nowUnixMs / 1000),
70
+ managedCoreWallet: {
71
+ walletName: sanitizeWalletName(options.walletRootId),
72
+ internalPassphrase: options.internalCoreWalletPassphrase,
73
+ descriptorChecksum: null,
74
+ fundingAddress0: null,
75
+ fundingScriptPubKeyHex0: null,
76
+ proofStatus: "not-proven",
77
+ lastImportedAtUnixMs: null,
78
+ lastVerifiedAtUnixMs: null,
79
+ },
80
+ identities: [
81
+ {
82
+ index: 0,
83
+ scriptPubKeyHex: options.material.funding.scriptPubKeyHex,
84
+ address: options.material.funding.address,
85
+ status: "funding",
86
+ assignedDomainNames: [],
87
+ },
88
+ ],
89
+ domains: [],
90
+ miningState: {
91
+ runMode: "stopped",
92
+ state: "idle",
93
+ pauseReason: null,
94
+ currentPublishState: "none",
95
+ currentDomain: null,
96
+ currentDomainId: null,
97
+ currentDomainIndex: null,
98
+ currentSenderScriptPubKeyHex: null,
99
+ currentTxid: null,
100
+ currentWtxid: null,
101
+ currentFeeRateSatVb: null,
102
+ currentAbsoluteFeeSats: null,
103
+ currentScore: null,
104
+ currentSentence: null,
105
+ currentEncodedSentenceBytesHex: null,
106
+ currentBip39WordIndices: null,
107
+ currentBlendSeedHex: null,
108
+ currentBlockTargetHeight: null,
109
+ currentReferencedBlockHashDisplay: null,
110
+ currentIntentFingerprintHex: null,
111
+ liveMiningFamilyInMempool: null,
112
+ currentPublishDecision: null,
113
+ replacementCount: 0,
114
+ currentBlockFeeSpentSats: "0",
115
+ sessionFeeSpentSats: "0",
116
+ lifetimeFeeSpentSats: "0",
117
+ sharedMiningConflictOutpoint: null,
118
+ },
119
+ hookClientState: {
120
+ mining: {
121
+ mode: "builtin",
122
+ validationState: "never",
123
+ lastValidationAtUnixMs: null,
124
+ lastValidationError: null,
125
+ validatedLaunchFingerprint: null,
126
+ validatedFullFingerprint: null,
127
+ fullTrustWarningAcknowledgedAtUnixMs: null,
128
+ consecutiveFailureCount: 0,
129
+ cooldownUntilUnixMs: null,
130
+ },
131
+ },
132
+ proactiveFamilies: [],
133
+ pendingMutations: [],
134
+ };
135
+ }
136
+ function createUnlockSession(state, unlockUntilUnixMs, secretKeyId, nowUnixMs) {
137
+ return {
138
+ schemaVersion: 1,
139
+ walletRootId: state.walletRootId,
140
+ sessionId: randomBytes(16).toString("hex"),
141
+ createdAtUnixMs: nowUnixMs,
142
+ unlockUntilUnixMs,
143
+ sourceStateRevision: state.stateRevision,
144
+ wrappedSessionKeyMaterial: secretKeyId,
145
+ };
146
+ }
147
+ function createPortableWalletArchivePayload(state, exportedAtUnixMs) {
148
+ return {
149
+ schemaVersion: 1,
150
+ exportedAtUnixMs,
151
+ walletRootId: state.walletRootId,
152
+ network: state.network,
153
+ anchorValueSats: state.anchorValueSats,
154
+ nextDedicatedIndex: state.nextDedicatedIndex,
155
+ fundingIndex: state.fundingIndex,
156
+ mnemonic: {
157
+ phrase: state.mnemonic.phrase,
158
+ language: state.mnemonic.language,
159
+ },
160
+ expected: {
161
+ masterFingerprintHex: state.keys.masterFingerprintHex,
162
+ accountPath: state.keys.accountPath,
163
+ accountXpub: state.keys.accountXpub,
164
+ publicExternalDescriptor: stripDescriptorChecksum(state.descriptor.publicExternal),
165
+ descriptorChecksum: state.descriptor.checksum,
166
+ rangeEnd: state.descriptor.rangeEnd,
167
+ safetyMargin: state.descriptor.safetyMargin,
168
+ fundingAddress0: state.funding.address,
169
+ fundingScriptPubKeyHex0: state.funding.scriptPubKeyHex,
170
+ walletBirthTime: state.walletBirthTime,
171
+ },
172
+ identities: state.identities,
173
+ domains: state.domains,
174
+ miningState: normalizeMiningStateRecord(state.miningState),
175
+ hookClientState: state.hookClientState,
176
+ proactiveFamilies: state.proactiveFamilies.filter((family) => family.status === "confirmed" || family.status === "canceled"),
177
+ };
178
+ }
179
+ function createWalletStateFromPortableArchive(options) {
180
+ const material = deriveWalletMaterialFromMnemonic(options.payload.mnemonic.phrase);
181
+ if (material.keys.masterFingerprintHex !== options.payload.expected.masterFingerprintHex
182
+ || material.keys.accountPath !== options.payload.expected.accountPath
183
+ || material.keys.accountXpub !== options.payload.expected.accountXpub
184
+ || stripDescriptorChecksum(material.descriptor.publicExternal) !== stripDescriptorChecksum(options.payload.expected.publicExternalDescriptor)
185
+ || material.funding.address !== options.payload.expected.fundingAddress0
186
+ || material.funding.scriptPubKeyHex !== options.payload.expected.fundingScriptPubKeyHex0) {
187
+ throw new Error("wallet_import_material_mismatch");
188
+ }
189
+ const baseState = createInitialWalletState({
190
+ walletRootId: options.payload.walletRootId,
191
+ nowUnixMs: options.nowUnixMs,
192
+ material,
193
+ internalCoreWalletPassphrase: options.internalCoreWalletPassphrase,
194
+ });
195
+ return {
196
+ ...baseState,
197
+ walletRootId: options.payload.walletRootId,
198
+ network: options.payload.network,
199
+ anchorValueSats: options.payload.anchorValueSats,
200
+ nextDedicatedIndex: options.payload.nextDedicatedIndex,
201
+ fundingIndex: options.payload.fundingIndex,
202
+ walletBirthTime: options.payload.expected.walletBirthTime,
203
+ descriptor: {
204
+ ...baseState.descriptor,
205
+ checksum: options.payload.expected.descriptorChecksum,
206
+ rangeEnd: options.payload.expected.rangeEnd,
207
+ safetyMargin: options.payload.expected.safetyMargin,
208
+ },
209
+ identities: options.payload.identities,
210
+ domains: options.payload.domains,
211
+ miningState: normalizeMiningStateRecord(options.payload.miningState),
212
+ hookClientState: options.payload.hookClientState,
213
+ proactiveFamilies: options.payload.proactiveFamilies,
214
+ pendingMutations: [],
215
+ };
216
+ }
217
+ function isExportBlockedByLocalState(state) {
218
+ if (state.miningState.state === "repair-required"
219
+ || state.miningState.currentPublishState === "broadcasting"
220
+ || state.miningState.currentPublishState === "broadcast-unknown"
221
+ || state.miningState.currentPublishState === "in-mempool") {
222
+ return "wallet_export_requires_quiescent_local_state";
223
+ }
224
+ if (state.proactiveFamilies.some((family) => family.status === "draft"
225
+ || family.status === "broadcasting"
226
+ || family.status === "broadcast-unknown"
227
+ || family.status === "live"
228
+ || family.status === "repair-required")) {
229
+ return "wallet_export_requires_quiescent_local_state";
230
+ }
231
+ if (state.domains.some((domain) => domain.localAnchorIntent === "repair-required")) {
232
+ return "wallet_export_requires_quiescent_local_state";
233
+ }
234
+ if ((state.pendingMutations ?? []).some((mutation) => mutation.status === "draft"
235
+ || mutation.status === "broadcasting"
236
+ || mutation.status === "broadcast-unknown"
237
+ || mutation.status === "live"
238
+ || mutation.status === "repair-required")) {
239
+ return "wallet_export_requires_quiescent_local_state";
240
+ }
241
+ return null;
242
+ }
243
+ async function promptRequiredValue(prompter, message) {
244
+ const value = (await prompter.prompt(message)).trim();
245
+ if (value === "") {
246
+ throw new Error("wallet_prompt_value_required");
247
+ }
248
+ return value;
249
+ }
250
+ async function promptForArchivePassphrase(prompter, promptPrefix) {
251
+ const first = await promptRequiredValue(prompter, `${promptPrefix} passphrase: `);
252
+ const second = await promptRequiredValue(prompter, `Confirm ${promptPrefix.toLowerCase()} passphrase: `);
253
+ if (first !== second) {
254
+ throw new Error("wallet_archive_passphrase_mismatch");
255
+ }
256
+ return first;
257
+ }
258
+ async function confirmTypedAcknowledgement(prompter, expected, message) {
259
+ const answer = (await prompter.prompt(message)).trim();
260
+ if (answer !== expected) {
261
+ throw new Error("wallet_typed_confirmation_rejected");
262
+ }
263
+ }
264
+ async function confirmOverwriteIfNeeded(prompter, path) {
265
+ if (!await pathExists(path)) {
266
+ return;
267
+ }
268
+ const answer = (await prompter.prompt(`Archive ${path} already exists. Overwrite it? Type yes to continue: `)).trim().toLowerCase();
269
+ if (answer !== "yes") {
270
+ throw new Error("wallet_export_overwrite_declined");
271
+ }
272
+ }
273
+ async function readManagedSnapshotTip(options) {
274
+ const daemon = await attachOrStartIndexerDaemon({
275
+ dataDir: options.dataDir,
276
+ databasePath: options.databasePath,
277
+ walletRootId: options.walletRootId,
278
+ });
279
+ try {
280
+ const lease = await readSnapshotWithRetry(daemon, options.walletRootId);
281
+ return {
282
+ nodeBestHeight: lease.status.coreBestHeight,
283
+ snapshotHeight: lease.payload.tip?.height ?? null,
284
+ };
285
+ }
286
+ finally {
287
+ await daemon.close().catch(() => undefined);
288
+ }
289
+ }
290
+ async function recreateManagedCoreWalletReplica(state, provider, paths, dataDir, nowUnixMs, options = {}) {
291
+ const walletName = sanitizeWalletName(state.walletRootId);
292
+ const walletDir = join(dataDir, "wallets", walletName);
293
+ const quarantineDir = `${walletDir}.quarantine-${nowUnixMs}`;
294
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
295
+ dataDir,
296
+ chain: "main",
297
+ startHeight: 0,
298
+ walletRootId: state.walletRootId,
299
+ managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
300
+ });
301
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
302
+ if (rpc.unloadWallet != null) {
303
+ await rpc.unloadWallet(walletName, false).catch(() => undefined);
304
+ }
305
+ if (await pathExists(walletDir)) {
306
+ await rename(walletDir, quarantineDir).catch(() => undefined);
307
+ }
308
+ return importDescriptorIntoManagedCoreWallet({
309
+ ...state,
310
+ managedCoreWallet: {
311
+ ...state.managedCoreWallet,
312
+ proofStatus: "not-proven",
313
+ },
314
+ }, provider, paths, dataDir, nowUnixMs, options.attachService, options.rpcFactory);
315
+ }
316
+ async function ensureIndexerDatabaseHealthy(options) {
317
+ try {
318
+ if (await pathExists(options.databasePath)) {
319
+ const header = await readFile(options.databasePath).then((buffer) => buffer.subarray(0, 16).toString("utf8"));
320
+ if (header.length > 0 && header !== "SQLite format 3\u0000") {
321
+ throw new Error("indexer_database_not_sqlite");
322
+ }
323
+ }
324
+ const store = await openSqliteStore({ filename: options.databasePath });
325
+ try {
326
+ const client = await openClient({ store });
327
+ try {
328
+ await client.getTip();
329
+ }
330
+ finally {
331
+ await client.close();
332
+ }
333
+ }
334
+ finally {
335
+ await store.close();
336
+ }
337
+ return false;
338
+ }
339
+ catch {
340
+ if (!options.resetIfNeeded) {
341
+ throw new Error("wallet_repair_indexer_reset_requires_yes");
342
+ }
343
+ await rm(options.databasePath, { force: true }).catch(() => undefined);
344
+ await rm(`${options.databasePath}-wal`, { force: true }).catch(() => undefined);
345
+ await rm(`${options.databasePath}-shm`, { force: true }).catch(() => undefined);
346
+ await mkdir(dirname(options.databasePath), { recursive: true });
347
+ return true;
348
+ }
349
+ }
350
+ function mapIndexerCompatibilityToRepairIssue(compatibility) {
351
+ switch (compatibility) {
352
+ case "service-version-mismatch":
353
+ return "service-version-mismatch";
354
+ case "wallet-root-mismatch":
355
+ return "wallet-root-mismatch";
356
+ case "schema-mismatch":
357
+ return "schema-mismatch";
358
+ default:
359
+ return "none";
360
+ }
361
+ }
362
+ function mapBitcoindCompatibilityToRepairIssue(compatibility) {
363
+ switch (compatibility) {
364
+ case "service-version-mismatch":
365
+ return "service-version-mismatch";
366
+ case "wallet-root-mismatch":
367
+ return "wallet-root-mismatch";
368
+ case "runtime-mismatch":
369
+ return "runtime-mismatch";
370
+ default:
371
+ return "none";
372
+ }
373
+ }
374
+ function mapBitcoindRepairHealth(options) {
375
+ if (options.serviceState === null) {
376
+ return "unavailable";
377
+ }
378
+ if (options.serviceState === "starting" || options.serviceState === "stopping") {
379
+ return "starting";
380
+ }
381
+ if (options.serviceState !== "ready") {
382
+ return "failed";
383
+ }
384
+ if (options.replica?.proofStatus === "missing" || options.replica?.proofStatus === "mismatch") {
385
+ return "failed";
386
+ }
387
+ if (options.catchingUp) {
388
+ return "catching-up";
389
+ }
390
+ return "ready";
391
+ }
392
+ function mapLeaseStateToRepairHealth(state) {
393
+ switch (state) {
394
+ case "synced":
395
+ return "synced";
396
+ case "catching-up":
397
+ case "reorging":
398
+ return "catching-up";
399
+ case "starting":
400
+ case "stopping":
401
+ return "starting";
402
+ default:
403
+ return "failed";
404
+ }
405
+ }
406
+ const INDEXER_DAEMON_HEARTBEAT_STALE_MS = 15_000;
407
+ async function verifyIndexerPostRepairHealth(options) {
408
+ try {
409
+ const lease = await readSnapshotWithRetry(options.daemon, options.walletRootId);
410
+ return {
411
+ health: mapLeaseStateToRepairHealth(lease.status.state),
412
+ daemonInstanceId: lease.status.daemonInstanceId,
413
+ };
414
+ }
415
+ catch (leaseError) {
416
+ const probe = await options.probeIndexerDaemon({
417
+ dataDir: options.dataDir,
418
+ walletRootId: options.walletRootId,
419
+ });
420
+ try {
421
+ if (probe.compatibility === "compatible"
422
+ && probe.status !== null
423
+ && (options.nowUnixMs - probe.status.heartbeatAtUnixMs) <= INDEXER_DAEMON_HEARTBEAT_STALE_MS
424
+ && (probe.status.state === "starting" || probe.status.state === "catching-up" || probe.status.state === "reorging")) {
425
+ return {
426
+ health: mapLeaseStateToRepairHealth(probe.status.state),
427
+ daemonInstanceId: probe.status.daemonInstanceId,
428
+ };
429
+ }
430
+ }
431
+ finally {
432
+ await probe.client?.close().catch(() => undefined);
433
+ }
434
+ throw leaseError;
435
+ }
436
+ }
437
+ async function isProcessAlive(pid) {
438
+ if (pid === null) {
439
+ return false;
440
+ }
441
+ try {
442
+ process.kill(pid, 0);
443
+ return true;
444
+ }
445
+ catch (error) {
446
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
447
+ return false;
448
+ }
449
+ return true;
450
+ }
451
+ }
452
+ async function waitForProcessExit(pid, timeoutMs = 15_000, errorCode = "indexer_daemon_stop_timeout") {
453
+ const deadline = Date.now() + timeoutMs;
454
+ while (Date.now() < deadline) {
455
+ if (!await isProcessAlive(pid)) {
456
+ return;
457
+ }
458
+ await new Promise((resolve) => setTimeout(resolve, 100));
459
+ }
460
+ throw new Error(errorCode);
461
+ }
462
+ async function clearIndexerDaemonArtifacts(servicePaths) {
463
+ await rm(servicePaths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
464
+ await rm(servicePaths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
465
+ }
466
+ async function clearManagedBitcoindArtifacts(servicePaths) {
467
+ await rm(servicePaths.bitcoindStatusPath, { force: true }).catch(() => undefined);
468
+ await rm(servicePaths.bitcoindPidPath, { force: true }).catch(() => undefined);
469
+ await rm(servicePaths.bitcoindReadyPath, { force: true }).catch(() => undefined);
470
+ await rm(servicePaths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
471
+ }
472
+ function createSilentNonInteractivePrompter() {
473
+ return {
474
+ isInteractive: false,
475
+ writeLine() { },
476
+ async prompt() {
477
+ return "";
478
+ },
479
+ };
480
+ }
481
+ function applyRepairStoppedMiningState(state) {
482
+ const miningState = normalizeMiningStateRecord(state.miningState);
483
+ return {
484
+ ...state,
485
+ miningState: {
486
+ ...miningState,
487
+ runMode: "stopped",
488
+ state: miningState.liveMiningFamilyInMempool
489
+ ? miningState.state === "paused-stale"
490
+ ? "paused-stale"
491
+ : "paused"
492
+ : miningState.state === "repair-required"
493
+ ? "repair-required"
494
+ : "idle",
495
+ pauseReason: miningState.liveMiningFamilyInMempool
496
+ ? miningState.state === "paused-stale"
497
+ ? "stale-block-context"
498
+ : "wallet-repair"
499
+ : miningState.state === "repair-required"
500
+ ? miningState.pauseReason
501
+ : null,
502
+ },
503
+ };
504
+ }
505
+ function createStoppedBackgroundRuntimeSnapshot(snapshot, nowUnixMs) {
506
+ return {
507
+ ...snapshot,
508
+ updatedAtUnixMs: nowUnixMs,
509
+ runMode: "stopped",
510
+ backgroundWorkerPid: null,
511
+ backgroundWorkerRunId: null,
512
+ backgroundWorkerHeartbeatAtUnixMs: null,
513
+ backgroundWorkerHealth: null,
514
+ currentPhase: "idle",
515
+ note: snapshot.liveMiningFamilyInMempool
516
+ ? "Background mining stopped for wallet repair. The last mining transaction may still confirm from mempool."
517
+ : "Background mining stopped for wallet repair.",
518
+ };
519
+ }
520
+ async function persistRepairState(options) {
521
+ const nextState = {
522
+ ...options.state,
523
+ stateRevision: options.state.stateRevision + 1,
524
+ lastWrittenAtUnixMs: options.nowUnixMs,
525
+ };
526
+ if (options.replacePrimary) {
527
+ await rm(options.paths.walletStatePath, { force: true }).catch(() => undefined);
528
+ }
529
+ await saveWalletState({
530
+ primaryPath: options.paths.walletStatePath,
531
+ backupPath: options.paths.walletStateBackupPath,
532
+ }, nextState, {
533
+ provider: options.provider,
534
+ secretReference: createWalletSecretReference(nextState.walletRootId),
535
+ });
536
+ return nextState;
537
+ }
538
+ async function stopBackgroundMiningForRepair(options) {
539
+ const pid = options.snapshot.backgroundWorkerPid;
540
+ if (pid !== null) {
541
+ try {
542
+ process.kill(pid, "SIGKILL");
543
+ }
544
+ catch (error) {
545
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
546
+ throw error;
547
+ }
548
+ }
549
+ await waitForProcessExit(pid, 15_000, "background_mining_stop_timeout");
550
+ }
551
+ await saveMiningRuntimeStatus(options.paths.miningStatusPath, createStoppedBackgroundRuntimeSnapshot(options.snapshot, options.nowUnixMs));
552
+ }
553
+ async function canResumeBackgroundMiningAfterRepair(options) {
554
+ if (options.unlockUntilUnixMs === null
555
+ || options.unlockUntilUnixMs <= options.nowUnixMs
556
+ || options.bitcoindPostRepairHealth !== "ready"
557
+ || options.indexerPostRepairHealth !== "synced"
558
+ || normalizeMiningStateRecord(options.repairedState.miningState).state === "repair-required") {
559
+ return false;
560
+ }
561
+ const hookMode = options.repairedState.hookClientState.mining?.mode ?? "builtin";
562
+ if (hookMode === "custom") {
563
+ const inspection = await inspectMiningHookState({
564
+ hookRootPath: options.paths.hooksMiningDir,
565
+ entrypointPath: options.paths.hooksMiningEntrypointPath,
566
+ packagePath: options.paths.hooksMiningPackageJsonPath,
567
+ localState: options.repairedState.hookClientState.mining ?? null,
568
+ verify: false,
569
+ nowUnixMs: options.nowUnixMs,
570
+ });
571
+ return inspection.operatorValidationState === "current" && !inspection.cooldownActive;
572
+ }
573
+ try {
574
+ const config = await loadClientConfig({
575
+ path: options.paths.clientConfigPath,
576
+ provider: options.provider,
577
+ });
578
+ return config?.mining.builtIn != null;
579
+ }
580
+ catch {
581
+ return false;
582
+ }
583
+ }
584
+ export function parseUnlockDurationToMs(raw) {
585
+ if (raw == null || raw.trim() === "") {
586
+ return DEFAULT_UNLOCK_DURATION_MS;
587
+ }
588
+ const match = /^([1-9][0-9]*)([smhd])$/i.exec(raw.trim());
589
+ if (match == null) {
590
+ throw new Error("wallet_unlock_duration_invalid");
591
+ }
592
+ const value = Number.parseInt(match[1], 10);
593
+ const unit = match[2].toLowerCase();
594
+ const multiplier = unit === "s"
595
+ ? 1_000
596
+ : unit === "m"
597
+ ? 60_000
598
+ : unit === "h"
599
+ ? 3_600_000
600
+ : 86_400_000;
601
+ const duration = value * multiplier;
602
+ if (!Number.isFinite(duration) || duration <= 0) {
603
+ throw new Error("wallet_unlock_duration_invalid");
604
+ }
605
+ return duration;
606
+ }
607
+ async function ensureWalletNotInitialized(paths) {
608
+ if (await pathExists(paths.walletStatePath) || await pathExists(paths.walletStateBackupPath)) {
609
+ throw new Error("wallet_already_initialized");
610
+ }
611
+ }
612
+ async function confirmMnemonic(prompter, words) {
613
+ const challenge = createMnemonicConfirmationChallenge(words);
614
+ for (const entry of challenge) {
615
+ const answer = (await prompter.prompt(`Confirm word #${entry.index + 1}: `)).trim().toLowerCase();
616
+ if (answer !== entry.word) {
617
+ throw new Error(`wallet_init_confirmation_failed_word_${entry.index + 1}`);
618
+ }
619
+ }
620
+ }
621
+ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dataDir, nowUnixMs, attachService = attachOrStartManagedBitcoindService, rpcFactory = createRpcClient) {
622
+ const node = await attachService({
623
+ dataDir,
624
+ chain: "main",
625
+ startHeight: 0,
626
+ walletRootId: state.walletRootId,
627
+ managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
628
+ });
629
+ const rpc = rpcFactory(node.rpc);
630
+ await createManagedWalletReplica(rpc, state.walletRootId, {
631
+ managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
632
+ });
633
+ const privateDescriptor = await rpc.getDescriptorInfo(state.descriptor.privateExternal);
634
+ const publicDescriptor = await rpc.getDescriptorInfo(state.descriptor.publicExternal);
635
+ const walletName = sanitizeWalletName(state.walletRootId);
636
+ await rpc.walletPassphrase(walletName, state.managedCoreWallet.internalPassphrase, 10);
637
+ try {
638
+ const importResults = await rpc.importDescriptors(walletName, [{
639
+ desc: privateDescriptor.descriptor,
640
+ timestamp: state.walletBirthTime,
641
+ active: false,
642
+ internal: false,
643
+ range: [0, state.descriptor.rangeEnd],
644
+ }]);
645
+ if (!importResults.every((result) => result.success)) {
646
+ throw new Error(`wallet_descriptor_import_failed_${JSON.stringify(importResults)}`);
647
+ }
648
+ }
649
+ finally {
650
+ await rpc.walletLock(walletName).catch(() => undefined);
651
+ }
652
+ const derivedFunding = await rpc.deriveAddresses(publicDescriptor.descriptor, [0, 0]);
653
+ if (derivedFunding[0] !== state.funding.address) {
654
+ throw new Error("wallet_funding_address_verification_failed");
655
+ }
656
+ const descriptors = await rpc.listDescriptors(walletName);
657
+ const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc === privateDescriptor.descriptor);
658
+ if (importedDescriptor == null) {
659
+ throw new Error("wallet_descriptor_not_present_after_import");
660
+ }
661
+ const verifiedReplica = {
662
+ walletRootId: state.walletRootId,
663
+ walletName,
664
+ loaded: true,
665
+ descriptors: true,
666
+ privateKeysEnabled: true,
667
+ created: false,
668
+ proofStatus: "ready",
669
+ descriptorChecksum: privateDescriptor.checksum,
670
+ fundingAddress0: state.funding.address,
671
+ fundingScriptPubKeyHex0: state.funding.scriptPubKeyHex,
672
+ message: null,
673
+ };
674
+ const nextState = {
675
+ ...state,
676
+ stateRevision: state.stateRevision + 1,
677
+ lastWrittenAtUnixMs: nowUnixMs,
678
+ descriptor: {
679
+ ...state.descriptor,
680
+ privateExternal: privateDescriptor.descriptor,
681
+ publicExternal: publicDescriptor.descriptor,
682
+ checksum: privateDescriptor.checksum,
683
+ },
684
+ managedCoreWallet: {
685
+ ...state.managedCoreWallet,
686
+ walletName,
687
+ descriptorChecksum: privateDescriptor.checksum,
688
+ fundingAddress0: verifiedReplica.fundingAddress0 ?? null,
689
+ fundingScriptPubKeyHex0: verifiedReplica.fundingScriptPubKeyHex0 ?? null,
690
+ proofStatus: "ready",
691
+ lastImportedAtUnixMs: nowUnixMs,
692
+ lastVerifiedAtUnixMs: nowUnixMs,
693
+ },
694
+ };
695
+ await saveWalletState({
696
+ primaryPath: paths.walletStatePath,
697
+ backupPath: paths.walletStateBackupPath,
698
+ }, nextState, {
699
+ provider,
700
+ secretReference: createWalletSecretReference(state.walletRootId),
701
+ });
702
+ return nextState;
703
+ }
704
+ export async function verifyManagedCoreWalletReplica(state, dataDir, dependencies = {}) {
705
+ const walletName = state.managedCoreWallet.walletName;
706
+ try {
707
+ const node = dependencies.nodeHandle ?? await (dependencies.attachService ?? attachOrStartManagedBitcoindService)({
708
+ dataDir,
709
+ chain: "main",
710
+ startHeight: 0,
711
+ walletRootId: state.walletRootId,
712
+ });
713
+ const rpc = (dependencies.rpcFactory ?? createRpcClient)(node.rpc);
714
+ const info = await rpc.getWalletInfo(walletName);
715
+ const descriptors = await rpc.listDescriptors(walletName);
716
+ const matchingDescriptor = state.managedCoreWallet.descriptorChecksum === null
717
+ ? null
718
+ : descriptors.descriptors.find((entry) => entry.desc.endsWith(`#${state.managedCoreWallet.descriptorChecksum}`));
719
+ if (matchingDescriptor == null) {
720
+ return {
721
+ walletRootId: state.walletRootId,
722
+ walletName,
723
+ loaded: true,
724
+ descriptors: info.descriptors,
725
+ privateKeysEnabled: info.private_keys_enabled,
726
+ created: false,
727
+ proofStatus: "missing",
728
+ descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
729
+ fundingAddress0: state.managedCoreWallet.fundingAddress0,
730
+ fundingScriptPubKeyHex0: state.managedCoreWallet.fundingScriptPubKeyHex0,
731
+ message: "Expected descriptor is missing from the managed Core wallet.",
732
+ };
733
+ }
734
+ const derived = await rpc.deriveAddresses(state.descriptor.publicExternal, [0, 0]);
735
+ if (derived[0] !== state.funding.address) {
736
+ return {
737
+ walletRootId: state.walletRootId,
738
+ walletName,
739
+ loaded: true,
740
+ descriptors: info.descriptors,
741
+ privateKeysEnabled: info.private_keys_enabled,
742
+ created: false,
743
+ proofStatus: "mismatch",
744
+ descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
745
+ fundingAddress0: derived[0] ?? null,
746
+ fundingScriptPubKeyHex0: null,
747
+ message: "The managed Core wallet funding address does not match the trusted wallet state.",
748
+ };
749
+ }
750
+ return {
751
+ walletRootId: state.walletRootId,
752
+ walletName,
753
+ loaded: true,
754
+ descriptors: info.descriptors,
755
+ privateKeysEnabled: info.private_keys_enabled,
756
+ created: false,
757
+ proofStatus: "ready",
758
+ descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
759
+ fundingAddress0: state.funding.address,
760
+ fundingScriptPubKeyHex0: state.funding.scriptPubKeyHex,
761
+ message: null,
762
+ };
763
+ }
764
+ catch (error) {
765
+ return {
766
+ walletRootId: state.walletRootId,
767
+ walletName,
768
+ loaded: false,
769
+ descriptors: false,
770
+ privateKeysEnabled: false,
771
+ created: false,
772
+ proofStatus: "not-proven",
773
+ descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
774
+ fundingAddress0: state.managedCoreWallet.fundingAddress0,
775
+ fundingScriptPubKeyHex0: state.managedCoreWallet.fundingScriptPubKeyHex0,
776
+ message: error instanceof Error ? error.message : String(error),
777
+ };
778
+ }
779
+ }
780
+ export async function loadUnlockedWalletState(options = {}) {
781
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
782
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
783
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
784
+ try {
785
+ const session = await loadUnlockSession(paths.walletUnlockSessionPath, {
786
+ provider,
787
+ });
788
+ if (session.unlockUntilUnixMs <= nowUnixMs) {
789
+ await clearUnlockSession(paths.walletUnlockSessionPath);
790
+ return null;
791
+ }
792
+ const loaded = await loadWalletState({
793
+ primaryPath: paths.walletStatePath,
794
+ backupPath: paths.walletStateBackupPath,
795
+ }, {
796
+ provider,
797
+ });
798
+ if (loaded.state.walletRootId !== session.walletRootId
799
+ || loaded.state.stateRevision !== session.sourceStateRevision) {
800
+ await clearUnlockSession(paths.walletUnlockSessionPath);
801
+ return null;
802
+ }
803
+ return {
804
+ session,
805
+ state: {
806
+ ...loaded.state,
807
+ miningState: normalizeMiningStateRecord(loaded.state.miningState),
808
+ },
809
+ source: loaded.source,
810
+ };
811
+ }
812
+ catch {
813
+ return null;
814
+ }
815
+ }
816
+ export async function initializeWallet(options) {
817
+ if (!options.prompter.isInteractive) {
818
+ throw new Error("wallet_init_requires_tty");
819
+ }
820
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
821
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
822
+ const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
823
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
824
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
825
+ purpose: "wallet-init",
826
+ walletRootId: null,
827
+ });
828
+ try {
829
+ await ensureWalletNotInitialized(paths);
830
+ const material = generateWalletMaterial();
831
+ let mnemonicRevealed = false;
832
+ options.prompter.writeLine("Cogcoin Wallet Initialization");
833
+ options.prompter.writeLine("Write down this 24-word recovery phrase. It will only be shown once:");
834
+ options.prompter.writeLine(material.mnemonic.phrase);
835
+ mnemonicRevealed = true;
836
+ try {
837
+ await confirmMnemonic(options.prompter, material.mnemonic.words);
838
+ }
839
+ finally {
840
+ if (mnemonicRevealed) {
841
+ await Promise.resolve()
842
+ .then(() => options.prompter.clearSensitiveDisplay?.("mnemonic-reveal"))
843
+ .catch(() => undefined);
844
+ }
845
+ }
846
+ const walletRootId = createWalletRootId();
847
+ const internalCoreWalletPassphrase = createInternalCoreWalletPassphrase();
848
+ const secretReference = createWalletSecretReference(walletRootId);
849
+ const secret = randomBytes(32);
850
+ await provider.storeSecret(secretReference.keyId, secret);
851
+ const initialState = createInitialWalletState({
852
+ walletRootId,
853
+ nowUnixMs,
854
+ material,
855
+ internalCoreWalletPassphrase,
856
+ });
857
+ await saveWalletState({
858
+ primaryPath: paths.walletStatePath,
859
+ backupPath: paths.walletStateBackupPath,
860
+ }, initialState, {
861
+ provider,
862
+ secretReference,
863
+ });
864
+ const verifiedState = await importDescriptorIntoManagedCoreWallet(initialState, provider, paths, options.dataDir, nowUnixMs, options.attachService, options.rpcFactory);
865
+ const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
866
+ await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(verifiedState, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
867
+ provider,
868
+ secretReference,
869
+ });
870
+ return {
871
+ walletRootId,
872
+ fundingAddress: verifiedState.funding.address,
873
+ unlockUntilUnixMs,
874
+ state: verifiedState,
875
+ };
876
+ }
877
+ finally {
878
+ await controlLock.release();
879
+ }
880
+ }
881
+ export async function unlockWallet(options = {}) {
882
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
883
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
884
+ const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
885
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
886
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
887
+ purpose: "wallet-unlock",
888
+ walletRootId: null,
889
+ });
890
+ try {
891
+ const loaded = await loadWalletState({
892
+ primaryPath: paths.walletStatePath,
893
+ backupPath: paths.walletStateBackupPath,
894
+ }, {
895
+ provider,
896
+ });
897
+ const secretReference = createWalletSecretReference(loaded.state.walletRootId);
898
+ const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
899
+ await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(loaded.state, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
900
+ provider,
901
+ secretReference,
902
+ });
903
+ return {
904
+ unlockUntilUnixMs,
905
+ state: loaded.state,
906
+ source: loaded.source,
907
+ };
908
+ }
909
+ finally {
910
+ await controlLock.release();
911
+ }
912
+ }
913
+ export async function lockWallet(options) {
914
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
915
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
916
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
917
+ purpose: "wallet-lock",
918
+ walletRootId: null,
919
+ });
920
+ try {
921
+ let walletRootId = null;
922
+ let coreLocked = false;
923
+ try {
924
+ const loaded = await loadWalletState({
925
+ primaryPath: paths.walletStatePath,
926
+ backupPath: paths.walletStateBackupPath,
927
+ }, {
928
+ provider,
929
+ });
930
+ walletRootId = loaded.state.walletRootId;
931
+ try {
932
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
933
+ dataDir: options.dataDir,
934
+ chain: "main",
935
+ startHeight: 0,
936
+ walletRootId,
937
+ });
938
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
939
+ await rpc.walletLock(loaded.state.managedCoreWallet.walletName).catch(() => undefined);
940
+ coreLocked = true;
941
+ }
942
+ catch {
943
+ coreLocked = false;
944
+ }
945
+ }
946
+ catch {
947
+ walletRootId = null;
948
+ }
949
+ await clearUnlockSession(paths.walletUnlockSessionPath);
950
+ return {
951
+ walletRootId,
952
+ coreLocked,
953
+ };
954
+ }
955
+ finally {
956
+ await controlLock.release();
957
+ }
958
+ }
959
+ export async function exportWallet(options) {
960
+ if (!options.prompter.isInteractive) {
961
+ throw new Error("wallet_export_requires_tty");
962
+ }
963
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
964
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
965
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
966
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
967
+ purpose: "wallet-export",
968
+ walletRootId: null,
969
+ });
970
+ try {
971
+ const unlocked = await loadUnlockedWalletState({
972
+ provider,
973
+ nowUnixMs,
974
+ paths,
975
+ });
976
+ if (unlocked === null) {
977
+ throw new Error("wallet_locked");
978
+ }
979
+ const blockedReason = isExportBlockedByLocalState(unlocked.state);
980
+ if (blockedReason !== null) {
981
+ throw new Error(blockedReason);
982
+ }
983
+ const replica = await verifyManagedCoreWalletReplica(unlocked.state, options.dataDir, {
984
+ attachService: options.attachService,
985
+ rpcFactory: options.rpcFactory,
986
+ });
987
+ if (replica.proofStatus !== "ready") {
988
+ throw new Error("wallet_export_core_replica_not_ready");
989
+ }
990
+ const tips = await (options.readSnapshotTip ?? readManagedSnapshotTip)({
991
+ dataDir: options.dataDir,
992
+ databasePath: options.databasePath,
993
+ walletRootId: unlocked.state.walletRootId,
994
+ });
995
+ if (tips.snapshotHeight === null || tips.nodeBestHeight === null || tips.snapshotHeight !== tips.nodeBestHeight) {
996
+ throw new Error("wallet_export_tip_mismatch");
997
+ }
998
+ await confirmOverwriteIfNeeded(options.prompter, options.archivePath);
999
+ const passphrase = await promptForArchivePassphrase(options.prompter, "Archive");
1000
+ await writePortableWalletArchive(options.archivePath, createPortableWalletArchivePayload(unlocked.state, nowUnixMs), passphrase);
1001
+ return {
1002
+ archivePath: options.archivePath,
1003
+ walletRootId: unlocked.state.walletRootId,
1004
+ };
1005
+ }
1006
+ finally {
1007
+ await controlLock.release();
1008
+ }
1009
+ }
1010
+ export async function importWallet(options) {
1011
+ if (!options.prompter.isInteractive) {
1012
+ throw new Error("wallet_import_requires_tty");
1013
+ }
1014
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
1015
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
1016
+ const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
1017
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1018
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1019
+ purpose: "wallet-import",
1020
+ walletRootId: null,
1021
+ });
1022
+ try {
1023
+ const archivePassphrase = await promptRequiredValue(options.prompter, "Archive passphrase: ");
1024
+ const payload = await readPortableWalletArchive(options.archivePath, archivePassphrase);
1025
+ const replacementStateExists = await pathExists(paths.walletStatePath) || await pathExists(paths.walletStateBackupPath);
1026
+ const importedWalletDir = join(options.dataDir, "wallets", sanitizeWalletName(payload.walletRootId));
1027
+ const replacementCoreWalletExists = await pathExists(importedWalletDir);
1028
+ if (replacementStateExists || replacementCoreWalletExists) {
1029
+ await confirmTypedAcknowledgement(options.prompter, "IMPORT", "Type IMPORT to replace the existing local wallet state and managed Core wallet replica: ");
1030
+ }
1031
+ let previousWalletRootId = null;
1032
+ try {
1033
+ const loaded = await loadWalletState({
1034
+ primaryPath: paths.walletStatePath,
1035
+ backupPath: paths.walletStateBackupPath,
1036
+ }, {
1037
+ provider,
1038
+ });
1039
+ previousWalletRootId = loaded.state.walletRootId;
1040
+ }
1041
+ catch {
1042
+ previousWalletRootId = null;
1043
+ }
1044
+ const secretReference = createWalletSecretReference(payload.walletRootId);
1045
+ const replacementSecret = randomBytes(32);
1046
+ await provider.storeSecret(secretReference.keyId, replacementSecret);
1047
+ const initialState = createWalletStateFromPortableArchive({
1048
+ payload,
1049
+ nowUnixMs,
1050
+ internalCoreWalletPassphrase: createInternalCoreWalletPassphrase(),
1051
+ });
1052
+ await clearUnlockSession(paths.walletUnlockSessionPath);
1053
+ await saveWalletState({
1054
+ primaryPath: paths.walletStatePath,
1055
+ backupPath: paths.walletStateBackupPath,
1056
+ }, initialState, {
1057
+ provider,
1058
+ secretReference,
1059
+ });
1060
+ const importedState = await recreateManagedCoreWalletReplica(initialState, provider, paths, options.dataDir, nowUnixMs, {
1061
+ attachService: options.attachService,
1062
+ rpcFactory: options.rpcFactory,
1063
+ });
1064
+ const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
1065
+ await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(importedState, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
1066
+ provider,
1067
+ secretReference,
1068
+ });
1069
+ if (previousWalletRootId !== null && previousWalletRootId !== payload.walletRootId) {
1070
+ await provider.deleteSecret(createWalletSecretReference(previousWalletRootId).keyId).catch(() => undefined);
1071
+ }
1072
+ await (options.attachIndexerDaemon ?? attachOrStartIndexerDaemon)({
1073
+ dataDir: options.dataDir,
1074
+ databasePath: options.databasePath,
1075
+ walletRootId: importedState.walletRootId,
1076
+ }).then((daemon) => daemon.close());
1077
+ return {
1078
+ archivePath: options.archivePath,
1079
+ walletRootId: importedState.walletRootId,
1080
+ fundingAddress: importedState.funding.address,
1081
+ unlockUntilUnixMs,
1082
+ state: importedState,
1083
+ };
1084
+ }
1085
+ finally {
1086
+ await controlLock.release();
1087
+ }
1088
+ }
1089
+ export async function repairWallet(options) {
1090
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
1091
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
1092
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1093
+ const probeManagedBitcoind = options.probeBitcoindService ?? probeManagedBitcoindService;
1094
+ const attachManagedBitcoind = options.attachService ?? attachOrStartManagedBitcoindService;
1095
+ const probeManagedIndexerDaemon = options.probeIndexerDaemon ?? probeIndexerDaemon;
1096
+ const attachManagedIndexerDaemon = options.attachIndexerDaemon ?? attachOrStartIndexerDaemon;
1097
+ const requestMiningPreemptionForRepair = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
1098
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1099
+ purpose: "wallet-repair",
1100
+ walletRootId: null,
1101
+ });
1102
+ try {
1103
+ let miningPreemption = null;
1104
+ let loaded;
1105
+ try {
1106
+ loaded = await loadWalletState({
1107
+ primaryPath: paths.walletStatePath,
1108
+ backupPath: paths.walletStateBackupPath,
1109
+ }, {
1110
+ provider,
1111
+ });
1112
+ }
1113
+ catch {
1114
+ throw new Error("local-state-corrupt");
1115
+ }
1116
+ const recoveredFromBackup = loaded.source === "backup";
1117
+ const secretReference = createWalletSecretReference(loaded.state.walletRootId);
1118
+ let repairedState = loaded.state;
1119
+ let repairStateNeedsPersist = false;
1120
+ const servicePaths = resolveManagedServicePaths(options.dataDir, repairedState.walletRootId);
1121
+ const preRepairMiningRuntime = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
1122
+ const backgroundWorkerAlive = preRepairMiningRuntime?.runMode === "background"
1123
+ && preRepairMiningRuntime.backgroundWorkerPid !== null
1124
+ && await isProcessAlive(preRepairMiningRuntime.backgroundWorkerPid);
1125
+ const preRepairUnlockedState = await loadUnlockedWalletState({
1126
+ provider,
1127
+ nowUnixMs,
1128
+ paths,
1129
+ });
1130
+ const miningPreRepairRunMode = backgroundWorkerAlive
1131
+ ? "background"
1132
+ : preRepairMiningRuntime?.runMode === "foreground"
1133
+ ? "foreground"
1134
+ : "stopped";
1135
+ const miningWasResumable = miningPreRepairRunMode === "background"
1136
+ && preRepairUnlockedState !== null
1137
+ && normalizeMiningStateRecord(repairedState.miningState).state !== "repair-required";
1138
+ const savedUnlockUntilUnixMs = miningWasResumable
1139
+ ? preRepairUnlockedState?.session.unlockUntilUnixMs ?? null
1140
+ : null;
1141
+ let initialBitcoindProbe = {
1142
+ compatibility: "unreachable",
1143
+ status: null,
1144
+ error: null,
1145
+ };
1146
+ let bitcoindServiceAction = "none";
1147
+ let bitcoindCompatibilityIssue = "none";
1148
+ let managedCoreReplicaAction = "none";
1149
+ let indexerDaemonAction = "none";
1150
+ let indexerCompatibilityIssue = "none";
1151
+ let miningResumeAction = miningPreRepairRunMode === "background"
1152
+ ? "skipped-not-resumable"
1153
+ : "none";
1154
+ let miningPostRepairRunMode = "stopped";
1155
+ let miningResumeError = null;
1156
+ try {
1157
+ miningPreemption = await requestMiningPreemptionForRepair({
1158
+ paths,
1159
+ reason: "wallet-repair",
1160
+ });
1161
+ if (backgroundWorkerAlive && preRepairMiningRuntime !== null) {
1162
+ const miningLock = await acquireFileLock(paths.miningControlLockPath, {
1163
+ purpose: "wallet-repair-stop-background",
1164
+ });
1165
+ try {
1166
+ await stopBackgroundMiningForRepair({
1167
+ paths,
1168
+ snapshot: preRepairMiningRuntime,
1169
+ nowUnixMs,
1170
+ });
1171
+ }
1172
+ finally {
1173
+ await miningLock.release();
1174
+ }
1175
+ repairedState = applyRepairStoppedMiningState(repairedState);
1176
+ repairStateNeedsPersist = true;
1177
+ }
1178
+ if (!(options.assumeYes ?? false)) {
1179
+ await ensureIndexerDatabaseHealthy({
1180
+ databasePath: options.databasePath,
1181
+ dataDir: options.dataDir,
1182
+ walletRootId: repairedState.walletRootId,
1183
+ resetIfNeeded: false,
1184
+ });
1185
+ }
1186
+ const bitcoindLock = await acquireFileLock(servicePaths.bitcoindLockPath, {
1187
+ purpose: "managed-bitcoind-repair",
1188
+ walletRootId: repairedState.walletRootId,
1189
+ dataDir: options.dataDir,
1190
+ });
1191
+ let resetIndexerDatabase = false;
1192
+ let bitcoindHandle = null;
1193
+ let bitcoindPostRepairHealth = "unavailable";
1194
+ try {
1195
+ initialBitcoindProbe = await probeManagedBitcoind({
1196
+ dataDir: options.dataDir,
1197
+ chain: "main",
1198
+ startHeight: 0,
1199
+ walletRootId: repairedState.walletRootId,
1200
+ });
1201
+ bitcoindCompatibilityIssue = mapBitcoindCompatibilityToRepairIssue(initialBitcoindProbe.compatibility);
1202
+ if (initialBitcoindProbe.compatibility === "service-version-mismatch"
1203
+ || initialBitcoindProbe.compatibility === "wallet-root-mismatch"
1204
+ || initialBitcoindProbe.compatibility === "runtime-mismatch") {
1205
+ const processId = initialBitcoindProbe.status?.processId ?? null;
1206
+ if (processId === null) {
1207
+ throw new Error("managed_bitcoind_process_id_unavailable");
1208
+ }
1209
+ try {
1210
+ process.kill(processId, "SIGTERM");
1211
+ }
1212
+ catch (error) {
1213
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
1214
+ throw error;
1215
+ }
1216
+ }
1217
+ await waitForProcessExit(processId, 15_000, "managed_bitcoind_stop_timeout");
1218
+ await clearManagedBitcoindArtifacts(servicePaths);
1219
+ bitcoindServiceAction = "stopped-incompatible-service";
1220
+ }
1221
+ else if (initialBitcoindProbe.compatibility === "unreachable") {
1222
+ const hasStaleArtifacts = await pathExists(servicePaths.bitcoindStatusPath)
1223
+ || await pathExists(servicePaths.bitcoindPidPath)
1224
+ || await pathExists(servicePaths.bitcoindReadyPath)
1225
+ || await pathExists(servicePaths.bitcoindWalletStatusPath);
1226
+ if (hasStaleArtifacts) {
1227
+ await clearManagedBitcoindArtifacts(servicePaths);
1228
+ bitcoindServiceAction = "cleared-stale-artifacts";
1229
+ }
1230
+ }
1231
+ else if (initialBitcoindProbe.compatibility === "protocol-error") {
1232
+ throw new Error(initialBitcoindProbe.error ?? "managed_bitcoind_protocol_error");
1233
+ }
1234
+ }
1235
+ finally {
1236
+ await bitcoindLock.release();
1237
+ }
1238
+ bitcoindHandle = await attachManagedBitcoind({
1239
+ dataDir: options.dataDir,
1240
+ chain: "main",
1241
+ startHeight: 0,
1242
+ walletRootId: repairedState.walletRootId,
1243
+ });
1244
+ const bitcoindRpc = (options.rpcFactory ?? createRpcClient)(bitcoindHandle.rpc);
1245
+ let replica = await verifyManagedCoreWalletReplica(repairedState, options.dataDir, {
1246
+ nodeHandle: bitcoindHandle,
1247
+ attachService: options.attachService,
1248
+ rpcFactory: options.rpcFactory,
1249
+ });
1250
+ let recreatedManagedCoreWallet = false;
1251
+ if (replica.proofStatus !== "ready") {
1252
+ repairedState = await recreateManagedCoreWalletReplica(repairedState, provider, paths, options.dataDir, nowUnixMs, {
1253
+ attachService: options.attachService,
1254
+ rpcFactory: options.rpcFactory,
1255
+ });
1256
+ recreatedManagedCoreWallet = true;
1257
+ managedCoreReplicaAction = "recreated";
1258
+ repairStateNeedsPersist = false;
1259
+ replica = await verifyManagedCoreWalletReplica(repairedState, options.dataDir, {
1260
+ nodeHandle: bitcoindHandle,
1261
+ attachService: options.attachService,
1262
+ rpcFactory: options.rpcFactory,
1263
+ });
1264
+ }
1265
+ const finalBitcoindStatus = await bitcoindHandle.refreshServiceStatus?.() ?? null;
1266
+ const chainInfo = await bitcoindRpc.getBlockchainInfo();
1267
+ bitcoindPostRepairHealth = mapBitcoindRepairHealth({
1268
+ serviceState: finalBitcoindStatus?.state ?? null,
1269
+ catchingUp: chainInfo.blocks < chainInfo.headers,
1270
+ replica,
1271
+ });
1272
+ if (bitcoindServiceAction === "none" && initialBitcoindProbe.compatibility === "unreachable") {
1273
+ bitcoindServiceAction = "restarted-compatible-service";
1274
+ }
1275
+ let initialIndexerDaemonInstanceId = null;
1276
+ let preAttachIndexerDaemonInstanceId = null;
1277
+ const indexerLock = await acquireFileLock(servicePaths.indexerDaemonLockPath, {
1278
+ purpose: "indexer-daemon-repair",
1279
+ walletRootId: repairedState.walletRootId,
1280
+ dataDir: options.dataDir,
1281
+ databasePath: options.databasePath,
1282
+ });
1283
+ try {
1284
+ const initialProbe = await probeManagedIndexerDaemon({
1285
+ dataDir: options.dataDir,
1286
+ walletRootId: repairedState.walletRootId,
1287
+ });
1288
+ indexerCompatibilityIssue = mapIndexerCompatibilityToRepairIssue(initialProbe.compatibility);
1289
+ initialIndexerDaemonInstanceId = initialProbe.status?.daemonInstanceId ?? null;
1290
+ if (initialProbe.compatibility === "compatible") {
1291
+ await initialProbe.client?.close().catch(() => undefined);
1292
+ }
1293
+ else if (initialProbe.compatibility === "service-version-mismatch"
1294
+ || initialProbe.compatibility === "wallet-root-mismatch"
1295
+ || initialProbe.compatibility === "schema-mismatch") {
1296
+ const processId = initialProbe.status?.processId ?? null;
1297
+ if (processId === null) {
1298
+ throw new Error("indexer_daemon_process_id_unavailable");
1299
+ }
1300
+ try {
1301
+ process.kill(processId, "SIGTERM");
1302
+ }
1303
+ catch (error) {
1304
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
1305
+ throw error;
1306
+ }
1307
+ }
1308
+ await waitForProcessExit(processId);
1309
+ await clearIndexerDaemonArtifacts(servicePaths);
1310
+ indexerDaemonAction = "stopped-incompatible-daemon";
1311
+ }
1312
+ else if (initialProbe.compatibility === "unreachable") {
1313
+ const hasStaleArtifacts = await pathExists(servicePaths.indexerDaemonSocketPath)
1314
+ || await pathExists(servicePaths.indexerDaemonStatusPath);
1315
+ if (hasStaleArtifacts) {
1316
+ await clearIndexerDaemonArtifacts(servicePaths);
1317
+ indexerDaemonAction = "cleared-stale-artifacts";
1318
+ }
1319
+ }
1320
+ else {
1321
+ throw new Error(initialProbe.error ?? "indexer_daemon_protocol_error");
1322
+ }
1323
+ resetIndexerDatabase = await ensureIndexerDatabaseHealthy({
1324
+ databasePath: options.databasePath,
1325
+ dataDir: options.dataDir,
1326
+ walletRootId: repairedState.walletRootId,
1327
+ resetIfNeeded: options.assumeYes ?? false,
1328
+ });
1329
+ }
1330
+ finally {
1331
+ await indexerLock.release();
1332
+ }
1333
+ if (recoveredFromBackup) {
1334
+ repairedState = await persistRepairState({
1335
+ state: repairedState,
1336
+ provider,
1337
+ paths,
1338
+ nowUnixMs,
1339
+ replacePrimary: true,
1340
+ });
1341
+ repairStateNeedsPersist = false;
1342
+ }
1343
+ else if (repairStateNeedsPersist) {
1344
+ repairedState = await persistRepairState({
1345
+ state: repairedState,
1346
+ provider,
1347
+ paths,
1348
+ nowUnixMs,
1349
+ });
1350
+ repairStateNeedsPersist = false;
1351
+ }
1352
+ const preAttachProbe = await probeManagedIndexerDaemon({
1353
+ dataDir: options.dataDir,
1354
+ walletRootId: repairedState.walletRootId,
1355
+ });
1356
+ if (preAttachProbe.compatibility === "compatible") {
1357
+ preAttachIndexerDaemonInstanceId = preAttachProbe.status?.daemonInstanceId ?? null;
1358
+ await preAttachProbe.client?.close().catch(() => undefined);
1359
+ }
1360
+ else if (preAttachProbe.compatibility !== "unreachable") {
1361
+ throw new Error(preAttachProbe.error ?? "indexer_daemon_protocol_error");
1362
+ }
1363
+ const daemon = await attachManagedIndexerDaemon({
1364
+ dataDir: options.dataDir,
1365
+ databasePath: options.databasePath,
1366
+ walletRootId: repairedState.walletRootId,
1367
+ });
1368
+ try {
1369
+ const { health: indexerPostRepairHealth, daemonInstanceId: postRepairDaemonInstanceId, } = await verifyIndexerPostRepairHealth({
1370
+ daemon,
1371
+ probeIndexerDaemon: probeManagedIndexerDaemon,
1372
+ dataDir: options.dataDir,
1373
+ walletRootId: repairedState.walletRootId,
1374
+ nowUnixMs,
1375
+ });
1376
+ const restartedIndexerDaemon = indexerDaemonAction !== "none" || preAttachProbe.compatibility === "unreachable";
1377
+ if (restartedIndexerDaemon
1378
+ && initialIndexerDaemonInstanceId !== null
1379
+ && postRepairDaemonInstanceId === initialIndexerDaemonInstanceId) {
1380
+ throw new Error("indexer_daemon_repair_identity_not_rotated");
1381
+ }
1382
+ if (!restartedIndexerDaemon
1383
+ && preAttachProbe.compatibility === "compatible"
1384
+ && preAttachIndexerDaemonInstanceId !== null
1385
+ && postRepairDaemonInstanceId !== preAttachIndexerDaemonInstanceId) {
1386
+ throw new Error("indexer_daemon_repair_identity_changed");
1387
+ }
1388
+ if (indexerDaemonAction === "none" && preAttachProbe.compatibility === "unreachable") {
1389
+ indexerDaemonAction = "restarted-compatible-daemon";
1390
+ }
1391
+ let keepUnlockSession = false;
1392
+ if (miningWasResumable) {
1393
+ const postRepairResumeReady = await canResumeBackgroundMiningAfterRepair({
1394
+ provider,
1395
+ paths,
1396
+ repairedState,
1397
+ nowUnixMs,
1398
+ bitcoindPostRepairHealth,
1399
+ indexerPostRepairHealth,
1400
+ unlockUntilUnixMs: savedUnlockUntilUnixMs,
1401
+ });
1402
+ if (!postRepairResumeReady) {
1403
+ miningResumeAction = "skipped-post-repair-blocked";
1404
+ }
1405
+ else if (savedUnlockUntilUnixMs === null || savedUnlockUntilUnixMs <= nowUnixMs) {
1406
+ miningResumeAction = "skipped-post-repair-blocked";
1407
+ }
1408
+ else {
1409
+ await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(repairedState, savedUnlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
1410
+ provider,
1411
+ secretReference,
1412
+ });
1413
+ keepUnlockSession = true;
1414
+ try {
1415
+ const startBackgroundMining = options.startBackgroundMining
1416
+ ?? (await import("./mining/runner.js")).startBackgroundMining;
1417
+ const resumed = await startBackgroundMining({
1418
+ dataDir: options.dataDir,
1419
+ databasePath: options.databasePath,
1420
+ provider,
1421
+ paths,
1422
+ prompter: createSilentNonInteractivePrompter(),
1423
+ });
1424
+ if (resumed.snapshot?.runMode === "background") {
1425
+ miningResumeAction = "resumed-background";
1426
+ miningPostRepairRunMode = "background";
1427
+ }
1428
+ else {
1429
+ miningResumeAction = "resume-failed";
1430
+ miningResumeError = "Background mining did not report a background runtime after repair.";
1431
+ }
1432
+ }
1433
+ catch (error) {
1434
+ miningResumeAction = "resume-failed";
1435
+ miningResumeError = error instanceof Error ? error.message : String(error);
1436
+ }
1437
+ }
1438
+ }
1439
+ if (!keepUnlockSession) {
1440
+ await clearUnlockSession(paths.walletUnlockSessionPath);
1441
+ }
1442
+ return {
1443
+ walletRootId: repairedState.walletRootId,
1444
+ recoveredFromBackup,
1445
+ recreatedManagedCoreWallet,
1446
+ resetIndexerDatabase,
1447
+ bitcoindServiceAction,
1448
+ bitcoindCompatibilityIssue,
1449
+ managedCoreReplicaAction,
1450
+ bitcoindPostRepairHealth,
1451
+ indexerDaemonAction,
1452
+ indexerCompatibilityIssue,
1453
+ indexerPostRepairHealth,
1454
+ miningPreRepairRunMode,
1455
+ miningResumeAction,
1456
+ miningPostRepairRunMode,
1457
+ miningResumeError,
1458
+ note: resetIndexerDatabase
1459
+ ? "Indexer artifacts were reset and may still be catching up."
1460
+ : null,
1461
+ };
1462
+ }
1463
+ finally {
1464
+ await daemon.close().catch(() => undefined);
1465
+ await bitcoindHandle?.stop?.().catch(() => undefined);
1466
+ }
1467
+ }
1468
+ finally {
1469
+ await miningPreemption?.release().catch(() => undefined);
1470
+ }
1471
+ }
1472
+ finally {
1473
+ await controlLock.release();
1474
+ }
1475
+ }