@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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -7
  3. package/dist/bitcoind/indexer-daemon.js +43 -158
  4. package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
  5. package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
  6. package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
  7. package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
  8. package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
  9. package/dist/bitcoind/managed-runtime/status.js +59 -0
  10. package/dist/bitcoind/managed-runtime/types.d.ts +37 -0
  11. package/dist/bitcoind/managed-runtime/types.js +1 -0
  12. package/dist/bitcoind/service.d.ts +2 -7
  13. package/dist/bitcoind/service.js +46 -94
  14. package/dist/wallet/lifecycle/access.d.ts +5 -0
  15. package/dist/wallet/lifecycle/access.js +79 -0
  16. package/dist/wallet/lifecycle/context.d.ts +26 -0
  17. package/dist/wallet/lifecycle/context.js +58 -0
  18. package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
  19. package/dist/wallet/lifecycle/managed-core.js +3 -63
  20. package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
  21. package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
  22. package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
  23. package/dist/wallet/lifecycle/repair-indexer.js +117 -0
  24. package/dist/wallet/lifecycle/repair.d.ts +2 -4
  25. package/dist/wallet/lifecycle/repair.js +77 -318
  26. package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
  27. package/dist/wallet/lifecycle/setup-prompts.js +88 -0
  28. package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
  29. package/dist/wallet/lifecycle/setup-state.js +159 -0
  30. package/dist/wallet/lifecycle/setup.d.ts +3 -4
  31. package/dist/wallet/lifecycle/setup.js +45 -351
  32. package/dist/wallet/lifecycle/types.d.ts +33 -2
  33. package/dist/wallet/read/context.js +13 -188
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.5` 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.
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 IndexerDaemonCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "schema-mismatch" | "unreachable" | "protocol-error";
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, readFile, rm } from "node:fs/promises";
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 { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
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 readJsonFile(paths.indexerDaemonStatusPath);
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: buildStatusFromSnapshotHandle(handle),
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 readJsonFile(paths.indexerDaemonStatusPath);
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
- if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
471
- if (!isStaleIndexerDaemonVersion(existingProbe.status, expectedBinaryVersion)) {
472
- try {
473
- return await requestBackgroundFollow(existingProbe.client, existingProbe.status);
474
- }
475
- catch {
476
- await existingProbe.client.close().catch(() => undefined);
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
- else {
359
+ catch {
480
360
  await existingProbe.client.close().catch(() => undefined);
481
361
  }
482
362
  }
483
- if (existingProbe.compatibility !== "unreachable" && existingProbe.compatibility !== "compatible") {
484
- throw new Error(existingProbe.error ?? "indexer_daemon_protocol_error");
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
- if (liveProbe.compatibility === "compatible" && liveProbe.client !== null) {
496
- if (!isStaleIndexerDaemonVersion(liveProbe.status, expectedBinaryVersion)) {
497
- try {
498
- return await requestBackgroundFollow(liveProbe.client, liveProbe.status);
499
- }
500
- catch {
501
- await liveProbe.client.close().catch(() => undefined);
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
- else {
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.compatibility !== "unreachable") {
523
- throw new Error(liveProbe.error ?? "indexer_daemon_protocol_error");
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 readJsonFile(paths.indexerDaemonStatusPath);
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 };