@cogcoin/client 1.2.3 → 1.2.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 (36) hide show
  1. package/README.md +12 -2
  2. package/dist/bitcoind/managed-bitcoind-service-config.d.ts +5 -1
  3. package/dist/bitcoind/managed-bitcoind-service-config.js +27 -18
  4. package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +46 -3
  5. package/dist/bitcoind/managed-bitcoind-service-status.d.ts +9 -2
  6. package/dist/bitcoind/managed-bitcoind-service-status.js +65 -9
  7. package/dist/bitcoind/managed-bitcoind-service-types.d.ts +8 -0
  8. package/dist/bitcoind/managed-runtime/bitcoind-policy.js +27 -0
  9. package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +6 -0
  10. package/dist/bitcoind/managed-runtime/types.d.ts +2 -2
  11. package/dist/bitcoind/retryable-rpc.d.ts +1 -0
  12. package/dist/bitcoind/retryable-rpc.js +19 -2
  13. package/dist/cli/command-registry.js +2 -2
  14. package/dist/cli/commands/service-runtime.js +22 -2
  15. package/dist/cli/commands/status.js +7 -1
  16. package/dist/cli/commands/wallet-admin.js +8 -2
  17. package/dist/cli/context.js +2 -0
  18. package/dist/cli/output/rules/cli-surface.js +7 -0
  19. package/dist/cli/output/rules/services.js +7 -0
  20. package/dist/cli/parse.js +9 -0
  21. package/dist/cli/status-format.d.ts +2 -2
  22. package/dist/cli/status-format.js +167 -28
  23. package/dist/cli/types.d.ts +3 -0
  24. package/dist/passive-status.d.ts +49 -1
  25. package/dist/passive-status.js +208 -2
  26. package/dist/wallet/lifecycle/context.js +1 -0
  27. package/dist/wallet/lifecycle/repair-bitcoind.js +44 -1
  28. package/dist/wallet/lifecycle/repair-runtime.d.ts +3 -1
  29. package/dist/wallet/lifecycle/repair-runtime.js +12 -0
  30. package/dist/wallet/lifecycle/repair.js +31 -9
  31. package/dist/wallet/lifecycle/types.d.ts +7 -0
  32. package/dist/wallet/mining/competitiveness.js +2 -2
  33. package/dist/wallet/mining/lifecycle.js +4 -0
  34. package/dist/wallet/mining/mempool-index.js +29 -3
  35. package/dist/wallet/read/managed-bitcoind.js +9 -0
  36. package/package.json +1 -1
@@ -1,8 +1,10 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { resolveManagedServicePaths } from "./bitcoind/service-paths.js";
3
4
  import { openReadonlySqliteDatabase } from "./sqlite/driver.js";
4
5
  import { loadLatestCheckpoint } from "./sqlite/checkpoints.js";
5
6
  import { loadTipMeta } from "./sqlite/tip-meta.js";
7
+ import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, } from "./wallet/state/storage.js";
6
8
  function fileExists(path) {
7
9
  return stat(path).then(() => true, () => false);
8
10
  }
@@ -21,6 +23,193 @@ function readBootstrapState(raw) {
21
23
  updatedAt: parsed.updatedAt ?? null,
22
24
  };
23
25
  }
26
+ function formatUnknownError(error) {
27
+ return error instanceof Error ? error.message : String(error);
28
+ }
29
+ function isMissingFileError(error) {
30
+ return error instanceof Error
31
+ && "code" in error
32
+ && error.code === "ENOENT";
33
+ }
34
+ async function inspectWalletStatus(runtimePaths) {
35
+ if (runtimePaths === undefined) {
36
+ return {
37
+ walletRootId: null,
38
+ source: "none",
39
+ error: null,
40
+ };
41
+ }
42
+ try {
43
+ const raw = await loadRawWalletStateEnvelope({
44
+ primaryPath: runtimePaths.walletStatePath,
45
+ backupPath: runtimePaths.walletStateBackupPath,
46
+ });
47
+ if (raw === null) {
48
+ return {
49
+ walletRootId: null,
50
+ source: "none",
51
+ error: null,
52
+ };
53
+ }
54
+ return {
55
+ walletRootId: extractWalletRootIdHintFromWalletStateEnvelope(raw.envelope),
56
+ source: "wallet-state",
57
+ error: null,
58
+ };
59
+ }
60
+ catch (error) {
61
+ return {
62
+ walletRootId: null,
63
+ source: "unreadable",
64
+ error: formatUnknownError(error),
65
+ };
66
+ }
67
+ }
68
+ async function readRuntimeStatusFile(statusPath) {
69
+ if (statusPath === null) {
70
+ return {
71
+ status: null,
72
+ present: false,
73
+ error: null,
74
+ };
75
+ }
76
+ try {
77
+ return {
78
+ status: JSON.parse(await readFile(statusPath, "utf8")),
79
+ present: true,
80
+ error: null,
81
+ };
82
+ }
83
+ catch (error) {
84
+ if (isMissingFileError(error)) {
85
+ return {
86
+ status: null,
87
+ present: false,
88
+ error: null,
89
+ };
90
+ }
91
+ return {
92
+ status: null,
93
+ present: true,
94
+ error: formatUnknownError(error),
95
+ };
96
+ }
97
+ }
98
+ function emptyManagedBitcoindStatus(statusPath, present, error) {
99
+ return {
100
+ statusPath,
101
+ present,
102
+ state: null,
103
+ processId: null,
104
+ walletRootId: null,
105
+ heartbeatAtUnixMs: null,
106
+ updatedAtUnixMs: null,
107
+ lastError: null,
108
+ error,
109
+ };
110
+ }
111
+ function emptyIndexerStatus(statusPath, present, error) {
112
+ return {
113
+ statusPath,
114
+ present,
115
+ state: null,
116
+ processId: null,
117
+ walletRootId: null,
118
+ coreBestHeight: null,
119
+ appliedTipHeight: null,
120
+ appliedTipHash: null,
121
+ heartbeatAtUnixMs: null,
122
+ updatedAtUnixMs: null,
123
+ lastError: null,
124
+ error,
125
+ };
126
+ }
127
+ function emptyMiningStatus(statusPath, present, error) {
128
+ return {
129
+ statusPath,
130
+ present,
131
+ runMode: null,
132
+ miningState: null,
133
+ currentPhase: null,
134
+ backgroundWorkerPid: null,
135
+ backgroundWorkerHealth: null,
136
+ updatedAtUnixMs: null,
137
+ lastError: null,
138
+ note: null,
139
+ error,
140
+ };
141
+ }
142
+ function resolvePassiveServiceStatusPaths(bitcoinDataDir, runtimePaths, walletRootId) {
143
+ if (walletRootId !== null) {
144
+ const servicePaths = resolveManagedServicePaths(bitcoinDataDir, walletRootId);
145
+ return {
146
+ bitcoindStatusPath: servicePaths.bitcoindStatusPath,
147
+ indexerStatusPath: servicePaths.indexerDaemonStatusPath,
148
+ miningStatusPath: runtimePaths?.miningStatusPath ?? null,
149
+ };
150
+ }
151
+ return {
152
+ bitcoindStatusPath: runtimePaths?.bitcoindStatusPath ?? null,
153
+ indexerStatusPath: runtimePaths?.indexerStatusPath ?? null,
154
+ miningStatusPath: runtimePaths?.miningStatusPath ?? null,
155
+ };
156
+ }
157
+ async function inspectManagedBitcoindStatus(statusPath) {
158
+ const result = await readRuntimeStatusFile(statusPath);
159
+ if (result.status === null) {
160
+ return emptyManagedBitcoindStatus(statusPath, result.present, result.error);
161
+ }
162
+ return {
163
+ statusPath,
164
+ present: true,
165
+ state: result.status.state ?? null,
166
+ processId: result.status.processId ?? null,
167
+ walletRootId: result.status.walletRootId ?? null,
168
+ heartbeatAtUnixMs: result.status.heartbeatAtUnixMs ?? null,
169
+ updatedAtUnixMs: result.status.updatedAtUnixMs ?? null,
170
+ lastError: result.status.lastError ?? null,
171
+ error: null,
172
+ };
173
+ }
174
+ async function inspectIndexerStatus(statusPath) {
175
+ const result = await readRuntimeStatusFile(statusPath);
176
+ if (result.status === null) {
177
+ return emptyIndexerStatus(statusPath, result.present, result.error);
178
+ }
179
+ return {
180
+ statusPath,
181
+ present: true,
182
+ state: result.status.state ?? null,
183
+ processId: result.status.processId ?? null,
184
+ walletRootId: result.status.walletRootId ?? null,
185
+ coreBestHeight: result.status.coreBestHeight ?? null,
186
+ appliedTipHeight: result.status.appliedTipHeight ?? null,
187
+ appliedTipHash: result.status.appliedTipHash ?? null,
188
+ heartbeatAtUnixMs: result.status.heartbeatAtUnixMs ?? null,
189
+ updatedAtUnixMs: result.status.updatedAtUnixMs ?? null,
190
+ lastError: result.status.lastError ?? null,
191
+ error: null,
192
+ };
193
+ }
194
+ async function inspectMiningStatus(statusPath) {
195
+ const result = await readRuntimeStatusFile(statusPath);
196
+ if (result.status === null) {
197
+ return emptyMiningStatus(statusPath, result.present, result.error);
198
+ }
199
+ return {
200
+ statusPath,
201
+ present: true,
202
+ runMode: result.status.runMode ?? null,
203
+ miningState: result.status.miningState ?? null,
204
+ currentPhase: result.status.currentPhase ?? null,
205
+ backgroundWorkerPid: result.status.backgroundWorkerPid ?? null,
206
+ backgroundWorkerHealth: result.status.backgroundWorkerHealth ?? null,
207
+ updatedAtUnixMs: result.status.updatedAtUnixMs ?? null,
208
+ lastError: result.status.lastError ?? null,
209
+ note: result.status.note ?? null,
210
+ error: null,
211
+ };
212
+ }
24
213
  async function inspectSqliteStore(dbPath) {
25
214
  const database = await openReadonlySqliteDatabase(dbPath);
26
215
  try {
@@ -50,9 +239,14 @@ async function inspectSqliteStore(dbPath) {
50
239
  await database.close();
51
240
  }
52
241
  }
53
- export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
242
+ export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir, runtimePaths) {
54
243
  const storeExists = await fileExists(dbPath);
55
244
  const bootstrapPath = join(bitcoinDataDir, "bootstrap", "state.json");
245
+ const wallet = await inspectWalletStatus(runtimePaths);
246
+ const statusPaths = resolvePassiveServiceStatusPaths(bitcoinDataDir, runtimePaths, wallet.walletRootId);
247
+ const managedBitcoind = await inspectManagedBitcoindStatus(statusPaths.bitcoindStatusPath);
248
+ const indexer = await inspectIndexerStatus(statusPaths.indexerStatusPath);
249
+ const mining = await inspectMiningStatus(statusPaths.miningStatusPath);
56
250
  let bootstrap = null;
57
251
  try {
58
252
  bootstrap = readBootstrapState(await readFile(bootstrapPath, "utf8"));
@@ -64,11 +258,15 @@ export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
64
258
  return {
65
259
  dbPath,
66
260
  bitcoinDataDir,
261
+ wallet,
67
262
  storeInitialized: false,
68
263
  storeExists: false,
69
264
  indexedTip: null,
70
265
  latestCheckpoint: null,
71
266
  bootstrap,
267
+ managedBitcoind,
268
+ indexer,
269
+ mining,
72
270
  storeError: null,
73
271
  };
74
272
  }
@@ -77,11 +275,15 @@ export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
77
275
  return {
78
276
  dbPath,
79
277
  bitcoinDataDir,
278
+ wallet,
80
279
  storeInitialized: store.storeInitialized,
81
280
  storeExists: true,
82
281
  indexedTip: store.indexedTip,
83
282
  latestCheckpoint: store.latestCheckpoint,
84
283
  bootstrap,
284
+ managedBitcoind,
285
+ indexer,
286
+ mining,
85
287
  storeError: null,
86
288
  };
87
289
  }
@@ -89,12 +291,16 @@ export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
89
291
  return {
90
292
  dbPath,
91
293
  bitcoinDataDir,
294
+ wallet,
92
295
  storeInitialized: false,
93
296
  storeExists: true,
94
297
  indexedTip: null,
95
298
  latestCheckpoint: null,
96
299
  bootstrap,
97
- storeError: error instanceof Error ? error.message : String(error),
300
+ managedBitcoind,
301
+ indexer,
302
+ mining,
303
+ storeError: formatUnknownError(error),
98
304
  };
99
305
  }
100
306
  }
@@ -47,6 +47,7 @@ export function resolveWalletRepairContext(options) {
47
47
  attachIndexerDaemon: options.attachIndexerDaemon ?? attachOrStartIndexerDaemon,
48
48
  probeIndexerDaemon: options.probeIndexerDaemon ?? probeIndexerDaemon,
49
49
  requestMiningPreemption: options.requestMiningPreemption,
50
+ progress: options.progress ?? (() => undefined),
50
51
  };
51
52
  }
52
53
  export async function acquireWalletControlLock(paths, purpose) {
@@ -4,7 +4,7 @@ import { persistWalletCoinControlStateIfNeeded } from "../coin-control.js";
4
4
  import { createWalletSecretReference } from "../state/provider.js";
5
5
  import { recreateManagedCoreWalletReplica, verifyManagedCoreWalletReplica } from "./managed-core.js";
6
6
  import { pathExists } from "./context.js";
7
- import { clearManagedBitcoindArtifactsForDataDir, isManagedBitcoindRpcUnavailableError, mapBitcoindCompatibilityToRepairIssue, mapBitcoindRepairHealth, waitForProcessExit, } from "./repair-runtime.js";
7
+ import { clearManagedBitcoindArtifactsForDataDir, isManagedBitcoindRpcUnavailableError, isManagedBitcoindStartupWarmupError, mapBitcoindCompatibilityToRepairIssue, mapBitcoindRepairHealth, reportRepairProgress, waitForProcessExit, } from "./repair-runtime.js";
8
8
  export async function repairManagedBitcoindStage(options) {
9
9
  let state = options.state;
10
10
  let repairStateNeedsPersist = options.repairStateNeedsPersist;
@@ -18,19 +18,36 @@ export async function repairManagedBitcoindStage(options) {
18
18
  error: null,
19
19
  };
20
20
  let bitcoindPostRepairHealth = "unavailable";
21
+ const rpcReadyProgress = async (event) => {
22
+ await reportRepairProgress(options.context, event.code, event.message);
23
+ };
21
24
  const bitcoindLock = await acquireFileLock(options.servicePaths.bitcoindLockPath, {
22
25
  purpose: "managed-bitcoind-repair",
23
26
  walletRootId: state.walletRootId,
24
27
  dataDir: options.context.dataDir,
25
28
  });
26
29
  try {
30
+ await reportRepairProgress(options.context, "bitcoind-check", "Checking managed bitcoind...");
27
31
  initialBitcoindProbe = await options.context.probeBitcoindService({
28
32
  dataDir: options.context.dataDir,
29
33
  chain: "main",
30
34
  startHeight: 0,
31
35
  walletRootId: state.walletRootId,
36
+ rpcReadyProgress,
32
37
  });
33
38
  bitcoindCompatibilityIssue = mapBitcoindCompatibilityToRepairIssue(initialBitcoindProbe.compatibility);
39
+ if (initialBitcoindProbe.compatibility === "starting") {
40
+ await reportRepairProgress(options.context, "bitcoind-starting", "Bitcoin Core is loading the block index; leaving managed bitcoind running.");
41
+ return {
42
+ state,
43
+ repairStateNeedsPersist,
44
+ recreatedManagedCoreWallet,
45
+ bitcoindServiceAction,
46
+ bitcoindCompatibilityIssue,
47
+ managedCoreReplicaAction,
48
+ bitcoindPostRepairHealth: "starting",
49
+ };
50
+ }
34
51
  if (initialBitcoindProbe.compatibility === "service-version-mismatch"
35
52
  || initialBitcoindProbe.compatibility === "wallet-root-mismatch"
36
53
  || initialBitcoindProbe.compatibility === "runtime-mismatch"
@@ -40,10 +57,14 @@ export async function repairManagedBitcoindStage(options) {
40
57
  if (initialBitcoindProbe.compatibility !== "rawtx-zmq-missing") {
41
58
  throw new Error("managed_bitcoind_process_id_unavailable");
42
59
  }
60
+ await reportRepairProgress(options.context, "bitcoind-clear-stale-rawtx", "Clearing stale managed bitcoind runtime missing rawtx ZMQ...");
43
61
  await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
44
62
  bitcoindServiceAction = "restarted-missing-rawtx-zmq";
45
63
  }
46
64
  else {
65
+ await reportRepairProgress(options.context, "bitcoind-stop-incompatible", initialBitcoindProbe.compatibility === "rawtx-zmq-missing"
66
+ ? "Stopping stale managed bitcoind missing rawtx ZMQ..."
67
+ : "Stopping incompatible managed bitcoind service...");
47
68
  try {
48
69
  process.kill(processId, "SIGTERM");
49
70
  }
@@ -53,6 +74,7 @@ export async function repairManagedBitcoindStage(options) {
53
74
  }
54
75
  }
55
76
  await waitForProcessExit(processId, 15_000, "managed_bitcoind_stop_timeout");
77
+ await reportRepairProgress(options.context, "bitcoind-clear-artifacts", "Clearing managed bitcoind runtime artifacts...");
56
78
  await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
57
79
  bitcoindServiceAction = initialBitcoindProbe.compatibility === "rawtx-zmq-missing"
58
80
  ? "restarted-missing-rawtx-zmq"
@@ -68,6 +90,7 @@ export async function repairManagedBitcoindStage(options) {
68
90
  options.servicePaths.bitcoindWalletStatusPath,
69
91
  ].map(pathExists));
70
92
  if (hasStaleArtifacts.some(Boolean)) {
93
+ await reportRepairProgress(options.context, "bitcoind-clear-stale", "Clearing stale managed bitcoind artifacts...");
71
94
  await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
72
95
  bitcoindServiceAction = "cleared-stale-artifacts";
73
96
  }
@@ -83,12 +106,17 @@ export async function repairManagedBitcoindStage(options) {
83
106
  let bitcoindHandle = null;
84
107
  let handleClosed = false;
85
108
  try {
109
+ await reportRepairProgress(options.context, attachAttempt === 0 ? "bitcoind-start" : "bitcoind-retry-start", bitcoindServiceAction === "none"
110
+ ? "Attaching to managed bitcoind..."
111
+ : "Starting managed bitcoind with current ZMQ config...");
86
112
  bitcoindHandle = await options.context.attachService({
87
113
  dataDir: options.context.dataDir,
88
114
  chain: "main",
89
115
  startHeight: 0,
90
116
  walletRootId: state.walletRootId,
117
+ rpcReadyProgress,
91
118
  });
119
+ await reportRepairProgress(options.context, "bitcoind-normalize-wallet", "Checking managed Bitcoin wallet state...");
92
120
  const rpc = options.context.rpcFactory(bitcoindHandle.rpc);
93
121
  const normalizedDescriptorState = await normalizeWalletDescriptorState(state, rpc);
94
122
  if (normalizedDescriptorState.changed) {
@@ -116,6 +144,7 @@ export async function repairManagedBitcoindStage(options) {
116
144
  rpcFactory: options.context.rpcFactory,
117
145
  });
118
146
  if (replica.proofStatus !== "ready") {
147
+ await reportRepairProgress(options.context, "bitcoind-recreate-replica", "Recreating managed Core wallet replica...");
119
148
  state = await recreateManagedCoreWalletReplica(state, options.context.provider, options.context.paths, options.context.dataDir, options.context.nowUnixMs, {
120
149
  attachService: options.context.attachService,
121
150
  rpcFactory: options.context.rpcFactory,
@@ -129,6 +158,7 @@ export async function repairManagedBitcoindStage(options) {
129
158
  rpcFactory: options.context.rpcFactory,
130
159
  });
131
160
  }
161
+ await reportRepairProgress(options.context, "bitcoind-final-health", "Checking managed bitcoind post-repair health...");
132
162
  const finalBitcoindStatus = await bitcoindHandle.refreshServiceStatus?.() ?? null;
133
163
  const chainInfo = await rpc.getBlockchainInfo();
134
164
  bitcoindPostRepairHealth = mapBitcoindRepairHealth({
@@ -150,6 +180,18 @@ export async function repairManagedBitcoindStage(options) {
150
180
  };
151
181
  }
152
182
  catch (error) {
183
+ if (isManagedBitcoindStartupWarmupError(error)) {
184
+ await reportRepairProgress(options.context, "bitcoind-starting", "Bitcoin Core is loading the block index; leaving managed bitcoind running.");
185
+ return {
186
+ state,
187
+ repairStateNeedsPersist,
188
+ recreatedManagedCoreWallet,
189
+ bitcoindServiceAction,
190
+ bitcoindCompatibilityIssue,
191
+ managedCoreReplicaAction,
192
+ bitcoindPostRepairHealth: "starting",
193
+ };
194
+ }
153
195
  if (bitcoindHandle !== null) {
154
196
  await bitcoindHandle.stop?.().catch(() => undefined);
155
197
  handleClosed = true;
@@ -161,6 +203,7 @@ export async function repairManagedBitcoindStage(options) {
161
203
  dataDir: options.context.dataDir,
162
204
  });
163
205
  try {
206
+ await reportRepairProgress(options.context, "bitcoind-clear-stale-rpc", "Clearing stale managed bitcoind RPC artifacts...");
164
207
  await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
165
208
  }
166
209
  finally {
@@ -2,7 +2,7 @@ import { attachOrStartIndexerDaemon, probeIndexerDaemon } from "../../bitcoind/i
2
2
  import { probeManagedBitcoindService } from "../../bitcoind/service.js";
3
3
  import { resolveManagedServicePaths } from "../../bitcoind/service-paths.js";
4
4
  import type { ManagedBitcoindServiceStatus } from "../../bitcoind/types.js";
5
- import type { WalletRepairResult } from "./types.js";
5
+ import type { WalletRepairContext, WalletRepairResult } from "./types.js";
6
6
  export declare function ensureIndexerDatabaseHealthy(options: {
7
7
  databasePath: string;
8
8
  dataDir: string;
@@ -18,6 +18,8 @@ export declare function mapBitcoindRepairHealth(options: {
18
18
  proofStatus?: "missing" | "mismatch" | "ready" | "not-proven";
19
19
  } | null;
20
20
  }): WalletRepairResult["bitcoindPostRepairHealth"];
21
+ export declare function reportRepairProgress(context: Pick<WalletRepairContext, "progress">, code: string, message: string): Promise<void>;
22
+ export declare function isManagedBitcoindStartupWarmupError(error: unknown): boolean;
21
23
  export declare function verifyIndexerPostRepairHealth(options: {
22
24
  daemon: Awaited<ReturnType<typeof attachOrStartIndexerDaemon>>;
23
25
  probeIndexerDaemon: typeof probeIndexerDaemon;
@@ -93,6 +93,18 @@ export function mapBitcoindRepairHealth(options) {
93
93
  }
94
94
  return "ready";
95
95
  }
96
+ export async function reportRepairProgress(context, code, message) {
97
+ await context.progress({ code, message });
98
+ }
99
+ export function isManagedBitcoindStartupWarmupError(error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ return message === "managed_bitcoind_service_starting"
102
+ || message === "bitcoind_rpc_timeout"
103
+ || message === "bitcoind_cookie_timeout"
104
+ || /^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)
105
+ || message.startsWith("The managed Bitcoin RPC cookie file is unavailable at ")
106
+ || message.startsWith("The managed Bitcoin RPC cookie file could not be read at ");
107
+ }
96
108
  function mapLeaseStateToRepairHealth(state) {
97
109
  switch (state) {
98
110
  case "synced":
@@ -6,7 +6,7 @@ import { acquireWalletControlLock, resolveWalletRepairContext, } from "./context
6
6
  import { repairManagedBitcoindStage } from "./repair-bitcoind.js";
7
7
  import { repairManagedIndexerStage } from "./repair-indexer.js";
8
8
  import { applyRepairStoppedMiningState, cleanupMiningForRepair, persistRepairState, resumeBackgroundMiningAfterRepair, } from "./repair-mining.js";
9
- import { clearOrphanedRepairLocks, ensureIndexerDatabaseHealthy, } from "./repair-runtime.js";
9
+ import { clearOrphanedRepairLocks, ensureIndexerDatabaseHealthy, reportRepairProgress, } from "./repair-runtime.js";
10
10
  export async function repairWallet(options) {
11
11
  const context = resolveWalletRepairContext(options);
12
12
  await clearOrphanedRepairLocks([
@@ -17,6 +17,7 @@ export async function repairWallet(options) {
17
17
  try {
18
18
  let loaded;
19
19
  try {
20
+ await reportRepairProgress(context, "wallet-check", "Checking wallet state...");
20
21
  loaded = await loadWalletState({
21
22
  primaryPath: context.paths.walletStatePath,
22
23
  backupPath: context.paths.walletStateBackupPath,
@@ -31,10 +32,12 @@ export async function repairWallet(options) {
31
32
  let repairedState = loaded.state;
32
33
  let repairStateNeedsPersist = false;
33
34
  const servicePaths = resolveManagedServicePaths(context.dataDir, repairedState.walletRootId);
35
+ await reportRepairProgress(context, "lock-cleanup", "Checking repair locks...");
34
36
  await clearOrphanedRepairLocks([
35
37
  servicePaths.bitcoindLockPath,
36
38
  servicePaths.indexerDaemonLockPath,
37
39
  ]);
40
+ await reportRepairProgress(context, "mining-cleanup", "Checking mining runtime...");
38
41
  const preRepairMiningRuntime = await loadMiningRuntimeStatus(context.paths.miningStatusPath).catch(() => null);
39
42
  const miningCleanup = await cleanupMiningForRepair({
40
43
  paths: context.paths,
@@ -48,6 +51,7 @@ export async function repairWallet(options) {
48
51
  repairStateNeedsPersist = true;
49
52
  }
50
53
  if (!context.assumeYes) {
54
+ await reportRepairProgress(context, "indexer-database-check", "Checking local indexer database...");
51
55
  await ensureIndexerDatabaseHealthy({
52
56
  databasePath: context.databasePath,
53
57
  dataDir: context.dataDir,
@@ -64,7 +68,12 @@ export async function repairWallet(options) {
64
68
  });
65
69
  repairedState = bitcoindStage.state;
66
70
  repairStateNeedsPersist = bitcoindStage.repairStateNeedsPersist;
71
+ const repairNotes = [];
72
+ if (bitcoindStage.bitcoindPostRepairHealth === "starting") {
73
+ repairNotes.push("Managed bitcoind was restarted and is still loading the block index; rerun mining after it reaches ready.");
74
+ }
67
75
  if (recoveredFromBackup) {
76
+ await reportRepairProgress(context, "wallet-state-persist", "Persisting repaired wallet state...");
68
77
  repairedState = await persistRepairState({
69
78
  state: repairedState,
70
79
  provider: context.provider,
@@ -75,6 +84,7 @@ export async function repairWallet(options) {
75
84
  repairStateNeedsPersist = false;
76
85
  }
77
86
  else if (repairStateNeedsPersist) {
87
+ await reportRepairProgress(context, "wallet-state-persist", "Persisting repaired wallet state...");
78
88
  repairedState = await persistRepairState({
79
89
  state: repairedState,
80
90
  provider: context.provider,
@@ -83,11 +93,25 @@ export async function repairWallet(options) {
83
93
  });
84
94
  repairStateNeedsPersist = false;
85
95
  }
86
- const indexerStage = await repairManagedIndexerStage({
87
- context,
88
- servicePaths,
89
- state: repairedState,
90
- });
96
+ const indexerStage = bitcoindStage.bitcoindPostRepairHealth === "starting"
97
+ ? {
98
+ resetIndexerDatabase: false,
99
+ indexerDaemonAction: "none",
100
+ indexerCompatibilityIssue: "none",
101
+ indexerPostRepairHealth: "starting",
102
+ }
103
+ : await (async () => {
104
+ await reportRepairProgress(context, "indexer-check", "Checking indexer...");
105
+ return repairManagedIndexerStage({
106
+ context,
107
+ servicePaths,
108
+ state: repairedState,
109
+ });
110
+ })();
111
+ if (indexerStage.resetIndexerDatabase) {
112
+ repairNotes.push("Indexer artifacts were reset and may still be catching up.");
113
+ }
114
+ await reportRepairProgress(context, "mining-resume", "Checking whether mining should resume...");
91
115
  const miningResume = await resumeBackgroundMiningAfterRepair({
92
116
  miningPreRepairRunMode,
93
117
  provider: context.provider,
@@ -113,9 +137,7 @@ export async function repairWallet(options) {
113
137
  miningResumeAction: miningResume.miningResumeAction,
114
138
  miningPostRepairRunMode: miningResume.miningPostRepairRunMode,
115
139
  miningResumeError: miningResume.miningResumeError,
116
- note: indexerStage.resetIndexerDatabase
117
- ? "Indexer artifacts were reset and may still be catching up."
118
- : null,
140
+ note: repairNotes.length > 0 ? repairNotes.join(" ") : null,
119
141
  };
120
142
  }
121
143
  finally {
@@ -49,6 +49,11 @@ export interface WalletRepairResult {
49
49
  miningResumeError: string | null;
50
50
  note: string | null;
51
51
  }
52
+ export interface WalletRepairProgressEvent {
53
+ code: string;
54
+ message: string;
55
+ }
56
+ export type WalletRepairProgressReporter = (event: WalletRepairProgressEvent) => void | Promise<void>;
52
57
  export interface WalletLifecycleRpcClient {
53
58
  getDescriptorInfo(descriptor: string): Promise<{
54
59
  descriptor: string;
@@ -126,6 +131,7 @@ export interface WalletRepairDependencies extends WalletManagedCoreDependencies
126
131
  attachIndexerDaemon?: typeof attachOrStartIndexerDaemon;
127
132
  probeIndexerDaemon?: typeof probeIndexerDaemon;
128
133
  requestMiningPreemption?: typeof requestMiningGenerationPreemption;
134
+ progress?: WalletRepairProgressReporter;
129
135
  }
130
136
  export interface WalletRepairContext extends WalletManagedCoreContext {
131
137
  dataDir: string;
@@ -135,6 +141,7 @@ export interface WalletRepairContext extends WalletManagedCoreContext {
135
141
  attachIndexerDaemon: NonNullable<WalletRepairDependencies["attachIndexerDaemon"]>;
136
142
  probeIndexerDaemon: NonNullable<WalletRepairDependencies["probeIndexerDaemon"]>;
137
143
  requestMiningPreemption?: WalletRepairDependencies["requestMiningPreemption"];
144
+ progress: WalletRepairProgressReporter;
138
145
  }
139
146
  export interface WalletBitcoindRepairStageResult {
140
147
  state: WalletStateV1;
@@ -673,7 +673,7 @@ export async function runCompetitivenessGate(options) {
673
673
  });
674
674
  options.throwIfStopping?.();
675
675
  const txid = visibleTxids[index];
676
- const context = cacheState.rawTxContexts.get(txid);
676
+ const context = rawTxContexts.get(txid);
677
677
  const mempoolEntry = mempoolEntries[txid];
678
678
  if (context === undefined || context.payload === null || context.senderScriptHex === null || mempoolEntry === undefined) {
679
679
  continue;
@@ -685,7 +685,7 @@ export async function runCompetitivenessGate(options) {
685
685
  const overlayDomain = await resolveOverlayAuthorizedMiningDomain({
686
686
  readContext: options.readContext,
687
687
  txid,
688
- txContexts: cacheState.rawTxContexts,
688
+ txContexts: rawTxContexts,
689
689
  domainId: decoded.domainId,
690
690
  senderScriptHex: context.senderScriptHex,
691
691
  });
@@ -159,6 +159,10 @@ export async function handleRecoverableMiningBitcoindFailure(options) {
159
159
  rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
160
160
  options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
161
161
  }
162
+ else if (probe.compatibility === "starting") {
163
+ rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
164
+ options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
165
+ }
162
166
  else if (probe.compatibility === "unreachable") {
163
167
  const identityChanged = rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
164
168
  const livePid = isMiningBitcoindRecoveryPidAlive(probe.status?.processId ?? null);