@cogcoin/client 1.0.2 → 1.1.1

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 (81) hide show
  1. package/README.md +3 -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/client/sync-engine.js +1 -1
  7. package/dist/bitcoind/indexer-daemon-main.js +171 -35
  8. package/dist/bitcoind/indexer-daemon.d.ts +11 -3
  9. package/dist/bitcoind/indexer-daemon.js +147 -59
  10. package/dist/bitcoind/indexer-monitor.d.ts +12 -0
  11. package/dist/bitcoind/indexer-monitor.js +93 -0
  12. package/dist/bitcoind/progress/controller.js +4 -1
  13. package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
  14. package/dist/bitcoind/progress/follow-scene.js +94 -5
  15. package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
  16. package/dist/bitcoind/progress/tty-renderer.js +2 -0
  17. package/dist/bitcoind/testing.d.ts +0 -1
  18. package/dist/bitcoind/testing.js +0 -1
  19. package/dist/bitcoind/types.d.ts +5 -2
  20. package/dist/cli/commands/follow.js +44 -49
  21. package/dist/cli/commands/mining-admin.js +56 -2
  22. package/dist/cli/commands/mining-read.js +43 -3
  23. package/dist/cli/commands/mining-runtime.js +91 -73
  24. package/dist/cli/commands/service-runtime.js +42 -2
  25. package/dist/cli/commands/status.js +3 -1
  26. package/dist/cli/commands/sync.js +50 -90
  27. package/dist/cli/commands/wallet-admin.js +21 -3
  28. package/dist/cli/commands/wallet-read.js +2 -0
  29. package/dist/cli/context.d.ts +0 -1
  30. package/dist/cli/context.js +7 -24
  31. package/dist/cli/managed-indexer-observer.d.ts +33 -0
  32. package/dist/cli/managed-indexer-observer.js +163 -0
  33. package/dist/cli/mining-format.d.ts +3 -1
  34. package/dist/cli/mining-format.js +35 -0
  35. package/dist/cli/mining-json.d.ts +11 -1
  36. package/dist/cli/mining-json.js +9 -0
  37. package/dist/cli/output.js +24 -0
  38. package/dist/cli/parse.d.ts +1 -1
  39. package/dist/cli/parse.js +23 -0
  40. package/dist/cli/read-json.d.ts +13 -1
  41. package/dist/cli/read-json.js +31 -0
  42. package/dist/cli/runner.js +4 -2
  43. package/dist/cli/signals.d.ts +12 -0
  44. package/dist/cli/signals.js +31 -13
  45. package/dist/cli/types.d.ts +8 -4
  46. package/dist/cli/update-service.d.ts +2 -12
  47. package/dist/cli/update-service.js +2 -68
  48. package/dist/package-version.d.ts +1 -0
  49. package/dist/package-version.js +17 -0
  50. package/dist/semver.d.ts +12 -0
  51. package/dist/semver.js +68 -0
  52. package/dist/wallet/lifecycle.js +0 -6
  53. package/dist/wallet/mining/config.js +54 -3
  54. package/dist/wallet/mining/control.d.ts +5 -2
  55. package/dist/wallet/mining/control.js +153 -34
  56. package/dist/wallet/mining/domain-prompts.d.ts +17 -0
  57. package/dist/wallet/mining/domain-prompts.js +130 -0
  58. package/dist/wallet/mining/index.d.ts +2 -1
  59. package/dist/wallet/mining/index.js +1 -0
  60. package/dist/wallet/mining/runner.d.ts +58 -2
  61. package/dist/wallet/mining/runner.js +553 -331
  62. package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
  63. package/dist/wallet/mining/sentences.js +7 -4
  64. package/dist/wallet/mining/types.d.ts +26 -0
  65. package/dist/wallet/mining/visualizer.d.ts +3 -0
  66. package/dist/wallet/mining/visualizer.js +106 -12
  67. package/dist/wallet/read/context.d.ts +1 -0
  68. package/dist/wallet/read/context.js +19 -10
  69. package/dist/wallet/reset.js +0 -1
  70. package/dist/wallet/state/client-password-agent.js +4 -1
  71. package/dist/wallet/state/client-password.js +15 -8
  72. package/dist/wallet/tx/anchor.js +0 -1
  73. package/dist/wallet/tx/bitcoin-transfer.js +0 -1
  74. package/dist/wallet/tx/cog.js +0 -3
  75. package/dist/wallet/tx/common.js +1 -1
  76. package/dist/wallet/tx/domain-admin.js +0 -1
  77. package/dist/wallet/tx/domain-market.js +0 -3
  78. package/dist/wallet/tx/field.js +0 -1
  79. package/dist/wallet/tx/register.js +0 -1
  80. package/dist/wallet/tx/reputation.js +0 -1
  81. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.0.2` 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
 
@@ -135,9 +135,10 @@ The installed `cogcoin` command covers the first-party local wallet and node wor
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
- - 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`
139
139
 
140
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.
141
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.
142
143
  Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
143
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
  }
@@ -317,6 +317,6 @@ export async function syncToTip(dependencies) {
317
317
  lastError: message,
318
318
  message: "Managed sync can be resumed after the last error.",
319
319
  });
320
- throw new Error(message);
320
+ throw new Error(message, { cause: error instanceof Error ? error : undefined });
321
321
  }
322
322
  }
@@ -3,15 +3,21 @@ 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";
8
+ import { readPackageVersionFromDisk } from "../package-version.js";
7
9
  import { openSqliteStore } from "../sqlite/index.js";
8
10
  import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
9
11
  import { createRpcClient } from "./node.js";
10
12
  import { normalizeCogcoinProcessingStartHeight } from "./processing-start-height.js";
13
+ import { createBootstrapProgress } from "./progress/formatting.js";
11
14
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
12
15
  import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
13
16
  const SNAPSHOT_TTL_MS = 30_000;
14
17
  const HEARTBEAT_INTERVAL_MS = 1_000;
18
+ const FORCE_RESUME_ERROR_ENV = "COGCOIN_TEST_INDEXER_DAEMON_FORCE_RESUME_ERROR";
19
+ const BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS = 30_000;
20
+ const BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR = "indexer_daemon_background_follow_resume_timeout";
15
21
  function parseArg(name) {
16
22
  const prefix = `--${name}=`;
17
23
  const value = process.argv.find((entry) => entry.startsWith(prefix));
@@ -34,14 +40,20 @@ async function readJsonFile(filePath) {
34
40
  async function readManagedBitcoindStatus(paths) {
35
41
  return readJsonFile(paths.bitcoindStatusPath);
36
42
  }
37
- async function readPackageVersionFromDisk() {
43
+ async function withTimeout(promise, timeoutMs, errorCode) {
44
+ let timeoutId = null;
38
45
  try {
39
- const raw = await readFile(new URL("../../package.json", import.meta.url), "utf8");
40
- const parsed = JSON.parse(raw);
41
- return parsed.version ?? "0.0.0";
46
+ return await Promise.race([
47
+ promise,
48
+ new Promise((_, reject) => {
49
+ timeoutId = setTimeout(() => reject(new Error(errorCode)), timeoutMs);
50
+ }),
51
+ ]);
42
52
  }
43
- catch {
44
- return "0.0.0";
53
+ finally {
54
+ if (timeoutId !== null) {
55
+ clearTimeout(timeoutId);
56
+ }
45
57
  }
46
58
  }
47
59
  function createSnapshotKey(appliedTip) {
@@ -156,7 +168,7 @@ async function main() {
156
168
  const walletRootId = parseArg("wallet-root-id") || UNINITIALIZED_WALLET_ROOT_ID;
157
169
  const paths = resolveManagedServicePaths(dataDir, walletRootId);
158
170
  const daemonInstanceId = randomUUID();
159
- const binaryVersion = await readPackageVersionFromDisk();
171
+ const binaryVersion = await readPackageVersionFromDisk().catch(() => "0.0.0");
160
172
  const genesisParameters = await loadBundledGenesisParameters();
161
173
  const startedAtUnixMs = Date.now();
162
174
  const snapshots = new Map();
@@ -177,6 +189,12 @@ async function main() {
177
189
  let backgroundStore = null;
178
190
  let backgroundClient = null;
179
191
  let backgroundResumePromise = null;
192
+ let backgroundFollowError = null;
193
+ let backgroundFollowActive = false;
194
+ let bootstrapPhase = "paused";
195
+ let bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
196
+ let cogcoinSyncHeight = null;
197
+ let cogcoinSyncTargetHeight = null;
180
198
  await mkdir(paths.indexerServiceRoot, { recursive: true });
181
199
  await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
182
200
  const observeAppliedTip = (appliedTip, now) => {
@@ -240,12 +258,39 @@ async function main() {
240
258
  lastAppliedAtUnixMs,
241
259
  activeSnapshotCount: snapshots.size,
242
260
  lastError,
261
+ backgroundFollowActive,
262
+ bootstrapPhase,
263
+ bootstrapProgress: { ...bootstrapProgress },
264
+ cogcoinSyncHeight,
265
+ cogcoinSyncTargetHeight,
243
266
  });
244
267
  const writeStatus = async () => {
245
268
  const status = buildStatus();
246
269
  await writeRuntimeStatusFile(paths.indexerDaemonStatusPath, status);
247
270
  return status;
248
271
  };
272
+ const recordBackgroundFollowFailure = async (message) => {
273
+ const now = Date.now();
274
+ heartbeatAtUnixMs = now;
275
+ updatedAtUnixMs = now;
276
+ state = "failed";
277
+ lastError = message;
278
+ backgroundFollowError = message;
279
+ backgroundFollowActive = false;
280
+ bootstrapPhase = "error";
281
+ bootstrapProgress = {
282
+ ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
283
+ blocks: coreBestHeight,
284
+ headers: coreBestHeight,
285
+ targetHeight: coreBestHeight,
286
+ message,
287
+ lastError: message,
288
+ updatedAt: now,
289
+ };
290
+ cogcoinSyncHeight = appliedTipHeight;
291
+ cogcoinSyncTargetHeight = coreBestHeight;
292
+ await writeStatus();
293
+ };
249
294
  const refreshStatus = async () => {
250
295
  const now = Date.now();
251
296
  heartbeatAtUnixMs = now;
@@ -254,23 +299,88 @@ async function main() {
254
299
  readCoreTipStatus(paths),
255
300
  readAppliedTipStatus(databasePath),
256
301
  ]);
302
+ const backgroundStatus = await backgroundClient?.getNodeStatus().catch(() => null) ?? null;
303
+ if (backgroundStatus?.following === true) {
304
+ backgroundFollowError = null;
305
+ }
257
306
  rpcReachable = coreStatus.rpcReachable;
258
307
  coreBestHeight = coreStatus.coreBestHeight;
259
308
  coreBestHash = coreStatus.coreBestHash;
260
309
  observeAppliedTip(indexedStatus.appliedTip, now);
310
+ backgroundFollowActive = backgroundStatus?.following ?? (backgroundClient !== null);
311
+ bootstrapPhase = backgroundStatus?.bootstrapPhase ?? (backgroundFollowActive ? "follow_tip" : "paused");
312
+ bootstrapProgress = backgroundStatus?.bootstrapProgress ?? createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA);
313
+ cogcoinSyncHeight = backgroundStatus?.cogcoinSyncHeight ?? indexedStatus.appliedTip?.height ?? null;
314
+ cogcoinSyncTargetHeight = backgroundStatus?.cogcoinSyncTargetHeight ?? coreStatus.coreBestHeight;
315
+ if (backgroundStatus === null && backgroundFollowError !== null) {
316
+ state = "failed";
317
+ lastError = backgroundFollowError;
318
+ backgroundFollowActive = false;
319
+ bootstrapPhase = "error";
320
+ bootstrapProgress = {
321
+ ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
322
+ blocks: coreStatus.coreBestHeight,
323
+ headers: coreStatus.coreBestHeight,
324
+ targetHeight: coreStatus.coreBestHeight,
325
+ message: backgroundFollowError,
326
+ lastError: backgroundFollowError,
327
+ updatedAt: now,
328
+ };
329
+ cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
330
+ cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
331
+ return writeStatus();
332
+ }
261
333
  if (indexedStatus.schemaMismatch) {
262
334
  state = "schema-mismatch";
263
335
  lastError = indexedStatus.error;
336
+ bootstrapPhase = "error";
337
+ bootstrapProgress = {
338
+ ...bootstrapProgress,
339
+ phase: "error",
340
+ message: indexedStatus.error ?? "Indexer schema mismatch.",
341
+ lastError: indexedStatus.error,
342
+ updatedAt: now,
343
+ };
264
344
  return writeStatus();
265
345
  }
266
346
  if (indexedStatus.error !== null) {
267
347
  state = "failed";
268
348
  lastError = indexedStatus.error;
349
+ bootstrapPhase = "error";
350
+ bootstrapProgress = {
351
+ ...bootstrapProgress,
352
+ phase: "error",
353
+ message: indexedStatus.error,
354
+ lastError: indexedStatus.error,
355
+ updatedAt: now,
356
+ };
269
357
  return writeStatus();
270
358
  }
271
359
  const leaseState = deriveLeaseState(coreStatus, indexedStatus.appliedTip);
272
360
  state = leaseState.state;
273
361
  lastError = leaseState.lastError;
362
+ if (lastError !== null) {
363
+ bootstrapPhase = leaseState.state === "starting" ? "paused" : "error";
364
+ bootstrapProgress = {
365
+ ...bootstrapProgress,
366
+ phase: bootstrapPhase,
367
+ message: lastError,
368
+ lastError,
369
+ updatedAt: now,
370
+ };
371
+ }
372
+ else if (backgroundStatus === null) {
373
+ bootstrapPhase = leaseState.state === "synced" ? "follow_tip" : "paused";
374
+ bootstrapProgress = {
375
+ ...createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA),
376
+ blocks: coreStatus.coreBestHeight,
377
+ headers: coreStatus.coreBestHeight,
378
+ targetHeight: coreStatus.coreBestHeight,
379
+ updatedAt: now,
380
+ };
381
+ cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
382
+ cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
383
+ }
274
384
  return writeStatus();
275
385
  };
276
386
  const pauseBackgroundFollow = async () => {
@@ -283,6 +393,12 @@ async function main() {
283
393
  backgroundStore = null;
284
394
  await client?.close().catch(() => undefined);
285
395
  await store?.close().catch(() => undefined);
396
+ backgroundFollowError = null;
397
+ backgroundFollowActive = false;
398
+ bootstrapPhase = "paused";
399
+ bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
400
+ cogcoinSyncHeight = appliedTipHeight;
401
+ cogcoinSyncTargetHeight = coreBestHeight;
286
402
  };
287
403
  const resumeBackgroundFollow = async () => {
288
404
  if (backgroundClient !== null) {
@@ -292,35 +408,50 @@ async function main() {
292
408
  return backgroundResumePromise;
293
409
  }
294
410
  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
- });
411
+ let store = null;
303
412
  try {
413
+ const forcedResumeError = process.env[FORCE_RESUME_ERROR_ENV]?.trim();
414
+ if (forcedResumeError) {
415
+ throw new Error(forcedResumeError);
416
+ }
417
+ const bitcoindStatus = await readManagedBitcoindStatus(paths);
418
+ store = await openSqliteStore({ filename: databasePath });
419
+ const openedStore = store;
420
+ const chain = bitcoindStatus?.chain ?? "main";
421
+ const startHeight = normalizeCogcoinProcessingStartHeight({
422
+ chain,
423
+ startHeight: bitcoindStatus?.startHeight,
424
+ genesisParameters,
425
+ });
304
426
  const client = await openManagedBitcoindClientInternal({
305
- store,
427
+ store: openedStore,
306
428
  dataDir,
307
429
  chain,
308
430
  startHeight,
309
431
  walletRootId,
310
432
  progressOutput: "none",
311
433
  });
312
- try {
313
- await client.startFollowingTip();
314
- backgroundStore = store;
315
- backgroundClient = client;
316
- }
317
- catch (error) {
434
+ backgroundStore = openedStore;
435
+ backgroundClient = client;
436
+ backgroundFollowError = null;
437
+ backgroundFollowActive = true;
438
+ void client.startFollowingTip().catch(async (error) => {
439
+ if (backgroundClient !== client || backgroundStore !== openedStore) {
440
+ return;
441
+ }
442
+ backgroundClient = null;
443
+ backgroundStore = null;
444
+ backgroundFollowActive = false;
318
445
  await client.close().catch(() => undefined);
319
- throw error;
320
- }
446
+ await openedStore.close().catch(() => undefined);
447
+ const message = error instanceof Error ? error.message : String(error);
448
+ await recordBackgroundFollowFailure(message).catch(() => undefined);
449
+ });
321
450
  }
322
451
  catch (error) {
323
- await store.close().catch(() => undefined);
452
+ await store?.close().catch(() => undefined);
453
+ const message = error instanceof Error ? error.message : String(error);
454
+ await recordBackgroundFollowFailure(message).catch(() => undefined);
324
455
  throw error;
325
456
  }
326
457
  })();
@@ -434,6 +565,11 @@ async function main() {
434
565
  lastAppliedAtUnixMs: leaseStatus.lastAppliedAtUnixMs,
435
566
  activeSnapshotCount: leaseStatus.activeSnapshotCount,
436
567
  lastError: leaseStatus.lastError,
568
+ backgroundFollowActive: leaseStatus.backgroundFollowActive ?? false,
569
+ bootstrapPhase: leaseStatus.bootstrapPhase ?? null,
570
+ bootstrapProgress: leaseStatus.bootstrapProgress ?? null,
571
+ cogcoinSyncHeight: leaseStatus.cogcoinSyncHeight ?? null,
572
+ cogcoinSyncTargetHeight: leaseStatus.cogcoinSyncTargetHeight ?? null,
437
573
  tipHeight: snapshot.tipHeight,
438
574
  tipHash: snapshot.tipHash,
439
575
  openedAtUnixMs: snapshot.openedAtUnixMs,
@@ -494,17 +630,17 @@ async function main() {
494
630
  });
495
631
  return;
496
632
  }
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
633
  if (request.method === "ResumeBackgroundFollow") {
507
- await resumeBackgroundFollow();
634
+ try {
635
+ await withTimeout(resumeBackgroundFollow(), BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS, BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR);
636
+ }
637
+ catch (error) {
638
+ if (error instanceof Error
639
+ && error.message === BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR) {
640
+ await recordBackgroundFollowFailure(error.message).catch(() => undefined);
641
+ }
642
+ throw error;
643
+ }
508
644
  writeResponse({
509
645
  id: request.id,
510
646
  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
  }
@@ -90,6 +95,7 @@ export declare function stopIndexerDaemonServiceWithLockHeld(options: {
90
95
  walletRootId?: string;
91
96
  shutdownTimeoutMs?: number;
92
97
  paths?: ReturnType<typeof resolveManagedServicePaths>;
98
+ processId?: number | null;
93
99
  }): Promise<IndexerDaemonStopResult>;
94
100
  export declare function probeIndexerDaemon(options: {
95
101
  dataDir: string;
@@ -107,6 +113,8 @@ export declare function attachOrStartIndexerDaemon(options: {
107
113
  startupTimeoutMs?: number;
108
114
  shutdownTimeoutMs?: number;
109
115
  serviceLifetime?: ManagedIndexerDaemonServiceLifetime;
116
+ ensureBackgroundFollow?: boolean;
117
+ expectedBinaryVersion?: string | null;
110
118
  }): Promise<IndexerDaemonClient>;
111
119
  export declare function stopIndexerDaemonService(options: {
112
120
  dataDir: string;