@cogcoin/client 1.2.3 → 1.2.4
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 +5 -1
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +43 -2
- package/dist/bitcoind/managed-bitcoind-service-status.js +5 -2
- 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/commands/wallet-admin.js +8 -2
- package/dist/cli/output/rules/services.js +7 -0
- package/dist/wallet/lifecycle/context.js +1 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +40 -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/lifecycle.js +4 -0
- package/dist/wallet/read/managed-bitcoind.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.2.
|
|
3
|
+
`@cogcoin/client@1.2.4` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
@@ -126,6 +126,10 @@ The published package depends on:
|
|
|
126
126
|
|
|
127
127
|
`@cogcoin/vectors@1.0.1` is kept as a repository development dependency for conformance tests and is not part of the published runtime dependency surface.
|
|
128
128
|
|
|
129
|
+
## Upgrade Notes For `1.2.4`
|
|
130
|
+
|
|
131
|
+
`@cogcoin/client@1.2.4` treats a live managed Bitcoin Core process returning startup `-28 Loading block index...` as a normal `starting` state. `cogcoin mine` waits instead of repeatedly restarting the process, and `cogcoin repair` prints progress during long checks. If repair restarts a stale managed bitcoind to add raw transaction ZMQ support and Core is still loading, repair exits with a clear note to wait and rerun `cogcoin status` or `cogcoin mine`.
|
|
132
|
+
|
|
129
133
|
## Upgrade Notes For `1.2.0`
|
|
130
134
|
|
|
131
135
|
`@cogcoin/client@1.2.0` updates the runtime indexer to `@cogcoin/indexer@1.0.2`. Existing wallet state, mining configuration, Bitcoin Core data, and secrets remain compatible and are not reset.
|
|
@@ -7,6 +7,7 @@ import { stopIndexerDaemonServiceWithLockHeld } from "./indexer-daemon.js";
|
|
|
7
7
|
import { readManagedBitcoindObservedStatus, listManagedBitcoindStatusCandidates } from "./managed-runtime/bitcoind-status.js";
|
|
8
8
|
import { attachOrStartManagedBitcoindRuntime, probeManagedBitcoindRuntime } from "./managed-runtime/bitcoind-runtime.js";
|
|
9
9
|
import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
|
|
10
|
+
import { isManagedRpcWarmupError } from "./retryable-rpc.js";
|
|
10
11
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
11
12
|
import { DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS, } from "./types.js";
|
|
12
13
|
import { buildManagedServiceArgsForTesting, LOCAL_HOST, resolveManagedBitcoindRuntimeConfig, SUPPORTED_BITCOIND_VERSION, verifyManagedBitcoindVersion, writeManagedBitcoindRuntimeConfigFile, writeManagedBitcoindRuntimeConfigFileFromStatus, writeBitcoinConfForTesting, } from "./managed-bitcoind-service-config.js";
|
|
@@ -192,6 +193,30 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
192
193
|
child.unref();
|
|
193
194
|
}
|
|
194
195
|
const rpc = createRpcClient(rpcConfig);
|
|
196
|
+
const startedAtUnixMs = Date.now();
|
|
197
|
+
const serviceInstanceId = randomBytes(16).toString("hex");
|
|
198
|
+
const startingStatus = createBitcoindServiceStatus({
|
|
199
|
+
binaryVersion,
|
|
200
|
+
serviceInstanceId,
|
|
201
|
+
state: "starting",
|
|
202
|
+
processId: child.pid ?? null,
|
|
203
|
+
walletRootId: startOptions.walletRootId,
|
|
204
|
+
chain: startOptions.chain,
|
|
205
|
+
dataDir: startOptions.dataDir,
|
|
206
|
+
runtimeRoot: paths.walletRuntimeRoot,
|
|
207
|
+
startHeight: startOptions.startHeight,
|
|
208
|
+
rpc: rpcConfig,
|
|
209
|
+
zmq: zmqConfig,
|
|
210
|
+
p2pPort: runtimeConfig.p2pPort,
|
|
211
|
+
getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
|
|
212
|
+
getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
|
|
213
|
+
walletReplica: null,
|
|
214
|
+
startedAtUnixMs,
|
|
215
|
+
heartbeatAtUnixMs: startedAtUnixMs,
|
|
216
|
+
lastError: "Managed bitcoind service is starting.",
|
|
217
|
+
});
|
|
218
|
+
await writeManagedBitcoindRuntimeConfigFile(paths.bitcoindRuntimeConfigPath, runtimeConfig);
|
|
219
|
+
await writeManagedBitcoindStatus(paths, startingStatus);
|
|
195
220
|
try {
|
|
196
221
|
await waitForManagedBitcoindRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
|
|
197
222
|
await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint, {
|
|
@@ -199,6 +224,19 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
199
224
|
});
|
|
200
225
|
}
|
|
201
226
|
catch (error) {
|
|
227
|
+
const processAlive = child.pid !== undefined && await isManagedBitcoindProcessAlive(child.pid);
|
|
228
|
+
if (startOptions.serviceLifetime !== "ephemeral"
|
|
229
|
+
&& processAlive
|
|
230
|
+
&& isManagedRpcWarmupError(error)) {
|
|
231
|
+
const nowUnixMs = Date.now();
|
|
232
|
+
await writeManagedBitcoindStatus(paths, {
|
|
233
|
+
...startingStatus,
|
|
234
|
+
heartbeatAtUnixMs: nowUnixMs,
|
|
235
|
+
updatedAtUnixMs: nowUnixMs,
|
|
236
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
237
|
+
});
|
|
238
|
+
throw new Error("managed_bitcoind_service_starting");
|
|
239
|
+
}
|
|
202
240
|
if (child.pid !== undefined) {
|
|
203
241
|
try {
|
|
204
242
|
process.kill(child.pid, "SIGTERM");
|
|
@@ -215,7 +253,7 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
215
253
|
runtimeConfig,
|
|
216
254
|
status: createBitcoindServiceStatus({
|
|
217
255
|
binaryVersion,
|
|
218
|
-
serviceInstanceId
|
|
256
|
+
serviceInstanceId,
|
|
219
257
|
state: "ready",
|
|
220
258
|
processId: child.pid ?? null,
|
|
221
259
|
walletRootId: startOptions.walletRootId,
|
|
@@ -229,7 +267,7 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
229
267
|
getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
|
|
230
268
|
getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
|
|
231
269
|
walletReplica,
|
|
232
|
-
startedAtUnixMs
|
|
270
|
+
startedAtUnixMs,
|
|
233
271
|
heartbeatAtUnixMs: nowUnixMs,
|
|
234
272
|
lastError: walletReplica.message ?? null,
|
|
235
273
|
}),
|
|
@@ -241,6 +279,9 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
241
279
|
({ runtimeConfig, status } = await startManagedProcess(runtimeOptions));
|
|
242
280
|
}
|
|
243
281
|
catch (error) {
|
|
282
|
+
if (isManagedRpcWarmupError(error)) {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
244
285
|
if (runtimeOptions.getblockArchivePath === undefined || runtimeOptions.getblockArchivePath === null) {
|
|
245
286
|
throw error;
|
|
246
287
|
}
|
|
@@ -8,6 +8,7 @@ import { MANAGED_BITCOIND_SERVICE_API_VERSION, } from "./types.js";
|
|
|
8
8
|
import { createMissingManagedWalletReplicaStatus, loadManagedWalletReplicaIfPresent, } from "./managed-bitcoind-service-replica.js";
|
|
9
9
|
import { DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS, isManagedBitcoindProcessAlive, sleep, } from "./managed-bitcoind-service-process.js";
|
|
10
10
|
import { waitForManagedBitcoindCookie, } from "./managed-bitcoind-service-config.js";
|
|
11
|
+
import { isManagedRpcWarmupError } from "./retryable-rpc.js";
|
|
11
12
|
export async function waitForManagedBitcoindRpcReady(rpc, cookieFile, expectedChain, timeoutMs) {
|
|
12
13
|
await waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleep);
|
|
13
14
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -117,12 +118,14 @@ export async function refreshManagedBitcoindStatus(status, paths, options) {
|
|
|
117
118
|
return nextStatus;
|
|
118
119
|
}
|
|
119
120
|
catch (error) {
|
|
121
|
+
const processAlive = await isManagedBitcoindProcessAlive(status.processId);
|
|
122
|
+
const stillStarting = processAlive && isManagedRpcWarmupError(error);
|
|
120
123
|
const nextStatus = {
|
|
121
124
|
...status,
|
|
122
125
|
walletRootId: targetWalletRootId,
|
|
123
126
|
runtimeRoot: paths.walletRuntimeRoot,
|
|
124
|
-
state: "failed",
|
|
125
|
-
processId:
|
|
127
|
+
state: stillStarting ? "starting" : "failed",
|
|
128
|
+
processId: processAlive ? status.processId : null,
|
|
126
129
|
heartbeatAtUnixMs: nowUnixMs,
|
|
127
130
|
updatedAtUnixMs: nowUnixMs,
|
|
128
131
|
lastError: error instanceof Error ? error.message : String(error),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { isManagedRpcWarmupError } from "../retryable-rpc.js";
|
|
2
3
|
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
3
4
|
import { MANAGED_BITCOIND_SERVICE_API_VERSION } from "../types.js";
|
|
4
5
|
function isRuntimeMismatchError(error) {
|
|
@@ -64,6 +65,13 @@ export function mapManagedBitcoindRuntimeProbeFailure(error, status) {
|
|
|
64
65
|
error: "managed_bitcoind_runtime_mismatch",
|
|
65
66
|
};
|
|
66
67
|
}
|
|
68
|
+
if (isManagedRpcWarmupError(error)) {
|
|
69
|
+
return {
|
|
70
|
+
compatibility: "starting",
|
|
71
|
+
status,
|
|
72
|
+
error: error instanceof Error ? error.message : String(error),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
67
75
|
if (isUnreachableManagedBitcoindError(error)) {
|
|
68
76
|
return {
|
|
69
77
|
compatibility: "unreachable",
|
|
@@ -84,6 +92,12 @@ export function resolveManagedBitcoindProbeDecision(probe) {
|
|
|
84
92
|
error: null,
|
|
85
93
|
};
|
|
86
94
|
}
|
|
95
|
+
if (probe.compatibility === "starting") {
|
|
96
|
+
return {
|
|
97
|
+
action: "wait",
|
|
98
|
+
error: probe.error ?? "managed_bitcoind_service_starting",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
87
101
|
if (probe.compatibility === "unreachable") {
|
|
88
102
|
return {
|
|
89
103
|
action: "start",
|
|
@@ -121,6 +135,12 @@ function mapManagedBitcoindStartupError(message) {
|
|
|
121
135
|
status: null,
|
|
122
136
|
message: "The live managed bitcoind service runtime does not match this wallet's expected data directory or chain.",
|
|
123
137
|
};
|
|
138
|
+
case "managed_bitcoind_service_starting":
|
|
139
|
+
return {
|
|
140
|
+
health: "starting",
|
|
141
|
+
status: null,
|
|
142
|
+
message: "Managed bitcoind service is still starting.",
|
|
143
|
+
};
|
|
124
144
|
case "managed_bitcoind_protocol_error":
|
|
125
145
|
return {
|
|
126
146
|
health: "unavailable",
|
|
@@ -128,6 +148,13 @@ function mapManagedBitcoindStartupError(message) {
|
|
|
128
148
|
message: "The managed bitcoind runtime artifacts are invalid or incomplete.",
|
|
129
149
|
};
|
|
130
150
|
default:
|
|
151
|
+
if (/^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)) {
|
|
152
|
+
return {
|
|
153
|
+
health: "starting",
|
|
154
|
+
status: null,
|
|
155
|
+
message,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
131
158
|
return {
|
|
132
159
|
health: "unavailable",
|
|
133
160
|
status: null,
|
|
@@ -40,6 +40,9 @@ export async function attachOrStartManagedBitcoindRuntime(options, dependencies)
|
|
|
40
40
|
}
|
|
41
41
|
throw new Error("managed_bitcoind_protocol_error");
|
|
42
42
|
}
|
|
43
|
+
if (existingDecision.action === "wait") {
|
|
44
|
+
return waitForManagedBitcoindRuntime(options, dependencies);
|
|
45
|
+
}
|
|
43
46
|
if (existingDecision.action === "reject") {
|
|
44
47
|
throw new Error(existingDecision.error ?? "managed_bitcoind_protocol_error");
|
|
45
48
|
}
|
|
@@ -56,6 +59,9 @@ export async function attachOrStartManagedBitcoindRuntime(options, dependencies)
|
|
|
56
59
|
}
|
|
57
60
|
throw new Error("managed_bitcoind_protocol_error");
|
|
58
61
|
}
|
|
62
|
+
if (liveDecision.action === "wait") {
|
|
63
|
+
return waitForManagedBitcoindRuntime(options, dependencies);
|
|
64
|
+
}
|
|
59
65
|
if (liveDecision.action === "reject") {
|
|
60
66
|
throw new Error(liveDecision.error ?? "managed_bitcoind_protocol_error");
|
|
61
67
|
}
|
|
@@ -2,7 +2,7 @@ import type { ClientTip } from "../../types.js";
|
|
|
2
2
|
import type { ManagedServicePaths } from "../service-paths.js";
|
|
3
3
|
import type { ManagedBitcoindObservedStatus, ManagedIndexerDaemonObservedStatus, ManagedIndexerTruthSource } from "../types.js";
|
|
4
4
|
import type { WalletBitcoindStatus, WalletIndexerStatus, WalletNodeStatus, WalletServiceHealth, WalletSnapshotView } from "../../wallet/read/types.js";
|
|
5
|
-
export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "rawtx-zmq-missing" | "unreachable" | "protocol-error";
|
|
5
|
+
export type ManagedBitcoindServiceCompatibility = "compatible" | "starting" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "rawtx-zmq-missing" | "unreachable" | "protocol-error";
|
|
6
6
|
export interface ManagedBitcoindServiceProbeResult {
|
|
7
7
|
compatibility: ManagedBitcoindServiceCompatibility;
|
|
8
8
|
status: ManagedBitcoindObservedStatus | null;
|
|
@@ -20,7 +20,7 @@ export interface ManagedIndexerDaemonProbeResult<TClient> {
|
|
|
20
20
|
error: string | null;
|
|
21
21
|
}
|
|
22
22
|
export interface ManagedBitcoindProbeDecision {
|
|
23
|
-
action: "attach" | "start" | "reject";
|
|
23
|
+
action: "attach" | "wait" | "start" | "reject";
|
|
24
24
|
error: string | null;
|
|
25
25
|
}
|
|
26
26
|
export interface IndexerDaemonProbeDecision {
|
|
@@ -8,4 +8,5 @@ export declare function createManagedRpcRetryState(): ManagedRpcRetryState;
|
|
|
8
8
|
export declare function resetManagedRpcRetryState(state: ManagedRpcRetryState): void;
|
|
9
9
|
export declare function consumeManagedRpcRetryDelayMs(state: ManagedRpcRetryState): number;
|
|
10
10
|
export declare function isRetryableManagedRpcError(error: unknown): boolean;
|
|
11
|
+
export declare function isManagedRpcWarmupError(error: unknown): boolean;
|
|
11
12
|
export declare function describeManagedRpcRetryError(error: unknown): string;
|
|
@@ -15,15 +15,32 @@ export function consumeManagedRpcRetryDelayMs(state) {
|
|
|
15
15
|
return delayMs;
|
|
16
16
|
}
|
|
17
17
|
export function isRetryableManagedRpcError(error) {
|
|
18
|
+
if (isManagedRpcWarmupError(error)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
if (message.startsWith("The managed Bitcoin RPC request to ")) {
|
|
23
|
+
return message.includes(" failed");
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export function isManagedRpcWarmupError(error) {
|
|
18
28
|
const message = error instanceof Error ? error.message : String(error);
|
|
19
|
-
if (message === "
|
|
29
|
+
if (message === "managed_bitcoind_service_starting"
|
|
30
|
+
|| message === "bitcoind_rpc_timeout"
|
|
31
|
+
|| message === "bitcoind_cookie_timeout") {
|
|
20
32
|
return true;
|
|
21
33
|
}
|
|
22
34
|
if (/^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)) {
|
|
23
35
|
return true;
|
|
24
36
|
}
|
|
25
37
|
if (message.startsWith("The managed Bitcoin RPC request to ")) {
|
|
26
|
-
return message.includes(" failed")
|
|
38
|
+
return message.includes(" failed")
|
|
39
|
+
&& (message.includes("ECONNREFUSED")
|
|
40
|
+
|| message.includes("ECONNRESET")
|
|
41
|
+
|| message.includes("socket hang up")
|
|
42
|
+
|| message.includes("timeout")
|
|
43
|
+
|| message.includes("aborted"));
|
|
27
44
|
}
|
|
28
45
|
return message.startsWith("The managed Bitcoin RPC cookie file is unavailable at ")
|
|
29
46
|
|| message.startsWith("The managed Bitcoin RPC cookie file could not be read at ");
|
|
@@ -38,7 +38,10 @@ function getResetNextSteps(result) {
|
|
|
38
38
|
? ["Run `cogcoin init` to create or restore a wallet."]
|
|
39
39
|
: ["Run `cogcoin sync` to bootstrap assumeutxo and the managed Bitcoin/indexer state."];
|
|
40
40
|
}
|
|
41
|
-
function getRepairNextSteps() {
|
|
41
|
+
function getRepairNextSteps(result) {
|
|
42
|
+
if (result.bitcoindPostRepairHealth === "starting") {
|
|
43
|
+
return ["Wait for Bitcoin Core to finish loading, then run `cogcoin status` or rerun `cogcoin mine`."];
|
|
44
|
+
}
|
|
42
45
|
return ["Run `cogcoin status` to review the repaired local state."];
|
|
43
46
|
}
|
|
44
47
|
function formatResetBitcoinDataDirStatus(result) {
|
|
@@ -116,7 +119,7 @@ function buildRepairWarningEntries(result) {
|
|
|
116
119
|
return entries;
|
|
117
120
|
}
|
|
118
121
|
function formatRepairResultText(result) {
|
|
119
|
-
const nextStep = getRepairNextSteps()[0] ?? null;
|
|
122
|
+
const nextStep = getRepairNextSteps(result)[0] ?? null;
|
|
120
123
|
const warningEntries = buildRepairWarningEntries(result);
|
|
121
124
|
const sections = [
|
|
122
125
|
formatAdminSection("Wallet", [
|
|
@@ -244,6 +247,9 @@ export async function runWalletAdminCommand(parsed, context) {
|
|
|
244
247
|
provider: repairProvider,
|
|
245
248
|
assumeYes: parsed.assumeYes,
|
|
246
249
|
paths: runtimePaths,
|
|
250
|
+
progress: async (event) => {
|
|
251
|
+
writeLine(context.stdout, event.message);
|
|
252
|
+
},
|
|
247
253
|
});
|
|
248
254
|
writeLine(context.stdout, formatRepairResultText(result));
|
|
249
255
|
return 0;
|
|
@@ -21,6 +21,13 @@ export const serviceErrorRules = [
|
|
|
21
21
|
next: "Rerun the command in an interactive terminal.",
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
if (errorCode === "managed_bitcoind_service_starting" || /^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(errorCode)) {
|
|
25
|
+
return {
|
|
26
|
+
what: "Bitcoin Core is still loading.",
|
|
27
|
+
why: "The managed Bitcoin RPC service is alive, but Bitcoin Core is still warming up and returned its normal startup -28 response.",
|
|
28
|
+
next: "Wait for the node to finish loading, then rerun the command. Use `cogcoin status` to check readiness.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
24
31
|
if (errorCode.includes("tip_mismatch") || errorCode.includes("stale") || errorCode.includes("catching_up") || errorCode.includes("starting")) {
|
|
25
32
|
return {
|
|
26
33
|
what: "Trusted service state is not ready.",
|
|
@@ -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;
|
|
@@ -24,6 +24,7 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
24
24
|
dataDir: options.context.dataDir,
|
|
25
25
|
});
|
|
26
26
|
try {
|
|
27
|
+
await reportRepairProgress(options.context, "bitcoind-check", "Checking managed bitcoind...");
|
|
27
28
|
initialBitcoindProbe = await options.context.probeBitcoindService({
|
|
28
29
|
dataDir: options.context.dataDir,
|
|
29
30
|
chain: "main",
|
|
@@ -31,6 +32,18 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
31
32
|
walletRootId: state.walletRootId,
|
|
32
33
|
});
|
|
33
34
|
bitcoindCompatibilityIssue = mapBitcoindCompatibilityToRepairIssue(initialBitcoindProbe.compatibility);
|
|
35
|
+
if (initialBitcoindProbe.compatibility === "starting") {
|
|
36
|
+
await reportRepairProgress(options.context, "bitcoind-starting", "Bitcoin Core is loading the block index; leaving managed bitcoind running.");
|
|
37
|
+
return {
|
|
38
|
+
state,
|
|
39
|
+
repairStateNeedsPersist,
|
|
40
|
+
recreatedManagedCoreWallet,
|
|
41
|
+
bitcoindServiceAction,
|
|
42
|
+
bitcoindCompatibilityIssue,
|
|
43
|
+
managedCoreReplicaAction,
|
|
44
|
+
bitcoindPostRepairHealth: "starting",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
34
47
|
if (initialBitcoindProbe.compatibility === "service-version-mismatch"
|
|
35
48
|
|| initialBitcoindProbe.compatibility === "wallet-root-mismatch"
|
|
36
49
|
|| initialBitcoindProbe.compatibility === "runtime-mismatch"
|
|
@@ -40,10 +53,14 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
40
53
|
if (initialBitcoindProbe.compatibility !== "rawtx-zmq-missing") {
|
|
41
54
|
throw new Error("managed_bitcoind_process_id_unavailable");
|
|
42
55
|
}
|
|
56
|
+
await reportRepairProgress(options.context, "bitcoind-clear-stale-rawtx", "Clearing stale managed bitcoind runtime missing rawtx ZMQ...");
|
|
43
57
|
await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
|
|
44
58
|
bitcoindServiceAction = "restarted-missing-rawtx-zmq";
|
|
45
59
|
}
|
|
46
60
|
else {
|
|
61
|
+
await reportRepairProgress(options.context, "bitcoind-stop-incompatible", initialBitcoindProbe.compatibility === "rawtx-zmq-missing"
|
|
62
|
+
? "Stopping stale managed bitcoind missing rawtx ZMQ..."
|
|
63
|
+
: "Stopping incompatible managed bitcoind service...");
|
|
47
64
|
try {
|
|
48
65
|
process.kill(processId, "SIGTERM");
|
|
49
66
|
}
|
|
@@ -53,6 +70,7 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
72
|
await waitForProcessExit(processId, 15_000, "managed_bitcoind_stop_timeout");
|
|
73
|
+
await reportRepairProgress(options.context, "bitcoind-clear-artifacts", "Clearing managed bitcoind runtime artifacts...");
|
|
56
74
|
await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
|
|
57
75
|
bitcoindServiceAction = initialBitcoindProbe.compatibility === "rawtx-zmq-missing"
|
|
58
76
|
? "restarted-missing-rawtx-zmq"
|
|
@@ -68,6 +86,7 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
68
86
|
options.servicePaths.bitcoindWalletStatusPath,
|
|
69
87
|
].map(pathExists));
|
|
70
88
|
if (hasStaleArtifacts.some(Boolean)) {
|
|
89
|
+
await reportRepairProgress(options.context, "bitcoind-clear-stale", "Clearing stale managed bitcoind artifacts...");
|
|
71
90
|
await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
|
|
72
91
|
bitcoindServiceAction = "cleared-stale-artifacts";
|
|
73
92
|
}
|
|
@@ -83,12 +102,17 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
83
102
|
let bitcoindHandle = null;
|
|
84
103
|
let handleClosed = false;
|
|
85
104
|
try {
|
|
105
|
+
await reportRepairProgress(options.context, attachAttempt === 0 ? "bitcoind-start" : "bitcoind-retry-start", bitcoindServiceAction === "none"
|
|
106
|
+
? "Attaching to managed bitcoind..."
|
|
107
|
+
: "Starting managed bitcoind with current ZMQ config...");
|
|
108
|
+
await reportRepairProgress(options.context, "bitcoind-wait-rpc", "Waiting for Bitcoin Core RPC readiness...");
|
|
86
109
|
bitcoindHandle = await options.context.attachService({
|
|
87
110
|
dataDir: options.context.dataDir,
|
|
88
111
|
chain: "main",
|
|
89
112
|
startHeight: 0,
|
|
90
113
|
walletRootId: state.walletRootId,
|
|
91
114
|
});
|
|
115
|
+
await reportRepairProgress(options.context, "bitcoind-normalize-wallet", "Checking managed Bitcoin wallet state...");
|
|
92
116
|
const rpc = options.context.rpcFactory(bitcoindHandle.rpc);
|
|
93
117
|
const normalizedDescriptorState = await normalizeWalletDescriptorState(state, rpc);
|
|
94
118
|
if (normalizedDescriptorState.changed) {
|
|
@@ -116,6 +140,7 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
116
140
|
rpcFactory: options.context.rpcFactory,
|
|
117
141
|
});
|
|
118
142
|
if (replica.proofStatus !== "ready") {
|
|
143
|
+
await reportRepairProgress(options.context, "bitcoind-recreate-replica", "Recreating managed Core wallet replica...");
|
|
119
144
|
state = await recreateManagedCoreWalletReplica(state, options.context.provider, options.context.paths, options.context.dataDir, options.context.nowUnixMs, {
|
|
120
145
|
attachService: options.context.attachService,
|
|
121
146
|
rpcFactory: options.context.rpcFactory,
|
|
@@ -129,6 +154,7 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
129
154
|
rpcFactory: options.context.rpcFactory,
|
|
130
155
|
});
|
|
131
156
|
}
|
|
157
|
+
await reportRepairProgress(options.context, "bitcoind-final-health", "Checking managed bitcoind post-repair health...");
|
|
132
158
|
const finalBitcoindStatus = await bitcoindHandle.refreshServiceStatus?.() ?? null;
|
|
133
159
|
const chainInfo = await rpc.getBlockchainInfo();
|
|
134
160
|
bitcoindPostRepairHealth = mapBitcoindRepairHealth({
|
|
@@ -150,6 +176,18 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
150
176
|
};
|
|
151
177
|
}
|
|
152
178
|
catch (error) {
|
|
179
|
+
if (isManagedBitcoindStartupWarmupError(error)) {
|
|
180
|
+
await reportRepairProgress(options.context, "bitcoind-starting", "Bitcoin Core is loading the block index; leaving managed bitcoind running.");
|
|
181
|
+
return {
|
|
182
|
+
state,
|
|
183
|
+
repairStateNeedsPersist,
|
|
184
|
+
recreatedManagedCoreWallet,
|
|
185
|
+
bitcoindServiceAction,
|
|
186
|
+
bitcoindCompatibilityIssue,
|
|
187
|
+
managedCoreReplicaAction,
|
|
188
|
+
bitcoindPostRepairHealth: "starting",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
153
191
|
if (bitcoindHandle !== null) {
|
|
154
192
|
await bitcoindHandle.stop?.().catch(() => undefined);
|
|
155
193
|
handleClosed = true;
|
|
@@ -161,6 +199,7 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
161
199
|
dataDir: options.context.dataDir,
|
|
162
200
|
});
|
|
163
201
|
try {
|
|
202
|
+
await reportRepairProgress(options.context, "bitcoind-clear-stale-rpc", "Clearing stale managed bitcoind RPC artifacts...");
|
|
164
203
|
await clearManagedBitcoindArtifactsForDataDir(options.servicePaths, options.context.dataDir);
|
|
165
204
|
}
|
|
166
205
|
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;
|
|
@@ -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);
|
|
@@ -54,6 +54,15 @@ async function attachNodeStatus(options, dependencies) {
|
|
|
54
54
|
startupTimeoutMs: options.startupTimeoutMs,
|
|
55
55
|
});
|
|
56
56
|
const decision = resolveManagedBitcoindProbeDecision(probe);
|
|
57
|
+
if (decision.action === "wait") {
|
|
58
|
+
return {
|
|
59
|
+
handle: null,
|
|
60
|
+
rpc: null,
|
|
61
|
+
status: null,
|
|
62
|
+
observedStatus: probe.status,
|
|
63
|
+
error: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
57
66
|
if (decision.action === "reject") {
|
|
58
67
|
return {
|
|
59
68
|
handle: null,
|
package/package.json
CHANGED