@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
@@ -3,11 +3,18 @@ import { spawn } from "node:child_process";
3
3
  import { mkdir, readFile, 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";
6
7
  import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
7
8
  import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
8
9
  import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
9
10
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
10
11
  const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
12
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
13
+ const FORCE_KILL_TIMEOUT_MS = 5_000;
14
+ const INDEXER_DAEMON_REQUEST_TIMEOUT_MS = 15_000;
15
+ const INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS = 35_000;
16
+ const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
17
+ export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
11
18
  async function readJsonFile(filePath) {
12
19
  try {
13
20
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -53,11 +60,16 @@ async function clearIndexerDaemonRuntimeArtifacts(paths) {
53
60
  await rm(paths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
54
61
  await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
55
62
  }
63
+ function ignoreProcessNotFound(error) {
64
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
65
+ throw error;
66
+ }
67
+ }
56
68
  export async function stopIndexerDaemonServiceWithLockHeld(options) {
57
69
  const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
58
70
  const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
59
71
  const status = await readJsonFile(paths.indexerDaemonStatusPath);
60
- const processId = status?.processId ?? null;
72
+ const processId = options.processId ?? status?.processId ?? null;
61
73
  if (status === null || processId === null || !await isProcessAlive(processId)) {
62
74
  await clearIndexerDaemonRuntimeArtifacts(paths);
63
75
  return {
@@ -69,11 +81,23 @@ export async function stopIndexerDaemonServiceWithLockHeld(options) {
69
81
  process.kill(processId, "SIGTERM");
70
82
  }
71
83
  catch (error) {
72
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
84
+ ignoreProcessNotFound(error);
85
+ }
86
+ try {
87
+ await waitForProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, "indexer_daemon_stop_timeout");
88
+ }
89
+ catch (error) {
90
+ if (!(error instanceof Error) || error.message !== "indexer_daemon_stop_timeout") {
73
91
  throw error;
74
92
  }
93
+ try {
94
+ process.kill(processId, "SIGKILL");
95
+ }
96
+ catch (killError) {
97
+ ignoreProcessNotFound(killError);
98
+ }
99
+ await waitForProcessExit(processId, FORCE_KILL_TIMEOUT_MS, "indexer_daemon_stop_timeout");
75
100
  }
76
- await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
77
101
  await clearIndexerDaemonRuntimeArtifacts(paths);
78
102
  return {
79
103
  status: "stopped",
@@ -95,7 +119,9 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
95
119
  socket.destroy();
96
120
  handler();
97
121
  };
98
- socket.setTimeout(15_000);
122
+ socket.setTimeout(request.method === "ResumeBackgroundFollow"
123
+ ? INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS
124
+ : INDEXER_DAEMON_REQUEST_TIMEOUT_MS);
99
125
  socket.on("connect", () => {
100
126
  socket.write(`${JSON.stringify(request)}\n`);
101
127
  });
@@ -169,12 +195,6 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
169
195
  token,
170
196
  });
171
197
  },
172
- async pauseBackgroundFollow() {
173
- await sendRequest({
174
- id: randomUUID(),
175
- method: "PauseBackgroundFollow",
176
- });
177
- },
178
198
  async resumeBackgroundFollow() {
179
199
  await sendRequest({
180
200
  id: randomUUID(),
@@ -271,8 +291,23 @@ function buildStatusFromSnapshotHandle(handle) {
271
291
  lastAppliedAtUnixMs: handle.lastAppliedAtUnixMs,
272
292
  activeSnapshotCount: handle.activeSnapshotCount,
273
293
  lastError: handle.lastError,
294
+ backgroundFollowActive: handle.backgroundFollowActive,
295
+ bootstrapPhase: handle.bootstrapPhase,
296
+ bootstrapProgress: handle.bootstrapProgress,
297
+ cogcoinSyncHeight: handle.cogcoinSyncHeight,
298
+ cogcoinSyncTargetHeight: handle.cogcoinSyncTargetHeight,
274
299
  };
275
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
+ }
276
311
  async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
277
312
  const client = createIndexerDaemonClient(socketPath);
278
313
  try {
@@ -368,15 +403,84 @@ export async function readObservedIndexerDaemonStatus(options) {
368
403
  return readJsonFile(paths.indexerDaemonStatusPath);
369
404
  }
370
405
  export async function attachOrStartIndexerDaemon(options) {
406
+ const requestBackgroundFollow = async (client, observedStatus = null) => {
407
+ if (options.ensureBackgroundFollow !== true) {
408
+ return client;
409
+ }
410
+ if (observedStatus?.backgroundFollowActive === true) {
411
+ return client;
412
+ }
413
+ await client.resumeBackgroundFollow();
414
+ const status = await client.getStatus();
415
+ if (status.backgroundFollowActive !== true) {
416
+ throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE);
417
+ }
418
+ return client;
419
+ };
371
420
  const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
372
421
  const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
373
422
  const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
374
423
  const serviceLifetime = options.serviceLifetime ?? "persistent";
424
+ const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
425
+ const startDaemon = async () => {
426
+ await mkdir(paths.indexerServiceRoot, { recursive: true });
427
+ const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
428
+ const spawnOptions = serviceLifetime === "ephemeral"
429
+ ? {
430
+ stdio: "ignore",
431
+ }
432
+ : {
433
+ detached: true,
434
+ stdio: "ignore",
435
+ };
436
+ const child = spawn(process.execPath, [
437
+ daemonEntryPath,
438
+ `--data-dir=${options.dataDir}`,
439
+ `--database-path=${options.databasePath}`,
440
+ `--wallet-root-id=${walletRootId}`,
441
+ ], {
442
+ ...spawnOptions,
443
+ });
444
+ if (serviceLifetime !== "ephemeral") {
445
+ child.unref();
446
+ }
447
+ try {
448
+ await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
449
+ }
450
+ catch (error) {
451
+ if (child.pid !== undefined) {
452
+ try {
453
+ process.kill(child.pid, "SIGTERM");
454
+ }
455
+ catch {
456
+ // ignore shutdown failures while unwinding startup errors
457
+ }
458
+ }
459
+ throw error;
460
+ }
461
+ return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
462
+ dataDir: options.dataDir,
463
+ walletRootId,
464
+ serviceLifetime,
465
+ ownership: "started",
466
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
467
+ });
468
+ };
375
469
  const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
376
470
  if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
377
- return existingProbe.client;
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
+ }
478
+ }
479
+ else {
480
+ await existingProbe.client.close().catch(() => undefined);
481
+ }
378
482
  }
379
- if (existingProbe.compatibility !== "unreachable") {
483
+ if (existingProbe.compatibility !== "unreachable" && existingProbe.compatibility !== "compatible") {
380
484
  throw new Error(existingProbe.error ?? "indexer_daemon_protocol_error");
381
485
  }
382
486
  try {
@@ -389,53 +493,43 @@ export async function attachOrStartIndexerDaemon(options) {
389
493
  try {
390
494
  const liveProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
391
495
  if (liveProbe.compatibility === "compatible" && liveProbe.client !== null) {
392
- return liveProbe.client;
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
+ }
510
+ }
511
+ else {
512
+ await liveProbe.client.close().catch(() => undefined);
513
+ await stopIndexerDaemonServiceWithLockHeld({
514
+ dataDir: options.dataDir,
515
+ walletRootId,
516
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
517
+ paths,
518
+ processId: liveProbe.status?.processId ?? null,
519
+ });
520
+ }
393
521
  }
394
- if (liveProbe.compatibility !== "unreachable") {
522
+ else if (liveProbe.compatibility !== "unreachable") {
395
523
  throw new Error(liveProbe.error ?? "indexer_daemon_protocol_error");
396
524
  }
397
- await mkdir(paths.indexerServiceRoot, { recursive: true });
398
- const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
399
- const spawnOptions = serviceLifetime === "ephemeral"
400
- ? {
401
- stdio: "ignore",
402
- }
403
- : {
404
- detached: true,
405
- stdio: "ignore",
406
- };
407
- const child = spawn(process.execPath, [
408
- daemonEntryPath,
409
- `--data-dir=${options.dataDir}`,
410
- `--database-path=${options.databasePath}`,
411
- `--wallet-root-id=${walletRootId}`,
412
- ], {
413
- ...spawnOptions,
414
- });
415
- if (serviceLifetime !== "ephemeral") {
416
- child.unref();
417
- }
525
+ const daemon = await startDaemon();
418
526
  try {
419
- await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
527
+ return await requestBackgroundFollow(daemon);
420
528
  }
421
529
  catch (error) {
422
- if (child.pid !== undefined) {
423
- try {
424
- process.kill(child.pid, "SIGTERM");
425
- }
426
- catch {
427
- // ignore shutdown failures while unwinding startup errors
428
- }
429
- }
430
- throw error;
530
+ await daemon.close().catch(() => undefined);
531
+ throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, { cause: error });
431
532
  }
432
- return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
433
- dataDir: options.dataDir,
434
- walletRootId,
435
- serviceLifetime,
436
- ownership: "started",
437
- shutdownTimeoutMs: options.shutdownTimeoutMs,
438
- });
439
533
  }
440
534
  finally {
441
535
  await lock.release();
@@ -444,13 +538,7 @@ export async function attachOrStartIndexerDaemon(options) {
444
538
  catch (error) {
445
539
  if (error instanceof FileLockBusyError) {
446
540
  await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
447
- return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
448
- dataDir: options.dataDir,
449
- walletRootId,
450
- serviceLifetime,
451
- ownership: "attached",
452
- shutdownTimeoutMs: options.shutdownTimeoutMs,
453
- });
541
+ return attachOrStartIndexerDaemon(options);
454
542
  }
455
543
  throw error;
456
544
  }
@@ -0,0 +1,12 @@
1
+ import type { ManagedIndexerDaemonObservedStatus } from "./types.js";
2
+ export interface ManagedIndexerMonitor {
3
+ getStatus(): Promise<ManagedIndexerDaemonObservedStatus>;
4
+ close(): Promise<void>;
5
+ }
6
+ export declare function openManagedIndexerMonitor(options: {
7
+ dataDir: string;
8
+ databasePath: string;
9
+ walletRootId?: string;
10
+ startupTimeoutMs?: number;
11
+ expectedBinaryVersion?: string | null;
12
+ }): Promise<ManagedIndexerMonitor>;
@@ -0,0 +1,93 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { loadBundledGenesisParameters } from "@cogcoin/indexer";
3
+ import { readPackageVersionFromDisk } from "../package-version.js";
4
+ import { attachOrStartIndexerDaemon, readObservedIndexerDaemonStatus, } from "./indexer-daemon.js";
5
+ import { resolveCogcoinProcessingStartHeight } from "./processing-start-height.js";
6
+ import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
7
+ import { attachOrStartManagedBitcoindService } from "./service.js";
8
+ async function readJsonFile(filePath) {
9
+ try {
10
+ return JSON.parse(await readFile(filePath, "utf8"));
11
+ }
12
+ catch (error) {
13
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
14
+ return null;
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+ async function resolveStartOptions(options) {
20
+ const paths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
21
+ const observedStatus = await readJsonFile(paths.bitcoindStatusPath).catch(() => null);
22
+ if (observedStatus !== null) {
23
+ return {
24
+ chain: observedStatus.chain,
25
+ startHeight: observedStatus.startHeight,
26
+ };
27
+ }
28
+ const genesisParameters = await loadBundledGenesisParameters();
29
+ return {
30
+ chain: "main",
31
+ startHeight: resolveCogcoinProcessingStartHeight(genesisParameters),
32
+ };
33
+ }
34
+ export async function openManagedIndexerMonitor(options) {
35
+ const expectedBinaryVersion = options.expectedBinaryVersion === undefined
36
+ ? await readPackageVersionFromDisk()
37
+ : options.expectedBinaryVersion;
38
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
39
+ const startOptions = await resolveStartOptions({
40
+ dataDir: options.dataDir,
41
+ walletRootId,
42
+ });
43
+ await attachOrStartManagedBitcoindService({
44
+ dataDir: options.dataDir,
45
+ chain: startOptions.chain,
46
+ startHeight: startOptions.startHeight,
47
+ walletRootId,
48
+ startupTimeoutMs: options.startupTimeoutMs,
49
+ });
50
+ const daemon = await attachOrStartIndexerDaemon({
51
+ dataDir: options.dataDir,
52
+ databasePath: options.databasePath,
53
+ walletRootId,
54
+ startupTimeoutMs: options.startupTimeoutMs,
55
+ ensureBackgroundFollow: true,
56
+ expectedBinaryVersion,
57
+ });
58
+ return createManagedIndexerMonitor({
59
+ daemon,
60
+ dataDir: options.dataDir,
61
+ walletRootId,
62
+ });
63
+ }
64
+ function createManagedIndexerMonitor(options) {
65
+ let closed = false;
66
+ return {
67
+ async getStatus() {
68
+ if (closed) {
69
+ throw new Error("managed_indexer_monitor_closed");
70
+ }
71
+ try {
72
+ return await options.daemon.getStatus();
73
+ }
74
+ catch (error) {
75
+ const observed = await readObservedIndexerDaemonStatus({
76
+ dataDir: options.dataDir,
77
+ walletRootId: options.walletRootId,
78
+ }).catch(() => null);
79
+ if (observed !== null) {
80
+ return observed;
81
+ }
82
+ throw error;
83
+ }
84
+ },
85
+ async close() {
86
+ if (closed) {
87
+ return;
88
+ }
89
+ closed = true;
90
+ await options.daemon.close().catch(() => undefined);
91
+ },
92
+ };
93
+ }
@@ -128,8 +128,11 @@ export class ManagedProgressController {
128
128
  this.#cogcoinSyncTargetHeight = null;
129
129
  }
130
130
  if (this.#followVisualMode) {
131
+ const followIndexedHeight = phase === "follow_tip"
132
+ ? this.#cogcoinSyncHeight ?? this.#progress.blocks ?? this.#followScene.indexedHeight
133
+ : undefined;
131
134
  syncFollowSceneState(this.#followScene, {
132
- indexedHeight: phase === "follow_tip" ? this.#cogcoinSyncHeight : undefined,
135
+ indexedHeight: followIndexedHeight,
133
136
  nodeHeight: this.#progress.blocks,
134
137
  liveActivated: phase === "follow_tip" || this.#followScene.liveActivated,
135
138
  });
@@ -19,16 +19,20 @@ export interface FollowSceneStateForTesting {
19
19
  export interface FollowFrameRenderOptions {
20
20
  artworkCogText?: string | null;
21
21
  artworkSatText?: string | null;
22
+ artworkStatusLeftText?: string | null;
23
+ artworkStatusRightText?: string | null;
22
24
  }
23
25
  export declare function createFollowSceneState(indexedHeight?: number | null, blockTimesByHeight?: Record<number, number>): FollowSceneStateForTesting;
24
26
  export declare function setFollowBlockTime(state: FollowSceneStateForTesting, height: number, blockTime: number): void;
25
27
  export declare function replaceFollowBlockTimes(state: FollowSceneStateForTesting, blockTimesByHeight: Record<number, number>): void;
26
28
  export declare function formatCompactFollowAgeLabel(blockTime: number, now: number): string;
27
29
  export declare const formatCompactFollowAgeLabelForTesting: typeof formatCompactFollowAgeLabel;
28
- export declare function syncFollowSceneState(state: FollowSceneStateForTesting, { indexedHeight, nodeHeight, liveActivated, }: {
30
+ export declare function syncFollowSceneState(state: FollowSceneStateForTesting, { indexedHeight, nodeHeight, liveActivated, authoritativeTip, settleLatest, }: {
29
31
  indexedHeight?: number | null;
30
32
  nodeHeight?: number | null;
31
33
  liveActivated?: boolean;
34
+ authoritativeTip?: boolean;
35
+ settleLatest?: boolean;
32
36
  }): void;
33
37
  export declare function advanceFollowSceneState(state: FollowSceneStateForTesting, now: number): void;
34
38
  export declare function renderFollowFrame(state: FollowSceneStateForTesting, statusFieldText: string, now: number, options?: FollowFrameRenderOptions): string[];
@@ -39,6 +43,8 @@ export declare function syncFollowSceneStateForTesting(state: FollowSceneStateFo
39
43
  indexedHeight?: number | null;
40
44
  nodeHeight?: number | null;
41
45
  liveActivated?: boolean;
46
+ authoritativeTip?: boolean;
47
+ settleLatest?: boolean;
42
48
  }): FollowSceneStateForTesting;
43
49
  export declare function advanceFollowSceneStateForTesting(state: FollowSceneStateForTesting, now: number): FollowSceneStateForTesting;
44
50
  export declare function renderFollowFrameForTesting(state: FollowSceneStateForTesting, statusFieldText?: string, now?: number, options?: FollowFrameRenderOptions): string[];
@@ -1,12 +1,13 @@
1
1
  import { loadArtTemplate, loadFollowCarTemplate } from "./assets.js";
2
2
  import { FIELD_LEFT, FIELD_WIDTH, FOLLOW_AGE_ROW, FOLLOW_APPROACH_MS, FOLLOW_CAR_HEIGHT, FOLLOW_CAR_PITCH, FOLLOW_CAR_TOP, FOLLOW_CAR_WIDTH, FOLLOW_CENTER_SLOT_X, FOLLOW_CLIP_MAX_COLUMN, FOLLOW_CLIP_MIN_COLUMN, FOLLOW_CONNECTION_SLOT_X, FOLLOW_FAST_APPROACH_MS, FOLLOW_FAST_SHIFT_MS, FOLLOW_PENDING_ENTER_MS, FOLLOW_PENDING_LABEL, FOLLOW_PENDING_OFFSCREEN_LEFT_X, FOLLOW_PENDING_SLOT_X, FOLLOW_RIGHT_SLOT_XS, FOLLOW_SHIFT_MS, FOLLOW_WINDOW_LEFT, MESSAGE_FIELD_ROW, NEUTRAL_MESSAGE_TITLE, STATUS_FIELD_ROW, } from "./constants.js";
3
- import { centerLine, computeCenteredLeftPadding, overlayCenteredField, replaceSegment, rightAlignLine, truncateLine, } from "./formatting.js";
3
+ import { centerLine, computeCenteredLeftPadding, replaceSegment, rightAlignLine, truncateLine, } from "./formatting.js";
4
4
  const FOLLOW_TITLE_LEFT = computeCenteredLeftPadding(NEUTRAL_MESSAGE_TITLE, FIELD_WIDTH);
5
5
  const FOLLOW_TITLE_WIDTH = NEUTRAL_MESSAGE_TITLE.length;
6
6
  const FOLLOW_COG_LEFT = 0;
7
7
  const FOLLOW_COG_WIDTH = FOLLOW_TITLE_LEFT;
8
8
  const FOLLOW_SAT_LEFT = FOLLOW_TITLE_LEFT + FOLLOW_TITLE_WIDTH;
9
9
  const FOLLOW_SAT_WIDTH = FIELD_WIDTH - FOLLOW_SAT_LEFT;
10
+ const FOLLOW_STATUS_VERSION_GAP = 2;
10
11
  export function createFollowSceneState(indexedHeight = null, blockTimesByHeight = {}) {
11
12
  return {
12
13
  liveActivated: false,
@@ -79,8 +80,55 @@ function renderFollowHeaderField(options) {
79
80
  }
80
81
  return field;
81
82
  }
83
+ function renderFollowStatusField(statusFieldText, options) {
84
+ const leftText = options.artworkStatusLeftText === null || options.artworkStatusLeftText === undefined
85
+ ? ""
86
+ : truncateLine(options.artworkStatusLeftText, FIELD_WIDTH);
87
+ const rightText = options.artworkStatusRightText === null || options.artworkStatusRightText === undefined
88
+ ? ""
89
+ : truncateLine(options.artworkStatusRightText, FIELD_WIDTH);
90
+ const leftWidth = Math.min(FIELD_WIDTH, leftText.length);
91
+ const rightWidth = Math.min(FIELD_WIDTH, rightText.length);
92
+ if (leftWidth === 0 && rightWidth === 0) {
93
+ return centerLine(statusFieldText, FIELD_WIDTH);
94
+ }
95
+ let field = centerLine(statusFieldText, FIELD_WIDTH);
96
+ if (leftWidth > 0) {
97
+ field = replaceSegment(field, 0, leftWidth, leftAlignLane(leftText, leftWidth));
98
+ if (leftWidth < FIELD_WIDTH) {
99
+ field = replaceSegment(field, leftWidth, Math.min(FOLLOW_STATUS_VERSION_GAP, FIELD_WIDTH - leftWidth), "".padEnd(Math.min(FOLLOW_STATUS_VERSION_GAP, FIELD_WIDTH - leftWidth), " "));
100
+ }
101
+ }
102
+ if (rightWidth > 0) {
103
+ if (rightWidth < FIELD_WIDTH) {
104
+ const gapStart = Math.max(0, FIELD_WIDTH - rightWidth - FOLLOW_STATUS_VERSION_GAP);
105
+ const gapWidth = Math.min(FOLLOW_STATUS_VERSION_GAP, FIELD_WIDTH - rightWidth);
106
+ field = replaceSegment(field, gapStart, gapWidth, "".padEnd(gapWidth, " "));
107
+ }
108
+ field = replaceSegment(field, FIELD_WIDTH - rightWidth, rightWidth, rightAlignLine(rightText, rightWidth));
109
+ }
110
+ return field;
111
+ }
82
112
  function highestTrackedFollowHeight(state) {
83
- return Math.max(state.indexedHeight ?? Number.NEGATIVE_INFINITY, state.displayedCenterHeight ?? Number.NEGATIVE_INFINITY, state.animation?.height ?? Number.NEGATIVE_INFINITY, ...state.queuedHeights);
113
+ let highest = Math.max(state.indexedHeight ?? Number.NEGATIVE_INFINITY, state.displayedCenterHeight ?? Number.NEGATIVE_INFINITY, state.animation?.height ?? Number.NEGATIVE_INFINITY);
114
+ for (const height of state.queuedHeights) {
115
+ if (height > highest) {
116
+ highest = height;
117
+ }
118
+ }
119
+ return highest;
120
+ }
121
+ function resolveLatestAuthoritativeFollowHeight(indexedHeight, nodeHeight) {
122
+ if (indexedHeight === null) {
123
+ return nodeHeight;
124
+ }
125
+ if (nodeHeight === null) {
126
+ return indexedHeight;
127
+ }
128
+ return Math.max(indexedHeight, nodeHeight);
129
+ }
130
+ function resolveDisplayedFollowHeight(state) {
131
+ return state.displayedCenterHeight ?? state.indexedHeight;
84
132
  }
85
133
  function resetFollowSceneState(state) {
86
134
  state.displayedCenterHeight = state.indexedHeight;
@@ -90,13 +138,33 @@ function resetFollowSceneState(state) {
90
138
  state.pendingStaticX = null;
91
139
  state.animation = null;
92
140
  }
141
+ function resyncFollowSceneToLatestHeight(state, latestHeight) {
142
+ state.displayedCenterHeight = latestHeight;
143
+ state.queuedHeights = [];
144
+ state.pendingLabel = null;
145
+ state.pendingStaticX = null;
146
+ state.animation = null;
147
+ }
148
+ function shouldResyncAuthoritativeFollowScene(options) {
149
+ if (options.latestHeight === null) {
150
+ return false;
151
+ }
152
+ if (options.settleLatest) {
153
+ return true;
154
+ }
155
+ const displayedHeight = resolveDisplayedFollowHeight(options.state);
156
+ if (displayedHeight === null) {
157
+ return false;
158
+ }
159
+ return options.latestHeight - displayedHeight > 1;
160
+ }
93
161
  function enqueueFollowHeights(state, nodeHeight) {
94
162
  const startHeight = Math.max(state.indexedHeight ?? -1, highestTrackedFollowHeight(state));
95
163
  for (let height = startHeight + 1; height <= nodeHeight; height += 1) {
96
164
  state.queuedHeights.push(height);
97
165
  }
98
166
  }
99
- export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, }) {
167
+ export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, authoritativeTip, settleLatest, }) {
100
168
  const wasLive = state.liveActivated;
101
169
  const nextLive = liveActivated ?? state.liveActivated;
102
170
  let shouldReset = false;
@@ -119,7 +187,17 @@ export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveAct
119
187
  if (shouldReset) {
120
188
  resetFollowSceneState(state);
121
189
  }
122
- if (state.liveActivated && state.observedNodeHeight !== null) {
190
+ const latestAuthoritativeHeight = resolveLatestAuthoritativeFollowHeight(state.indexedHeight, state.observedNodeHeight);
191
+ if (state.liveActivated
192
+ && authoritativeTip === true
193
+ && shouldResyncAuthoritativeFollowScene({
194
+ state,
195
+ latestHeight: latestAuthoritativeHeight,
196
+ settleLatest: settleLatest === true,
197
+ })) {
198
+ resyncFollowSceneToLatestHeight(state, latestAuthoritativeHeight);
199
+ }
200
+ else if (state.liveActivated && state.observedNodeHeight !== null) {
123
201
  enqueueFollowHeights(state, state.observedNodeHeight);
124
202
  }
125
203
  if (!wasLive && state.liveActivated && state.displayedCenterHeight === null) {
@@ -363,7 +441,18 @@ export function renderFollowFrame(state, statusFieldText, now, options = {}) {
363
441
  if (headerRow !== undefined) {
364
442
  frame[MESSAGE_FIELD_ROW] = replaceSegment(headerRow, FIELD_LEFT, FIELD_WIDTH, renderFollowHeaderField(options));
365
443
  }
366
- overlayCenteredField(frame, STATUS_FIELD_ROW, statusFieldText);
444
+ if (statusFieldText.length > 0
445
+ || (options.artworkStatusLeftText !== null
446
+ && options.artworkStatusLeftText !== undefined
447
+ && options.artworkStatusLeftText.length > 0)
448
+ || (options.artworkStatusRightText !== null
449
+ && options.artworkStatusRightText !== undefined
450
+ && options.artworkStatusRightText.length > 0)) {
451
+ const statusRow = frame[STATUS_FIELD_ROW];
452
+ if (statusRow !== undefined) {
453
+ frame[STATUS_FIELD_ROW] = replaceSegment(statusRow, FIELD_LEFT, FIELD_WIDTH, renderFollowStatusField(statusFieldText, options));
454
+ }
455
+ }
367
456
  return frame;
368
457
  }
369
458
  export function createFollowSceneStateForTesting(indexedHeight = null, blockTimesByHeight = {}) {
@@ -10,6 +10,8 @@ interface RenderStream {
10
10
  export interface FollowSceneRenderOptions {
11
11
  artworkCogText?: string | null;
12
12
  artworkSatText?: string | null;
13
+ artworkStatusLeftText?: string | null;
14
+ artworkStatusRightText?: string | null;
13
15
  extraLines?: string[];
14
16
  }
15
17
  export declare class TtyProgressRenderer {
@@ -94,6 +94,8 @@ export class TtyProgressRenderer {
94
94
  ? [...renderFollowFrame(followScene, statusFieldText, now, {
95
95
  artworkCogText: renderOptions.artworkCogText ?? null,
96
96
  artworkSatText: renderOptions.artworkSatText ?? null,
97
+ artworkStatusLeftText: renderOptions.artworkStatusLeftText ?? null,
98
+ artworkStatusRightText: renderOptions.artworkStatusRightText ?? null,
97
99
  }), "", progressLine, "", ...extraLines]
98
100
  : [truncateLine(NEUTRAL_MESSAGE_TITLE, width), progressLine, "", ...extraLines];
99
101
  const frame = lines.join("\n");
@@ -1,6 +1,5 @@
1
1
  export { openManagedBitcoindClientInternal } from "./client.js";
2
2
  export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
3
- export { pauseIndexerDaemonForForegroundClientForTesting } from "./client/factory.js";
4
3
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
5
4
  export { normalizeRpcBlock } from "./normalize.js";
6
5
  export { BitcoinRpcClient } from "./rpc.js";
@@ -1,6 +1,5 @@
1
1
  export { openManagedBitcoindClientInternal } from "./client.js";
2
2
  export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
3
- export { pauseIndexerDaemonForForegroundClientForTesting } from "./client/factory.js";
4
3
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
5
4
  export { normalizeRpcBlock } from "./normalize.js";
6
5
  export { BitcoinRpcClient } from "./rpc.js";
@@ -154,7 +154,6 @@ export interface ManagedGetblockArchiveRestartRequest {
154
154
  }
155
155
  export interface ManagedBitcoindOptions extends ClientOptions {
156
156
  dataDir?: string;
157
- databasePath?: string;
158
157
  rpcPort?: number;
159
158
  zmqPort?: number;
160
159
  p2pPort?: number;
@@ -173,7 +172,6 @@ export interface ManagedBitcoindClient extends Client {
173
172
  syncToTip(): Promise<SyncResult>;
174
173
  startFollowingTip(): Promise<void>;
175
174
  getNodeStatus(): Promise<ManagedBitcoindStatus>;
176
- detachToBackgroundFollow(): Promise<void>;
177
175
  close(): Promise<void>;
178
176
  }
179
177
  export interface InternalManagedBitcoindOptions extends ManagedBitcoindOptions {
@@ -235,6 +233,11 @@ export interface ManagedIndexerDaemonStatus {
235
233
  lastAppliedAtUnixMs: number | null;
236
234
  activeSnapshotCount: number;
237
235
  lastError: string | null;
236
+ backgroundFollowActive?: boolean;
237
+ bootstrapPhase?: BootstrapPhase | null;
238
+ bootstrapProgress?: BootstrapProgress | null;
239
+ cogcoinSyncHeight?: number | null;
240
+ cogcoinSyncTargetHeight?: number | null;
238
241
  }
239
242
  export interface ManagedIndexerDaemonObservedStatus extends Omit<ManagedIndexerDaemonStatus, "serviceApiVersion" | "schemaVersion"> {
240
243
  serviceApiVersion: string;