@cogcoin/client 1.1.4 → 1.1.5

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 (83) hide show
  1. package/README.md +4 -5
  2. package/dist/bitcoind/progress/tty-renderer.js +3 -2
  3. package/dist/bitcoind/service.js +1 -1
  4. package/dist/cli/command-registry.d.ts +39 -0
  5. package/dist/cli/command-registry.js +1132 -0
  6. package/dist/cli/commands/client-admin.js +6 -56
  7. package/dist/cli/commands/mining-admin.js +9 -32
  8. package/dist/cli/commands/mining-read.js +15 -56
  9. package/dist/cli/commands/mining-runtime.js +258 -57
  10. package/dist/cli/commands/service-runtime.js +1 -64
  11. package/dist/cli/commands/status.js +2 -15
  12. package/dist/cli/commands/update.js +6 -21
  13. package/dist/cli/commands/wallet-admin.js +18 -120
  14. package/dist/cli/commands/wallet-mutation.js +4 -7
  15. package/dist/cli/commands/wallet-read.js +31 -138
  16. package/dist/cli/context.js +2 -4
  17. package/dist/cli/mining-format.js +8 -2
  18. package/dist/cli/mutation-command-groups.d.ts +11 -11
  19. package/dist/cli/mutation-command-groups.js +9 -18
  20. package/dist/cli/mutation-json.d.ts +1 -17
  21. package/dist/cli/mutation-json.js +1 -28
  22. package/dist/cli/mutation-success.d.ts +0 -1
  23. package/dist/cli/mutation-success.js +0 -19
  24. package/dist/cli/output.d.ts +1 -10
  25. package/dist/cli/output.js +52 -481
  26. package/dist/cli/parse.d.ts +1 -1
  27. package/dist/cli/parse.js +38 -695
  28. package/dist/cli/runner.js +28 -113
  29. package/dist/cli/types.d.ts +7 -8
  30. package/dist/cli/update-notifier.js +1 -1
  31. package/dist/cli/wallet-format.js +1 -1
  32. package/dist/wallet/lifecycle/managed-core.d.ts +23 -0
  33. package/dist/wallet/lifecycle/managed-core.js +257 -0
  34. package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
  35. package/dist/wallet/lifecycle/repair-mining.js +304 -0
  36. package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
  37. package/dist/wallet/lifecycle/repair-runtime.js +206 -0
  38. package/dist/wallet/lifecycle/repair.d.ts +11 -0
  39. package/dist/wallet/lifecycle/repair.js +368 -0
  40. package/dist/wallet/lifecycle/setup.d.ts +16 -0
  41. package/dist/wallet/lifecycle/setup.js +430 -0
  42. package/dist/wallet/lifecycle/types.d.ts +125 -0
  43. package/dist/wallet/lifecycle/types.js +1 -0
  44. package/dist/wallet/lifecycle.d.ts +4 -165
  45. package/dist/wallet/lifecycle.js +3 -1656
  46. package/dist/wallet/mining/candidate.d.ts +60 -0
  47. package/dist/wallet/mining/candidate.js +290 -0
  48. package/dist/wallet/mining/competitiveness.d.ts +22 -0
  49. package/dist/wallet/mining/competitiveness.js +640 -0
  50. package/dist/wallet/mining/control.js +7 -251
  51. package/dist/wallet/mining/cycle.d.ts +39 -0
  52. package/dist/wallet/mining/cycle.js +542 -0
  53. package/dist/wallet/mining/engine-state.d.ts +66 -0
  54. package/dist/wallet/mining/engine-state.js +211 -0
  55. package/dist/wallet/mining/engine-types.d.ts +173 -0
  56. package/dist/wallet/mining/engine-types.js +1 -0
  57. package/dist/wallet/mining/engine-utils.d.ts +7 -0
  58. package/dist/wallet/mining/engine-utils.js +75 -0
  59. package/dist/wallet/mining/events.d.ts +2 -0
  60. package/dist/wallet/mining/events.js +19 -0
  61. package/dist/wallet/mining/lifecycle.d.ts +71 -0
  62. package/dist/wallet/mining/lifecycle.js +355 -0
  63. package/dist/wallet/mining/projection.d.ts +61 -0
  64. package/dist/wallet/mining/projection.js +319 -0
  65. package/dist/wallet/mining/publish.d.ts +79 -0
  66. package/dist/wallet/mining/publish.js +614 -0
  67. package/dist/wallet/mining/runner.d.ts +12 -418
  68. package/dist/wallet/mining/runner.js +274 -3433
  69. package/dist/wallet/mining/supervisor.d.ts +134 -0
  70. package/dist/wallet/mining/supervisor.js +558 -0
  71. package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
  72. package/dist/wallet/mining/visualizer-sync.js +166 -0
  73. package/dist/wallet/mining/visualizer.d.ts +1 -0
  74. package/dist/wallet/mining/visualizer.js +33 -18
  75. package/dist/wallet/reset.d.ts +1 -1
  76. package/dist/wallet/reset.js +35 -11
  77. package/dist/wallet/runtime.d.ts +0 -6
  78. package/dist/wallet/runtime.js +2 -38
  79. package/dist/wallet/tx/common.d.ts +18 -0
  80. package/dist/wallet/tx/common.js +40 -26
  81. package/package.json +1 -1
  82. package/dist/wallet/state/seed-index.d.ts +0 -43
  83. package/dist/wallet/state/seed-index.js +0 -151
@@ -1,1657 +1,4 @@
1
- import { randomBytes } from "node:crypto";
2
- import { access, constants, mkdir, readFile, readdir, 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, withClaimedUninitializedManagedRuntime, } 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 { normalizeWalletStateRecord, persistWalletCoinControlStateIfNeeded, } from "./coin-control.js";
11
- import { normalizeWalletDescriptorState, persistNormalizedWalletDescriptorStateIfNeeded, persistWalletStateUpdate, resolveNormalizedWalletDescriptorState, stripDescriptorChecksum, } from "./descriptor-normalization.js";
12
- import { acquireFileLock, clearOrphanedFileLock, readLockMetadata } from "./fs/lock.js";
13
- import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, isEnglishMnemonicWord, validateEnglishMnemonic, } from "./material.js";
14
- import { deriveWalletRuntimePathsForSeed, resolveWalletRuntimePathsForTesting, } from "./runtime.js";
15
- import { readMiningGenerationActivity } from "./mining/coordination.js";
16
- import { loadClientConfig } from "./mining/config.js";
17
- import { loadMiningRuntimeStatus, saveMiningRuntimeStatus } from "./mining/runtime-artifacts.js";
18
- import { normalizeMiningStateRecord } from "./mining/state.js";
19
- import { renderWalletMnemonicRevealArt } from "./mnemonic-art.js";
20
- import { clearWalletPendingInitializationState, loadWalletPendingInitializationStateOrNull, saveWalletPendingInitializationState, } from "./state/pending-init.js";
21
- import { addImportedWalletSeedRecord, assertValidImportedWalletSeedName, ensureMainWalletSeedIndexRecord, findWalletSeedRecord, loadWalletSeedIndex, removeWalletSeedRecord, } from "./state/seed-index.js";
22
- import { createDefaultWalletSecretProvider, ensureClientPasswordConfigured, createWalletPendingInitSecretReference, createWalletRootId, createWalletSecretReference, withInteractiveWalletSecretProvider, } from "./state/provider.js";
23
- import { clearLegacyWalletLockArtifacts, withUnlockedManagedCoreWallet, } from "./managed-core-wallet.js";
24
- import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, loadWalletState, saveWalletState, } from "./state/storage.js";
25
1
  export { previewResetWallet, resetWallet, } from "./reset.js";
26
- function sanitizeWalletName(walletRootId) {
27
- return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
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
- async function readJsonFileOrNull(path) {
39
- try {
40
- return JSON.parse(await readFile(path, "utf8"));
41
- }
42
- catch (error) {
43
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
44
- return null;
45
- }
46
- return null;
47
- }
48
- }
49
- function resolvePendingInitializationStoragePaths(paths) {
50
- return {
51
- primaryPath: paths.walletInitPendingPath,
52
- backupPath: paths.walletInitPendingBackupPath,
53
- };
54
- }
55
- async function clearPendingInitialization(paths, provider) {
56
- await clearWalletPendingInitializationState(resolvePendingInitializationStoragePaths(paths), {
57
- provider,
58
- secretReference: createWalletPendingInitSecretReference(paths.walletStateRoot),
59
- });
60
- }
61
- async function loadOrCreatePendingInitializationMaterial(options) {
62
- try {
63
- const loaded = await loadWalletPendingInitializationStateOrNull(resolvePendingInitializationStoragePaths(options.paths), {
64
- provider: options.provider,
65
- });
66
- if (loaded !== null) {
67
- return deriveWalletMaterialFromMnemonic(loaded.state.mnemonic.phrase);
68
- }
69
- }
70
- catch {
71
- await clearPendingInitialization(options.paths, options.provider);
72
- }
73
- const material = generateWalletMaterial();
74
- const secretReference = createWalletPendingInitSecretReference(options.paths.walletStateRoot);
75
- const pendingState = {
76
- schemaVersion: 1,
77
- createdAtUnixMs: options.nowUnixMs,
78
- mnemonic: {
79
- phrase: material.mnemonic.phrase,
80
- language: material.mnemonic.language,
81
- },
82
- };
83
- await options.provider.storeSecret(secretReference.keyId, randomBytes(32));
84
- try {
85
- await saveWalletPendingInitializationState(resolvePendingInitializationStoragePaths(options.paths), pendingState, {
86
- provider: options.provider,
87
- secretReference,
88
- });
89
- }
90
- catch (error) {
91
- await options.provider.deleteSecret(secretReference.keyId).catch(() => undefined);
92
- throw error;
93
- }
94
- return material;
95
- }
96
- function createInitialWalletState(options) {
97
- return {
98
- schemaVersion: 5,
99
- stateRevision: 1,
100
- lastWrittenAtUnixMs: options.nowUnixMs,
101
- walletRootId: options.walletRootId,
102
- network: "mainnet",
103
- localScriptPubKeyHexes: [options.material.funding.scriptPubKeyHex],
104
- mnemonic: {
105
- phrase: options.material.mnemonic.phrase,
106
- language: options.material.mnemonic.language,
107
- },
108
- keys: {
109
- masterFingerprintHex: options.material.keys.masterFingerprintHex,
110
- accountPath: options.material.keys.accountPath,
111
- accountXprv: options.material.keys.accountXprv,
112
- accountXpub: options.material.keys.accountXpub,
113
- },
114
- descriptor: {
115
- privateExternal: options.material.descriptor.privateExternal,
116
- publicExternal: options.material.descriptor.publicExternal,
117
- checksum: options.material.descriptor.checksum,
118
- rangeEnd: options.material.descriptor.rangeEnd,
119
- safetyMargin: options.material.descriptor.safetyMargin,
120
- },
121
- funding: {
122
- address: options.material.funding.address,
123
- scriptPubKeyHex: options.material.funding.scriptPubKeyHex,
124
- },
125
- walletBirthTime: Math.floor(options.nowUnixMs / 1000),
126
- managedCoreWallet: {
127
- walletName: sanitizeWalletName(options.walletRootId),
128
- internalPassphrase: options.internalCoreWalletPassphrase,
129
- descriptorChecksum: null,
130
- walletAddress: null,
131
- walletScriptPubKeyHex: null,
132
- proofStatus: "not-proven",
133
- lastImportedAtUnixMs: null,
134
- lastVerifiedAtUnixMs: null,
135
- },
136
- domains: [],
137
- miningState: {
138
- runMode: "stopped",
139
- state: "idle",
140
- pauseReason: null,
141
- currentPublishState: "none",
142
- currentDomain: null,
143
- currentDomainId: null,
144
- currentDomainIndex: null,
145
- currentSenderScriptPubKeyHex: null,
146
- currentTxid: null,
147
- currentWtxid: null,
148
- currentFeeRateSatVb: null,
149
- currentAbsoluteFeeSats: null,
150
- currentScore: null,
151
- currentSentence: null,
152
- currentEncodedSentenceBytesHex: null,
153
- currentBip39WordIndices: null,
154
- currentBlendSeedHex: null,
155
- currentBlockTargetHeight: null,
156
- currentReferencedBlockHashDisplay: null,
157
- currentIntentFingerprintHex: null,
158
- livePublishInMempool: null,
159
- currentPublishDecision: null,
160
- replacementCount: 0,
161
- currentBlockFeeSpentSats: "0",
162
- sessionFeeSpentSats: "0",
163
- lifetimeFeeSpentSats: "0",
164
- sharedMiningConflictOutpoint: null,
165
- },
166
- pendingMutations: [],
167
- };
168
- }
169
- async function normalizeLoadedWalletStateIfNeeded(options) {
170
- let state = options.state;
171
- let source = options.source;
172
- if (options.dataDir !== undefined) {
173
- const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
174
- dataDir: options.dataDir,
175
- chain: "main",
176
- startHeight: 0,
177
- walletRootId: state.walletRootId,
178
- });
179
- try {
180
- const normalized = await persistNormalizedWalletDescriptorStateIfNeeded({
181
- state,
182
- access: {
183
- provider: options.provider,
184
- secretReference: createWalletSecretReference(state.walletRootId),
185
- },
186
- paths: options.paths,
187
- nowUnixMs: options.nowUnixMs,
188
- replacePrimary: options.source === "backup",
189
- rpc: (options.rpcFactory ?? createRpcClient)(node.rpc),
190
- });
191
- state = normalized.state;
192
- source = normalized.changed ? "primary" : options.source;
193
- const coinControl = await persistWalletCoinControlStateIfNeeded({
194
- state,
195
- access: {
196
- provider: options.provider,
197
- secretReference: createWalletSecretReference(state.walletRootId),
198
- },
199
- paths: options.paths,
200
- nowUnixMs: options.nowUnixMs,
201
- replacePrimary: source === "backup",
202
- rpc: createRpcClient(node.rpc),
203
- });
204
- state = coinControl.state;
205
- source = coinControl.changed ? "primary" : source;
206
- }
207
- finally {
208
- await node.stop?.().catch(() => undefined);
209
- }
210
- }
211
- return {
212
- state: normalizeWalletStateRecord({
213
- ...state,
214
- miningState: normalizeMiningStateRecord(state.miningState),
215
- }),
216
- source,
217
- };
218
- }
219
- async function promptRequiredValue(prompter, message) {
220
- const value = (await prompter.prompt(message)).trim();
221
- if (value === "") {
222
- throw new Error("wallet_prompt_value_required");
223
- }
224
- return value;
225
- }
226
- async function promptHiddenValue(prompter, message) {
227
- const value = prompter.promptHidden != null
228
- ? await prompter.promptHidden(message)
229
- : await prompter.prompt(message);
230
- return value.trim();
231
- }
232
- async function promptForRestoreMnemonic(prompter) {
233
- const words = [];
234
- for (let index = 0; index < 24; index += 1) {
235
- const word = (await promptRequiredValue(prompter, `Word ${index + 1} of 24: `)).toLowerCase();
236
- if (!isEnglishMnemonicWord(word)) {
237
- throw new Error("wallet_restore_mnemonic_invalid");
238
- }
239
- words.push(word);
240
- }
241
- const phrase = words.join(" ");
242
- if (!validateEnglishMnemonic(phrase)) {
243
- throw new Error("wallet_restore_mnemonic_invalid");
244
- }
245
- return phrase;
246
- }
247
- async function confirmTypedAcknowledgement(prompter, expected, message, errorCode = "wallet_typed_confirmation_rejected") {
248
- const answer = (await prompter.prompt(message)).trim();
249
- if (answer !== expected) {
250
- throw new Error(errorCode);
251
- }
252
- }
253
- async function confirmRestoreReplacement(prompter) {
254
- const answer = (await prompter.prompt("Type \"RESTORE\" to replace the existing local wallet state and managed Core wallet replica: ")).trim();
255
- if (answer !== "RESTORE") {
256
- throw new Error("wallet_restore_replace_confirmation_required");
257
- }
258
- }
259
- async function confirmYesNo(prompter, message) {
260
- const answer = (await prompter.prompt(message)).trim().toLowerCase();
261
- if (answer !== "yes") {
262
- throw new Error("wallet_delete_confirmation_required");
263
- }
264
- }
265
- async function recreateManagedCoreWalletReplica(state, provider, paths, dataDir, nowUnixMs, options = {}) {
266
- const walletName = sanitizeWalletName(state.walletRootId);
267
- const walletDir = join(dataDir, "wallets", walletName);
268
- const quarantineDir = `${walletDir}.quarantine-${nowUnixMs}`;
269
- const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
270
- dataDir,
271
- chain: "main",
272
- startHeight: 0,
273
- walletRootId: state.walletRootId,
274
- managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
275
- });
276
- const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
277
- if (rpc.unloadWallet != null) {
278
- await rpc.unloadWallet(walletName, false).catch(() => undefined);
279
- }
280
- if (await pathExists(walletDir)) {
281
- await rename(walletDir, quarantineDir).catch(() => undefined);
282
- }
283
- return importDescriptorIntoManagedCoreWallet({
284
- ...state,
285
- managedCoreWallet: {
286
- ...state.managedCoreWallet,
287
- proofStatus: "not-proven",
288
- },
289
- }, provider, paths, dataDir, nowUnixMs, options.attachService, options.rpcFactory);
290
- }
291
- async function ensureIndexerDatabaseHealthy(options) {
292
- try {
293
- if (await pathExists(options.databasePath)) {
294
- const header = await readFile(options.databasePath).then((buffer) => buffer.subarray(0, 16).toString("utf8"));
295
- if (header.length > 0 && header !== "SQLite format 3\u0000") {
296
- throw new Error("indexer_database_not_sqlite");
297
- }
298
- }
299
- const store = await openSqliteStore({ filename: options.databasePath });
300
- try {
301
- const client = await openClient({ store });
302
- try {
303
- await client.getTip();
304
- }
305
- finally {
306
- await client.close();
307
- }
308
- }
309
- finally {
310
- await store.close();
311
- }
312
- return false;
313
- }
314
- catch {
315
- if (!options.resetIfNeeded) {
316
- throw new Error("wallet_repair_indexer_reset_requires_yes");
317
- }
318
- await rm(options.databasePath, { force: true }).catch(() => undefined);
319
- await rm(`${options.databasePath}-wal`, { force: true }).catch(() => undefined);
320
- await rm(`${options.databasePath}-shm`, { force: true }).catch(() => undefined);
321
- await mkdir(dirname(options.databasePath), { recursive: true });
322
- return true;
323
- }
324
- }
325
- function mapIndexerCompatibilityToRepairIssue(compatibility) {
326
- switch (compatibility) {
327
- case "service-version-mismatch":
328
- return "service-version-mismatch";
329
- case "wallet-root-mismatch":
330
- return "wallet-root-mismatch";
331
- case "schema-mismatch":
332
- return "schema-mismatch";
333
- default:
334
- return "none";
335
- }
336
- }
337
- function mapBitcoindCompatibilityToRepairIssue(compatibility) {
338
- switch (compatibility) {
339
- case "service-version-mismatch":
340
- return "service-version-mismatch";
341
- case "wallet-root-mismatch":
342
- return "wallet-root-mismatch";
343
- case "runtime-mismatch":
344
- return "runtime-mismatch";
345
- default:
346
- return "none";
347
- }
348
- }
349
- function mapBitcoindRepairHealth(options) {
350
- if (options.serviceState === null) {
351
- return "unavailable";
352
- }
353
- if (options.serviceState === "starting" || options.serviceState === "stopping") {
354
- return "starting";
355
- }
356
- if (options.serviceState !== "ready") {
357
- return "failed";
358
- }
359
- if (options.replica?.proofStatus === "missing" || options.replica?.proofStatus === "mismatch") {
360
- return "failed";
361
- }
362
- if (options.catchingUp) {
363
- return "catching-up";
364
- }
365
- return "ready";
366
- }
367
- function mapLeaseStateToRepairHealth(state) {
368
- switch (state) {
369
- case "synced":
370
- return "synced";
371
- case "catching-up":
372
- case "reorging":
373
- return "catching-up";
374
- case "starting":
375
- case "stopping":
376
- return "starting";
377
- default:
378
- return "failed";
379
- }
380
- }
381
- const INDEXER_DAEMON_HEARTBEAT_STALE_MS = 15_000;
382
- async function verifyIndexerPostRepairHealth(options) {
383
- try {
384
- const lease = await readSnapshotWithRetry(options.daemon, options.walletRootId);
385
- return {
386
- health: mapLeaseStateToRepairHealth(lease.status.state),
387
- daemonInstanceId: lease.status.daemonInstanceId,
388
- };
389
- }
390
- catch (leaseError) {
391
- const probe = await options.probeIndexerDaemon({
392
- dataDir: options.dataDir,
393
- walletRootId: options.walletRootId,
394
- });
395
- try {
396
- if (probe.compatibility === "compatible"
397
- && probe.status !== null
398
- && (options.nowUnixMs - probe.status.heartbeatAtUnixMs) <= INDEXER_DAEMON_HEARTBEAT_STALE_MS
399
- && (probe.status.state === "starting" || probe.status.state === "catching-up" || probe.status.state === "reorging")) {
400
- return {
401
- health: mapLeaseStateToRepairHealth(probe.status.state),
402
- daemonInstanceId: probe.status.daemonInstanceId,
403
- };
404
- }
405
- }
406
- finally {
407
- await probe.client?.close().catch(() => undefined);
408
- }
409
- throw leaseError;
410
- }
411
- }
412
- async function isProcessAlive(pid) {
413
- if (pid === null) {
414
- return false;
415
- }
416
- try {
417
- process.kill(pid, 0);
418
- return true;
419
- }
420
- catch (error) {
421
- if (error instanceof Error && "code" in error && error.code === "ESRCH") {
422
- return false;
423
- }
424
- return true;
425
- }
426
- }
427
- async function waitForProcessExit(pid, timeoutMs = 15_000, errorCode = "indexer_daemon_stop_timeout") {
428
- const deadline = Date.now() + timeoutMs;
429
- while (Date.now() < deadline) {
430
- if (!await isProcessAlive(pid)) {
431
- return;
432
- }
433
- await new Promise((resolve) => setTimeout(resolve, 100));
434
- }
435
- throw new Error(errorCode);
436
- }
437
- async function clearIndexerDaemonArtifacts(servicePaths) {
438
- await rm(servicePaths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
439
- await rm(servicePaths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
440
- }
441
- async function clearManagedBitcoindArtifacts(servicePaths) {
442
- await rm(servicePaths.bitcoindStatusPath, { force: true }).catch(() => undefined);
443
- await rm(servicePaths.bitcoindPidPath, { force: true }).catch(() => undefined);
444
- await rm(servicePaths.bitcoindReadyPath, { force: true }).catch(() => undefined);
445
- await rm(servicePaths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
446
- }
447
- async function detectExistingManagedWalletReplica(dataDir) {
448
- try {
449
- const entries = await readdir(join(dataDir, "wallets"), { withFileTypes: true });
450
- return entries.some((entry) => entry.isDirectory() && entry.name.startsWith("cogcoin-"));
451
- }
452
- catch (error) {
453
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
454
- return false;
455
- }
456
- throw error;
457
- }
458
- }
459
- async function stopRecordedManagedProcess(pid, errorCode) {
460
- if (pid === null || !await isProcessAlive(pid)) {
461
- return;
462
- }
463
- try {
464
- process.kill(pid, "SIGTERM");
465
- }
466
- catch (error) {
467
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
468
- throw error;
469
- }
470
- }
471
- try {
472
- await waitForProcessExit(pid, 5_000, errorCode);
473
- return;
474
- }
475
- catch {
476
- try {
477
- process.kill(pid, "SIGKILL");
478
- }
479
- catch (error) {
480
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
481
- throw error;
482
- }
483
- }
484
- }
485
- await waitForProcessExit(pid, 5_000, errorCode);
486
- }
487
- async function clearOrphanedRepairLocks(lockPaths) {
488
- for (const lockPath of lockPaths) {
489
- await clearOrphanedFileLock(lockPath, isProcessAlive);
490
- }
491
- }
492
- async function clearPreviousManagedWalletRuntime(options) {
493
- if (options.walletRootId === null) {
494
- return;
495
- }
496
- const servicePaths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
497
- const bitcoindLock = await acquireFileLock(servicePaths.bitcoindLockPath, {
498
- purpose: "wallet-restore-cleanup",
499
- walletRootId: options.walletRootId,
500
- });
501
- const indexerLock = await acquireFileLock(servicePaths.indexerDaemonLockPath, {
502
- purpose: "wallet-restore-cleanup",
503
- walletRootId: options.walletRootId,
504
- });
505
- try {
506
- const bitcoindStatus = await readJsonFileOrNull(servicePaths.bitcoindStatusPath);
507
- const indexerStatus = await readJsonFileOrNull(servicePaths.indexerDaemonStatusPath);
508
- await stopRecordedManagedProcess(bitcoindStatus?.processId ?? null, "managed_bitcoind_stop_timeout");
509
- await stopRecordedManagedProcess(indexerStatus?.processId ?? null, "indexer_daemon_stop_timeout");
510
- await clearManagedBitcoindArtifacts(servicePaths);
511
- await clearIndexerDaemonArtifacts(servicePaths);
512
- await rm(servicePaths.walletRuntimeRoot, { recursive: true, force: true }).catch(() => undefined);
513
- await rm(servicePaths.indexerServiceRoot, { recursive: true, force: true }).catch(() => undefined);
514
- await rm(join(options.dataDir, "wallets", sanitizeWalletName(options.walletRootId)), { recursive: true, force: true }).catch(() => undefined);
515
- }
516
- finally {
517
- await indexerLock.release();
518
- await bitcoindLock.release();
519
- }
520
- }
521
- function formatRestoreCleanupWarning(error) {
522
- const reason = error instanceof Error && error.message.trim().length > 0
523
- ? ` (${error.message})`
524
- : "";
525
- return `Previous managed runtime cleanup did not complete${reason}. Run \`cogcoin repair\` if status shows stale or conflicting managed services.`;
526
- }
527
- function createSilentNonInteractivePrompter() {
528
- return {
529
- isInteractive: false,
530
- writeLine() { },
531
- async prompt() {
532
- return "";
533
- },
534
- };
535
- }
536
- function resolveMainWalletPaths(paths) {
537
- return deriveWalletRuntimePathsForSeed(paths, "main");
538
- }
539
- async function loadSharedWalletSeedIndex(paths, nowUnixMs) {
540
- const mainPaths = resolveMainWalletPaths(paths);
541
- return await loadWalletSeedIndex({
542
- paths: mainPaths,
543
- nowUnixMs,
544
- });
545
- }
546
- function applyRepairStoppedMiningState(state) {
547
- const miningState = normalizeMiningStateRecord(state.miningState);
548
- return {
549
- ...state,
550
- miningState: {
551
- ...miningState,
552
- runMode: "stopped",
553
- state: miningState.livePublishInMempool
554
- ? miningState.state === "paused-stale"
555
- ? "paused-stale"
556
- : "paused"
557
- : miningState.state === "repair-required"
558
- ? "repair-required"
559
- : "idle",
560
- pauseReason: miningState.livePublishInMempool
561
- ? miningState.state === "paused-stale"
562
- ? "stale-block-context"
563
- : "wallet-repair"
564
- : miningState.state === "repair-required"
565
- ? miningState.pauseReason
566
- : null,
567
- },
568
- };
569
- }
570
- function createStoppedBackgroundRuntimeSnapshot(snapshot, nowUnixMs) {
571
- return {
572
- ...snapshot,
573
- updatedAtUnixMs: nowUnixMs,
574
- runMode: "stopped",
575
- backgroundWorkerPid: null,
576
- backgroundWorkerRunId: null,
577
- backgroundWorkerHeartbeatAtUnixMs: null,
578
- backgroundWorkerHealth: null,
579
- currentPhase: "idle",
580
- note: snapshot.livePublishInMempool
581
- ? "Background mining stopped for wallet repair. The last mining transaction may still confirm from mempool."
582
- : "Background mining stopped for wallet repair.",
583
- };
584
- }
585
- function resolveMiningGenerationRequestPath(paths) {
586
- return join(paths.miningRoot, "generation-request.json");
587
- }
588
- function resolveMiningGenerationActivityPath(paths) {
589
- return join(paths.miningRoot, "generation-activity.json");
590
- }
591
- function normalizeRepairMiningPid(value) {
592
- return typeof value === "number" && Number.isInteger(value) && value > 0
593
- ? value
594
- : null;
595
- }
596
- function createRepairStoppedMiningNote(livePublishInMempool) {
597
- return livePublishInMempool
598
- ? "Background mining stopped for wallet repair. The last mining transaction may still confirm from mempool."
599
- : "Background mining stopped for wallet repair.";
600
- }
601
- function createStoppedMiningRuntimeSnapshotForRepair(options) {
602
- const stoppedMiningState = normalizeMiningStateRecord(applyRepairStoppedMiningState(options.state).miningState);
603
- const note = createRepairStoppedMiningNote(stoppedMiningState.livePublishInMempool);
604
- if (options.snapshot !== null) {
605
- return {
606
- ...createStoppedBackgroundRuntimeSnapshot(options.snapshot, options.nowUnixMs),
607
- miningState: stoppedMiningState.state,
608
- currentPublishState: stoppedMiningState.currentPublishState,
609
- targetBlockHeight: stoppedMiningState.currentBlockTargetHeight,
610
- referencedBlockHashDisplay: stoppedMiningState.currentReferencedBlockHashDisplay,
611
- currentDomainId: stoppedMiningState.currentDomainId,
612
- currentDomainName: stoppedMiningState.currentDomain,
613
- currentSentenceDisplay: stoppedMiningState.currentSentence,
614
- currentTxid: stoppedMiningState.currentTxid,
615
- currentWtxid: stoppedMiningState.currentWtxid,
616
- livePublishInMempool: stoppedMiningState.livePublishInMempool,
617
- currentFeeRateSatVb: stoppedMiningState.currentFeeRateSatVb,
618
- currentAbsoluteFeeSats: stoppedMiningState.currentAbsoluteFeeSats,
619
- currentBlockFeeSpentSats: stoppedMiningState.currentBlockFeeSpentSats,
620
- sessionFeeSpentSats: stoppedMiningState.sessionFeeSpentSats,
621
- lifetimeFeeSpentSats: stoppedMiningState.lifetimeFeeSpentSats,
622
- currentPublishDecision: stoppedMiningState.currentPublishDecision,
623
- pauseReason: stoppedMiningState.pauseReason,
624
- note,
625
- };
626
- }
627
- return {
628
- schemaVersion: 1,
629
- walletRootId: options.state.walletRootId,
630
- workerApiVersion: null,
631
- workerBinaryVersion: null,
632
- workerBuildId: null,
633
- updatedAtUnixMs: options.nowUnixMs,
634
- runMode: "stopped",
635
- backgroundWorkerPid: null,
636
- backgroundWorkerRunId: null,
637
- backgroundWorkerHeartbeatAtUnixMs: null,
638
- backgroundWorkerHealth: null,
639
- indexerDaemonState: null,
640
- indexerDaemonInstanceId: null,
641
- indexerSnapshotSeq: null,
642
- indexerSnapshotOpenedAtUnixMs: null,
643
- indexerTruthSource: undefined,
644
- indexerHeartbeatAtUnixMs: null,
645
- coreBestHeight: null,
646
- coreBestHash: null,
647
- indexerTipHeight: null,
648
- indexerTipHash: null,
649
- indexerReorgDepth: null,
650
- indexerTipAligned: null,
651
- corePublishState: null,
652
- providerState: null,
653
- lastSuspendDetectedAtUnixMs: null,
654
- reconnectSettledUntilUnixMs: null,
655
- tipSettledUntilUnixMs: null,
656
- miningState: stoppedMiningState.state,
657
- currentPhase: "idle",
658
- currentPublishState: stoppedMiningState.currentPublishState,
659
- targetBlockHeight: stoppedMiningState.currentBlockTargetHeight,
660
- referencedBlockHashDisplay: stoppedMiningState.currentReferencedBlockHashDisplay,
661
- currentDomainId: stoppedMiningState.currentDomainId,
662
- currentDomainName: stoppedMiningState.currentDomain,
663
- currentSentenceDisplay: stoppedMiningState.currentSentence,
664
- currentCanonicalBlend: null,
665
- currentTxid: stoppedMiningState.currentTxid,
666
- currentWtxid: stoppedMiningState.currentWtxid,
667
- livePublishInMempool: stoppedMiningState.livePublishInMempool,
668
- currentFeeRateSatVb: stoppedMiningState.currentFeeRateSatVb,
669
- currentAbsoluteFeeSats: stoppedMiningState.currentAbsoluteFeeSats,
670
- currentBlockFeeSpentSats: stoppedMiningState.currentBlockFeeSpentSats,
671
- sessionFeeSpentSats: stoppedMiningState.sessionFeeSpentSats,
672
- lifetimeFeeSpentSats: stoppedMiningState.lifetimeFeeSpentSats,
673
- sameDomainCompetitorSuppressed: null,
674
- higherRankedCompetitorDomainCount: null,
675
- dedupedCompetitorDomainCount: null,
676
- competitivenessGateIndeterminate: null,
677
- mempoolSequenceCacheStatus: null,
678
- currentPublishDecision: stoppedMiningState.currentPublishDecision,
679
- lastMempoolSequence: null,
680
- lastCompetitivenessGateAtUnixMs: null,
681
- pauseReason: stoppedMiningState.pauseReason,
682
- providerConfigured: false,
683
- providerKind: null,
684
- bitcoindHealth: "unavailable",
685
- bitcoindServiceState: null,
686
- bitcoindReplicaStatus: null,
687
- nodeHealth: "unavailable",
688
- indexerHealth: "unavailable",
689
- tipsAligned: null,
690
- lastEventAtUnixMs: null,
691
- lastError: null,
692
- note,
693
- };
694
- }
695
- async function persistRepairState(options) {
696
- return await persistWalletStateUpdate({
697
- state: options.state,
698
- access: {
699
- provider: options.provider,
700
- secretReference: createWalletSecretReference(options.state.walletRootId),
701
- },
702
- paths: options.paths,
703
- nowUnixMs: options.nowUnixMs,
704
- replacePrimary: options.replacePrimary,
705
- });
706
- }
707
- async function cleanupMiningForRepair(options) {
708
- const controlLockMetadata = await readLockMetadata(options.paths.miningControlLockPath).catch(() => null);
709
- const generationActivity = await readMiningGenerationActivity(options.paths).catch(() => null);
710
- const controlLockPid = normalizeRepairMiningPid(controlLockMetadata?.processId);
711
- const backgroundWorkerPid = normalizeRepairMiningPid(options.snapshot?.backgroundWorkerPid);
712
- const generationOwnerPid = normalizeRepairMiningPid(generationActivity?.generationOwnerPid);
713
- const discoveredPids = new Set();
714
- let backgroundWorkerAlive = false;
715
- let foregroundWorkerAlive = false;
716
- const pidSources = [
717
- { pid: backgroundWorkerPid, source: "background" },
718
- { pid: controlLockPid, source: "foreground" },
719
- { pid: generationOwnerPid, source: "foreground" },
720
- ];
721
- for (const source of pidSources) {
722
- if (source.pid === null || source.pid === process.pid || !await isProcessAlive(source.pid)) {
723
- continue;
724
- }
725
- discoveredPids.add(source.pid);
726
- if (source.source === "background") {
727
- backgroundWorkerAlive = true;
728
- }
729
- else {
730
- foregroundWorkerAlive = true;
731
- }
732
- }
733
- for (const pid of discoveredPids) {
734
- await stopRecordedManagedProcess(pid, "mining_process_stop_timeout");
735
- }
736
- await rm(options.paths.miningControlLockPath, { force: true }).catch(() => undefined);
737
- await rm(resolveMiningGenerationRequestPath(options.paths), { force: true }).catch(() => undefined);
738
- await rm(resolveMiningGenerationActivityPath(options.paths), { force: true }).catch(() => undefined);
739
- await saveMiningRuntimeStatus(options.paths.miningStatusPath, createStoppedMiningRuntimeSnapshotForRepair({
740
- state: options.state,
741
- snapshot: options.snapshot,
742
- nowUnixMs: options.nowUnixMs,
743
- }));
744
- return {
745
- preRepairRunMode: backgroundWorkerAlive
746
- ? "background"
747
- : foregroundWorkerAlive
748
- ? "foreground"
749
- : "stopped",
750
- };
751
- }
752
- async function canResumeBackgroundMiningAfterRepair(options) {
753
- if (options.bitcoindPostRepairHealth !== "ready"
754
- || options.indexerPostRepairHealth !== "synced"
755
- || normalizeMiningStateRecord(options.repairedState.miningState).state === "repair-required") {
756
- return false;
757
- }
758
- try {
759
- const config = await loadClientConfig({
760
- path: options.paths.clientConfigPath,
761
- provider: options.provider,
762
- });
763
- return config?.mining.builtIn != null;
764
- }
765
- catch {
766
- return false;
767
- }
768
- }
769
- async function ensureWalletNotInitialized(paths, provider) {
770
- if (await pathExists(paths.walletStatePath) || await pathExists(paths.walletStateBackupPath)) {
771
- await clearPendingInitialization(paths, provider);
772
- throw new Error("wallet_already_initialized");
773
- }
774
- }
775
- function isWalletSecretAccessError(error) {
776
- const message = error instanceof Error ? error.message : String(error);
777
- return message.startsWith("wallet_secret_missing_")
778
- || message.startsWith("wallet_secret_provider_");
779
- }
780
- function writeMnemonicReveal(prompter, phrase, introLines) {
781
- const words = phrase.trim().split(/\s+/);
782
- for (const line of introLines) {
783
- prompter.writeLine(line);
784
- }
785
- for (const line of renderWalletMnemonicRevealArt(words)) {
786
- prompter.writeLine(line);
787
- }
788
- prompter.writeLine("Single-line copy:");
789
- prompter.writeLine(phrase);
790
- }
791
- async function confirmMnemonic(prompter, words) {
792
- const challenge = createMnemonicConfirmationChallenge(words);
793
- for (const entry of challenge) {
794
- const answer = (await prompter.prompt(`Confirm word #${entry.index + 1}: `)).trim().toLowerCase();
795
- if (answer !== entry.word) {
796
- throw new Error(`wallet_init_confirmation_failed_word_${entry.index + 1}`);
797
- }
798
- }
799
- }
800
- async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dataDir, nowUnixMs, attachService = attachOrStartManagedBitcoindService, rpcFactory = createRpcClient) {
801
- const node = await attachService({
802
- dataDir,
803
- chain: "main",
804
- startHeight: 0,
805
- walletRootId: state.walletRootId,
806
- managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
807
- });
808
- const rpc = rpcFactory(node.rpc);
809
- await createManagedWalletReplica(rpc, state.walletRootId, {
810
- managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
811
- });
812
- const normalizedDescriptors = await resolveNormalizedWalletDescriptorState(state, rpc);
813
- const walletName = sanitizeWalletName(state.walletRootId);
814
- await withUnlockedManagedCoreWallet({
815
- rpc,
816
- walletName,
817
- internalPassphrase: state.managedCoreWallet.internalPassphrase,
818
- run: async () => {
819
- const importResults = await rpc.importDescriptors(walletName, [{
820
- desc: normalizedDescriptors.privateExternal,
821
- timestamp: state.walletBirthTime,
822
- active: false,
823
- internal: false,
824
- range: [0, state.descriptor.rangeEnd],
825
- }]);
826
- if (!importResults.every((result) => result.success)) {
827
- throw new Error(`wallet_descriptor_import_failed_${JSON.stringify(importResults)}`);
828
- }
829
- },
830
- });
831
- const derivedFunding = await rpc.deriveAddresses(normalizedDescriptors.publicExternal, [0, 0]);
832
- if (derivedFunding[0] !== state.funding.address) {
833
- throw new Error("wallet_funding_address_verification_failed");
834
- }
835
- const descriptors = await rpc.listDescriptors(walletName);
836
- const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc === normalizedDescriptors.publicExternal);
837
- if (importedDescriptor == null) {
838
- throw new Error("wallet_descriptor_not_present_after_import");
839
- }
840
- const verifiedReplica = {
841
- walletRootId: state.walletRootId,
842
- walletName,
843
- loaded: true,
844
- descriptors: true,
845
- privateKeysEnabled: true,
846
- created: false,
847
- proofStatus: "ready",
848
- descriptorChecksum: normalizedDescriptors.checksum,
849
- fundingAddress0: state.funding.address,
850
- fundingScriptPubKeyHex0: state.funding.scriptPubKeyHex,
851
- message: null,
852
- };
853
- const nextState = {
854
- ...state,
855
- stateRevision: state.stateRevision + 1,
856
- lastWrittenAtUnixMs: nowUnixMs,
857
- descriptor: {
858
- ...state.descriptor,
859
- privateExternal: normalizedDescriptors.privateExternal,
860
- publicExternal: normalizedDescriptors.publicExternal,
861
- checksum: normalizedDescriptors.checksum,
862
- },
863
- managedCoreWallet: {
864
- ...state.managedCoreWallet,
865
- walletName,
866
- descriptorChecksum: normalizedDescriptors.checksum,
867
- walletAddress: verifiedReplica.fundingAddress0 ?? null,
868
- walletScriptPubKeyHex: verifiedReplica.fundingScriptPubKeyHex0 ?? null,
869
- proofStatus: "ready",
870
- lastImportedAtUnixMs: nowUnixMs,
871
- lastVerifiedAtUnixMs: nowUnixMs,
872
- },
873
- };
874
- await saveWalletState({
875
- primaryPath: paths.walletStatePath,
876
- backupPath: paths.walletStateBackupPath,
877
- }, nextState, {
878
- provider,
879
- secretReference: createWalletSecretReference(state.walletRootId),
880
- });
881
- return nextState;
882
- }
883
- export async function verifyManagedCoreWalletReplica(state, dataDir, dependencies = {}) {
884
- const walletName = state.managedCoreWallet.walletName;
885
- try {
886
- const node = dependencies.nodeHandle ?? await (dependencies.attachService ?? attachOrStartManagedBitcoindService)({
887
- dataDir,
888
- chain: "main",
889
- startHeight: 0,
890
- walletRootId: state.walletRootId,
891
- });
892
- const rpc = (dependencies.rpcFactory ?? createRpcClient)(node.rpc);
893
- const info = await rpc.getWalletInfo(walletName);
894
- const descriptors = await rpc.listDescriptors(walletName);
895
- const matchingDescriptor = state.managedCoreWallet.descriptorChecksum === null
896
- ? null
897
- : descriptors.descriptors.find((entry) => entry.desc.endsWith(`#${state.managedCoreWallet.descriptorChecksum}`));
898
- if (matchingDescriptor == null) {
899
- return {
900
- walletRootId: state.walletRootId,
901
- walletName,
902
- loaded: true,
903
- descriptors: info.descriptors,
904
- privateKeysEnabled: info.private_keys_enabled,
905
- created: false,
906
- proofStatus: "missing",
907
- descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
908
- fundingAddress0: state.managedCoreWallet.walletAddress,
909
- fundingScriptPubKeyHex0: state.managedCoreWallet.walletScriptPubKeyHex,
910
- message: "Expected descriptor is missing from the managed Core wallet.",
911
- };
912
- }
913
- const derived = await rpc.deriveAddresses(state.descriptor.publicExternal, [0, 0]);
914
- if (derived[0] !== state.funding.address) {
915
- return {
916
- walletRootId: state.walletRootId,
917
- walletName,
918
- loaded: true,
919
- descriptors: info.descriptors,
920
- privateKeysEnabled: info.private_keys_enabled,
921
- created: false,
922
- proofStatus: "mismatch",
923
- descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
924
- fundingAddress0: derived[0] ?? null,
925
- fundingScriptPubKeyHex0: null,
926
- message: "The managed Core wallet funding address does not match the trusted wallet state.",
927
- };
928
- }
929
- return {
930
- walletRootId: state.walletRootId,
931
- walletName,
932
- loaded: true,
933
- descriptors: info.descriptors,
934
- privateKeysEnabled: info.private_keys_enabled,
935
- created: false,
936
- proofStatus: "ready",
937
- descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
938
- fundingAddress0: state.funding.address,
939
- fundingScriptPubKeyHex0: state.funding.scriptPubKeyHex,
940
- message: null,
941
- };
942
- }
943
- catch (error) {
944
- return {
945
- walletRootId: state.walletRootId,
946
- walletName,
947
- loaded: false,
948
- descriptors: false,
949
- privateKeysEnabled: false,
950
- created: false,
951
- proofStatus: "not-proven",
952
- descriptorChecksum: state.managedCoreWallet.descriptorChecksum,
953
- fundingAddress0: state.managedCoreWallet.walletAddress,
954
- fundingScriptPubKeyHex0: state.managedCoreWallet.walletScriptPubKeyHex,
955
- message: error instanceof Error ? error.message : String(error),
956
- };
957
- }
958
- }
959
- async function loadWalletStateForAccess(options = {}) {
960
- const provider = options.provider ?? createDefaultWalletSecretProvider();
961
- const nowUnixMs = options.nowUnixMs ?? Date.now();
962
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
963
- const loaded = await loadWalletState({
964
- primaryPath: paths.walletStatePath,
965
- backupPath: paths.walletStateBackupPath,
966
- }, {
967
- provider,
968
- });
969
- return normalizeLoadedWalletStateIfNeeded({
970
- provider,
971
- state: loaded.state,
972
- source: loaded.source,
973
- nowUnixMs,
974
- paths,
975
- dataDir: options.dataDir,
976
- attachService: options.attachService,
977
- rpcFactory: options.rpcFactory,
978
- });
979
- }
980
- export async function initializeWallet(options) {
981
- if (!options.prompter.isInteractive) {
982
- throw new Error("wallet_init_requires_tty");
983
- }
984
- const provider = options.provider ?? createDefaultWalletSecretProvider();
985
- const interactiveProvider = withInteractiveWalletSecretProvider(provider, options.prompter);
986
- const nowUnixMs = options.nowUnixMs ?? Date.now();
987
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
988
- if (paths.selectedSeedName !== "main") {
989
- throw new Error("wallet_init_seed_not_supported");
990
- }
991
- const controlLock = await acquireFileLock(paths.walletControlLockPath, {
992
- purpose: "wallet-init",
993
- walletRootId: null,
994
- });
995
- try {
996
- const passwordAction = await ensureClientPasswordConfigured(provider, options.prompter);
997
- const hasWalletState = await pathExists(paths.walletStatePath) || await pathExists(paths.walletStateBackupPath);
998
- if (hasWalletState) {
999
- await clearPendingInitialization(paths, interactiveProvider);
1000
- const loaded = await loadWalletStateForAccess({
1001
- provider: interactiveProvider,
1002
- nowUnixMs,
1003
- paths,
1004
- dataDir: options.dataDir,
1005
- attachService: options.attachService,
1006
- rpcFactory: options.rpcFactory,
1007
- });
1008
- return {
1009
- passwordAction,
1010
- walletAction: "already-initialized",
1011
- walletRootId: loaded.state.walletRootId,
1012
- fundingAddress: loaded.state.funding.address,
1013
- state: loaded.state,
1014
- };
1015
- }
1016
- const material = await loadOrCreatePendingInitializationMaterial({
1017
- provider: interactiveProvider,
1018
- paths,
1019
- nowUnixMs,
1020
- });
1021
- let mnemonicRevealed = false;
1022
- writeMnemonicReveal(options.prompter, material.mnemonic.phrase, [
1023
- "Cogcoin Wallet Initialization",
1024
- "Write down this 24-word recovery phrase.",
1025
- "The same phrase will be shown again until confirmation succeeds:",
1026
- "",
1027
- ]);
1028
- mnemonicRevealed = true;
1029
- try {
1030
- await confirmMnemonic(options.prompter, material.mnemonic.words);
1031
- }
1032
- finally {
1033
- if (mnemonicRevealed) {
1034
- await Promise.resolve()
1035
- .then(() => options.prompter.clearSensitiveDisplay?.("mnemonic-reveal"))
1036
- .catch(() => undefined);
1037
- }
1038
- }
1039
- const walletRootId = createWalletRootId();
1040
- const internalCoreWalletPassphrase = createInternalCoreWalletPassphrase();
1041
- const secretReference = createWalletSecretReference(walletRootId);
1042
- const secret = randomBytes(32);
1043
- await interactiveProvider.storeSecret(secretReference.keyId, secret);
1044
- const initialState = createInitialWalletState({
1045
- walletRootId,
1046
- nowUnixMs,
1047
- material,
1048
- internalCoreWalletPassphrase,
1049
- });
1050
- const verifiedState = await withClaimedUninitializedManagedRuntime({
1051
- dataDir: options.dataDir,
1052
- walletRootId,
1053
- }, async () => {
1054
- await saveWalletState({
1055
- primaryPath: paths.walletStatePath,
1056
- backupPath: paths.walletStateBackupPath,
1057
- }, initialState, {
1058
- provider: interactiveProvider,
1059
- secretReference,
1060
- });
1061
- return importDescriptorIntoManagedCoreWallet(initialState, interactiveProvider, paths, options.dataDir, nowUnixMs, options.attachService, options.rpcFactory);
1062
- });
1063
- await clearLegacyWalletLockArtifacts(paths.walletRuntimeRoot);
1064
- await clearPendingInitialization(paths, interactiveProvider);
1065
- await ensureMainWalletSeedIndexRecord({
1066
- paths: resolveMainWalletPaths(paths),
1067
- walletRootId,
1068
- nowUnixMs,
1069
- });
1070
- return {
1071
- passwordAction,
1072
- walletAction: "initialized",
1073
- walletRootId,
1074
- fundingAddress: verifiedState.funding.address,
1075
- state: verifiedState,
1076
- };
1077
- }
1078
- finally {
1079
- await controlLock.release();
1080
- }
1081
- }
1082
- export async function showWalletMnemonic(options) {
1083
- if (!options.prompter.isInteractive) {
1084
- throw new Error("wallet_show_mnemonic_requires_tty");
1085
- }
1086
- const provider = options.provider ?? createDefaultWalletSecretProvider();
1087
- const interactiveProvider = withInteractiveWalletSecretProvider(provider, options.prompter);
1088
- const nowUnixMs = options.nowUnixMs ?? Date.now();
1089
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1090
- const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1091
- purpose: "wallet-show-mnemonic",
1092
- walletRootId: null,
1093
- });
1094
- try {
1095
- const [hasPrimaryStateFile, hasBackupStateFile] = await Promise.all([
1096
- pathExists(paths.walletStatePath),
1097
- pathExists(paths.walletStateBackupPath),
1098
- ]);
1099
- if (!hasPrimaryStateFile && !hasBackupStateFile) {
1100
- throw new Error("wallet_uninitialized");
1101
- }
1102
- const loaded = await loadWalletStateForAccess({
1103
- provider: interactiveProvider,
1104
- nowUnixMs,
1105
- paths,
1106
- }).catch((error) => {
1107
- if (isWalletSecretAccessError(error)) {
1108
- throw new Error("wallet_secret_provider_unavailable");
1109
- }
1110
- throw new Error("local-state-corrupt");
1111
- });
1112
- await confirmTypedAcknowledgement(options.prompter, "show mnemonic", "Type \"show mnemonic\" to continue: ", "wallet_show_mnemonic_typed_ack_required");
1113
- let mnemonicRevealed = false;
1114
- writeMnemonicReveal(options.prompter, loaded.state.mnemonic.phrase, [
1115
- "Cogcoin Wallet Recovery Phrase",
1116
- "This 24-word recovery phrase controls the wallet.",
1117
- "",
1118
- ]);
1119
- mnemonicRevealed = true;
1120
- try {
1121
- await options.prompter.prompt("Press Enter to clear the recovery phrase from the screen: ");
1122
- }
1123
- finally {
1124
- if (mnemonicRevealed) {
1125
- await Promise.resolve()
1126
- .then(() => options.prompter.clearSensitiveDisplay?.("mnemonic-reveal"))
1127
- .catch(() => undefined);
1128
- }
1129
- }
1130
- }
1131
- finally {
1132
- await controlLock.release();
1133
- }
1134
- }
1135
- export async function restoreWalletFromMnemonic(options) {
1136
- if (!options.prompter.isInteractive) {
1137
- throw new Error("wallet_restore_requires_tty");
1138
- }
1139
- const provider = options.provider ?? createDefaultWalletSecretProvider();
1140
- const interactiveProvider = withInteractiveWalletSecretProvider(provider, options.prompter);
1141
- const nowUnixMs = options.nowUnixMs ?? Date.now();
1142
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1143
- const seedName = assertValidImportedWalletSeedName(paths.selectedSeedName);
1144
- const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1145
- purpose: "wallet-restore",
1146
- walletRootId: null,
1147
- });
1148
- try {
1149
- const mainPaths = resolveMainWalletPaths(paths);
1150
- const seedIndex = await loadSharedWalletSeedIndex(paths, nowUnixMs);
1151
- if (findWalletSeedRecord(seedIndex, "main") === null) {
1152
- throw new Error("wallet_restore_requires_main_wallet");
1153
- }
1154
- if (findWalletSeedRecord(seedIndex, seedName) !== null) {
1155
- throw new Error("wallet_seed_name_exists");
1156
- }
1157
- const passwordAction = await ensureClientPasswordConfigured(provider, options.prompter);
1158
- await ensureWalletNotInitialized(paths, interactiveProvider);
1159
- let promptPhaseStarted = false;
1160
- let mnemonicPhrase;
1161
- try {
1162
- promptPhaseStarted = true;
1163
- mnemonicPhrase = await promptForRestoreMnemonic(options.prompter);
1164
- }
1165
- finally {
1166
- if (promptPhaseStarted) {
1167
- await options.prompter.clearSensitiveDisplay?.("restore-mnemonic-entry");
1168
- }
1169
- }
1170
- await clearPendingInitialization(paths, interactiveProvider);
1171
- const material = deriveWalletMaterialFromMnemonic(mnemonicPhrase);
1172
- const walletRootId = createWalletRootId();
1173
- const internalCoreWalletPassphrase = createInternalCoreWalletPassphrase();
1174
- const secretReference = createWalletSecretReference(walletRootId);
1175
- const secret = randomBytes(32);
1176
- await interactiveProvider.storeSecret(secretReference.keyId, secret);
1177
- const initialState = createInitialWalletState({
1178
- walletRootId,
1179
- nowUnixMs,
1180
- material,
1181
- internalCoreWalletPassphrase,
1182
- });
1183
- await clearLegacyWalletLockArtifacts(paths.walletRuntimeRoot);
1184
- await saveWalletState({
1185
- primaryPath: paths.walletStatePath,
1186
- backupPath: paths.walletStateBackupPath,
1187
- }, initialState, {
1188
- provider: interactiveProvider,
1189
- secretReference,
1190
- });
1191
- const restoredState = await recreateManagedCoreWalletReplica(initialState, interactiveProvider, paths, options.dataDir, nowUnixMs, {
1192
- attachService: options.attachService,
1193
- rpcFactory: options.rpcFactory,
1194
- });
1195
- await clearLegacyWalletLockArtifacts(paths.walletRuntimeRoot);
1196
- await clearPendingInitialization(paths, interactiveProvider);
1197
- await addImportedWalletSeedRecord({
1198
- paths: mainPaths,
1199
- seedName,
1200
- walletRootId,
1201
- nowUnixMs,
1202
- });
1203
- return {
1204
- passwordAction,
1205
- seedName,
1206
- walletRootId,
1207
- fundingAddress: restoredState.funding.address,
1208
- state: restoredState,
1209
- warnings: [],
1210
- };
1211
- }
1212
- finally {
1213
- await controlLock.release();
1214
- }
1215
- }
1216
- export async function deleteImportedWalletSeed(options) {
1217
- const provider = options.provider ?? createDefaultWalletSecretProvider();
1218
- const nowUnixMs = options.nowUnixMs ?? Date.now();
1219
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1220
- if ((paths.selectedSeedName ?? "main") === "main") {
1221
- throw new Error("wallet_delete_main_not_supported");
1222
- }
1223
- const seedName = assertValidImportedWalletSeedName(paths.selectedSeedName);
1224
- const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1225
- purpose: "wallet-delete",
1226
- walletRootId: null,
1227
- });
1228
- try {
1229
- const mainPaths = resolveMainWalletPaths(paths);
1230
- const seedIndex = await loadSharedWalletSeedIndex(paths, nowUnixMs);
1231
- const seedRecord = findWalletSeedRecord(seedIndex, seedName);
1232
- if (seedRecord === null) {
1233
- throw new Error("wallet_seed_not_found");
1234
- }
1235
- if (seedRecord.kind !== "imported") {
1236
- throw new Error("wallet_delete_main_not_supported");
1237
- }
1238
- if (!options.assumeYes) {
1239
- await confirmYesNo(options.prompter, `Delete imported seed "${seedName}" and release its local wallet artifacts? Type yes to continue: `);
1240
- }
1241
- const probeManagedBitcoind = options.probeBitcoindService ?? probeManagedBitcoindService;
1242
- const managedBitcoindProbe = await probeManagedBitcoind({
1243
- dataDir: options.dataDir,
1244
- chain: "main",
1245
- startHeight: 0,
1246
- }).catch(() => ({
1247
- compatibility: "unreachable",
1248
- status: null,
1249
- error: null,
1250
- }));
1251
- if (managedBitcoindProbe.compatibility !== "compatible" && managedBitcoindProbe.compatibility !== "unreachable") {
1252
- throw new Error(managedBitcoindProbe.error ?? "managed_bitcoind_protocol_error");
1253
- }
1254
- if (managedBitcoindProbe.compatibility === "compatible") {
1255
- const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
1256
- dataDir: options.dataDir,
1257
- chain: "main",
1258
- startHeight: 0,
1259
- });
1260
- const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1261
- const walletName = sanitizeWalletName(seedRecord.walletRootId);
1262
- if (rpc.unloadWallet != null) {
1263
- await rpc.unloadWallet(walletName, false).catch(() => undefined);
1264
- }
1265
- }
1266
- await clearLegacyWalletLockArtifacts(paths.walletRuntimeRoot).catch(() => undefined);
1267
- await clearPendingInitialization(paths, provider).catch(() => undefined);
1268
- await provider.deleteSecret(createWalletSecretReference(seedRecord.walletRootId).keyId).catch(() => undefined);
1269
- await rm(paths.walletStateRoot, { recursive: true, force: true }).catch(() => undefined);
1270
- await rm(paths.walletRuntimeRoot, { recursive: true, force: true }).catch(() => undefined);
1271
- await rm(join(options.dataDir, "wallets", sanitizeWalletName(seedRecord.walletRootId)), {
1272
- recursive: true,
1273
- force: true,
1274
- }).catch(() => undefined);
1275
- await removeWalletSeedRecord({
1276
- paths: mainPaths,
1277
- seedName,
1278
- nowUnixMs,
1279
- });
1280
- return {
1281
- seedName,
1282
- walletRootId: seedRecord.walletRootId,
1283
- deleted: true,
1284
- };
1285
- }
1286
- finally {
1287
- await controlLock.release();
1288
- }
1289
- }
1290
- export async function repairWallet(options) {
1291
- const provider = options.provider ?? createDefaultWalletSecretProvider();
1292
- const nowUnixMs = options.nowUnixMs ?? Date.now();
1293
- const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1294
- const probeManagedBitcoind = options.probeBitcoindService ?? probeManagedBitcoindService;
1295
- const attachManagedBitcoind = options.attachService ?? attachOrStartManagedBitcoindService;
1296
- const probeManagedIndexerDaemon = options.probeIndexerDaemon ?? probeIndexerDaemon;
1297
- const attachManagedIndexerDaemon = options.attachIndexerDaemon ?? attachOrStartIndexerDaemon;
1298
- await clearOrphanedRepairLocks([
1299
- paths.walletControlLockPath,
1300
- paths.miningControlLockPath,
1301
- ]);
1302
- const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1303
- purpose: "wallet-repair",
1304
- walletRootId: null,
1305
- });
1306
- try {
1307
- let loaded;
1308
- try {
1309
- loaded = await loadWalletState({
1310
- primaryPath: paths.walletStatePath,
1311
- backupPath: paths.walletStateBackupPath,
1312
- }, {
1313
- provider,
1314
- });
1315
- }
1316
- catch (error) {
1317
- throw new Error("local-state-corrupt");
1318
- }
1319
- const recoveredFromBackup = loaded.source === "backup";
1320
- const secretReference = createWalletSecretReference(loaded.state.walletRootId);
1321
- let repairedState = loaded.state;
1322
- let repairStateNeedsPersist = false;
1323
- const servicePaths = resolveManagedServicePaths(options.dataDir, repairedState.walletRootId);
1324
- await clearOrphanedRepairLocks([
1325
- servicePaths.bitcoindLockPath,
1326
- servicePaths.indexerDaemonLockPath,
1327
- ]);
1328
- const preRepairMiningRuntime = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
1329
- const miningCleanup = await cleanupMiningForRepair({
1330
- paths,
1331
- state: repairedState,
1332
- snapshot: preRepairMiningRuntime,
1333
- nowUnixMs,
1334
- });
1335
- const miningPreRepairRunMode = miningCleanup.preRepairRunMode;
1336
- const miningWasResumable = miningPreRepairRunMode === "background"
1337
- && normalizeMiningStateRecord(repairedState.miningState).state !== "repair-required";
1338
- let initialBitcoindProbe = {
1339
- compatibility: "unreachable",
1340
- status: null,
1341
- error: null,
1342
- };
1343
- let bitcoindServiceAction = "none";
1344
- let bitcoindCompatibilityIssue = "none";
1345
- let managedCoreReplicaAction = "none";
1346
- let indexerDaemonAction = "none";
1347
- let indexerCompatibilityIssue = "none";
1348
- let miningResumeAction = miningPreRepairRunMode === "background"
1349
- ? "skipped-not-resumable"
1350
- : "none";
1351
- let miningPostRepairRunMode = "stopped";
1352
- let miningResumeError = null;
1353
- if (miningPreRepairRunMode !== "stopped" || preRepairMiningRuntime?.runMode !== "stopped") {
1354
- repairedState = applyRepairStoppedMiningState(repairedState);
1355
- repairStateNeedsPersist = true;
1356
- }
1357
- if (!(options.assumeYes ?? false)) {
1358
- await ensureIndexerDatabaseHealthy({
1359
- databasePath: options.databasePath,
1360
- dataDir: options.dataDir,
1361
- walletRootId: repairedState.walletRootId,
1362
- resetIfNeeded: false,
1363
- });
1364
- }
1365
- const bitcoindLock = await acquireFileLock(servicePaths.bitcoindLockPath, {
1366
- purpose: "managed-bitcoind-repair",
1367
- walletRootId: repairedState.walletRootId,
1368
- dataDir: options.dataDir,
1369
- });
1370
- let resetIndexerDatabase = false;
1371
- let bitcoindHandle = null;
1372
- let bitcoindPostRepairHealth = "unavailable";
1373
- try {
1374
- initialBitcoindProbe = await probeManagedBitcoind({
1375
- dataDir: options.dataDir,
1376
- chain: "main",
1377
- startHeight: 0,
1378
- walletRootId: repairedState.walletRootId,
1379
- });
1380
- bitcoindCompatibilityIssue = mapBitcoindCompatibilityToRepairIssue(initialBitcoindProbe.compatibility);
1381
- if (initialBitcoindProbe.compatibility === "service-version-mismatch"
1382
- || initialBitcoindProbe.compatibility === "wallet-root-mismatch"
1383
- || initialBitcoindProbe.compatibility === "runtime-mismatch") {
1384
- const processId = initialBitcoindProbe.status?.processId ?? null;
1385
- if (processId === null) {
1386
- throw new Error("managed_bitcoind_process_id_unavailable");
1387
- }
1388
- try {
1389
- process.kill(processId, "SIGTERM");
1390
- }
1391
- catch (error) {
1392
- if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
1393
- throw error;
1394
- }
1395
- }
1396
- await waitForProcessExit(processId, 15_000, "managed_bitcoind_stop_timeout");
1397
- await clearManagedBitcoindArtifacts(servicePaths);
1398
- bitcoindServiceAction = "stopped-incompatible-service";
1399
- }
1400
- else if (initialBitcoindProbe.compatibility === "unreachable") {
1401
- const hasStaleArtifacts = await pathExists(servicePaths.bitcoindStatusPath)
1402
- || await pathExists(servicePaths.bitcoindPidPath)
1403
- || await pathExists(servicePaths.bitcoindReadyPath)
1404
- || await pathExists(servicePaths.bitcoindWalletStatusPath);
1405
- if (hasStaleArtifacts) {
1406
- await clearManagedBitcoindArtifacts(servicePaths);
1407
- bitcoindServiceAction = "cleared-stale-artifacts";
1408
- }
1409
- }
1410
- else if (initialBitcoindProbe.compatibility === "protocol-error") {
1411
- throw new Error(initialBitcoindProbe.error ?? "managed_bitcoind_protocol_error");
1412
- }
1413
- }
1414
- finally {
1415
- await bitcoindLock.release();
1416
- }
1417
- bitcoindHandle = await attachManagedBitcoind({
1418
- dataDir: options.dataDir,
1419
- chain: "main",
1420
- startHeight: 0,
1421
- walletRootId: repairedState.walletRootId,
1422
- });
1423
- const bitcoindRpc = (options.rpcFactory ?? createRpcClient)(bitcoindHandle.rpc);
1424
- const normalizedDescriptorState = await normalizeWalletDescriptorState(repairedState, bitcoindRpc);
1425
- if (normalizedDescriptorState.changed) {
1426
- repairedState = normalizedDescriptorState.state;
1427
- repairStateNeedsPersist = true;
1428
- }
1429
- const reconciledCoinControl = await persistWalletCoinControlStateIfNeeded({
1430
- state: repairedState,
1431
- access: {
1432
- provider,
1433
- secretReference,
1434
- },
1435
- paths,
1436
- nowUnixMs,
1437
- replacePrimary: recoveredFromBackup && !repairStateNeedsPersist,
1438
- rpc: (options.rpcFactory ?? createRpcClient)(bitcoindHandle.rpc),
1439
- });
1440
- repairedState = reconciledCoinControl.state;
1441
- if (reconciledCoinControl.changed) {
1442
- repairStateNeedsPersist = false;
1443
- }
1444
- let replica = await verifyManagedCoreWalletReplica(repairedState, options.dataDir, {
1445
- nodeHandle: bitcoindHandle,
1446
- attachService: options.attachService,
1447
- rpcFactory: options.rpcFactory,
1448
- });
1449
- let recreatedManagedCoreWallet = false;
1450
- if (replica.proofStatus !== "ready") {
1451
- repairedState = await recreateManagedCoreWalletReplica(repairedState, provider, paths, options.dataDir, nowUnixMs, {
1452
- attachService: options.attachService,
1453
- rpcFactory: options.rpcFactory,
1454
- });
1455
- recreatedManagedCoreWallet = true;
1456
- managedCoreReplicaAction = "recreated";
1457
- repairStateNeedsPersist = false;
1458
- replica = await verifyManagedCoreWalletReplica(repairedState, options.dataDir, {
1459
- nodeHandle: bitcoindHandle,
1460
- attachService: options.attachService,
1461
- rpcFactory: options.rpcFactory,
1462
- });
1463
- }
1464
- const finalBitcoindStatus = await bitcoindHandle.refreshServiceStatus?.() ?? null;
1465
- const chainInfo = await bitcoindRpc.getBlockchainInfo();
1466
- bitcoindPostRepairHealth = mapBitcoindRepairHealth({
1467
- serviceState: finalBitcoindStatus?.state ?? null,
1468
- catchingUp: chainInfo.blocks < chainInfo.headers,
1469
- replica,
1470
- });
1471
- if (bitcoindServiceAction === "none" && initialBitcoindProbe.compatibility === "unreachable") {
1472
- bitcoindServiceAction = "restarted-compatible-service";
1473
- }
1474
- let initialIndexerDaemonInstanceId = null;
1475
- let preAttachIndexerDaemonInstanceId = null;
1476
- const indexerLock = await acquireFileLock(servicePaths.indexerDaemonLockPath, {
1477
- purpose: "indexer-daemon-repair",
1478
- walletRootId: repairedState.walletRootId,
1479
- dataDir: options.dataDir,
1480
- databasePath: options.databasePath,
1481
- });
1482
- try {
1483
- const initialProbe = await probeManagedIndexerDaemon({
1484
- dataDir: options.dataDir,
1485
- walletRootId: repairedState.walletRootId,
1486
- });
1487
- indexerCompatibilityIssue = mapIndexerCompatibilityToRepairIssue(initialProbe.compatibility);
1488
- initialIndexerDaemonInstanceId = initialProbe.status?.daemonInstanceId ?? null;
1489
- if (initialProbe.compatibility === "compatible") {
1490
- await initialProbe.client?.close().catch(() => undefined);
1491
- }
1492
- else if (initialProbe.compatibility === "service-version-mismatch"
1493
- || initialProbe.compatibility === "wallet-root-mismatch"
1494
- || initialProbe.compatibility === "schema-mismatch") {
1495
- const processId = initialProbe.status?.processId ?? null;
1496
- if (processId === null) {
1497
- throw new Error("indexer_daemon_process_id_unavailable");
1498
- }
1499
- try {
1500
- process.kill(processId, "SIGTERM");
1501
- }
1502
- catch (error) {
1503
- if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
1504
- throw error;
1505
- }
1506
- }
1507
- await waitForProcessExit(processId);
1508
- await clearIndexerDaemonArtifacts(servicePaths);
1509
- indexerDaemonAction = "stopped-incompatible-daemon";
1510
- }
1511
- else if (initialProbe.compatibility === "unreachable") {
1512
- const hasStaleArtifacts = await pathExists(servicePaths.indexerDaemonSocketPath)
1513
- || await pathExists(servicePaths.indexerDaemonStatusPath);
1514
- if (hasStaleArtifacts) {
1515
- await clearIndexerDaemonArtifacts(servicePaths);
1516
- indexerDaemonAction = "cleared-stale-artifacts";
1517
- }
1518
- }
1519
- else {
1520
- throw new Error(initialProbe.error ?? "indexer_daemon_protocol_error");
1521
- }
1522
- resetIndexerDatabase = await ensureIndexerDatabaseHealthy({
1523
- databasePath: options.databasePath,
1524
- dataDir: options.dataDir,
1525
- walletRootId: repairedState.walletRootId,
1526
- resetIfNeeded: options.assumeYes ?? false,
1527
- });
1528
- }
1529
- finally {
1530
- await indexerLock.release();
1531
- }
1532
- if (recoveredFromBackup) {
1533
- repairedState = await persistRepairState({
1534
- state: repairedState,
1535
- provider,
1536
- paths,
1537
- nowUnixMs,
1538
- replacePrimary: true,
1539
- });
1540
- repairStateNeedsPersist = false;
1541
- }
1542
- else if (repairStateNeedsPersist) {
1543
- repairedState = await persistRepairState({
1544
- state: repairedState,
1545
- provider,
1546
- paths,
1547
- nowUnixMs,
1548
- });
1549
- repairStateNeedsPersist = false;
1550
- }
1551
- const preAttachProbe = await probeManagedIndexerDaemon({
1552
- dataDir: options.dataDir,
1553
- walletRootId: repairedState.walletRootId,
1554
- });
1555
- if (preAttachProbe.compatibility === "compatible") {
1556
- preAttachIndexerDaemonInstanceId = preAttachProbe.status?.daemonInstanceId ?? null;
1557
- await preAttachProbe.client?.close().catch(() => undefined);
1558
- }
1559
- else if (preAttachProbe.compatibility !== "unreachable") {
1560
- throw new Error(preAttachProbe.error ?? "indexer_daemon_protocol_error");
1561
- }
1562
- const daemon = await attachManagedIndexerDaemon({
1563
- dataDir: options.dataDir,
1564
- databasePath: options.databasePath,
1565
- walletRootId: repairedState.walletRootId,
1566
- });
1567
- try {
1568
- const { health: indexerPostRepairHealth, daemonInstanceId: postRepairDaemonInstanceId, } = await verifyIndexerPostRepairHealth({
1569
- daemon,
1570
- probeIndexerDaemon: probeManagedIndexerDaemon,
1571
- dataDir: options.dataDir,
1572
- walletRootId: repairedState.walletRootId,
1573
- nowUnixMs,
1574
- });
1575
- const restartedIndexerDaemon = indexerDaemonAction !== "none" || preAttachProbe.compatibility === "unreachable";
1576
- if (restartedIndexerDaemon
1577
- && initialIndexerDaemonInstanceId !== null
1578
- && postRepairDaemonInstanceId === initialIndexerDaemonInstanceId) {
1579
- throw new Error("indexer_daemon_repair_identity_not_rotated");
1580
- }
1581
- if (!restartedIndexerDaemon
1582
- && preAttachProbe.compatibility === "compatible"
1583
- && preAttachIndexerDaemonInstanceId !== null
1584
- && postRepairDaemonInstanceId !== preAttachIndexerDaemonInstanceId) {
1585
- throw new Error("indexer_daemon_repair_identity_changed");
1586
- }
1587
- if (indexerDaemonAction === "none" && preAttachProbe.compatibility === "unreachable") {
1588
- indexerDaemonAction = "restarted-compatible-daemon";
1589
- }
1590
- if (miningWasResumable) {
1591
- const postRepairResumeReady = await canResumeBackgroundMiningAfterRepair({
1592
- provider,
1593
- paths,
1594
- repairedState,
1595
- bitcoindPostRepairHealth,
1596
- indexerPostRepairHealth,
1597
- });
1598
- if (!postRepairResumeReady) {
1599
- miningResumeAction = "skipped-post-repair-blocked";
1600
- }
1601
- else {
1602
- try {
1603
- const startBackgroundMining = options.startBackgroundMining
1604
- ?? (await import("./mining/runner.js")).startBackgroundMining;
1605
- const resumed = await startBackgroundMining({
1606
- dataDir: options.dataDir,
1607
- databasePath: options.databasePath,
1608
- provider,
1609
- paths,
1610
- prompter: createSilentNonInteractivePrompter(),
1611
- });
1612
- if (resumed.snapshot?.runMode === "background") {
1613
- miningResumeAction = "resumed-background";
1614
- miningPostRepairRunMode = "background";
1615
- }
1616
- else {
1617
- miningResumeAction = "resume-failed";
1618
- miningResumeError = "Background mining did not report a background runtime after repair.";
1619
- }
1620
- }
1621
- catch (error) {
1622
- miningResumeAction = "resume-failed";
1623
- miningResumeError = error instanceof Error ? error.message : String(error);
1624
- }
1625
- }
1626
- }
1627
- await clearLegacyWalletLockArtifacts(paths.walletRuntimeRoot);
1628
- return {
1629
- walletRootId: repairedState.walletRootId,
1630
- recoveredFromBackup,
1631
- recreatedManagedCoreWallet,
1632
- resetIndexerDatabase,
1633
- bitcoindServiceAction,
1634
- bitcoindCompatibilityIssue,
1635
- managedCoreReplicaAction,
1636
- bitcoindPostRepairHealth,
1637
- indexerDaemonAction,
1638
- indexerCompatibilityIssue,
1639
- indexerPostRepairHealth,
1640
- miningPreRepairRunMode,
1641
- miningResumeAction,
1642
- miningPostRepairRunMode,
1643
- miningResumeError,
1644
- note: resetIndexerDatabase
1645
- ? "Indexer artifacts were reset and may still be catching up."
1646
- : null,
1647
- };
1648
- }
1649
- finally {
1650
- await daemon.close().catch(() => undefined);
1651
- await bitcoindHandle?.stop?.().catch(() => undefined);
1652
- }
1653
- }
1654
- finally {
1655
- await controlLock.release();
1656
- }
1657
- }
2
+ export { verifyManagedCoreWalletReplica } from "./lifecycle/managed-core.js";
3
+ export { initializeWallet, showWalletMnemonic } from "./lifecycle/setup.js";
4
+ export { repairWallet } from "./lifecycle/repair.js";