@cogcoin/client 1.0.2 → 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 (76) 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/indexer-daemon-main.js +173 -28
  7. package/dist/bitcoind/indexer-daemon.d.ts +11 -3
  8. package/dist/bitcoind/indexer-daemon.js +123 -57
  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/testing.d.ts +0 -1
  16. package/dist/bitcoind/testing.js +0 -1
  17. package/dist/bitcoind/types.d.ts +5 -2
  18. package/dist/cli/commands/follow.js +44 -49
  19. package/dist/cli/commands/mining-admin.js +56 -2
  20. package/dist/cli/commands/mining-read.js +43 -3
  21. package/dist/cli/commands/mining-runtime.js +91 -73
  22. package/dist/cli/commands/service-runtime.js +42 -2
  23. package/dist/cli/commands/status.js +3 -1
  24. package/dist/cli/commands/sync.js +50 -90
  25. package/dist/cli/commands/wallet-admin.js +21 -3
  26. package/dist/cli/commands/wallet-read.js +2 -0
  27. package/dist/cli/context.js +5 -1
  28. package/dist/cli/managed-indexer-observer.d.ts +33 -0
  29. package/dist/cli/managed-indexer-observer.js +163 -0
  30. package/dist/cli/mining-format.d.ts +3 -1
  31. package/dist/cli/mining-format.js +35 -0
  32. package/dist/cli/mining-json.d.ts +11 -1
  33. package/dist/cli/mining-json.js +9 -0
  34. package/dist/cli/output.js +24 -0
  35. package/dist/cli/parse.d.ts +1 -1
  36. package/dist/cli/parse.js +23 -0
  37. package/dist/cli/read-json.d.ts +13 -1
  38. package/dist/cli/read-json.js +31 -0
  39. package/dist/cli/runner.js +4 -2
  40. package/dist/cli/signals.d.ts +12 -0
  41. package/dist/cli/signals.js +31 -13
  42. package/dist/cli/types.d.ts +8 -4
  43. package/dist/cli/update-service.d.ts +2 -12
  44. package/dist/cli/update-service.js +2 -68
  45. package/dist/semver.d.ts +12 -0
  46. package/dist/semver.js +68 -0
  47. package/dist/wallet/lifecycle.js +0 -6
  48. package/dist/wallet/mining/config.js +54 -3
  49. package/dist/wallet/mining/control.d.ts +5 -2
  50. package/dist/wallet/mining/control.js +153 -34
  51. package/dist/wallet/mining/domain-prompts.d.ts +17 -0
  52. package/dist/wallet/mining/domain-prompts.js +130 -0
  53. package/dist/wallet/mining/index.d.ts +2 -1
  54. package/dist/wallet/mining/index.js +1 -0
  55. package/dist/wallet/mining/runner.d.ts +58 -2
  56. package/dist/wallet/mining/runner.js +553 -331
  57. package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
  58. package/dist/wallet/mining/sentences.js +7 -4
  59. package/dist/wallet/mining/types.d.ts +26 -0
  60. package/dist/wallet/mining/visualizer.d.ts +3 -0
  61. package/dist/wallet/mining/visualizer.js +106 -12
  62. package/dist/wallet/read/context.d.ts +1 -0
  63. package/dist/wallet/read/context.js +15 -10
  64. package/dist/wallet/reset.js +0 -1
  65. package/dist/wallet/state/client-password-agent.js +4 -1
  66. package/dist/wallet/state/client-password.js +15 -8
  67. package/dist/wallet/tx/anchor.js +0 -1
  68. package/dist/wallet/tx/bitcoin-transfer.js +0 -1
  69. package/dist/wallet/tx/cog.js +0 -3
  70. package/dist/wallet/tx/common.js +1 -1
  71. package/dist/wallet/tx/domain-admin.js +0 -1
  72. package/dist/wallet/tx/domain-market.js +0 -3
  73. package/dist/wallet/tx/field.js +0 -1
  74. package/dist/wallet/tx/register.js +0 -1
  75. package/dist/wallet/tx/reputation.js +0 -1
  76. package/package.json +1 -1
@@ -3,11 +3,16 @@ 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 } 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 INDEXER_DAEMON_REQUEST_TIMEOUT_MS = 15_000;
13
+ const INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS = 35_000;
14
+ const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
15
+ export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
11
16
  async function readJsonFile(filePath) {
12
17
  try {
13
18
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -57,7 +62,7 @@ export async function stopIndexerDaemonServiceWithLockHeld(options) {
57
62
  const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
58
63
  const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
59
64
  const status = await readJsonFile(paths.indexerDaemonStatusPath);
60
- const processId = status?.processId ?? null;
65
+ const processId = options.processId ?? status?.processId ?? null;
61
66
  if (status === null || processId === null || !await isProcessAlive(processId)) {
62
67
  await clearIndexerDaemonRuntimeArtifacts(paths);
63
68
  return {
@@ -95,7 +100,9 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
95
100
  socket.destroy();
96
101
  handler();
97
102
  };
98
- socket.setTimeout(15_000);
103
+ socket.setTimeout(request.method === "ResumeBackgroundFollow"
104
+ ? INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS
105
+ : INDEXER_DAEMON_REQUEST_TIMEOUT_MS);
99
106
  socket.on("connect", () => {
100
107
  socket.write(`${JSON.stringify(request)}\n`);
101
108
  });
@@ -169,12 +176,6 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
169
176
  token,
170
177
  });
171
178
  },
172
- async pauseBackgroundFollow() {
173
- await sendRequest({
174
- id: randomUUID(),
175
- method: "PauseBackgroundFollow",
176
- });
177
- },
178
179
  async resumeBackgroundFollow() {
179
180
  await sendRequest({
180
181
  id: randomUUID(),
@@ -271,8 +272,20 @@ function buildStatusFromSnapshotHandle(handle) {
271
272
  lastAppliedAtUnixMs: handle.lastAppliedAtUnixMs,
272
273
  activeSnapshotCount: handle.activeSnapshotCount,
273
274
  lastError: handle.lastError,
275
+ backgroundFollowActive: handle.backgroundFollowActive,
276
+ bootstrapPhase: handle.bootstrapPhase,
277
+ bootstrapProgress: handle.bootstrapProgress,
278
+ cogcoinSyncHeight: handle.cogcoinSyncHeight,
279
+ cogcoinSyncTargetHeight: handle.cogcoinSyncTargetHeight,
274
280
  };
275
281
  }
282
+ function isStaleIndexerDaemonVersion(status, expectedBinaryVersion) {
283
+ if (status === null || expectedBinaryVersion === null || expectedBinaryVersion === undefined) {
284
+ return false;
285
+ }
286
+ const comparison = compareSemver(status.binaryVersion, expectedBinaryVersion);
287
+ return comparison !== null && comparison < 0;
288
+ }
276
289
  async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
277
290
  const client = createIndexerDaemonClient(socketPath);
278
291
  try {
@@ -368,15 +381,84 @@ export async function readObservedIndexerDaemonStatus(options) {
368
381
  return readJsonFile(paths.indexerDaemonStatusPath);
369
382
  }
370
383
  export async function attachOrStartIndexerDaemon(options) {
384
+ const requestBackgroundFollow = async (client, observedStatus = null) => {
385
+ if (options.ensureBackgroundFollow !== true) {
386
+ return client;
387
+ }
388
+ if (observedStatus?.backgroundFollowActive === true) {
389
+ return client;
390
+ }
391
+ await client.resumeBackgroundFollow();
392
+ const status = await client.getStatus();
393
+ if (status.backgroundFollowActive !== true) {
394
+ throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE);
395
+ }
396
+ return client;
397
+ };
371
398
  const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
372
399
  const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
373
400
  const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
374
401
  const serviceLifetime = options.serviceLifetime ?? "persistent";
402
+ const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
403
+ const startDaemon = async () => {
404
+ await mkdir(paths.indexerServiceRoot, { recursive: true });
405
+ const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
406
+ const spawnOptions = serviceLifetime === "ephemeral"
407
+ ? {
408
+ stdio: "ignore",
409
+ }
410
+ : {
411
+ detached: true,
412
+ stdio: "ignore",
413
+ };
414
+ const child = spawn(process.execPath, [
415
+ daemonEntryPath,
416
+ `--data-dir=${options.dataDir}`,
417
+ `--database-path=${options.databasePath}`,
418
+ `--wallet-root-id=${walletRootId}`,
419
+ ], {
420
+ ...spawnOptions,
421
+ });
422
+ if (serviceLifetime !== "ephemeral") {
423
+ child.unref();
424
+ }
425
+ try {
426
+ await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
427
+ }
428
+ catch (error) {
429
+ if (child.pid !== undefined) {
430
+ try {
431
+ process.kill(child.pid, "SIGTERM");
432
+ }
433
+ catch {
434
+ // ignore shutdown failures while unwinding startup errors
435
+ }
436
+ }
437
+ throw error;
438
+ }
439
+ return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
440
+ dataDir: options.dataDir,
441
+ walletRootId,
442
+ serviceLifetime,
443
+ ownership: "started",
444
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
445
+ });
446
+ };
375
447
  const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
376
448
  if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
377
- return existingProbe.client;
449
+ if (!isStaleIndexerDaemonVersion(existingProbe.status, expectedBinaryVersion)) {
450
+ try {
451
+ return await requestBackgroundFollow(existingProbe.client, existingProbe.status);
452
+ }
453
+ catch {
454
+ await existingProbe.client.close().catch(() => undefined);
455
+ }
456
+ }
457
+ else {
458
+ await existingProbe.client.close().catch(() => undefined);
459
+ }
378
460
  }
379
- if (existingProbe.compatibility !== "unreachable") {
461
+ if (existingProbe.compatibility !== "unreachable" && existingProbe.compatibility !== "compatible") {
380
462
  throw new Error(existingProbe.error ?? "indexer_daemon_protocol_error");
381
463
  }
382
464
  try {
@@ -389,53 +471,43 @@ export async function attachOrStartIndexerDaemon(options) {
389
471
  try {
390
472
  const liveProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
391
473
  if (liveProbe.compatibility === "compatible" && liveProbe.client !== null) {
392
- return liveProbe.client;
474
+ if (!isStaleIndexerDaemonVersion(liveProbe.status, expectedBinaryVersion)) {
475
+ try {
476
+ return await requestBackgroundFollow(liveProbe.client, liveProbe.status);
477
+ }
478
+ catch {
479
+ await liveProbe.client.close().catch(() => undefined);
480
+ await stopIndexerDaemonServiceWithLockHeld({
481
+ dataDir: options.dataDir,
482
+ walletRootId,
483
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
484
+ paths,
485
+ processId: liveProbe.status?.processId ?? null,
486
+ });
487
+ }
488
+ }
489
+ else {
490
+ await liveProbe.client.close().catch(() => undefined);
491
+ await stopIndexerDaemonServiceWithLockHeld({
492
+ dataDir: options.dataDir,
493
+ walletRootId,
494
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
495
+ paths,
496
+ processId: liveProbe.status?.processId ?? null,
497
+ });
498
+ }
393
499
  }
394
- if (liveProbe.compatibility !== "unreachable") {
500
+ else if (liveProbe.compatibility !== "unreachable") {
395
501
  throw new Error(liveProbe.error ?? "indexer_daemon_protocol_error");
396
502
  }
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
- }
503
+ const daemon = await startDaemon();
418
504
  try {
419
- await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
505
+ return await requestBackgroundFollow(daemon);
420
506
  }
421
507
  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;
508
+ await daemon.close().catch(() => undefined);
509
+ throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, { cause: error });
431
510
  }
432
- return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
433
- dataDir: options.dataDir,
434
- walletRootId,
435
- serviceLifetime,
436
- ownership: "started",
437
- shutdownTimeoutMs: options.shutdownTimeoutMs,
438
- });
439
511
  }
440
512
  finally {
441
513
  await lock.release();
@@ -444,13 +516,7 @@ export async function attachOrStartIndexerDaemon(options) {
444
516
  catch (error) {
445
517
  if (error instanceof FileLockBusyError) {
446
518
  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
- });
519
+ return attachOrStartIndexerDaemon(options);
454
520
  }
455
521
  throw error;
456
522
  }
@@ -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,89 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { loadBundledGenesisParameters } from "@cogcoin/indexer";
3
+ import { attachOrStartIndexerDaemon, readObservedIndexerDaemonStatus, } from "./indexer-daemon.js";
4
+ import { resolveCogcoinProcessingStartHeight } from "./processing-start-height.js";
5
+ import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
6
+ import { attachOrStartManagedBitcoindService } from "./service.js";
7
+ async function readJsonFile(filePath) {
8
+ try {
9
+ return JSON.parse(await readFile(filePath, "utf8"));
10
+ }
11
+ catch (error) {
12
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
13
+ return null;
14
+ }
15
+ throw error;
16
+ }
17
+ }
18
+ async function resolveStartOptions(options) {
19
+ const paths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
20
+ const observedStatus = await readJsonFile(paths.bitcoindStatusPath).catch(() => null);
21
+ if (observedStatus !== null) {
22
+ return {
23
+ chain: observedStatus.chain,
24
+ startHeight: observedStatus.startHeight,
25
+ };
26
+ }
27
+ const genesisParameters = await loadBundledGenesisParameters();
28
+ return {
29
+ chain: "main",
30
+ startHeight: resolveCogcoinProcessingStartHeight(genesisParameters),
31
+ };
32
+ }
33
+ export async function openManagedIndexerMonitor(options) {
34
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
35
+ const startOptions = await resolveStartOptions({
36
+ dataDir: options.dataDir,
37
+ walletRootId,
38
+ });
39
+ await attachOrStartManagedBitcoindService({
40
+ dataDir: options.dataDir,
41
+ chain: startOptions.chain,
42
+ startHeight: startOptions.startHeight,
43
+ walletRootId,
44
+ startupTimeoutMs: options.startupTimeoutMs,
45
+ });
46
+ const daemon = await attachOrStartIndexerDaemon({
47
+ dataDir: options.dataDir,
48
+ databasePath: options.databasePath,
49
+ walletRootId,
50
+ startupTimeoutMs: options.startupTimeoutMs,
51
+ ensureBackgroundFollow: true,
52
+ expectedBinaryVersion: options.expectedBinaryVersion,
53
+ });
54
+ return createManagedIndexerMonitor({
55
+ daemon,
56
+ dataDir: options.dataDir,
57
+ walletRootId,
58
+ });
59
+ }
60
+ function createManagedIndexerMonitor(options) {
61
+ let closed = false;
62
+ return {
63
+ async getStatus() {
64
+ if (closed) {
65
+ throw new Error("managed_indexer_monitor_closed");
66
+ }
67
+ try {
68
+ return await options.daemon.getStatus();
69
+ }
70
+ catch (error) {
71
+ const observed = await readObservedIndexerDaemonStatus({
72
+ dataDir: options.dataDir,
73
+ walletRootId: options.walletRootId,
74
+ }).catch(() => null);
75
+ if (observed !== null) {
76
+ return observed;
77
+ }
78
+ throw error;
79
+ }
80
+ },
81
+ async close() {
82
+ if (closed) {
83
+ return;
84
+ }
85
+ closed = true;
86
+ await options.daemon.close().catch(() => undefined);
87
+ },
88
+ };
89
+ }
@@ -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,9 +80,50 @@ 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
113
  return Math.max(state.indexedHeight ?? Number.NEGATIVE_INFINITY, state.displayedCenterHeight ?? Number.NEGATIVE_INFINITY, state.animation?.height ?? Number.NEGATIVE_INFINITY, ...state.queuedHeights);
84
114
  }
115
+ function resolveLatestAuthoritativeFollowHeight(indexedHeight, nodeHeight) {
116
+ if (indexedHeight === null) {
117
+ return nodeHeight;
118
+ }
119
+ if (nodeHeight === null) {
120
+ return indexedHeight;
121
+ }
122
+ return Math.max(indexedHeight, nodeHeight);
123
+ }
124
+ function resolveDisplayedFollowHeight(state) {
125
+ return state.displayedCenterHeight ?? state.indexedHeight;
126
+ }
85
127
  function resetFollowSceneState(state) {
86
128
  state.displayedCenterHeight = state.indexedHeight;
87
129
  state.blockTimesByHeight = {};
@@ -90,13 +132,33 @@ function resetFollowSceneState(state) {
90
132
  state.pendingStaticX = null;
91
133
  state.animation = null;
92
134
  }
135
+ function resyncFollowSceneToLatestHeight(state, latestHeight) {
136
+ state.displayedCenterHeight = latestHeight;
137
+ state.queuedHeights = [];
138
+ state.pendingLabel = null;
139
+ state.pendingStaticX = null;
140
+ state.animation = null;
141
+ }
142
+ function shouldResyncAuthoritativeFollowScene(options) {
143
+ if (options.latestHeight === null) {
144
+ return false;
145
+ }
146
+ if (options.settleLatest) {
147
+ return true;
148
+ }
149
+ const displayedHeight = resolveDisplayedFollowHeight(options.state);
150
+ if (displayedHeight === null) {
151
+ return false;
152
+ }
153
+ return options.latestHeight - displayedHeight > 1;
154
+ }
93
155
  function enqueueFollowHeights(state, nodeHeight) {
94
156
  const startHeight = Math.max(state.indexedHeight ?? -1, highestTrackedFollowHeight(state));
95
157
  for (let height = startHeight + 1; height <= nodeHeight; height += 1) {
96
158
  state.queuedHeights.push(height);
97
159
  }
98
160
  }
99
- export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, }) {
161
+ export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, authoritativeTip, settleLatest, }) {
100
162
  const wasLive = state.liveActivated;
101
163
  const nextLive = liveActivated ?? state.liveActivated;
102
164
  let shouldReset = false;
@@ -119,7 +181,17 @@ export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveAct
119
181
  if (shouldReset) {
120
182
  resetFollowSceneState(state);
121
183
  }
122
- if (state.liveActivated && state.observedNodeHeight !== null) {
184
+ const latestAuthoritativeHeight = resolveLatestAuthoritativeFollowHeight(state.indexedHeight, state.observedNodeHeight);
185
+ if (state.liveActivated
186
+ && authoritativeTip === true
187
+ && shouldResyncAuthoritativeFollowScene({
188
+ state,
189
+ latestHeight: latestAuthoritativeHeight,
190
+ settleLatest: settleLatest === true,
191
+ })) {
192
+ resyncFollowSceneToLatestHeight(state, latestAuthoritativeHeight);
193
+ }
194
+ else if (state.liveActivated && state.observedNodeHeight !== null) {
123
195
  enqueueFollowHeights(state, state.observedNodeHeight);
124
196
  }
125
197
  if (!wasLive && state.liveActivated && state.displayedCenterHeight === null) {
@@ -363,7 +435,18 @@ export function renderFollowFrame(state, statusFieldText, now, options = {}) {
363
435
  if (headerRow !== undefined) {
364
436
  frame[MESSAGE_FIELD_ROW] = replaceSegment(headerRow, FIELD_LEFT, FIELD_WIDTH, renderFollowHeaderField(options));
365
437
  }
366
- overlayCenteredField(frame, STATUS_FIELD_ROW, statusFieldText);
438
+ if (statusFieldText.length > 0
439
+ || (options.artworkStatusLeftText !== null
440
+ && options.artworkStatusLeftText !== undefined
441
+ && options.artworkStatusLeftText.length > 0)
442
+ || (options.artworkStatusRightText !== null
443
+ && options.artworkStatusRightText !== undefined
444
+ && options.artworkStatusRightText.length > 0)) {
445
+ const statusRow = frame[STATUS_FIELD_ROW];
446
+ if (statusRow !== undefined) {
447
+ frame[STATUS_FIELD_ROW] = replaceSegment(statusRow, FIELD_LEFT, FIELD_WIDTH, renderFollowStatusField(statusFieldText, options));
448
+ }
449
+ }
367
450
  return frame;
368
451
  }
369
452
  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;