@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.
- package/README.md +12 -2
- package/dist/bitcoind/managed-bitcoind-service-config.d.ts +5 -1
- package/dist/bitcoind/managed-bitcoind-service-config.js +27 -18
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +46 -3
- package/dist/bitcoind/managed-bitcoind-service-status.d.ts +9 -2
- package/dist/bitcoind/managed-bitcoind-service-status.js +65 -9
- package/dist/bitcoind/managed-bitcoind-service-types.d.ts +8 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +27 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +6 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +2 -2
- package/dist/bitcoind/retryable-rpc.d.ts +1 -0
- package/dist/bitcoind/retryable-rpc.js +19 -2
- package/dist/cli/command-registry.js +2 -2
- package/dist/cli/commands/service-runtime.js +22 -2
- package/dist/cli/commands/status.js +7 -1
- package/dist/cli/commands/wallet-admin.js +8 -2
- package/dist/cli/context.js +2 -0
- package/dist/cli/output/rules/cli-surface.js +7 -0
- package/dist/cli/output/rules/services.js +7 -0
- package/dist/cli/parse.js +9 -0
- package/dist/cli/status-format.d.ts +2 -2
- package/dist/cli/status-format.js +167 -28
- package/dist/cli/types.d.ts +3 -0
- package/dist/passive-status.d.ts +49 -1
- package/dist/passive-status.js +208 -2
- package/dist/wallet/lifecycle/context.js +1 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +44 -1
- package/dist/wallet/lifecycle/repair-runtime.d.ts +3 -1
- package/dist/wallet/lifecycle/repair-runtime.js +12 -0
- package/dist/wallet/lifecycle/repair.js +31 -9
- package/dist/wallet/lifecycle/types.d.ts +7 -0
- package/dist/wallet/mining/competitiveness.js +2 -2
- package/dist/wallet/mining/lifecycle.js +4 -0
- package/dist/wallet/mining/mempool-index.js +29 -3
- package/dist/wallet/read/managed-bitcoind.js +9 -0
- package/package.json +1 -1
package/dist/passive-status.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
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 =
|
|
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:
|
|
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);
|