@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.
- package/README.md +4 -5
- package/dist/bitcoind/progress/tty-renderer.js +3 -2
- package/dist/bitcoind/service.js +1 -1
- package/dist/cli/command-registry.d.ts +39 -0
- package/dist/cli/command-registry.js +1132 -0
- package/dist/cli/commands/client-admin.js +6 -56
- package/dist/cli/commands/mining-admin.js +9 -32
- package/dist/cli/commands/mining-read.js +15 -56
- package/dist/cli/commands/mining-runtime.js +258 -57
- package/dist/cli/commands/service-runtime.js +1 -64
- package/dist/cli/commands/status.js +2 -15
- package/dist/cli/commands/update.js +6 -21
- package/dist/cli/commands/wallet-admin.js +18 -120
- package/dist/cli/commands/wallet-mutation.js +4 -7
- package/dist/cli/commands/wallet-read.js +31 -138
- package/dist/cli/context.js +2 -4
- package/dist/cli/mining-format.js +8 -2
- package/dist/cli/mutation-command-groups.d.ts +11 -11
- package/dist/cli/mutation-command-groups.js +9 -18
- package/dist/cli/mutation-json.d.ts +1 -17
- package/dist/cli/mutation-json.js +1 -28
- package/dist/cli/mutation-success.d.ts +0 -1
- package/dist/cli/mutation-success.js +0 -19
- package/dist/cli/output.d.ts +1 -10
- package/dist/cli/output.js +52 -481
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +38 -695
- package/dist/cli/runner.js +28 -113
- package/dist/cli/types.d.ts +7 -8
- package/dist/cli/update-notifier.js +1 -1
- package/dist/cli/wallet-format.js +1 -1
- package/dist/wallet/lifecycle/managed-core.d.ts +23 -0
- package/dist/wallet/lifecycle/managed-core.js +257 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
- package/dist/wallet/lifecycle/repair-mining.js +304 -0
- package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
- package/dist/wallet/lifecycle/repair-runtime.js +206 -0
- package/dist/wallet/lifecycle/repair.d.ts +11 -0
- package/dist/wallet/lifecycle/repair.js +368 -0
- package/dist/wallet/lifecycle/setup.d.ts +16 -0
- package/dist/wallet/lifecycle/setup.js +430 -0
- package/dist/wallet/lifecycle/types.d.ts +125 -0
- package/dist/wallet/lifecycle/types.js +1 -0
- package/dist/wallet/lifecycle.d.ts +4 -165
- package/dist/wallet/lifecycle.js +3 -1656
- package/dist/wallet/mining/candidate.d.ts +60 -0
- package/dist/wallet/mining/candidate.js +290 -0
- package/dist/wallet/mining/competitiveness.d.ts +22 -0
- package/dist/wallet/mining/competitiveness.js +640 -0
- package/dist/wallet/mining/control.js +7 -251
- package/dist/wallet/mining/cycle.d.ts +39 -0
- package/dist/wallet/mining/cycle.js +542 -0
- package/dist/wallet/mining/engine-state.d.ts +66 -0
- package/dist/wallet/mining/engine-state.js +211 -0
- package/dist/wallet/mining/engine-types.d.ts +173 -0
- package/dist/wallet/mining/engine-types.js +1 -0
- package/dist/wallet/mining/engine-utils.d.ts +7 -0
- package/dist/wallet/mining/engine-utils.js +75 -0
- package/dist/wallet/mining/events.d.ts +2 -0
- package/dist/wallet/mining/events.js +19 -0
- package/dist/wallet/mining/lifecycle.d.ts +71 -0
- package/dist/wallet/mining/lifecycle.js +355 -0
- package/dist/wallet/mining/projection.d.ts +61 -0
- package/dist/wallet/mining/projection.js +319 -0
- package/dist/wallet/mining/publish.d.ts +79 -0
- package/dist/wallet/mining/publish.js +614 -0
- package/dist/wallet/mining/runner.d.ts +12 -418
- package/dist/wallet/mining/runner.js +274 -3433
- package/dist/wallet/mining/supervisor.d.ts +134 -0
- package/dist/wallet/mining/supervisor.js +558 -0
- package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
- package/dist/wallet/mining/visualizer-sync.js +166 -0
- package/dist/wallet/mining/visualizer.d.ts +1 -0
- package/dist/wallet/mining/visualizer.js +33 -18
- package/dist/wallet/reset.d.ts +1 -1
- package/dist/wallet/reset.js +35 -11
- package/dist/wallet/runtime.d.ts +0 -6
- package/dist/wallet/runtime.js +2 -38
- package/dist/wallet/tx/common.d.ts +18 -0
- package/dist/wallet/tx/common.js +40 -26
- package/package.json +1 -1
- package/dist/wallet/state/seed-index.d.ts +0 -43
- package/dist/wallet/state/seed-index.js +0 -151
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readLockMetadata } from "../fs/lock.js";
|
|
4
|
+
import { readMiningGenerationActivity } from "../mining/coordination.js";
|
|
5
|
+
import { loadClientConfig } from "../mining/config.js";
|
|
6
|
+
import { saveMiningRuntimeStatus } from "../mining/runtime-artifacts.js";
|
|
7
|
+
import { normalizeMiningStateRecord } from "../mining/state.js";
|
|
8
|
+
import { createWalletSecretReference } from "../state/provider.js";
|
|
9
|
+
import { persistWalletStateUpdate } from "../descriptor-normalization.js";
|
|
10
|
+
import { isProcessAlive, stopRecordedManagedProcess } from "./repair-runtime.js";
|
|
11
|
+
export function createSilentNonInteractivePrompter() {
|
|
12
|
+
return {
|
|
13
|
+
isInteractive: false,
|
|
14
|
+
writeLine() { },
|
|
15
|
+
async prompt() {
|
|
16
|
+
return "";
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function applyRepairStoppedMiningState(state) {
|
|
21
|
+
const miningState = normalizeMiningStateRecord(state.miningState);
|
|
22
|
+
return {
|
|
23
|
+
...state,
|
|
24
|
+
miningState: {
|
|
25
|
+
...miningState,
|
|
26
|
+
runMode: "stopped",
|
|
27
|
+
state: miningState.livePublishInMempool
|
|
28
|
+
? miningState.state === "paused-stale"
|
|
29
|
+
? "paused-stale"
|
|
30
|
+
: "paused"
|
|
31
|
+
: miningState.state === "repair-required"
|
|
32
|
+
? "repair-required"
|
|
33
|
+
: "idle",
|
|
34
|
+
pauseReason: miningState.livePublishInMempool
|
|
35
|
+
? miningState.state === "paused-stale"
|
|
36
|
+
? "stale-block-context"
|
|
37
|
+
: "wallet-repair"
|
|
38
|
+
: miningState.state === "repair-required"
|
|
39
|
+
? miningState.pauseReason
|
|
40
|
+
: null,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createStoppedBackgroundRuntimeSnapshot(snapshot, nowUnixMs) {
|
|
45
|
+
return {
|
|
46
|
+
...snapshot,
|
|
47
|
+
updatedAtUnixMs: nowUnixMs,
|
|
48
|
+
runMode: "stopped",
|
|
49
|
+
backgroundWorkerPid: null,
|
|
50
|
+
backgroundWorkerRunId: null,
|
|
51
|
+
backgroundWorkerHeartbeatAtUnixMs: null,
|
|
52
|
+
backgroundWorkerHealth: null,
|
|
53
|
+
currentPhase: "idle",
|
|
54
|
+
note: snapshot.livePublishInMempool
|
|
55
|
+
? "Background mining stopped for wallet repair. The last mining transaction may still confirm from mempool."
|
|
56
|
+
: "Background mining stopped for wallet repair.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function resolveMiningGenerationRequestPath(paths) {
|
|
60
|
+
return join(paths.miningRoot, "generation-request.json");
|
|
61
|
+
}
|
|
62
|
+
function resolveMiningGenerationActivityPath(paths) {
|
|
63
|
+
return join(paths.miningRoot, "generation-activity.json");
|
|
64
|
+
}
|
|
65
|
+
function normalizeRepairMiningPid(value) {
|
|
66
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
67
|
+
? value
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
function createRepairStoppedMiningNote(livePublishInMempool) {
|
|
71
|
+
return livePublishInMempool
|
|
72
|
+
? "Background mining stopped for wallet repair. The last mining transaction may still confirm from mempool."
|
|
73
|
+
: "Background mining stopped for wallet repair.";
|
|
74
|
+
}
|
|
75
|
+
export function createStoppedMiningRuntimeSnapshotForRepair(options) {
|
|
76
|
+
const stoppedMiningState = normalizeMiningStateRecord(applyRepairStoppedMiningState(options.state).miningState);
|
|
77
|
+
const note = createRepairStoppedMiningNote(stoppedMiningState.livePublishInMempool);
|
|
78
|
+
if (options.snapshot !== null) {
|
|
79
|
+
return {
|
|
80
|
+
...createStoppedBackgroundRuntimeSnapshot(options.snapshot, options.nowUnixMs),
|
|
81
|
+
miningState: stoppedMiningState.state,
|
|
82
|
+
currentPublishState: stoppedMiningState.currentPublishState,
|
|
83
|
+
targetBlockHeight: stoppedMiningState.currentBlockTargetHeight,
|
|
84
|
+
referencedBlockHashDisplay: stoppedMiningState.currentReferencedBlockHashDisplay,
|
|
85
|
+
currentDomainId: stoppedMiningState.currentDomainId,
|
|
86
|
+
currentDomainName: stoppedMiningState.currentDomain,
|
|
87
|
+
currentSentenceDisplay: stoppedMiningState.currentSentence,
|
|
88
|
+
currentTxid: stoppedMiningState.currentTxid,
|
|
89
|
+
currentWtxid: stoppedMiningState.currentWtxid,
|
|
90
|
+
livePublishInMempool: stoppedMiningState.livePublishInMempool,
|
|
91
|
+
currentFeeRateSatVb: stoppedMiningState.currentFeeRateSatVb,
|
|
92
|
+
currentAbsoluteFeeSats: stoppedMiningState.currentAbsoluteFeeSats,
|
|
93
|
+
currentBlockFeeSpentSats: stoppedMiningState.currentBlockFeeSpentSats,
|
|
94
|
+
sessionFeeSpentSats: stoppedMiningState.sessionFeeSpentSats,
|
|
95
|
+
lifetimeFeeSpentSats: stoppedMiningState.lifetimeFeeSpentSats,
|
|
96
|
+
currentPublishDecision: stoppedMiningState.currentPublishDecision,
|
|
97
|
+
pauseReason: stoppedMiningState.pauseReason,
|
|
98
|
+
note,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
walletRootId: options.state.walletRootId,
|
|
104
|
+
workerApiVersion: null,
|
|
105
|
+
workerBinaryVersion: null,
|
|
106
|
+
workerBuildId: null,
|
|
107
|
+
updatedAtUnixMs: options.nowUnixMs,
|
|
108
|
+
runMode: "stopped",
|
|
109
|
+
backgroundWorkerPid: null,
|
|
110
|
+
backgroundWorkerRunId: null,
|
|
111
|
+
backgroundWorkerHeartbeatAtUnixMs: null,
|
|
112
|
+
backgroundWorkerHealth: null,
|
|
113
|
+
indexerDaemonState: null,
|
|
114
|
+
indexerDaemonInstanceId: null,
|
|
115
|
+
indexerSnapshotSeq: null,
|
|
116
|
+
indexerSnapshotOpenedAtUnixMs: null,
|
|
117
|
+
indexerTruthSource: undefined,
|
|
118
|
+
indexerHeartbeatAtUnixMs: null,
|
|
119
|
+
coreBestHeight: null,
|
|
120
|
+
coreBestHash: null,
|
|
121
|
+
indexerTipHeight: null,
|
|
122
|
+
indexerTipHash: null,
|
|
123
|
+
indexerReorgDepth: null,
|
|
124
|
+
indexerTipAligned: null,
|
|
125
|
+
corePublishState: null,
|
|
126
|
+
providerState: null,
|
|
127
|
+
lastSuspendDetectedAtUnixMs: null,
|
|
128
|
+
reconnectSettledUntilUnixMs: null,
|
|
129
|
+
tipSettledUntilUnixMs: null,
|
|
130
|
+
miningState: stoppedMiningState.state,
|
|
131
|
+
currentPhase: "idle",
|
|
132
|
+
currentPublishState: stoppedMiningState.currentPublishState,
|
|
133
|
+
targetBlockHeight: stoppedMiningState.currentBlockTargetHeight,
|
|
134
|
+
referencedBlockHashDisplay: stoppedMiningState.currentReferencedBlockHashDisplay,
|
|
135
|
+
currentDomainId: stoppedMiningState.currentDomainId,
|
|
136
|
+
currentDomainName: stoppedMiningState.currentDomain,
|
|
137
|
+
currentSentenceDisplay: stoppedMiningState.currentSentence,
|
|
138
|
+
currentCanonicalBlend: null,
|
|
139
|
+
currentTxid: stoppedMiningState.currentTxid,
|
|
140
|
+
currentWtxid: stoppedMiningState.currentWtxid,
|
|
141
|
+
livePublishInMempool: stoppedMiningState.livePublishInMempool,
|
|
142
|
+
currentFeeRateSatVb: stoppedMiningState.currentFeeRateSatVb,
|
|
143
|
+
currentAbsoluteFeeSats: stoppedMiningState.currentAbsoluteFeeSats,
|
|
144
|
+
currentBlockFeeSpentSats: stoppedMiningState.currentBlockFeeSpentSats,
|
|
145
|
+
sessionFeeSpentSats: stoppedMiningState.sessionFeeSpentSats,
|
|
146
|
+
lifetimeFeeSpentSats: stoppedMiningState.lifetimeFeeSpentSats,
|
|
147
|
+
sameDomainCompetitorSuppressed: null,
|
|
148
|
+
higherRankedCompetitorDomainCount: null,
|
|
149
|
+
dedupedCompetitorDomainCount: null,
|
|
150
|
+
competitivenessGateIndeterminate: null,
|
|
151
|
+
mempoolSequenceCacheStatus: null,
|
|
152
|
+
currentPublishDecision: stoppedMiningState.currentPublishDecision,
|
|
153
|
+
lastMempoolSequence: null,
|
|
154
|
+
lastCompetitivenessGateAtUnixMs: null,
|
|
155
|
+
pauseReason: stoppedMiningState.pauseReason,
|
|
156
|
+
providerConfigured: false,
|
|
157
|
+
providerKind: null,
|
|
158
|
+
bitcoindHealth: "unavailable",
|
|
159
|
+
bitcoindServiceState: null,
|
|
160
|
+
bitcoindReplicaStatus: null,
|
|
161
|
+
nodeHealth: "unavailable",
|
|
162
|
+
indexerHealth: "unavailable",
|
|
163
|
+
tipsAligned: null,
|
|
164
|
+
lastEventAtUnixMs: null,
|
|
165
|
+
lastError: null,
|
|
166
|
+
note,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export async function persistRepairState(options) {
|
|
170
|
+
return await persistWalletStateUpdate({
|
|
171
|
+
state: options.state,
|
|
172
|
+
access: {
|
|
173
|
+
provider: options.provider,
|
|
174
|
+
secretReference: createWalletSecretReference(options.state.walletRootId),
|
|
175
|
+
},
|
|
176
|
+
paths: options.paths,
|
|
177
|
+
nowUnixMs: options.nowUnixMs,
|
|
178
|
+
replacePrimary: options.replacePrimary,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
export async function cleanupMiningForRepair(options) {
|
|
182
|
+
const controlLockMetadata = await readLockMetadata(options.paths.miningControlLockPath).catch(() => null);
|
|
183
|
+
const generationActivity = await readMiningGenerationActivity(options.paths).catch(() => null);
|
|
184
|
+
const controlLockPid = normalizeRepairMiningPid(controlLockMetadata?.processId);
|
|
185
|
+
const backgroundWorkerPid = normalizeRepairMiningPid(options.snapshot?.backgroundWorkerPid);
|
|
186
|
+
const generationOwnerPid = normalizeRepairMiningPid(generationActivity?.generationOwnerPid);
|
|
187
|
+
const discoveredPids = new Set();
|
|
188
|
+
let backgroundWorkerAlive = false;
|
|
189
|
+
let foregroundWorkerAlive = false;
|
|
190
|
+
const pidSources = [
|
|
191
|
+
{ pid: backgroundWorkerPid, source: "background" },
|
|
192
|
+
{ pid: controlLockPid, source: "foreground" },
|
|
193
|
+
{ pid: generationOwnerPid, source: "foreground" },
|
|
194
|
+
];
|
|
195
|
+
for (const source of pidSources) {
|
|
196
|
+
if (source.pid === null || source.pid === process.pid || !await isProcessAlive(source.pid)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
discoveredPids.add(source.pid);
|
|
200
|
+
if (source.source === "background") {
|
|
201
|
+
backgroundWorkerAlive = true;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
foregroundWorkerAlive = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const pid of discoveredPids) {
|
|
208
|
+
await stopRecordedManagedProcess(pid, "mining_process_stop_timeout");
|
|
209
|
+
}
|
|
210
|
+
await rm(options.paths.miningControlLockPath, { force: true }).catch(() => undefined);
|
|
211
|
+
await rm(resolveMiningGenerationRequestPath(options.paths), { force: true }).catch(() => undefined);
|
|
212
|
+
await rm(resolveMiningGenerationActivityPath(options.paths), { force: true }).catch(() => undefined);
|
|
213
|
+
await saveMiningRuntimeStatus(options.paths.miningStatusPath, createStoppedMiningRuntimeSnapshotForRepair({
|
|
214
|
+
state: options.state,
|
|
215
|
+
snapshot: options.snapshot,
|
|
216
|
+
nowUnixMs: options.nowUnixMs,
|
|
217
|
+
}));
|
|
218
|
+
return {
|
|
219
|
+
preRepairRunMode: backgroundWorkerAlive
|
|
220
|
+
? "background"
|
|
221
|
+
: foregroundWorkerAlive
|
|
222
|
+
? "foreground"
|
|
223
|
+
: "stopped",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export async function canResumeBackgroundMiningAfterRepair(options) {
|
|
227
|
+
if (options.bitcoindPostRepairHealth !== "ready"
|
|
228
|
+
|| options.indexerPostRepairHealth !== "synced"
|
|
229
|
+
|| normalizeMiningStateRecord(options.repairedState.miningState).state === "repair-required") {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const config = await loadClientConfig({
|
|
234
|
+
path: options.paths.clientConfigPath,
|
|
235
|
+
provider: options.provider,
|
|
236
|
+
});
|
|
237
|
+
return config?.mining.builtIn != null;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export async function resumeBackgroundMiningAfterRepair(options) {
|
|
244
|
+
const miningWasResumable = options.miningPreRepairRunMode === "background"
|
|
245
|
+
&& normalizeMiningStateRecord(options.repairedState.miningState).state !== "repair-required";
|
|
246
|
+
if (options.miningPreRepairRunMode !== "background") {
|
|
247
|
+
return {
|
|
248
|
+
miningResumeAction: "none",
|
|
249
|
+
miningPostRepairRunMode: "stopped",
|
|
250
|
+
miningResumeError: null,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (!miningWasResumable) {
|
|
254
|
+
return {
|
|
255
|
+
miningResumeAction: "skipped-not-resumable",
|
|
256
|
+
miningPostRepairRunMode: "stopped",
|
|
257
|
+
miningResumeError: null,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const postRepairResumeReady = await canResumeBackgroundMiningAfterRepair({
|
|
261
|
+
provider: options.provider,
|
|
262
|
+
paths: options.paths,
|
|
263
|
+
repairedState: options.repairedState,
|
|
264
|
+
bitcoindPostRepairHealth: options.bitcoindPostRepairHealth,
|
|
265
|
+
indexerPostRepairHealth: options.indexerPostRepairHealth,
|
|
266
|
+
});
|
|
267
|
+
if (!postRepairResumeReady) {
|
|
268
|
+
return {
|
|
269
|
+
miningResumeAction: "skipped-post-repair-blocked",
|
|
270
|
+
miningPostRepairRunMode: "stopped",
|
|
271
|
+
miningResumeError: null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const startBackgroundMining = options.startBackgroundMining
|
|
276
|
+
?? (await import("../mining/runner.js")).startBackgroundMining;
|
|
277
|
+
const resumed = await startBackgroundMining({
|
|
278
|
+
dataDir: options.dataDir,
|
|
279
|
+
databasePath: options.databasePath,
|
|
280
|
+
provider: options.provider,
|
|
281
|
+
paths: options.paths,
|
|
282
|
+
prompter: createSilentNonInteractivePrompter(),
|
|
283
|
+
});
|
|
284
|
+
if (resumed.snapshot?.runMode === "background") {
|
|
285
|
+
return {
|
|
286
|
+
miningResumeAction: "resumed-background",
|
|
287
|
+
miningPostRepairRunMode: "background",
|
|
288
|
+
miningResumeError: null,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
miningResumeAction: "resume-failed",
|
|
293
|
+
miningPostRepairRunMode: "stopped",
|
|
294
|
+
miningResumeError: "Background mining did not report a background runtime after repair.",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
return {
|
|
299
|
+
miningResumeAction: "resume-failed",
|
|
300
|
+
miningPostRepairRunMode: "stopped",
|
|
301
|
+
miningResumeError: error instanceof Error ? error.message : String(error),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { attachOrStartIndexerDaemon, probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
|
|
2
|
+
import { probeManagedBitcoindService } from "../../bitcoind/service.js";
|
|
3
|
+
import { resolveManagedServicePaths } from "../../bitcoind/service-paths.js";
|
|
4
|
+
import type { ManagedBitcoindServiceStatus } from "../../bitcoind/types.js";
|
|
5
|
+
import type { WalletRepairResult } from "./types.js";
|
|
6
|
+
export declare function ensureIndexerDatabaseHealthy(options: {
|
|
7
|
+
databasePath: string;
|
|
8
|
+
dataDir: string;
|
|
9
|
+
walletRootId: string;
|
|
10
|
+
resetIfNeeded: boolean;
|
|
11
|
+
}): Promise<boolean>;
|
|
12
|
+
export declare function mapIndexerCompatibilityToRepairIssue(compatibility: Awaited<ReturnType<typeof probeIndexerDaemon>>["compatibility"]): WalletRepairResult["indexerCompatibilityIssue"];
|
|
13
|
+
export declare function mapBitcoindCompatibilityToRepairIssue(compatibility: Awaited<ReturnType<typeof probeManagedBitcoindService>>["compatibility"]): WalletRepairResult["bitcoindCompatibilityIssue"];
|
|
14
|
+
export declare function mapBitcoindRepairHealth(options: {
|
|
15
|
+
serviceState: ManagedBitcoindServiceStatus["state"] | null;
|
|
16
|
+
catchingUp: boolean;
|
|
17
|
+
replica: {
|
|
18
|
+
proofStatus?: "missing" | "mismatch" | "ready" | "not-proven";
|
|
19
|
+
} | null;
|
|
20
|
+
}): WalletRepairResult["bitcoindPostRepairHealth"];
|
|
21
|
+
export declare function verifyIndexerPostRepairHealth(options: {
|
|
22
|
+
daemon: Awaited<ReturnType<typeof attachOrStartIndexerDaemon>>;
|
|
23
|
+
probeIndexerDaemon: typeof probeIndexerDaemon;
|
|
24
|
+
dataDir: string;
|
|
25
|
+
walletRootId: string;
|
|
26
|
+
nowUnixMs: number;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
health: WalletRepairResult["indexerPostRepairHealth"];
|
|
29
|
+
daemonInstanceId: string;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function isProcessAlive(pid: number | null): Promise<boolean>;
|
|
32
|
+
export declare function waitForProcessExit(pid: number, timeoutMs?: number, errorCode?: string): Promise<void>;
|
|
33
|
+
export declare function clearIndexerDaemonArtifacts(servicePaths: ReturnType<typeof resolveManagedServicePaths>): Promise<void>;
|
|
34
|
+
export declare function clearManagedBitcoindArtifacts(servicePaths: ReturnType<typeof resolveManagedServicePaths>): Promise<void>;
|
|
35
|
+
export declare function stopRecordedManagedProcess(pid: number | null, errorCode: string): Promise<void>;
|
|
36
|
+
export declare function clearOrphanedRepairLocks(lockPaths: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { attachOrStartIndexerDaemon, probeIndexerDaemon, readSnapshotWithRetry, } from "../../bitcoind/indexer-daemon.js";
|
|
4
|
+
import { probeManagedBitcoindService } from "../../bitcoind/service.js";
|
|
5
|
+
import { resolveManagedServicePaths } from "../../bitcoind/service-paths.js";
|
|
6
|
+
import { openClient } from "../../client.js";
|
|
7
|
+
import { openSqliteStore } from "../../sqlite/index.js";
|
|
8
|
+
import { clearOrphanedFileLock } from "../fs/lock.js";
|
|
9
|
+
async function pathExists(path) {
|
|
10
|
+
try {
|
|
11
|
+
await access(path, constants.F_OK);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function ensureIndexerDatabaseHealthy(options) {
|
|
19
|
+
try {
|
|
20
|
+
if (await pathExists(options.databasePath)) {
|
|
21
|
+
const header = await readFile(options.databasePath).then((buffer) => buffer.subarray(0, 16).toString("utf8"));
|
|
22
|
+
if (header.length > 0 && header !== "SQLite format 3\u0000") {
|
|
23
|
+
throw new Error("indexer_database_not_sqlite");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const store = await openSqliteStore({ filename: options.databasePath });
|
|
27
|
+
try {
|
|
28
|
+
const client = await openClient({ store });
|
|
29
|
+
try {
|
|
30
|
+
await client.getTip();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await client.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await store.close();
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
if (!options.resetIfNeeded) {
|
|
43
|
+
throw new Error("wallet_repair_indexer_reset_requires_yes");
|
|
44
|
+
}
|
|
45
|
+
await rm(options.databasePath, { force: true }).catch(() => undefined);
|
|
46
|
+
await rm(`${options.databasePath}-wal`, { force: true }).catch(() => undefined);
|
|
47
|
+
await rm(`${options.databasePath}-shm`, { force: true }).catch(() => undefined);
|
|
48
|
+
await mkdir(dirname(options.databasePath), { recursive: true });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function mapIndexerCompatibilityToRepairIssue(compatibility) {
|
|
53
|
+
switch (compatibility) {
|
|
54
|
+
case "service-version-mismatch":
|
|
55
|
+
return "service-version-mismatch";
|
|
56
|
+
case "wallet-root-mismatch":
|
|
57
|
+
return "wallet-root-mismatch";
|
|
58
|
+
case "schema-mismatch":
|
|
59
|
+
return "schema-mismatch";
|
|
60
|
+
default:
|
|
61
|
+
return "none";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function mapBitcoindCompatibilityToRepairIssue(compatibility) {
|
|
65
|
+
switch (compatibility) {
|
|
66
|
+
case "service-version-mismatch":
|
|
67
|
+
return "service-version-mismatch";
|
|
68
|
+
case "wallet-root-mismatch":
|
|
69
|
+
return "wallet-root-mismatch";
|
|
70
|
+
case "runtime-mismatch":
|
|
71
|
+
return "runtime-mismatch";
|
|
72
|
+
default:
|
|
73
|
+
return "none";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function mapBitcoindRepairHealth(options) {
|
|
77
|
+
if (options.serviceState === null) {
|
|
78
|
+
return "unavailable";
|
|
79
|
+
}
|
|
80
|
+
if (options.serviceState === "starting" || options.serviceState === "stopping") {
|
|
81
|
+
return "starting";
|
|
82
|
+
}
|
|
83
|
+
if (options.serviceState !== "ready") {
|
|
84
|
+
return "failed";
|
|
85
|
+
}
|
|
86
|
+
if (options.replica?.proofStatus === "missing" || options.replica?.proofStatus === "mismatch") {
|
|
87
|
+
return "failed";
|
|
88
|
+
}
|
|
89
|
+
if (options.catchingUp) {
|
|
90
|
+
return "catching-up";
|
|
91
|
+
}
|
|
92
|
+
return "ready";
|
|
93
|
+
}
|
|
94
|
+
function mapLeaseStateToRepairHealth(state) {
|
|
95
|
+
switch (state) {
|
|
96
|
+
case "synced":
|
|
97
|
+
return "synced";
|
|
98
|
+
case "catching-up":
|
|
99
|
+
case "reorging":
|
|
100
|
+
return "catching-up";
|
|
101
|
+
case "starting":
|
|
102
|
+
case "stopping":
|
|
103
|
+
return "starting";
|
|
104
|
+
default:
|
|
105
|
+
return "failed";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const INDEXER_DAEMON_HEARTBEAT_STALE_MS = 15_000;
|
|
109
|
+
export async function verifyIndexerPostRepairHealth(options) {
|
|
110
|
+
try {
|
|
111
|
+
const lease = await readSnapshotWithRetry(options.daemon, options.walletRootId);
|
|
112
|
+
return {
|
|
113
|
+
health: mapLeaseStateToRepairHealth(lease.status.state),
|
|
114
|
+
daemonInstanceId: lease.status.daemonInstanceId,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (leaseError) {
|
|
118
|
+
const probe = await options.probeIndexerDaemon({
|
|
119
|
+
dataDir: options.dataDir,
|
|
120
|
+
walletRootId: options.walletRootId,
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
if (probe.compatibility === "compatible"
|
|
124
|
+
&& probe.status !== null
|
|
125
|
+
&& (options.nowUnixMs - probe.status.heartbeatAtUnixMs) <= INDEXER_DAEMON_HEARTBEAT_STALE_MS
|
|
126
|
+
&& (probe.status.state === "starting" || probe.status.state === "catching-up" || probe.status.state === "reorging")) {
|
|
127
|
+
return {
|
|
128
|
+
health: mapLeaseStateToRepairHealth(probe.status.state),
|
|
129
|
+
daemonInstanceId: probe.status.daemonInstanceId,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await probe.client?.close().catch(() => undefined);
|
|
135
|
+
}
|
|
136
|
+
throw leaseError;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function isProcessAlive(pid) {
|
|
140
|
+
if (pid === null) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
process.kill(pid, 0);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export async function waitForProcessExit(pid, timeoutMs = 15_000, errorCode = "indexer_daemon_stop_timeout") {
|
|
155
|
+
const deadline = Date.now() + timeoutMs;
|
|
156
|
+
while (Date.now() < deadline) {
|
|
157
|
+
if (!await isProcessAlive(pid)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
161
|
+
}
|
|
162
|
+
throw new Error(errorCode);
|
|
163
|
+
}
|
|
164
|
+
export async function clearIndexerDaemonArtifacts(servicePaths) {
|
|
165
|
+
await rm(servicePaths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
|
|
166
|
+
await rm(servicePaths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
167
|
+
}
|
|
168
|
+
export async function clearManagedBitcoindArtifacts(servicePaths) {
|
|
169
|
+
await rm(servicePaths.bitcoindStatusPath, { force: true }).catch(() => undefined);
|
|
170
|
+
await rm(servicePaths.bitcoindPidPath, { force: true }).catch(() => undefined);
|
|
171
|
+
await rm(servicePaths.bitcoindReadyPath, { force: true }).catch(() => undefined);
|
|
172
|
+
await rm(servicePaths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
|
|
173
|
+
}
|
|
174
|
+
export async function stopRecordedManagedProcess(pid, errorCode) {
|
|
175
|
+
if (pid === null || !await isProcessAlive(pid)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, "SIGTERM");
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
await waitForProcessExit(pid, 5_000, errorCode);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
try {
|
|
192
|
+
process.kill(pid, "SIGKILL");
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
await waitForProcessExit(pid, 5_000, errorCode);
|
|
201
|
+
}
|
|
202
|
+
export async function clearOrphanedRepairLocks(lockPaths) {
|
|
203
|
+
for (const lockPath of lockPaths) {
|
|
204
|
+
await clearOrphanedFileLock(lockPath, isProcessAlive);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type WalletRuntimePaths } from "../runtime.js";
|
|
2
|
+
import { type WalletSecretProvider } from "../state/provider.js";
|
|
3
|
+
import type { WalletRepairDependencies, WalletRepairResult } from "./types.js";
|
|
4
|
+
export declare function repairWallet(options: {
|
|
5
|
+
dataDir: string;
|
|
6
|
+
databasePath: string;
|
|
7
|
+
provider?: WalletSecretProvider;
|
|
8
|
+
assumeYes?: boolean;
|
|
9
|
+
nowUnixMs?: number;
|
|
10
|
+
paths?: WalletRuntimePaths;
|
|
11
|
+
} & WalletRepairDependencies): Promise<WalletRepairResult>;
|