@cogcoin/client 1.1.4 → 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 (107) hide show
  1. package/README.md +4 -5
  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/progress/tty-renderer.js +3 -2
  13. package/dist/bitcoind/service.d.ts +2 -7
  14. package/dist/bitcoind/service.js +46 -94
  15. package/dist/cli/command-registry.d.ts +39 -0
  16. package/dist/cli/command-registry.js +1132 -0
  17. package/dist/cli/commands/client-admin.js +6 -56
  18. package/dist/cli/commands/mining-admin.js +9 -32
  19. package/dist/cli/commands/mining-read.js +15 -56
  20. package/dist/cli/commands/mining-runtime.js +258 -57
  21. package/dist/cli/commands/service-runtime.js +1 -64
  22. package/dist/cli/commands/status.js +2 -15
  23. package/dist/cli/commands/update.js +6 -21
  24. package/dist/cli/commands/wallet-admin.js +18 -120
  25. package/dist/cli/commands/wallet-mutation.js +4 -7
  26. package/dist/cli/commands/wallet-read.js +31 -138
  27. package/dist/cli/context.js +2 -4
  28. package/dist/cli/mining-format.js +8 -2
  29. package/dist/cli/mutation-command-groups.d.ts +11 -11
  30. package/dist/cli/mutation-command-groups.js +9 -18
  31. package/dist/cli/mutation-json.d.ts +1 -17
  32. package/dist/cli/mutation-json.js +1 -28
  33. package/dist/cli/mutation-success.d.ts +0 -1
  34. package/dist/cli/mutation-success.js +0 -19
  35. package/dist/cli/output.d.ts +1 -10
  36. package/dist/cli/output.js +52 -481
  37. package/dist/cli/parse.d.ts +1 -1
  38. package/dist/cli/parse.js +38 -695
  39. package/dist/cli/runner.js +28 -113
  40. package/dist/cli/types.d.ts +7 -8
  41. package/dist/cli/update-notifier.js +1 -1
  42. package/dist/cli/wallet-format.js +1 -1
  43. package/dist/wallet/lifecycle/access.d.ts +5 -0
  44. package/dist/wallet/lifecycle/access.js +79 -0
  45. package/dist/wallet/lifecycle/context.d.ts +26 -0
  46. package/dist/wallet/lifecycle/context.js +58 -0
  47. package/dist/wallet/lifecycle/managed-core.d.ts +15 -0
  48. package/dist/wallet/lifecycle/managed-core.js +197 -0
  49. package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
  50. package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
  51. package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
  52. package/dist/wallet/lifecycle/repair-indexer.js +117 -0
  53. package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
  54. package/dist/wallet/lifecycle/repair-mining.js +304 -0
  55. package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
  56. package/dist/wallet/lifecycle/repair-runtime.js +206 -0
  57. package/dist/wallet/lifecycle/repair.d.ts +9 -0
  58. package/dist/wallet/lifecycle/repair.js +127 -0
  59. package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
  60. package/dist/wallet/lifecycle/setup-prompts.js +88 -0
  61. package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
  62. package/dist/wallet/lifecycle/setup-state.js +159 -0
  63. package/dist/wallet/lifecycle/setup.d.ts +15 -0
  64. package/dist/wallet/lifecycle/setup.js +124 -0
  65. package/dist/wallet/lifecycle/types.d.ts +156 -0
  66. package/dist/wallet/lifecycle/types.js +1 -0
  67. package/dist/wallet/lifecycle.d.ts +4 -165
  68. package/dist/wallet/lifecycle.js +3 -1656
  69. package/dist/wallet/mining/candidate.d.ts +60 -0
  70. package/dist/wallet/mining/candidate.js +290 -0
  71. package/dist/wallet/mining/competitiveness.d.ts +22 -0
  72. package/dist/wallet/mining/competitiveness.js +640 -0
  73. package/dist/wallet/mining/control.js +7 -251
  74. package/dist/wallet/mining/cycle.d.ts +39 -0
  75. package/dist/wallet/mining/cycle.js +542 -0
  76. package/dist/wallet/mining/engine-state.d.ts +66 -0
  77. package/dist/wallet/mining/engine-state.js +211 -0
  78. package/dist/wallet/mining/engine-types.d.ts +173 -0
  79. package/dist/wallet/mining/engine-types.js +1 -0
  80. package/dist/wallet/mining/engine-utils.d.ts +7 -0
  81. package/dist/wallet/mining/engine-utils.js +75 -0
  82. package/dist/wallet/mining/events.d.ts +2 -0
  83. package/dist/wallet/mining/events.js +19 -0
  84. package/dist/wallet/mining/lifecycle.d.ts +71 -0
  85. package/dist/wallet/mining/lifecycle.js +355 -0
  86. package/dist/wallet/mining/projection.d.ts +61 -0
  87. package/dist/wallet/mining/projection.js +319 -0
  88. package/dist/wallet/mining/publish.d.ts +79 -0
  89. package/dist/wallet/mining/publish.js +614 -0
  90. package/dist/wallet/mining/runner.d.ts +12 -418
  91. package/dist/wallet/mining/runner.js +274 -3433
  92. package/dist/wallet/mining/supervisor.d.ts +134 -0
  93. package/dist/wallet/mining/supervisor.js +558 -0
  94. package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
  95. package/dist/wallet/mining/visualizer-sync.js +166 -0
  96. package/dist/wallet/mining/visualizer.d.ts +1 -0
  97. package/dist/wallet/mining/visualizer.js +33 -18
  98. package/dist/wallet/read/context.js +13 -188
  99. package/dist/wallet/reset.d.ts +1 -1
  100. package/dist/wallet/reset.js +35 -11
  101. package/dist/wallet/runtime.d.ts +0 -6
  102. package/dist/wallet/runtime.js +2 -38
  103. package/dist/wallet/tx/common.d.ts +18 -0
  104. package/dist/wallet/tx/common.js +40 -26
  105. package/package.json +1 -1
  106. package/dist/wallet/state/seed-index.d.ts +0 -43
  107. package/dist/wallet/state/seed-index.js +0 -151
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.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.
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
 
@@ -131,21 +131,20 @@ Managed node subpath:
131
131
  The installed `cogcoin` command covers the first-party local wallet and node workflow:
132
132
 
133
133
  - update commands such as `update` to compare the current CLI version with the latest npm release and install it
134
- - wallet lifecycle commands such as `init`, `restore`, `wallet delete`, `wallet show-mnemonic`, and `repair`
134
+ - wallet lifecycle commands such as `init`, `reset`, `wallet show-mnemonic`, and `repair`
135
135
  - sync and service commands such as `status`, `sync`, `follow`, `bitcoin start`, `bitcoin stop`, `bitcoin status`, `indexer start`, `indexer stop`, and `indexer status`
136
136
  - domain and field commands such as `register`, `anchor`, `show`, `domains`, `fields`, `buy`, `sell`, and `transfer`
137
137
  - COG and reputation commands such as `send`, `cog lock`, `claim`, `reclaim`, `rep give`, and `rep revoke`
138
138
  - mining commands such as `mine`, `mine start`, `mine stop`, `mine status`, `mine log`, `mine setup`, `mine prompt`, and `mine prompt list`
139
139
 
140
- The CLI also supports stable `--output json` and `--output preview-json` envelopes on the commands that advertise machine-readable output.
141
140
  Use `cogcoin mine prompt <domain>` to set or clear a per-domain mining prompt override for one anchored root domain, and `cogcoin mine prompt list` to inspect the current per-domain prompt state alongside the global fallback prompt.
142
141
  Interactive text invocations periodically check the npm registry for newer `@cogcoin/client` releases and print `npm install -g @cogcoin/client` when a newer version is available.
143
142
  Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
144
143
  Ordinary `sync`, `follow`, and wallet-aware read/status flows detach from the managed Bitcoin and indexer services on exit instead of stopping them.
145
144
  Use the explicit `bitcoin ...` and `indexer ...` commands when you want direct service inspection or start/stop control.
146
145
  For provider-backed local wallets, normal reads, mutations, and mining setup flows load local wallet state on demand whenever the local secret provider is available.
147
- `cogcoin restore` and `cogcoin wallet restore` rebuild a fresh local wallet from a 24-word English BIP39 mnemonic and recreate the managed Core wallet replica.
148
- Run `cogcoin sync` afterward to bootstrap the managed Bitcoin/indexer state.
146
+ When no wallet exists yet, `cogcoin init` interactively lets you either create a new wallet or restore an existing one from a 24-word English BIP39 mnemonic, then continues into sync.
147
+ To replace an existing wallet with a different mnemonic, run `cogcoin reset`, choose `clear wallet entropy`, and then rerun `cogcoin init`.
149
148
 
150
149
  ## SQLite Store
151
150
 
@@ -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 };