@cogcoin/client 1.0.1 → 1.1.0

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 (79) hide show
  1. package/README.md +4 -2
  2. package/dist/bitcoind/client/factory.d.ts +0 -8
  3. package/dist/bitcoind/client/factory.js +1 -59
  4. package/dist/bitcoind/client/managed-client.d.ts +1 -3
  5. package/dist/bitcoind/client/managed-client.js +3 -47
  6. package/dist/bitcoind/indexer-daemon-main.js +173 -28
  7. package/dist/bitcoind/indexer-daemon.d.ts +14 -3
  8. package/dist/bitcoind/indexer-daemon.js +145 -29
  9. package/dist/bitcoind/indexer-monitor.d.ts +12 -0
  10. package/dist/bitcoind/indexer-monitor.js +89 -0
  11. package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
  12. package/dist/bitcoind/progress/follow-scene.js +87 -4
  13. package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
  14. package/dist/bitcoind/progress/tty-renderer.js +2 -0
  15. package/dist/bitcoind/retryable-rpc.js +3 -0
  16. package/dist/bitcoind/service.d.ts +1 -0
  17. package/dist/bitcoind/service.js +31 -9
  18. package/dist/bitcoind/testing.d.ts +0 -1
  19. package/dist/bitcoind/testing.js +0 -1
  20. package/dist/bitcoind/types.d.ts +5 -2
  21. package/dist/cli/commands/follow.js +44 -49
  22. package/dist/cli/commands/mining-admin.js +65 -2
  23. package/dist/cli/commands/mining-read.js +43 -3
  24. package/dist/cli/commands/mining-runtime.js +91 -73
  25. package/dist/cli/commands/service-runtime.js +42 -2
  26. package/dist/cli/commands/status.js +3 -1
  27. package/dist/cli/commands/sync.js +50 -90
  28. package/dist/cli/commands/update.d.ts +2 -0
  29. package/dist/cli/commands/update.js +101 -0
  30. package/dist/cli/commands/wallet-admin.js +21 -3
  31. package/dist/cli/commands/wallet-read.js +2 -0
  32. package/dist/cli/context.js +36 -1
  33. package/dist/cli/managed-indexer-observer.d.ts +33 -0
  34. package/dist/cli/managed-indexer-observer.js +163 -0
  35. package/dist/cli/mining-format.d.ts +3 -1
  36. package/dist/cli/mining-format.js +63 -0
  37. package/dist/cli/mining-json.d.ts +11 -1
  38. package/dist/cli/mining-json.js +15 -0
  39. package/dist/cli/output.js +74 -2
  40. package/dist/cli/parse.d.ts +1 -1
  41. package/dist/cli/parse.js +28 -0
  42. package/dist/cli/prompt.js +109 -0
  43. package/dist/cli/read-json.d.ts +26 -1
  44. package/dist/cli/read-json.js +48 -0
  45. package/dist/cli/runner.js +8 -2
  46. package/dist/cli/signals.d.ts +12 -0
  47. package/dist/cli/signals.js +31 -13
  48. package/dist/cli/types.d.ts +13 -4
  49. package/dist/cli/update-notifier.js +7 -222
  50. package/dist/cli/update-service.d.ts +34 -0
  51. package/dist/cli/update-service.js +152 -0
  52. package/dist/client/initialization.js +5 -0
  53. package/dist/semver.d.ts +12 -0
  54. package/dist/semver.js +68 -0
  55. package/dist/wallet/lifecycle.d.ts +10 -0
  56. package/dist/wallet/mining/config.js +64 -3
  57. package/dist/wallet/mining/control.d.ts +5 -1
  58. package/dist/wallet/mining/control.js +269 -26
  59. package/dist/wallet/mining/domain-prompts.d.ts +17 -0
  60. package/dist/wallet/mining/domain-prompts.js +130 -0
  61. package/dist/wallet/mining/index.d.ts +2 -1
  62. package/dist/wallet/mining/index.js +1 -0
  63. package/dist/wallet/mining/provider-model.d.ts +30 -0
  64. package/dist/wallet/mining/provider-model.js +134 -0
  65. package/dist/wallet/mining/runner.d.ts +156 -5
  66. package/dist/wallet/mining/runner.js +1019 -399
  67. package/dist/wallet/mining/runtime-artifacts.js +1 -0
  68. package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
  69. package/dist/wallet/mining/sentences.d.ts +2 -2
  70. package/dist/wallet/mining/sentences.js +32 -6
  71. package/dist/wallet/mining/types.d.ts +35 -1
  72. package/dist/wallet/mining/visualizer.d.ts +3 -0
  73. package/dist/wallet/mining/visualizer.js +132 -15
  74. package/dist/wallet/read/context.d.ts +1 -0
  75. package/dist/wallet/read/context.js +15 -7
  76. package/dist/wallet/state/client-password-agent.js +4 -1
  77. package/dist/wallet/state/client-password.js +15 -8
  78. package/dist/wallet/tx/common.js +1 -1
  79. package/package.json +3 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.0.0` 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.0` 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
 
@@ -130,13 +130,15 @@ Managed node subpath:
130
130
 
131
131
  The installed `cogcoin` command covers the first-party local wallet and node workflow:
132
132
 
133
+ - update commands such as `update` to compare the current CLI version with the latest npm release and install it
133
134
  - wallet lifecycle commands such as `init`, `restore`, `wallet delete`, `wallet show-mnemonic`, and `repair`
134
135
  - sync and service commands such as `status`, `sync`, `follow`, `bitcoin start`, `bitcoin stop`, `bitcoin status`, `indexer start`, `indexer stop`, and `indexer status`
135
136
  - domain and field commands such as `register`, `anchor`, `show`, `domains`, `fields`, `buy`, `sell`, and `transfer`
136
137
  - COG and reputation commands such as `send`, `cog lock`, `claim`, `reclaim`, `rep give`, and `rep revoke`
137
- - mining commands such as `mine`, `mine start`, `mine stop`, `mine status`, `mine log`, and `mine setup`
138
+ - mining commands such as `mine`, `mine start`, `mine stop`, `mine status`, `mine log`, `mine setup`, `mine prompt`, and `mine prompt list`
138
139
 
139
140
  The CLI also supports stable `--output json` and `--output preview-json` envelopes on the commands that advertise machine-readable output.
141
+ 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.
140
142
  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.
141
143
  Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
142
144
  Ordinary `sync`, `follow`, and wallet-aware read/status flows detach from the managed Bitcoin and indexer services on exit instead of stopping them.
@@ -1,11 +1,3 @@
1
- import { stopIndexerDaemonService, type IndexerDaemonClient } from "../indexer-daemon.js";
2
1
  import type { InternalManagedBitcoindOptions, ManagedBitcoindClient } from "../types.js";
3
- export declare function pauseIndexerDaemonForForegroundClientForTesting(options: {
4
- daemon: IndexerDaemonClient;
5
- dataDir: string;
6
- walletRootId: string;
7
- shutdownTimeoutMs?: number;
8
- stopDaemon?: typeof stopIndexerDaemonService;
9
- }): Promise<IndexerDaemonClient | null>;
10
2
  export declare function openManagedBitcoindClient(options: Omit<InternalManagedBitcoindOptions, "chain" | "startHeight">): Promise<ManagedBitcoindClient>;
11
3
  export declare function openManagedBitcoindClientInternal(options: InternalManagedBitcoindOptions): Promise<ManagedBitcoindClient>;
@@ -2,46 +2,12 @@ import { loadBundledGenesisParameters } from "@cogcoin/indexer";
2
2
  import { resolveDefaultBitcoindDataDirForTesting } from "../../app-paths.js";
3
3
  import { openClient } from "../../client.js";
4
4
  import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting, } from "../bootstrap.js";
5
- import { attachOrStartIndexerDaemon, stopIndexerDaemonService, } from "../indexer-daemon.js";
6
5
  import { createRpcClient } from "../node.js";
7
6
  import { assertCogcoinProcessingStartHeight, resolveCogcoinProcessingStartHeight, } from "../processing-start-height.js";
8
7
  import { ManagedProgressController } from "../progress.js";
9
8
  import { attachOrStartManagedBitcoindService, } from "../service.js";
10
9
  import { DefaultManagedBitcoindClient } from "./managed-client.js";
11
10
  const DEFAULT_SYNC_DEBOUNCE_MS = 250;
12
- function isRecoverableIndexerDaemonPauseError(error) {
13
- if (!(error instanceof Error)) {
14
- return false;
15
- }
16
- if (error.message === "indexer_daemon_request_timeout"
17
- || error.message === "indexer_daemon_connection_closed"
18
- || error.message === "indexer_daemon_protocol_error") {
19
- return true;
20
- }
21
- if ("code" in error) {
22
- const code = error.code;
23
- return code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET";
24
- }
25
- return false;
26
- }
27
- export async function pauseIndexerDaemonForForegroundClientForTesting(options) {
28
- try {
29
- await options.daemon.pauseBackgroundFollow();
30
- return options.daemon;
31
- }
32
- catch (error) {
33
- await options.daemon.close().catch(() => undefined);
34
- if (!isRecoverableIndexerDaemonPauseError(error)) {
35
- throw error;
36
- }
37
- await (options.stopDaemon ?? stopIndexerDaemonService)({
38
- dataDir: options.dataDir,
39
- walletRootId: options.walletRootId,
40
- shutdownTimeoutMs: options.shutdownTimeoutMs,
41
- });
42
- return null;
43
- }
44
- }
45
11
  async function createManagedBitcoindClient(options) {
46
12
  const genesisParameters = options.genesisParameters ?? await loadBundledGenesisParameters();
47
13
  assertCogcoinProcessingStartHeight({
@@ -83,31 +49,7 @@ async function createManagedBitcoindClient(options) {
83
49
  genesisParameters,
84
50
  snapshotInterval: options.snapshotInterval,
85
51
  });
86
- const indexerDaemon = options.databasePath
87
- ? await pauseIndexerDaemonForForegroundClientForTesting({
88
- daemon: await attachOrStartIndexerDaemon({
89
- dataDir,
90
- databasePath: options.databasePath,
91
- walletRootId: options.walletRootId,
92
- startupTimeoutMs: options.startupTimeoutMs,
93
- }),
94
- dataDir,
95
- walletRootId,
96
- shutdownTimeoutMs: options.shutdownTimeoutMs,
97
- })
98
- : null;
99
- // The persistent service may already exist from a non-processing attach path
100
- // that used startHeight 0. Cogcoin replay still begins at the requested
101
- // processing boundary for this managed client.
102
- const databasePath = options.databasePath ?? null;
103
- return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, indexerDaemon, databasePath
104
- ? async () => attachOrStartIndexerDaemon({
105
- dataDir,
106
- databasePath,
107
- walletRootId: options.walletRootId,
108
- startupTimeoutMs: options.startupTimeoutMs,
109
- })
110
- : null, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS, dataDir, walletRootId, options.startupTimeoutMs, options.shutdownTimeoutMs, options.fetchImpl);
52
+ return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS, dataDir, walletRootId, options.startupTimeoutMs, options.shutdownTimeoutMs, options.fetchImpl);
111
53
  }
112
54
  catch (error) {
113
55
  if (progressStarted) {
@@ -1,14 +1,13 @@
1
1
  import type { BitcoinBlock } from "@cogcoin/indexer/types";
2
2
  import type { ClientStoreAdapter } from "../../types.js";
3
3
  import { AssumeUtxoBootstrapController } from "../bootstrap.js";
4
- import type { IndexerDaemonClient } from "../indexer-daemon.js";
5
4
  import type { ManagedProgressController } from "../progress.js";
6
5
  import type { BitcoinRpcClient } from "../rpc.js";
7
6
  import type { ManagedBitcoindClient, ManagedBitcoindNodeHandle, ManagedBitcoindStatus, SyncResult } from "../types.js";
8
7
  import { type SyncRecoveryClient } from "./internal-types.js";
9
8
  export declare class DefaultManagedBitcoindClient implements ManagedBitcoindClient {
10
9
  #private;
11
- constructor(client: SyncRecoveryClient, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, indexerDaemon: IndexerDaemonClient | null, reattachIndexerDaemon: (() => Promise<IndexerDaemonClient | null>) | null, startHeight: number, syncDebounceMs: number, dataDir: string, walletRootId: string, startupTimeoutMs: number | undefined, shutdownTimeoutMs: number | undefined, fetchImpl: typeof fetch | undefined);
10
+ constructor(client: SyncRecoveryClient, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, startHeight: number, syncDebounceMs: number, dataDir: string, walletRootId: string, startupTimeoutMs: number | undefined, shutdownTimeoutMs: number | undefined, fetchImpl: typeof fetch | undefined);
12
11
  getTip(): Promise<import("../../types.js").ClientTip | null>;
13
12
  getState(): Promise<import("@cogcoin/indexer/types").IndexerState>;
14
13
  applyBlock(block: BitcoinBlock): Promise<import("../../types.js").ApplyBlockResult>;
@@ -18,5 +17,4 @@ export declare class DefaultManagedBitcoindClient implements ManagedBitcoindClie
18
17
  getNodeStatus(): Promise<ManagedBitcoindStatus>;
19
18
  close(): Promise<void>;
20
19
  playSyncCompletionScene(): Promise<void>;
21
- detachToBackgroundFollow(): Promise<void>;
22
20
  }
@@ -26,8 +26,6 @@ export class DefaultManagedBitcoindClient {
26
26
  #rpc;
27
27
  #progress;
28
28
  #bootstrap;
29
- #indexerDaemon;
30
- #reattachIndexerDaemon;
31
29
  #startHeight;
32
30
  #syncDebounceMs;
33
31
  #dataDir;
@@ -45,16 +43,13 @@ export class DefaultManagedBitcoindClient {
45
43
  #syncPromise = Promise.resolve(createInitialSyncResult());
46
44
  #debounceTimer = null;
47
45
  #syncAbortControllers = new Set();
48
- #backgroundFollowResumed = false;
49
- constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, reattachIndexerDaemon, startHeight, syncDebounceMs, dataDir, walletRootId, startupTimeoutMs, shutdownTimeoutMs, fetchImpl) {
46
+ constructor(client, store, node, rpc, progress, bootstrap, startHeight, syncDebounceMs, dataDir, walletRootId, startupTimeoutMs, shutdownTimeoutMs, fetchImpl) {
50
47
  this.#client = client;
51
48
  this.#store = store;
52
49
  this.#node = node;
53
50
  this.#rpc = rpc;
54
51
  this.#progress = progress;
55
52
  this.#bootstrap = bootstrap;
56
- this.#indexerDaemon = indexerDaemon;
57
- this.#reattachIndexerDaemon = reattachIndexerDaemon;
58
53
  this.#startHeight = startHeight;
59
54
  this.#syncDebounceMs = syncDebounceMs;
60
55
  this.#dataDir = dataDir;
@@ -119,7 +114,6 @@ export class DefaultManagedBitcoindClient {
119
114
  const indexedTip = await this.#client.getTip();
120
115
  const progressStatus = this.#progress.getStatusSnapshot();
121
116
  const serviceStatus = await this.#node.refreshServiceStatus?.();
122
- const daemonStatus = await this.#indexerDaemon?.getStatus().catch(() => null);
123
117
  try {
124
118
  const info = await this.#rpc.getBlockchainInfo();
125
119
  return {
@@ -143,7 +137,7 @@ export class DefaultManagedBitcoindClient {
143
137
  serviceUpdatedAtUnixMs: serviceStatus?.updatedAtUnixMs ?? null,
144
138
  walletReplica: serviceStatus?.walletReplica ?? null,
145
139
  serviceStatus: serviceStatus ?? null,
146
- indexerDaemon: daemonStatus ?? null,
140
+ indexerDaemon: null,
147
141
  };
148
142
  }
149
143
  catch {
@@ -168,7 +162,7 @@ export class DefaultManagedBitcoindClient {
168
162
  serviceUpdatedAtUnixMs: serviceStatus?.updatedAtUnixMs ?? null,
169
163
  walletReplica: serviceStatus?.walletReplica ?? null,
170
164
  serviceStatus: serviceStatus ?? null,
171
- indexerDaemon: daemonStatus ?? null,
165
+ indexerDaemon: null,
172
166
  };
173
167
  }
174
168
  }
@@ -197,20 +191,11 @@ export class DefaultManagedBitcoindClient {
197
191
  await this.#progress.close();
198
192
  await this.#node.stop();
199
193
  await this.#client.close();
200
- await this.#resumeIndexerBackgroundFollow();
201
- await this.#indexerDaemon?.close();
202
- this.#indexerDaemon = null;
203
194
  }
204
195
  async playSyncCompletionScene() {
205
196
  this.#assertOpen();
206
197
  await this.#progress.playCompletionScene();
207
198
  }
208
- async detachToBackgroundFollow() {
209
- this.#assertOpen();
210
- await this.#resumeIndexerBackgroundFollow();
211
- await this.#indexerDaemon?.close();
212
- this.#indexerDaemon = null;
213
- }
214
199
  async #setGetblockStatusMessage(currentHeight, message, targetHeight = currentHeight) {
215
200
  const safeTargetHeight = Math.max(currentHeight, targetHeight);
216
201
  await this.#progress.setPhase("bitcoin_sync", {
@@ -404,33 +389,4 @@ export class DefaultManagedBitcoindClient {
404
389
  throw new Error("managed_bitcoind_client_closed");
405
390
  }
406
391
  }
407
- async #resumeIndexerBackgroundFollow() {
408
- if (this.#backgroundFollowResumed) {
409
- return;
410
- }
411
- if (this.#indexerDaemon === null && this.#reattachIndexerDaemon === null) {
412
- this.#backgroundFollowResumed = true;
413
- return;
414
- }
415
- if (this.#indexerDaemon !== null) {
416
- try {
417
- await this.#indexerDaemon.resumeBackgroundFollow();
418
- this.#backgroundFollowResumed = true;
419
- return;
420
- }
421
- catch (error) {
422
- if (this.#reattachIndexerDaemon === null) {
423
- throw error;
424
- }
425
- }
426
- }
427
- const reattachIndexerDaemon = this.#reattachIndexerDaemon;
428
- if (reattachIndexerDaemon === null) {
429
- return;
430
- }
431
- const replacementDaemon = await reattachIndexerDaemon();
432
- this.#indexerDaemon = replacementDaemon;
433
- await replacementDaemon?.resumeBackgroundFollow();
434
- this.#backgroundFollowResumed = true;
435
- }
436
392
  }
@@ -3,15 +3,20 @@ import net from "node:net";
3
3
  import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
4
4
  import { loadBundledGenesisParameters, serializeIndexerState } from "@cogcoin/indexer";
5
5
  import { openManagedBitcoindClientInternal } from "./client.js";
6
+ import { DEFAULT_SNAPSHOT_METADATA } from "./bootstrap.js";
6
7
  import { openClient } from "../client.js";
7
8
  import { openSqliteStore } from "../sqlite/index.js";
8
9
  import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
9
10
  import { createRpcClient } from "./node.js";
10
11
  import { normalizeCogcoinProcessingStartHeight } from "./processing-start-height.js";
12
+ import { createBootstrapProgress } from "./progress/formatting.js";
11
13
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
12
14
  import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
13
15
  const SNAPSHOT_TTL_MS = 30_000;
14
16
  const HEARTBEAT_INTERVAL_MS = 1_000;
17
+ const FORCE_RESUME_ERROR_ENV = "COGCOIN_TEST_INDEXER_DAEMON_FORCE_RESUME_ERROR";
18
+ const BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS = 30_000;
19
+ const BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR = "indexer_daemon_background_follow_resume_timeout";
15
20
  function parseArg(name) {
16
21
  const prefix = `--${name}=`;
17
22
  const value = process.argv.find((entry) => entry.startsWith(prefix));
@@ -34,6 +39,22 @@ async function readJsonFile(filePath) {
34
39
  async function readManagedBitcoindStatus(paths) {
35
40
  return readJsonFile(paths.bitcoindStatusPath);
36
41
  }
42
+ async function withTimeout(promise, timeoutMs, errorCode) {
43
+ let timeoutId = null;
44
+ try {
45
+ return await Promise.race([
46
+ promise,
47
+ new Promise((_, reject) => {
48
+ timeoutId = setTimeout(() => reject(new Error(errorCode)), timeoutMs);
49
+ }),
50
+ ]);
51
+ }
52
+ finally {
53
+ if (timeoutId !== null) {
54
+ clearTimeout(timeoutId);
55
+ }
56
+ }
57
+ }
37
58
  async function readPackageVersionFromDisk() {
38
59
  try {
39
60
  const raw = await readFile(new URL("../../package.json", import.meta.url), "utf8");
@@ -177,6 +198,12 @@ async function main() {
177
198
  let backgroundStore = null;
178
199
  let backgroundClient = null;
179
200
  let backgroundResumePromise = null;
201
+ let backgroundFollowError = null;
202
+ let backgroundFollowActive = false;
203
+ let bootstrapPhase = "paused";
204
+ let bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
205
+ let cogcoinSyncHeight = null;
206
+ let cogcoinSyncTargetHeight = null;
180
207
  await mkdir(paths.indexerServiceRoot, { recursive: true });
181
208
  await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
182
209
  const observeAppliedTip = (appliedTip, now) => {
@@ -240,12 +267,39 @@ async function main() {
240
267
  lastAppliedAtUnixMs,
241
268
  activeSnapshotCount: snapshots.size,
242
269
  lastError,
270
+ backgroundFollowActive,
271
+ bootstrapPhase,
272
+ bootstrapProgress: { ...bootstrapProgress },
273
+ cogcoinSyncHeight,
274
+ cogcoinSyncTargetHeight,
243
275
  });
244
276
  const writeStatus = async () => {
245
277
  const status = buildStatus();
246
278
  await writeRuntimeStatusFile(paths.indexerDaemonStatusPath, status);
247
279
  return status;
248
280
  };
281
+ const recordBackgroundFollowFailure = async (message) => {
282
+ const now = Date.now();
283
+ heartbeatAtUnixMs = now;
284
+ updatedAtUnixMs = now;
285
+ state = "failed";
286
+ lastError = message;
287
+ backgroundFollowError = message;
288
+ backgroundFollowActive = false;
289
+ bootstrapPhase = "error";
290
+ bootstrapProgress = {
291
+ ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
292
+ blocks: coreBestHeight,
293
+ headers: coreBestHeight,
294
+ targetHeight: coreBestHeight,
295
+ message,
296
+ lastError: message,
297
+ updatedAt: now,
298
+ };
299
+ cogcoinSyncHeight = appliedTipHeight;
300
+ cogcoinSyncTargetHeight = coreBestHeight;
301
+ await writeStatus();
302
+ };
249
303
  const refreshStatus = async () => {
250
304
  const now = Date.now();
251
305
  heartbeatAtUnixMs = now;
@@ -254,23 +308,88 @@ async function main() {
254
308
  readCoreTipStatus(paths),
255
309
  readAppliedTipStatus(databasePath),
256
310
  ]);
311
+ const backgroundStatus = await backgroundClient?.getNodeStatus().catch(() => null) ?? null;
312
+ if (backgroundStatus?.following === true) {
313
+ backgroundFollowError = null;
314
+ }
257
315
  rpcReachable = coreStatus.rpcReachable;
258
316
  coreBestHeight = coreStatus.coreBestHeight;
259
317
  coreBestHash = coreStatus.coreBestHash;
260
318
  observeAppliedTip(indexedStatus.appliedTip, now);
319
+ backgroundFollowActive = backgroundStatus?.following ?? (backgroundClient !== null);
320
+ bootstrapPhase = backgroundStatus?.bootstrapPhase ?? (backgroundFollowActive ? "follow_tip" : "paused");
321
+ bootstrapProgress = backgroundStatus?.bootstrapProgress ?? createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA);
322
+ cogcoinSyncHeight = backgroundStatus?.cogcoinSyncHeight ?? indexedStatus.appliedTip?.height ?? null;
323
+ cogcoinSyncTargetHeight = backgroundStatus?.cogcoinSyncTargetHeight ?? coreStatus.coreBestHeight;
324
+ if (backgroundStatus === null && backgroundFollowError !== null) {
325
+ state = "failed";
326
+ lastError = backgroundFollowError;
327
+ backgroundFollowActive = false;
328
+ bootstrapPhase = "error";
329
+ bootstrapProgress = {
330
+ ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
331
+ blocks: coreStatus.coreBestHeight,
332
+ headers: coreStatus.coreBestHeight,
333
+ targetHeight: coreStatus.coreBestHeight,
334
+ message: backgroundFollowError,
335
+ lastError: backgroundFollowError,
336
+ updatedAt: now,
337
+ };
338
+ cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
339
+ cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
340
+ return writeStatus();
341
+ }
261
342
  if (indexedStatus.schemaMismatch) {
262
343
  state = "schema-mismatch";
263
344
  lastError = indexedStatus.error;
345
+ bootstrapPhase = "error";
346
+ bootstrapProgress = {
347
+ ...bootstrapProgress,
348
+ phase: "error",
349
+ message: indexedStatus.error ?? "Indexer schema mismatch.",
350
+ lastError: indexedStatus.error,
351
+ updatedAt: now,
352
+ };
264
353
  return writeStatus();
265
354
  }
266
355
  if (indexedStatus.error !== null) {
267
356
  state = "failed";
268
357
  lastError = indexedStatus.error;
358
+ bootstrapPhase = "error";
359
+ bootstrapProgress = {
360
+ ...bootstrapProgress,
361
+ phase: "error",
362
+ message: indexedStatus.error,
363
+ lastError: indexedStatus.error,
364
+ updatedAt: now,
365
+ };
269
366
  return writeStatus();
270
367
  }
271
368
  const leaseState = deriveLeaseState(coreStatus, indexedStatus.appliedTip);
272
369
  state = leaseState.state;
273
370
  lastError = leaseState.lastError;
371
+ if (lastError !== null) {
372
+ bootstrapPhase = leaseState.state === "starting" ? "paused" : "error";
373
+ bootstrapProgress = {
374
+ ...bootstrapProgress,
375
+ phase: bootstrapPhase,
376
+ message: lastError,
377
+ lastError,
378
+ updatedAt: now,
379
+ };
380
+ }
381
+ else if (backgroundStatus === null) {
382
+ bootstrapPhase = leaseState.state === "synced" ? "follow_tip" : "paused";
383
+ bootstrapProgress = {
384
+ ...createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA),
385
+ blocks: coreStatus.coreBestHeight,
386
+ headers: coreStatus.coreBestHeight,
387
+ targetHeight: coreStatus.coreBestHeight,
388
+ updatedAt: now,
389
+ };
390
+ cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
391
+ cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
392
+ }
274
393
  return writeStatus();
275
394
  };
276
395
  const pauseBackgroundFollow = async () => {
@@ -283,6 +402,12 @@ async function main() {
283
402
  backgroundStore = null;
284
403
  await client?.close().catch(() => undefined);
285
404
  await store?.close().catch(() => undefined);
405
+ backgroundFollowError = null;
406
+ backgroundFollowActive = false;
407
+ bootstrapPhase = "paused";
408
+ bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
409
+ cogcoinSyncHeight = appliedTipHeight;
410
+ cogcoinSyncTargetHeight = coreBestHeight;
286
411
  };
287
412
  const resumeBackgroundFollow = async () => {
288
413
  if (backgroundClient !== null) {
@@ -292,35 +417,50 @@ async function main() {
292
417
  return backgroundResumePromise;
293
418
  }
294
419
  backgroundResumePromise = (async () => {
295
- const bitcoindStatus = await readManagedBitcoindStatus(paths);
296
- const store = await openSqliteStore({ filename: databasePath });
297
- const chain = bitcoindStatus?.chain ?? "main";
298
- const startHeight = normalizeCogcoinProcessingStartHeight({
299
- chain,
300
- startHeight: bitcoindStatus?.startHeight,
301
- genesisParameters,
302
- });
420
+ let store = null;
303
421
  try {
422
+ const forcedResumeError = process.env[FORCE_RESUME_ERROR_ENV]?.trim();
423
+ if (forcedResumeError) {
424
+ throw new Error(forcedResumeError);
425
+ }
426
+ const bitcoindStatus = await readManagedBitcoindStatus(paths);
427
+ store = await openSqliteStore({ filename: databasePath });
428
+ const openedStore = store;
429
+ const chain = bitcoindStatus?.chain ?? "main";
430
+ const startHeight = normalizeCogcoinProcessingStartHeight({
431
+ chain,
432
+ startHeight: bitcoindStatus?.startHeight,
433
+ genesisParameters,
434
+ });
304
435
  const client = await openManagedBitcoindClientInternal({
305
- store,
436
+ store: openedStore,
306
437
  dataDir,
307
438
  chain,
308
439
  startHeight,
309
440
  walletRootId,
310
441
  progressOutput: "none",
311
442
  });
312
- try {
313
- await client.startFollowingTip();
314
- backgroundStore = store;
315
- backgroundClient = client;
316
- }
317
- catch (error) {
443
+ backgroundStore = openedStore;
444
+ backgroundClient = client;
445
+ backgroundFollowError = null;
446
+ backgroundFollowActive = true;
447
+ void client.startFollowingTip().catch(async (error) => {
448
+ if (backgroundClient !== client || backgroundStore !== openedStore) {
449
+ return;
450
+ }
451
+ backgroundClient = null;
452
+ backgroundStore = null;
453
+ backgroundFollowActive = false;
318
454
  await client.close().catch(() => undefined);
319
- throw error;
320
- }
455
+ await openedStore.close().catch(() => undefined);
456
+ const message = error instanceof Error ? error.message : String(error);
457
+ await recordBackgroundFollowFailure(message).catch(() => undefined);
458
+ });
321
459
  }
322
460
  catch (error) {
323
- await store.close().catch(() => undefined);
461
+ await store?.close().catch(() => undefined);
462
+ const message = error instanceof Error ? error.message : String(error);
463
+ await recordBackgroundFollowFailure(message).catch(() => undefined);
324
464
  throw error;
325
465
  }
326
466
  })();
@@ -434,6 +574,11 @@ async function main() {
434
574
  lastAppliedAtUnixMs: leaseStatus.lastAppliedAtUnixMs,
435
575
  activeSnapshotCount: leaseStatus.activeSnapshotCount,
436
576
  lastError: leaseStatus.lastError,
577
+ backgroundFollowActive: leaseStatus.backgroundFollowActive ?? false,
578
+ bootstrapPhase: leaseStatus.bootstrapPhase ?? null,
579
+ bootstrapProgress: leaseStatus.bootstrapProgress ?? null,
580
+ cogcoinSyncHeight: leaseStatus.cogcoinSyncHeight ?? null,
581
+ cogcoinSyncTargetHeight: leaseStatus.cogcoinSyncTargetHeight ?? null,
437
582
  tipHeight: snapshot.tipHeight,
438
583
  tipHash: snapshot.tipHash,
439
584
  openedAtUnixMs: snapshot.openedAtUnixMs,
@@ -494,17 +639,17 @@ async function main() {
494
639
  });
495
640
  return;
496
641
  }
497
- if (request.method === "PauseBackgroundFollow") {
498
- await pauseBackgroundFollow();
499
- writeResponse({
500
- id: request.id,
501
- ok: true,
502
- result: null,
503
- });
504
- return;
505
- }
506
642
  if (request.method === "ResumeBackgroundFollow") {
507
- await resumeBackgroundFollow();
643
+ try {
644
+ await withTimeout(resumeBackgroundFollow(), BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS, BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR);
645
+ }
646
+ catch (error) {
647
+ if (error instanceof Error
648
+ && error.message === BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR) {
649
+ await recordBackgroundFollowFailure(error.message).catch(() => undefined);
650
+ }
651
+ throw error;
652
+ }
508
653
  writeResponse({
509
654
  id: request.id,
510
655
  ok: true,
@@ -1,8 +1,9 @@
1
- import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
1
+ import { type BootstrapPhase, type BootstrapProgress, type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
2
2
  import { resolveManagedServicePaths } from "./service-paths.js";
3
+ export declare const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
3
4
  interface DaemonRequest {
4
5
  id: string;
5
- method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot" | "PauseBackgroundFollow" | "ResumeBackgroundFollow";
6
+ method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot" | "ResumeBackgroundFollow";
6
7
  token?: string;
7
8
  }
8
9
  interface DaemonResponse {
@@ -35,6 +36,11 @@ export interface IndexerSnapshotHandle {
35
36
  lastAppliedAtUnixMs: number | null;
36
37
  activeSnapshotCount: number;
37
38
  lastError: string | null;
39
+ backgroundFollowActive: boolean;
40
+ bootstrapPhase: BootstrapPhase | null;
41
+ bootstrapProgress: BootstrapProgress | null;
42
+ cogcoinSyncHeight: number | null;
43
+ cogcoinSyncTargetHeight: number | null;
38
44
  tipHeight: number | null;
39
45
  tipHash: string | null;
40
46
  openedAtUnixMs: number;
@@ -65,7 +71,6 @@ export interface IndexerDaemonClient {
65
71
  openSnapshot(): Promise<IndexerSnapshotHandle>;
66
72
  readSnapshot(token: string): Promise<IndexerSnapshotPayload>;
67
73
  closeSnapshot(token: string): Promise<void>;
68
- pauseBackgroundFollow(): Promise<void>;
69
74
  resumeBackgroundFollow(): Promise<void>;
70
75
  close(): Promise<void>;
71
76
  }
@@ -84,11 +89,13 @@ export interface CoherentIndexerSnapshotLease {
84
89
  payload: IndexerSnapshotPayload;
85
90
  status: ManagedIndexerDaemonStatus;
86
91
  }
92
+ type ManagedIndexerDaemonServiceLifetime = "persistent" | "ephemeral";
87
93
  export declare function stopIndexerDaemonServiceWithLockHeld(options: {
88
94
  dataDir: string;
89
95
  walletRootId?: string;
90
96
  shutdownTimeoutMs?: number;
91
97
  paths?: ReturnType<typeof resolveManagedServicePaths>;
98
+ processId?: number | null;
92
99
  }): Promise<IndexerDaemonStopResult>;
93
100
  export declare function probeIndexerDaemon(options: {
94
101
  dataDir: string;
@@ -104,6 +111,10 @@ export declare function attachOrStartIndexerDaemon(options: {
104
111
  databasePath: string;
105
112
  walletRootId?: string;
106
113
  startupTimeoutMs?: number;
114
+ shutdownTimeoutMs?: number;
115
+ serviceLifetime?: ManagedIndexerDaemonServiceLifetime;
116
+ ensureBackgroundFollow?: boolean;
117
+ expectedBinaryVersion?: string | null;
107
118
  }): Promise<IndexerDaemonClient>;
108
119
  export declare function stopIndexerDaemonService(options: {
109
120
  dataDir: string;