@cogcoin/client 1.1.5 → 1.1.6
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 +1 -1
- package/dist/bitcoind/indexer-daemon.d.ts +3 -7
- package/dist/bitcoind/indexer-daemon.js +43 -158
- package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
- package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/status.js +59 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +37 -0
- package/dist/bitcoind/managed-runtime/types.js +1 -0
- package/dist/bitcoind/service.d.ts +2 -7
- package/dist/bitcoind/service.js +46 -94
- package/dist/wallet/lifecycle/access.d.ts +5 -0
- package/dist/wallet/lifecycle/access.js +79 -0
- package/dist/wallet/lifecycle/context.d.ts +26 -0
- package/dist/wallet/lifecycle/context.js +58 -0
- package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
- package/dist/wallet/lifecycle/managed-core.js +3 -63
- package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
- package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
- package/dist/wallet/lifecycle/repair-indexer.js +117 -0
- package/dist/wallet/lifecycle/repair.d.ts +2 -4
- package/dist/wallet/lifecycle/repair.js +77 -318
- package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
- package/dist/wallet/lifecycle/setup-prompts.js +88 -0
- package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
- package/dist/wallet/lifecycle/setup-state.js +159 -0
- package/dist/wallet/lifecycle/setup.d.ts +3 -4
- package/dist/wallet/lifecycle/setup.js +45 -351
- package/dist/wallet/lifecycle/types.d.ts +33 -2
- package/dist/wallet/read/context.js +13 -188
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.1.
|
|
3
|
+
`@cogcoin/client@1.1.6` 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
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { ManagedIndexerDaemonProbeResult } from "./managed-runtime/types.js";
|
|
1
2
|
import { type BootstrapPhase, type BootstrapProgress, type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
|
|
2
3
|
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
4
|
+
export type { IndexerDaemonCompatibility } from "./managed-runtime/types.js";
|
|
3
5
|
export declare const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
|
|
4
6
|
interface DaemonRequest {
|
|
5
7
|
id: string;
|
|
@@ -74,13 +76,7 @@ export interface IndexerDaemonClient {
|
|
|
74
76
|
resumeBackgroundFollow(): Promise<void>;
|
|
75
77
|
close(): Promise<void>;
|
|
76
78
|
}
|
|
77
|
-
export type
|
|
78
|
-
export interface IndexerDaemonProbeResult {
|
|
79
|
-
compatibility: IndexerDaemonCompatibility;
|
|
80
|
-
status: ManagedIndexerDaemonObservedStatus | null;
|
|
81
|
-
client: IndexerDaemonClient | null;
|
|
82
|
-
error: string | null;
|
|
83
|
-
}
|
|
79
|
+
export type IndexerDaemonProbeResult = ManagedIndexerDaemonProbeResult<IndexerDaemonClient>;
|
|
84
80
|
export interface IndexerDaemonStopResult {
|
|
85
81
|
status: "stopped" | "not-running";
|
|
86
82
|
walletRootId: string;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdir,
|
|
3
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import net from "node:net";
|
|
6
|
-
import { compareSemver, parseSemver } from "../semver.js";
|
|
7
6
|
import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
|
|
8
7
|
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
9
|
-
import {
|
|
8
|
+
import { buildManagedIndexerStatusFromSnapshotHandle, mapIndexerDaemonTransportError, mapIndexerDaemonValidationError, resolveIndexerDaemonProbeDecision, validateIndexerDaemonStatus, validateIndexerSnapshotHandle, validateIndexerSnapshotPayload, } from "./managed-runtime/indexer-policy.js";
|
|
9
|
+
import { readJsonFileIfPresent } from "./managed-runtime/status.js";
|
|
10
|
+
import {} from "./types.js";
|
|
10
11
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
11
12
|
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
12
13
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
@@ -15,17 +16,6 @@ const INDEXER_DAEMON_REQUEST_TIMEOUT_MS = 15_000;
|
|
|
15
16
|
const INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS = 35_000;
|
|
16
17
|
const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
|
|
17
18
|
export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
|
|
18
|
-
async function readJsonFile(filePath) {
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(await readFile(filePath, "utf8"));
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
19
|
async function isProcessAlive(pid) {
|
|
30
20
|
if (pid === null) {
|
|
31
21
|
return false;
|
|
@@ -68,7 +58,7 @@ function ignoreProcessNotFound(error) {
|
|
|
68
58
|
export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
69
59
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
70
60
|
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
71
|
-
const status = await
|
|
61
|
+
const status = await readJsonFileIfPresent(paths.indexerDaemonStatusPath);
|
|
72
62
|
const processId = options.processId ?? status?.processId ?? null;
|
|
73
63
|
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
74
64
|
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
@@ -217,97 +207,6 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
|
|
|
217
207
|
},
|
|
218
208
|
};
|
|
219
209
|
}
|
|
220
|
-
function validateIndexerRuntimeIdentity(identity, expectedWalletRootId) {
|
|
221
|
-
if (identity.serviceApiVersion !== INDEXER_DAEMON_SERVICE_API_VERSION) {
|
|
222
|
-
throw new Error("indexer_daemon_service_version_mismatch");
|
|
223
|
-
}
|
|
224
|
-
if (identity.schemaVersion !== INDEXER_DAEMON_SCHEMA_VERSION || identity.state === "schema-mismatch") {
|
|
225
|
-
throw new Error("indexer_daemon_schema_mismatch");
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
function validateIndexerDaemonStatus(status, expectedWalletRootId) {
|
|
229
|
-
validateIndexerRuntimeIdentity(status, expectedWalletRootId);
|
|
230
|
-
}
|
|
231
|
-
function validateIndexerSnapshotHandle(handle, expectedWalletRootId) {
|
|
232
|
-
validateIndexerRuntimeIdentity(handle, expectedWalletRootId);
|
|
233
|
-
}
|
|
234
|
-
function validateIndexerSnapshotPayload(payload, handle, expectedWalletRootId) {
|
|
235
|
-
validateIndexerRuntimeIdentity(payload, expectedWalletRootId);
|
|
236
|
-
if (payload.token !== handle.token
|
|
237
|
-
|| payload.daemonInstanceId !== handle.daemonInstanceId
|
|
238
|
-
|| payload.processId !== handle.processId
|
|
239
|
-
|| payload.startedAtUnixMs !== handle.startedAtUnixMs
|
|
240
|
-
|| payload.snapshotSeq !== handle.snapshotSeq
|
|
241
|
-
|| payload.tipHeight !== handle.tipHeight
|
|
242
|
-
|| payload.tipHash !== handle.tipHash
|
|
243
|
-
|| payload.openedAtUnixMs !== handle.openedAtUnixMs) {
|
|
244
|
-
throw new Error("indexer_daemon_snapshot_identity_mismatch");
|
|
245
|
-
}
|
|
246
|
-
if (payload.tip === null) {
|
|
247
|
-
if (payload.tipHeight !== null || payload.tipHash !== null) {
|
|
248
|
-
throw new Error("indexer_daemon_snapshot_identity_mismatch");
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
else if (payload.tip.height !== payload.tipHeight || payload.tip.blockHashHex !== payload.tipHash) {
|
|
252
|
-
throw new Error("indexer_daemon_snapshot_identity_mismatch");
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
function isUnreachableIndexerDaemonError(error) {
|
|
256
|
-
if (error instanceof Error) {
|
|
257
|
-
if (error.message === "indexer_daemon_connection_closed"
|
|
258
|
-
|| error.message === "indexer_daemon_request_timeout"
|
|
259
|
-
|| error.message === "indexer_daemon_protocol_error") {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
if ("code" in error) {
|
|
263
|
-
const code = error.code;
|
|
264
|
-
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET";
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
function buildStatusFromSnapshotHandle(handle) {
|
|
270
|
-
return {
|
|
271
|
-
serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
|
|
272
|
-
binaryVersion: handle.binaryVersion,
|
|
273
|
-
buildId: handle.buildId,
|
|
274
|
-
updatedAtUnixMs: Math.max(handle.heartbeatAtUnixMs, handle.openedAtUnixMs),
|
|
275
|
-
walletRootId: handle.walletRootId,
|
|
276
|
-
daemonInstanceId: handle.daemonInstanceId,
|
|
277
|
-
schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
|
|
278
|
-
state: handle.state,
|
|
279
|
-
processId: handle.processId,
|
|
280
|
-
startedAtUnixMs: handle.startedAtUnixMs,
|
|
281
|
-
heartbeatAtUnixMs: handle.heartbeatAtUnixMs,
|
|
282
|
-
ipcReady: true,
|
|
283
|
-
rpcReachable: handle.rpcReachable,
|
|
284
|
-
coreBestHeight: handle.coreBestHeight,
|
|
285
|
-
coreBestHash: handle.coreBestHash,
|
|
286
|
-
appliedTipHeight: handle.appliedTipHeight,
|
|
287
|
-
appliedTipHash: handle.appliedTipHash,
|
|
288
|
-
snapshotSeq: handle.snapshotSeq,
|
|
289
|
-
backlogBlocks: handle.backlogBlocks,
|
|
290
|
-
reorgDepth: handle.reorgDepth,
|
|
291
|
-
lastAppliedAtUnixMs: handle.lastAppliedAtUnixMs,
|
|
292
|
-
activeSnapshotCount: handle.activeSnapshotCount,
|
|
293
|
-
lastError: handle.lastError,
|
|
294
|
-
backgroundFollowActive: handle.backgroundFollowActive,
|
|
295
|
-
bootstrapPhase: handle.bootstrapPhase,
|
|
296
|
-
bootstrapProgress: handle.bootstrapProgress,
|
|
297
|
-
cogcoinSyncHeight: handle.cogcoinSyncHeight,
|
|
298
|
-
cogcoinSyncTargetHeight: handle.cogcoinSyncTargetHeight,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
function isStaleIndexerDaemonVersion(status, expectedBinaryVersion) {
|
|
302
|
-
if (status === null || expectedBinaryVersion === null || expectedBinaryVersion === undefined) {
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
if (parseSemver(expectedBinaryVersion) === null) {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
const comparison = compareSemver(status.binaryVersion, expectedBinaryVersion);
|
|
309
|
-
return comparison === null || comparison < 0;
|
|
310
|
-
}
|
|
311
210
|
async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
|
|
312
211
|
const client = createIndexerDaemonClient(socketPath);
|
|
313
212
|
try {
|
|
@@ -323,30 +222,12 @@ async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
|
|
|
323
222
|
}
|
|
324
223
|
catch (error) {
|
|
325
224
|
await client.close().catch(() => undefined);
|
|
326
|
-
return
|
|
327
|
-
compatibility: error instanceof Error
|
|
328
|
-
? error.message === "indexer_daemon_service_version_mismatch"
|
|
329
|
-
? "service-version-mismatch"
|
|
330
|
-
: "schema-mismatch"
|
|
331
|
-
: "protocol-error",
|
|
332
|
-
status,
|
|
333
|
-
client: null,
|
|
334
|
-
error: error instanceof Error ? error.message : "indexer_daemon_protocol_error",
|
|
335
|
-
};
|
|
225
|
+
return mapIndexerDaemonValidationError(error, status);
|
|
336
226
|
}
|
|
337
227
|
}
|
|
338
228
|
catch (error) {
|
|
339
229
|
await client.close().catch(() => undefined);
|
|
340
|
-
return
|
|
341
|
-
compatibility: isUnreachableIndexerDaemonError(error) ? "unreachable" : "protocol-error",
|
|
342
|
-
status: null,
|
|
343
|
-
client: null,
|
|
344
|
-
error: isUnreachableIndexerDaemonError(error)
|
|
345
|
-
? null
|
|
346
|
-
: error instanceof Error
|
|
347
|
-
? "indexer_daemon_protocol_error"
|
|
348
|
-
: "indexer_daemon_protocol_error",
|
|
349
|
-
};
|
|
230
|
+
return mapIndexerDaemonTransportError(error);
|
|
350
231
|
}
|
|
351
232
|
}
|
|
352
233
|
async function waitForIndexerDaemon(dataDir, walletRootId, timeoutMs) {
|
|
@@ -380,7 +261,7 @@ export async function readSnapshotWithRetry(daemon, expectedWalletRootId) {
|
|
|
380
261
|
validateIndexerSnapshotPayload(payload, handle, expectedWalletRootId);
|
|
381
262
|
return {
|
|
382
263
|
payload,
|
|
383
|
-
status:
|
|
264
|
+
status: buildManagedIndexerStatusFromSnapshotHandle(handle),
|
|
384
265
|
};
|
|
385
266
|
}
|
|
386
267
|
catch (error) {
|
|
@@ -400,7 +281,7 @@ export async function readSnapshotWithRetry(daemon, expectedWalletRootId) {
|
|
|
400
281
|
export async function readObservedIndexerDaemonStatus(options) {
|
|
401
282
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
402
283
|
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
403
|
-
return
|
|
284
|
+
return readJsonFileIfPresent(paths.indexerDaemonStatusPath);
|
|
404
285
|
}
|
|
405
286
|
export async function attachOrStartIndexerDaemon(options) {
|
|
406
287
|
const requestBackgroundFollow = async (client, observedStatus = null) => {
|
|
@@ -467,21 +348,23 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
467
348
|
});
|
|
468
349
|
};
|
|
469
350
|
const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
351
|
+
const existingDecision = resolveIndexerDaemonProbeDecision({
|
|
352
|
+
probe: existingProbe,
|
|
353
|
+
expectedBinaryVersion,
|
|
354
|
+
});
|
|
355
|
+
if (existingDecision.action === "attach" && existingProbe.client !== null) {
|
|
356
|
+
try {
|
|
357
|
+
return await requestBackgroundFollow(existingProbe.client, existingProbe.status);
|
|
478
358
|
}
|
|
479
|
-
|
|
359
|
+
catch {
|
|
480
360
|
await existingProbe.client.close().catch(() => undefined);
|
|
481
361
|
}
|
|
482
362
|
}
|
|
483
|
-
if (
|
|
484
|
-
|
|
363
|
+
if (existingDecision.action === "replace" && existingProbe.client !== null) {
|
|
364
|
+
await existingProbe.client.close().catch(() => undefined);
|
|
365
|
+
}
|
|
366
|
+
if (existingDecision.action === "reject") {
|
|
367
|
+
throw new Error(existingDecision.error ?? "indexer_daemon_protocol_error");
|
|
485
368
|
}
|
|
486
369
|
try {
|
|
487
370
|
const lock = await acquireFileLock(paths.indexerDaemonLockPath, {
|
|
@@ -492,23 +375,15 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
492
375
|
});
|
|
493
376
|
try {
|
|
494
377
|
const liveProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
await stopIndexerDaemonServiceWithLockHeld({
|
|
503
|
-
dataDir: options.dataDir,
|
|
504
|
-
walletRootId,
|
|
505
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
506
|
-
paths,
|
|
507
|
-
processId: liveProbe.status?.processId ?? null,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
378
|
+
const liveDecision = resolveIndexerDaemonProbeDecision({
|
|
379
|
+
probe: liveProbe,
|
|
380
|
+
expectedBinaryVersion,
|
|
381
|
+
});
|
|
382
|
+
if (liveDecision.action === "attach" && liveProbe.client !== null) {
|
|
383
|
+
try {
|
|
384
|
+
return await requestBackgroundFollow(liveProbe.client, liveProbe.status);
|
|
510
385
|
}
|
|
511
|
-
|
|
386
|
+
catch {
|
|
512
387
|
await liveProbe.client.close().catch(() => undefined);
|
|
513
388
|
await stopIndexerDaemonServiceWithLockHeld({
|
|
514
389
|
dataDir: options.dataDir,
|
|
@@ -519,8 +394,18 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
519
394
|
});
|
|
520
395
|
}
|
|
521
396
|
}
|
|
522
|
-
else if (liveProbe.
|
|
523
|
-
|
|
397
|
+
else if (liveDecision.action === "replace" && liveProbe.client !== null) {
|
|
398
|
+
await liveProbe.client.close().catch(() => undefined);
|
|
399
|
+
await stopIndexerDaemonServiceWithLockHeld({
|
|
400
|
+
dataDir: options.dataDir,
|
|
401
|
+
walletRootId,
|
|
402
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
403
|
+
paths,
|
|
404
|
+
processId: liveProbe.status?.processId ?? null,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else if (liveDecision.action === "reject") {
|
|
408
|
+
throw new Error(liveDecision.error ?? "indexer_daemon_protocol_error");
|
|
524
409
|
}
|
|
525
410
|
const daemon = await startDaemon();
|
|
526
411
|
try {
|
|
@@ -572,7 +457,7 @@ export async function shutdownIndexerDaemonForTesting(options) {
|
|
|
572
457
|
export async function readIndexerDaemonStatusForTesting(options) {
|
|
573
458
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
574
459
|
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
575
|
-
return
|
|
460
|
+
return readJsonFileIfPresent(paths.indexerDaemonStatusPath);
|
|
576
461
|
}
|
|
577
462
|
export async function writeIndexerDaemonStatusForTesting(options, status) {
|
|
578
463
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ManagedBitcoindObservedStatus } from "../types.js";
|
|
2
|
+
import type { WalletBitcoindStatus, WalletNodeStatus } from "../../wallet/read/types.js";
|
|
3
|
+
import type { ManagedBitcoindProbeDecision, ManagedBitcoindServiceProbeResult } from "./types.js";
|
|
4
|
+
export declare function validateManagedBitcoindObservedStatus(status: ManagedBitcoindObservedStatus, options: {
|
|
5
|
+
chain: "main" | "regtest";
|
|
6
|
+
dataDir: string;
|
|
7
|
+
runtimeRoot: string;
|
|
8
|
+
}): void;
|
|
9
|
+
export declare function mapManagedBitcoindValidationError(error: unknown, status: ManagedBitcoindObservedStatus): ManagedBitcoindServiceProbeResult;
|
|
10
|
+
export declare function mapManagedBitcoindRuntimeProbeFailure(error: unknown, status: ManagedBitcoindObservedStatus): ManagedBitcoindServiceProbeResult;
|
|
11
|
+
export declare function resolveManagedBitcoindProbeDecision(probe: ManagedBitcoindServiceProbeResult): ManagedBitcoindProbeDecision;
|
|
12
|
+
export declare function deriveManagedBitcoindWalletStatus(options: {
|
|
13
|
+
status: ManagedBitcoindObservedStatus | null;
|
|
14
|
+
nodeStatus: WalletNodeStatus | null;
|
|
15
|
+
startupError: string | null;
|
|
16
|
+
}): WalletBitcoindStatus;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
3
|
+
import { MANAGED_BITCOIND_SERVICE_API_VERSION } from "../types.js";
|
|
4
|
+
function isRuntimeMismatchError(error) {
|
|
5
|
+
if (!(error instanceof Error)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
return error.message.startsWith("bitcoind_chain_expected_")
|
|
9
|
+
|| error.message === "managed_bitcoind_runtime_mismatch";
|
|
10
|
+
}
|
|
11
|
+
function isUnreachableManagedBitcoindError(error) {
|
|
12
|
+
if (error instanceof Error) {
|
|
13
|
+
if ("code" in error) {
|
|
14
|
+
const code = error.code;
|
|
15
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET";
|
|
16
|
+
}
|
|
17
|
+
return error.message === "bitcoind_cookie_timeout"
|
|
18
|
+
|| error.message.includes("cookie file is unavailable")
|
|
19
|
+
|| error.message.includes("ECONNREFUSED")
|
|
20
|
+
|| error.message.includes("ECONNRESET")
|
|
21
|
+
|| error.message.includes("socket hang up");
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
export function validateManagedBitcoindObservedStatus(status, options) {
|
|
26
|
+
const legacyRuntimeRoot = join(resolveManagedServicePaths(options.dataDir, status.walletRootId).runtimeRoot, status.walletRootId);
|
|
27
|
+
if (status.serviceApiVersion !== MANAGED_BITCOIND_SERVICE_API_VERSION) {
|
|
28
|
+
throw new Error("managed_bitcoind_service_version_mismatch");
|
|
29
|
+
}
|
|
30
|
+
// Managed bitcoind runtimes are adopted across wallet roots when the live
|
|
31
|
+
// runtime still points at the expected data dir and chain.
|
|
32
|
+
if (status.chain !== options.chain
|
|
33
|
+
|| status.dataDir !== options.dataDir
|
|
34
|
+
|| (status.runtimeRoot !== options.runtimeRoot && status.runtimeRoot !== legacyRuntimeRoot)) {
|
|
35
|
+
throw new Error("managed_bitcoind_runtime_mismatch");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function mapManagedBitcoindValidationError(error, status) {
|
|
39
|
+
return {
|
|
40
|
+
compatibility: error instanceof Error
|
|
41
|
+
? error.message === "managed_bitcoind_service_version_mismatch"
|
|
42
|
+
? "service-version-mismatch"
|
|
43
|
+
: "runtime-mismatch"
|
|
44
|
+
: "protocol-error",
|
|
45
|
+
status,
|
|
46
|
+
error: error instanceof Error ? error.message : "managed_bitcoind_protocol_error",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function mapManagedBitcoindRuntimeProbeFailure(error, status) {
|
|
50
|
+
if (isRuntimeMismatchError(error)) {
|
|
51
|
+
return {
|
|
52
|
+
compatibility: "runtime-mismatch",
|
|
53
|
+
status,
|
|
54
|
+
error: "managed_bitcoind_runtime_mismatch",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (isUnreachableManagedBitcoindError(error)) {
|
|
58
|
+
return {
|
|
59
|
+
compatibility: "unreachable",
|
|
60
|
+
status,
|
|
61
|
+
error: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
compatibility: "protocol-error",
|
|
66
|
+
status,
|
|
67
|
+
error: "managed_bitcoind_protocol_error",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function resolveManagedBitcoindProbeDecision(probe) {
|
|
71
|
+
if (probe.compatibility === "compatible") {
|
|
72
|
+
return {
|
|
73
|
+
action: "attach",
|
|
74
|
+
error: null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (probe.compatibility === "unreachable") {
|
|
78
|
+
return {
|
|
79
|
+
action: "start",
|
|
80
|
+
error: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
action: "reject",
|
|
85
|
+
error: probe.error ?? "managed_bitcoind_protocol_error",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function mapManagedBitcoindStartupError(message) {
|
|
89
|
+
switch (message) {
|
|
90
|
+
case "managed_bitcoind_service_start_timeout":
|
|
91
|
+
return {
|
|
92
|
+
health: "starting",
|
|
93
|
+
status: null,
|
|
94
|
+
message: "Managed bitcoind service is still starting.",
|
|
95
|
+
};
|
|
96
|
+
case "managed_bitcoind_service_version_mismatch":
|
|
97
|
+
return {
|
|
98
|
+
health: "service-version-mismatch",
|
|
99
|
+
status: null,
|
|
100
|
+
message: "The live managed bitcoind service is running an incompatible service version.",
|
|
101
|
+
};
|
|
102
|
+
case "managed_bitcoind_wallet_root_mismatch":
|
|
103
|
+
return {
|
|
104
|
+
health: "wallet-root-mismatch",
|
|
105
|
+
status: null,
|
|
106
|
+
message: "The live managed bitcoind service belongs to a different wallet root.",
|
|
107
|
+
};
|
|
108
|
+
case "managed_bitcoind_runtime_mismatch":
|
|
109
|
+
return {
|
|
110
|
+
health: "runtime-mismatch",
|
|
111
|
+
status: null,
|
|
112
|
+
message: "The live managed bitcoind service runtime does not match this wallet's expected data directory or chain.",
|
|
113
|
+
};
|
|
114
|
+
case "managed_bitcoind_protocol_error":
|
|
115
|
+
return {
|
|
116
|
+
health: "unavailable",
|
|
117
|
+
status: null,
|
|
118
|
+
message: "The managed bitcoind runtime artifacts are invalid or incomplete.",
|
|
119
|
+
};
|
|
120
|
+
default:
|
|
121
|
+
return {
|
|
122
|
+
health: "unavailable",
|
|
123
|
+
status: null,
|
|
124
|
+
message,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function deriveManagedBitcoindWalletStatus(options) {
|
|
129
|
+
if (options.startupError !== null) {
|
|
130
|
+
const mapped = mapManagedBitcoindStartupError(options.startupError);
|
|
131
|
+
return {
|
|
132
|
+
...mapped,
|
|
133
|
+
status: options.status,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (options.status === null) {
|
|
137
|
+
return {
|
|
138
|
+
health: "unavailable",
|
|
139
|
+
status: null,
|
|
140
|
+
message: "Managed bitcoind service is unavailable.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (options.status.state === "starting") {
|
|
144
|
+
return {
|
|
145
|
+
health: "starting",
|
|
146
|
+
status: options.status,
|
|
147
|
+
message: options.status.lastError ?? "Managed bitcoind service is still starting.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (options.status.state === "failed") {
|
|
151
|
+
return {
|
|
152
|
+
health: "failed",
|
|
153
|
+
status: options.status,
|
|
154
|
+
message: options.status.lastError ?? "Managed bitcoind service refresh failed.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const proofStatus = options.nodeStatus?.walletReplica?.proofStatus;
|
|
158
|
+
if (proofStatus === "missing") {
|
|
159
|
+
return {
|
|
160
|
+
health: "replica-missing",
|
|
161
|
+
status: options.status,
|
|
162
|
+
message: options.nodeStatus?.walletReplicaMessage ?? "Managed Core wallet replica is missing.",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (proofStatus === "mismatch") {
|
|
166
|
+
return {
|
|
167
|
+
health: "replica-mismatch",
|
|
168
|
+
status: options.status,
|
|
169
|
+
message: options.nodeStatus?.walletReplicaMessage ?? "Managed Core wallet replica does not match trusted wallet state.",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
health: "ready",
|
|
174
|
+
status: options.status,
|
|
175
|
+
message: options.nodeStatus?.walletReplicaMessage ?? options.status.lastError,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { WalletIndexerStatus } from "../../wallet/read/types.js";
|
|
2
|
+
import type { IndexerSnapshotHandle, IndexerSnapshotPayload } from "../indexer-daemon.js";
|
|
3
|
+
import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus, type ManagedIndexerTruthSource } from "../types.js";
|
|
4
|
+
import { buildManagedIndexerStatusFromSnapshotHandle } from "./status.js";
|
|
5
|
+
import type { IndexerDaemonProbeDecision, ManagedIndexerDaemonProbeResult, ManagedIndexerSnapshotLike } from "./types.js";
|
|
6
|
+
type IndexerRuntimeIdentityLike = {
|
|
7
|
+
serviceApiVersion: string;
|
|
8
|
+
schemaVersion: string;
|
|
9
|
+
walletRootId: string;
|
|
10
|
+
daemonInstanceId: string;
|
|
11
|
+
processId: number | null;
|
|
12
|
+
startedAtUnixMs: number;
|
|
13
|
+
state?: ManagedIndexerDaemonStatus["state"] | string;
|
|
14
|
+
};
|
|
15
|
+
export declare function validateIndexerRuntimeIdentity(identity: IndexerRuntimeIdentityLike, expectedWalletRootId: string): void;
|
|
16
|
+
export declare function validateIndexerDaemonStatus(status: ManagedIndexerDaemonObservedStatus, expectedWalletRootId: string): void;
|
|
17
|
+
export declare function validateIndexerSnapshotHandle(handle: IndexerSnapshotHandle, expectedWalletRootId: string): void;
|
|
18
|
+
export declare function validateIndexerSnapshotPayload(payload: IndexerSnapshotPayload, handle: IndexerSnapshotHandle, expectedWalletRootId: string): void;
|
|
19
|
+
export declare function mapIndexerDaemonValidationError<TClient>(error: unknown, status: ManagedIndexerDaemonObservedStatus): ManagedIndexerDaemonProbeResult<TClient>;
|
|
20
|
+
export declare function mapIndexerDaemonTransportError<TClient>(error: unknown): ManagedIndexerDaemonProbeResult<TClient>;
|
|
21
|
+
export declare function isStaleIndexerDaemonVersion(status: ManagedIndexerDaemonObservedStatus | null, expectedBinaryVersion: string | null | undefined): boolean;
|
|
22
|
+
export declare function resolveIndexerDaemonProbeDecision<TClient>(options: {
|
|
23
|
+
probe: ManagedIndexerDaemonProbeResult<TClient>;
|
|
24
|
+
expectedBinaryVersion: string | null | undefined;
|
|
25
|
+
}): IndexerDaemonProbeDecision;
|
|
26
|
+
export declare function deriveManagedIndexerWalletStatus(options: {
|
|
27
|
+
daemonStatus: ManagedIndexerDaemonStatus | null;
|
|
28
|
+
observedStatus?: ManagedIndexerDaemonObservedStatus | null;
|
|
29
|
+
snapshot: ManagedIndexerSnapshotLike | null;
|
|
30
|
+
source: ManagedIndexerTruthSource;
|
|
31
|
+
now: number;
|
|
32
|
+
startupError: string | null;
|
|
33
|
+
}): WalletIndexerStatus;
|
|
34
|
+
export { buildManagedIndexerStatusFromSnapshotHandle };
|