@cogcoin/client 0.5.5 → 0.5.7

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 (91) hide show
  1. package/README.md +11 -2
  2. package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
  3. package/dist/bitcoind/bootstrap/chainstate.js +4 -1
  4. package/dist/bitcoind/bootstrap/controller.d.ts +4 -1
  5. package/dist/bitcoind/bootstrap/controller.js +42 -5
  6. package/dist/bitcoind/bootstrap/getblock-archive.d.ts +39 -0
  7. package/dist/bitcoind/bootstrap/getblock-archive.js +548 -0
  8. package/dist/bitcoind/bootstrap/headers.d.ts +12 -0
  9. package/dist/bitcoind/bootstrap/headers.js +95 -10
  10. package/dist/bitcoind/bootstrap.d.ts +1 -0
  11. package/dist/bitcoind/bootstrap.js +1 -0
  12. package/dist/bitcoind/client/factory.js +91 -28
  13. package/dist/bitcoind/client/managed-client.d.ts +1 -1
  14. package/dist/bitcoind/client/managed-client.js +4 -3
  15. package/dist/bitcoind/client/sync-engine.js +55 -13
  16. package/dist/bitcoind/errors.js +18 -0
  17. package/dist/bitcoind/indexer-daemon-main.js +78 -0
  18. package/dist/bitcoind/indexer-daemon.d.ts +10 -1
  19. package/dist/bitcoind/indexer-daemon.js +44 -28
  20. package/dist/bitcoind/node.js +2 -0
  21. package/dist/bitcoind/processing-start-height.d.ts +7 -0
  22. package/dist/bitcoind/processing-start-height.js +9 -0
  23. package/dist/bitcoind/progress/constants.d.ts +1 -0
  24. package/dist/bitcoind/progress/constants.js +1 -0
  25. package/dist/bitcoind/progress/controller.d.ts +22 -0
  26. package/dist/bitcoind/progress/controller.js +49 -23
  27. package/dist/bitcoind/progress/formatting.js +29 -1
  28. package/dist/bitcoind/progress/render-policy.d.ts +35 -0
  29. package/dist/bitcoind/progress/render-policy.js +81 -0
  30. package/dist/bitcoind/retryable-rpc.d.ts +11 -0
  31. package/dist/bitcoind/retryable-rpc.js +30 -0
  32. package/dist/bitcoind/service-paths.js +2 -6
  33. package/dist/bitcoind/service.d.ts +21 -2
  34. package/dist/bitcoind/service.js +274 -122
  35. package/dist/bitcoind/testing.d.ts +2 -2
  36. package/dist/bitcoind/testing.js +2 -2
  37. package/dist/bitcoind/types.d.ts +36 -1
  38. package/dist/cli/commands/follow.js +11 -0
  39. package/dist/cli/commands/getblock-archive-restart.d.ts +5 -0
  40. package/dist/cli/commands/getblock-archive-restart.js +15 -0
  41. package/dist/cli/commands/mining-admin.js +4 -0
  42. package/dist/cli/commands/mining-read.js +8 -5
  43. package/dist/cli/commands/mining-runtime.js +4 -0
  44. package/dist/cli/commands/service-runtime.js +150 -134
  45. package/dist/cli/commands/status.js +2 -0
  46. package/dist/cli/commands/sync.js +11 -0
  47. package/dist/cli/commands/wallet-admin.js +106 -24
  48. package/dist/cli/commands/wallet-mutation.js +57 -4
  49. package/dist/cli/commands/wallet-read.js +2 -0
  50. package/dist/cli/context.js +8 -4
  51. package/dist/cli/mutation-command-groups.d.ts +2 -1
  52. package/dist/cli/mutation-command-groups.js +5 -0
  53. package/dist/cli/mutation-json.d.ts +18 -2
  54. package/dist/cli/mutation-json.js +49 -0
  55. package/dist/cli/mutation-success.d.ts +1 -0
  56. package/dist/cli/mutation-success.js +2 -2
  57. package/dist/cli/output.js +86 -1
  58. package/dist/cli/parse.d.ts +1 -1
  59. package/dist/cli/parse.js +133 -3
  60. package/dist/cli/preview-json.d.ts +10 -1
  61. package/dist/cli/preview-json.js +32 -0
  62. package/dist/cli/prompt.js +1 -1
  63. package/dist/cli/runner.js +4 -0
  64. package/dist/cli/types.d.ts +15 -5
  65. package/dist/cli/types.js +1 -1
  66. package/dist/cli/wallet-format.js +140 -14
  67. package/dist/wallet/lifecycle.d.ts +21 -1
  68. package/dist/wallet/lifecycle.js +252 -116
  69. package/dist/wallet/mining/visualizer.d.ts +11 -6
  70. package/dist/wallet/mining/visualizer.js +32 -15
  71. package/dist/wallet/read/context.js +10 -4
  72. package/dist/wallet/reset.d.ts +61 -2
  73. package/dist/wallet/reset.js +246 -89
  74. package/dist/wallet/root-resolution.d.ts +20 -0
  75. package/dist/wallet/root-resolution.js +37 -0
  76. package/dist/wallet/runtime.d.ts +13 -1
  77. package/dist/wallet/runtime.js +54 -11
  78. package/dist/wallet/state/crypto.d.ts +3 -0
  79. package/dist/wallet/state/crypto.js +3 -0
  80. package/dist/wallet/state/provider.d.ts +1 -0
  81. package/dist/wallet/state/provider.js +119 -3
  82. package/dist/wallet/state/seed-index.d.ts +43 -0
  83. package/dist/wallet/state/seed-index.js +151 -0
  84. package/dist/wallet/state/storage.d.ts +7 -1
  85. package/dist/wallet/state/storage.js +39 -0
  86. package/dist/wallet/tx/anchor.d.ts +22 -0
  87. package/dist/wallet/tx/anchor.js +215 -8
  88. package/dist/wallet/tx/index.d.ts +1 -1
  89. package/dist/wallet/tx/index.js +1 -1
  90. package/dist/wallet/types.d.ts +1 -0
  91. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
2
+ import { resolveManagedServicePaths } from "./service-paths.js";
2
3
  interface DaemonRequest {
3
4
  id: string;
4
- method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot";
5
+ method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot" | "PauseBackgroundFollow" | "ResumeBackgroundFollow";
5
6
  token?: string;
6
7
  }
7
8
  interface DaemonResponse {
@@ -64,6 +65,8 @@ export interface IndexerDaemonClient {
64
65
  openSnapshot(): Promise<IndexerSnapshotHandle>;
65
66
  readSnapshot(token: string): Promise<IndexerSnapshotPayload>;
66
67
  closeSnapshot(token: string): Promise<void>;
68
+ pauseBackgroundFollow(): Promise<void>;
69
+ resumeBackgroundFollow(): Promise<void>;
67
70
  close(): Promise<void>;
68
71
  }
69
72
  export type IndexerDaemonCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "schema-mismatch" | "unreachable" | "protocol-error";
@@ -81,6 +84,12 @@ export interface CoherentIndexerSnapshotLease {
81
84
  payload: IndexerSnapshotPayload;
82
85
  status: ManagedIndexerDaemonStatus;
83
86
  }
87
+ export declare function stopIndexerDaemonServiceWithLockHeld(options: {
88
+ dataDir: string;
89
+ walletRootId?: string;
90
+ shutdownTimeoutMs?: number;
91
+ paths?: ReturnType<typeof resolveManagedServicePaths>;
92
+ }): Promise<IndexerDaemonStopResult>;
84
93
  export declare function probeIndexerDaemon(options: {
85
94
  dataDir: string;
86
95
  walletRootId?: string;
@@ -53,6 +53,33 @@ async function clearIndexerDaemonRuntimeArtifacts(paths) {
53
53
  await rm(paths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
54
54
  await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
55
55
  }
56
+ export async function stopIndexerDaemonServiceWithLockHeld(options) {
57
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
58
+ const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
59
+ const status = await readJsonFile(paths.indexerDaemonStatusPath);
60
+ const processId = status?.processId ?? null;
61
+ if (status === null || processId === null || !await isProcessAlive(processId)) {
62
+ await clearIndexerDaemonRuntimeArtifacts(paths);
63
+ return {
64
+ status: "not-running",
65
+ walletRootId,
66
+ };
67
+ }
68
+ try {
69
+ process.kill(processId, "SIGTERM");
70
+ }
71
+ catch (error) {
72
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
73
+ throw error;
74
+ }
75
+ }
76
+ await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
77
+ await clearIndexerDaemonRuntimeArtifacts(paths);
78
+ return {
79
+ status: "stopped",
80
+ walletRootId,
81
+ };
82
+ }
56
83
  function createIndexerDaemonClient(socketPath) {
57
84
  async function sendRequest(request) {
58
85
  return new Promise((resolve, reject) => {
@@ -141,6 +168,18 @@ function createIndexerDaemonClient(socketPath) {
141
168
  token,
142
169
  });
143
170
  },
171
+ async pauseBackgroundFollow() {
172
+ await sendRequest({
173
+ id: randomUUID(),
174
+ method: "PauseBackgroundFollow",
175
+ });
176
+ },
177
+ async resumeBackgroundFollow() {
178
+ await sendRequest({
179
+ id: randomUUID(),
180
+ method: "ResumeBackgroundFollow",
181
+ });
182
+ },
144
183
  async close() {
145
184
  return;
146
185
  },
@@ -150,9 +189,6 @@ function validateIndexerRuntimeIdentity(identity, expectedWalletRootId) {
150
189
  if (identity.serviceApiVersion !== INDEXER_DAEMON_SERVICE_API_VERSION) {
151
190
  throw new Error("indexer_daemon_service_version_mismatch");
152
191
  }
153
- if (identity.walletRootId !== expectedWalletRootId) {
154
- throw new Error("indexer_daemon_wallet_root_mismatch");
155
- }
156
192
  if (identity.schemaVersion !== INDEXER_DAEMON_SCHEMA_VERSION || identity.state === "schema-mismatch") {
157
193
  throw new Error("indexer_daemon_schema_mismatch");
158
194
  }
@@ -244,9 +280,7 @@ async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
244
280
  compatibility: error instanceof Error
245
281
  ? error.message === "indexer_daemon_service_version_mismatch"
246
282
  ? "service-version-mismatch"
247
- : error.message === "indexer_daemon_wallet_root_mismatch"
248
- ? "wallet-root-mismatch"
249
- : "schema-mismatch"
283
+ : "schema-mismatch"
250
284
  : "protocol-error",
251
285
  status,
252
286
  client: null,
@@ -380,29 +414,11 @@ export async function stopIndexerDaemonService(options) {
380
414
  dataDir: options.dataDir,
381
415
  });
382
416
  try {
383
- const status = await readJsonFile(paths.indexerDaemonStatusPath);
384
- const processId = status?.processId ?? null;
385
- if (status === null || processId === null || !await isProcessAlive(processId)) {
386
- await clearIndexerDaemonRuntimeArtifacts(paths);
387
- return {
388
- status: "not-running",
389
- walletRootId,
390
- };
391
- }
392
- try {
393
- process.kill(processId, "SIGTERM");
394
- }
395
- catch (error) {
396
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
397
- throw error;
398
- }
399
- }
400
- await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
401
- await clearIndexerDaemonRuntimeArtifacts(paths);
402
- return {
403
- status: "stopped",
417
+ return await stopIndexerDaemonServiceWithLockHeld({
418
+ ...options,
404
419
  walletRootId,
405
- };
420
+ paths,
421
+ });
406
422
  }
407
423
  finally {
408
424
  await lock.release();
@@ -182,6 +182,8 @@ export async function launchManagedBitcoindNode(options) {
182
182
  expectedChain: resolvedOptions.chain,
183
183
  startHeight: resolvedOptions.startHeight,
184
184
  dataDir: resolvedOptions.dataDir,
185
+ getblockArchiveEndHeight: null,
186
+ getblockArchiveSha256: null,
185
187
  async validate() {
186
188
  await validateNodeConfigForTesting(rpcClient, resolvedOptions.chain, zmqEndpoint);
187
189
  },
@@ -0,0 +1,7 @@
1
+ import type { GenesisParameters } from "@cogcoin/indexer/types";
2
+ export declare function resolveCogcoinProcessingStartHeight(genesisParameters: GenesisParameters): number;
3
+ export declare function assertCogcoinProcessingStartHeight(options: {
4
+ chain: "main" | "regtest";
5
+ startHeight: number;
6
+ genesisParameters: GenesisParameters;
7
+ }): void;
@@ -0,0 +1,9 @@
1
+ export function resolveCogcoinProcessingStartHeight(genesisParameters) {
2
+ return genesisParameters.genesisBlock;
3
+ }
4
+ export function assertCogcoinProcessingStartHeight(options) {
5
+ const processingStartHeight = resolveCogcoinProcessingStartHeight(options.genesisParameters);
6
+ if (options.chain === "main" && options.startHeight < processingStartHeight) {
7
+ throw new Error("cogcoin_processing_start_height_before_genesis");
8
+ }
9
+ }
@@ -1,4 +1,5 @@
1
1
  export declare const PROGRESS_TICK_MS = 250;
2
+ export declare const HEADLESS_PROGRESS_TICK_MS = 1000;
2
3
  export declare const PREPARING_SYNC_LINE = "Preparing managed Cogcoin sync...";
3
4
  export declare const ART_WIDTH = 80;
4
5
  export declare const ART_HEIGHT = 13;
@@ -1,4 +1,5 @@
1
1
  export const PROGRESS_TICK_MS = 250;
2
+ export const HEADLESS_PROGRESS_TICK_MS = 1_000;
2
3
  export const PREPARING_SYNC_LINE = "Preparing managed Cogcoin sync...";
3
4
  export const ART_WIDTH = 80;
4
5
  export const ART_HEIGHT = 13;
@@ -1,9 +1,31 @@
1
+ import type { QuoteDisplayPhase } from "../quotes.js";
1
2
  import type { BootstrapPhase, BootstrapProgress, ManagedBitcoindProgressEvent, ProgressOutputMode, SnapshotMetadata, WritingQuote } from "../types.js";
3
+ import { type FollowSceneStateForTesting } from "./follow-scene.js";
4
+ import { type RenderClock, type TtyRenderStream } from "./render-policy.js";
5
+ interface QuoteRotatorLike {
6
+ current(now?: number): Promise<{
7
+ displayPhase: QuoteDisplayPhase;
8
+ currentQuote: WritingQuote | null;
9
+ displayStartedAt: number;
10
+ }>;
11
+ }
12
+ interface ProgressRendererLike {
13
+ render(displayPhase: QuoteDisplayPhase, quote: WritingQuote | null, progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, introElapsedMs?: number, statusFieldText?: string): void;
14
+ renderTrainScene(kind: "intro" | "completion", progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, elapsedMs: number, statusFieldText?: string): void;
15
+ renderFollowScene(progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, followScene: FollowSceneStateForTesting, statusFieldText?: string): void;
16
+ close(): void;
17
+ }
2
18
  interface ProgressControllerOptions {
3
19
  onProgress?: (event: ManagedBitcoindProgressEvent) => void;
4
20
  progressOutput?: ProgressOutputMode;
5
21
  quoteStatePath: string;
6
22
  snapshot: SnapshotMetadata;
23
+ quoteRotator?: QuoteRotatorLike;
24
+ rendererFactory?: (stream: TtyRenderStream) => ProgressRendererLike;
25
+ stream?: TtyRenderStream;
26
+ platform?: NodeJS.Platform;
27
+ env?: NodeJS.ProcessEnv;
28
+ clock?: RenderClock;
7
29
  }
8
30
  export declare class ManagedProgressController {
9
31
  #private;
@@ -1,12 +1,17 @@
1
1
  import { WritingQuoteRotator as QuoteRotator } from "../quotes.js";
2
- import { INTRO_TOTAL_MS, PROGRESS_TICK_MS } from "./constants.js";
2
+ import { INTRO_TOTAL_MS } from "./constants.js";
3
3
  import { advanceFollowSceneState, createFollowSceneState, replaceFollowBlockTimes, setFollowBlockTime, syncFollowSceneState, } from "./follow-scene.js";
4
4
  import { createBootstrapProgress, createDefaultMessage, resolveStatusFieldText, } from "./formatting.js";
5
+ import { DEFAULT_RENDER_CLOCK, resolveTtyRenderPolicy, TtyRenderThrottle, } from "./render-policy.js";
5
6
  import { TtyProgressRenderer } from "./tty-renderer.js";
6
7
  export class ManagedProgressController {
7
8
  #options;
8
9
  #snapshot;
9
10
  #outputMode;
11
+ #clock;
12
+ #renderStream;
13
+ #renderThrottle;
14
+ #renderIntervalMs;
10
15
  #quoteRotator = null;
11
16
  #renderer = null;
12
17
  #ticker = null;
@@ -24,26 +29,47 @@ export class ManagedProgressController {
24
29
  this.#snapshot = options.snapshot;
25
30
  this.#outputMode = options.progressOutput ?? "auto";
26
31
  this.#progress = createBootstrapProgress("paused", options.snapshot);
32
+ this.#clock = options.clock ?? DEFAULT_RENDER_CLOCK;
33
+ this.#renderStream = options.stream ?? process.stderr;
34
+ const renderPolicy = resolveTtyRenderPolicy(this.#outputMode, this.#renderStream, {
35
+ platform: options.platform,
36
+ env: options.env,
37
+ });
38
+ this.#renderIntervalMs = renderPolicy.repaintIntervalMs;
39
+ this.#renderThrottle = new TtyRenderThrottle({
40
+ clock: this.#clock,
41
+ intervalMs: this.#renderIntervalMs,
42
+ onRender: () => {
43
+ this.#renderToTty();
44
+ },
45
+ throttled: renderPolicy.linuxHeadlessThrottle,
46
+ });
27
47
  }
28
48
  async start() {
29
49
  if (this.#started) {
30
50
  return;
31
51
  }
32
52
  this.#started = true;
33
- this.#quoteRotator = await QuoteRotator.create(this.#options.quoteStatePath);
34
- if (this.#shouldRenderToTty()) {
35
- this.#renderer = new TtyProgressRenderer();
53
+ this.#quoteRotator = this.#options.quoteRotator
54
+ ?? await QuoteRotator.create(this.#options.quoteStatePath);
55
+ if (resolveTtyRenderPolicy(this.#outputMode, this.#renderStream, {
56
+ platform: this.#options.platform,
57
+ env: this.#options.env,
58
+ }).enabled) {
59
+ this.#renderer = this.#options.rendererFactory?.(this.#renderStream)
60
+ ?? new TtyProgressRenderer(this.#renderStream);
36
61
  }
37
62
  await this.#refresh();
38
- this.#ticker = setInterval(() => {
63
+ this.#ticker = this.#clock.setInterval(() => {
39
64
  void this.#refresh();
40
- }, PROGRESS_TICK_MS);
65
+ }, this.#renderIntervalMs);
41
66
  }
42
67
  async close() {
43
68
  if (this.#ticker !== null) {
44
- clearInterval(this.#ticker);
69
+ this.#clock.clearInterval(this.#ticker);
45
70
  this.#ticker = null;
46
71
  }
72
+ this.#renderThrottle.flush();
47
73
  this.#renderer?.close();
48
74
  this.#renderer = null;
49
75
  this.#started = false;
@@ -72,19 +98,20 @@ export class ManagedProgressController {
72
98
  if (!this.#started || this.#renderer === null) {
73
99
  return;
74
100
  }
101
+ this.#renderThrottle.flush();
75
102
  if (this.#ticker !== null) {
76
- clearInterval(this.#ticker);
103
+ this.#clock.clearInterval(this.#ticker);
77
104
  this.#ticker = null;
78
105
  }
79
- const startedAt = Date.now();
106
+ const startedAt = this.#clock.now();
80
107
  while (true) {
81
- const elapsedMs = Math.min(INTRO_TOTAL_MS, Date.now() - startedAt);
108
+ const elapsedMs = Math.min(INTRO_TOTAL_MS, this.#clock.now() - startedAt);
82
109
  this.#renderer.renderTrainScene("completion", this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, elapsedMs);
83
110
  if (elapsedMs >= INTRO_TOTAL_MS) {
84
111
  break;
85
112
  }
86
113
  await new Promise((resolve) => {
87
- setTimeout(resolve, PROGRESS_TICK_MS);
114
+ this.#clock.setTimeout(resolve, this.#renderIntervalMs);
88
115
  });
89
116
  }
90
117
  }
@@ -117,6 +144,7 @@ export class ManagedProgressController {
117
144
  phase: "cogcoin_sync",
118
145
  message: createDefaultMessage("cogcoin_sync"),
119
146
  etaSeconds,
147
+ lastError: null,
120
148
  updatedAt: Date.now(),
121
149
  };
122
150
  if (this.#followVisualMode) {
@@ -142,7 +170,7 @@ export class ManagedProgressController {
142
170
  if (!this.#started || this.#quoteRotator === null) {
143
171
  return;
144
172
  }
145
- const now = Date.now();
173
+ const now = this.#clock.now();
146
174
  if (this.#followVisualMode) {
147
175
  advanceFollowSceneState(this.#followScene, now);
148
176
  this.#currentQuote = null;
@@ -169,20 +197,18 @@ export class ManagedProgressController {
169
197
  catch {
170
198
  // User progress callbacks should never break managed sync.
171
199
  }
200
+ this.#renderThrottle.request();
201
+ }
202
+ #renderToTty() {
203
+ if (!this.#started || this.#renderer === null) {
204
+ return;
205
+ }
206
+ const now = this.#clock.now();
172
207
  const statusFieldText = resolveStatusFieldText(this.#progress, this.#snapshot.height, now);
173
208
  if (this.#followVisualMode) {
174
- this.#renderer?.renderFollowScene(this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, this.#followScene, statusFieldText);
209
+ this.#renderer.renderFollowScene(this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, this.#followScene, statusFieldText);
175
210
  return;
176
211
  }
177
- this.#renderer?.render(this.#currentDisplayPhase, this.#currentQuote, this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, Math.max(0, now - this.#currentDisplayStartedAt), statusFieldText);
178
- }
179
- #shouldRenderToTty() {
180
- if (this.#outputMode === "none") {
181
- return false;
182
- }
183
- if (this.#outputMode === "tty") {
184
- return true;
185
- }
186
- return process.stderr.isTTY === true;
212
+ this.#renderer.render(this.#currentDisplayPhase, this.#currentQuote, this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, Math.max(0, now - this.#currentDisplayStartedAt), statusFieldText);
187
213
  }
188
214
  }
@@ -1,10 +1,14 @@
1
1
  import { FIELD_LEFT, FIELD_WIDTH, PREPARING_SYNC_LINE, PROGRESS_TICK_MS, SCROLL_WINDOW_LEFT, SCROLL_WINDOW_WIDTH, STATUS_ELLIPSIS_TICK_MS, STATUS_ELLIPSIS_WIDTH, } from "./constants.js";
2
2
  export function createDefaultMessage(phase) {
3
3
  switch (phase) {
4
+ case "getblock_archive_download":
5
+ return "Downloading getblock archive.";
6
+ case "getblock_archive_import":
7
+ return "Bitcoin Core is importing getblock archive blocks.";
4
8
  case "snapshot_download":
5
9
  return "Downloading UTXO snapshot.";
6
10
  case "wait_headers_for_snapshot":
7
- return "Waiting for Bitcoin headers to reach the snapshot height.";
11
+ return "Pre-synchronizing blockheaders.";
8
12
  case "load_snapshot":
9
13
  return "Loading the UTXO snapshot into bitcoind.";
10
14
  case "bitcoin_sync":
@@ -121,10 +125,17 @@ function animateStatusEllipsis(now) {
121
125
  }
122
126
  export function resolveStatusFieldText(progress, snapshotHeight, now = 0) {
123
127
  switch (progress.phase) {
128
+ case "getblock_archive_download":
129
+ return `Downloading getblock archive${animateStatusEllipsis(now)}`;
130
+ case "getblock_archive_import":
131
+ return `Importing getblock archive${animateStatusEllipsis(now)}`;
124
132
  case "paused":
125
133
  case "snapshot_download":
126
134
  return `Downloading snapshot to ${snapshotHeight}${animateStatusEllipsis(now)}`;
127
135
  case "wait_headers_for_snapshot":
136
+ return progress.message === "Waiting for Bitcoin headers to reach the snapshot height."
137
+ ? `Waiting for Bitcoin headers to reach the snapshot height${animateStatusEllipsis(now)}`
138
+ : `Pre-synchronizing blockheaders${animateStatusEllipsis(now)}`;
128
139
  case "load_snapshot":
129
140
  case "bitcoin_sync":
130
141
  return `Syncing Bitcoin Blocks${animateStatusEllipsis(now)}`;
@@ -163,6 +174,16 @@ export const formatQuoteLineForTesting = formatQuoteLine;
163
174
  export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTargetHeight, width = 120, now = Date.now()) {
164
175
  let line;
165
176
  switch (progress.phase) {
177
+ case "getblock_archive_download": {
178
+ const current = progress.downloadedBytes ?? 0;
179
+ const total = progress.totalBytes ?? 0;
180
+ const bar = renderBar(current, total, 20);
181
+ const percent = progress.percent ?? (total > 0 ? (current / total) * 100 : 0);
182
+ const speed = progress.bytesPerSecond === null ? "--" : `${formatBytes(progress.bytesPerSecond)}/s`;
183
+ const resumed = progress.resumed ? " resumed" : "";
184
+ line = `${bar} ${percent.toFixed(2)}% ${formatBytes(current)} / ${formatBytes(total)} ${speed} ETA ${formatDuration(progress.etaSeconds)}${resumed}`;
185
+ break;
186
+ }
166
187
  case "snapshot_download": {
167
188
  const current = progress.downloadedBytes ?? 0;
168
189
  const total = progress.totalBytes ?? 0;
@@ -187,6 +208,13 @@ export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTarge
187
208
  line = `${bar} Bitcoin ${blocks.toLocaleString()} / ${target.toLocaleString()} ETA ${formatDuration(progress.etaSeconds)} ${progress.message}`;
188
209
  break;
189
210
  }
211
+ case "getblock_archive_import": {
212
+ const blocks = progress.blocks ?? 0;
213
+ const target = progress.targetHeight ?? blocks;
214
+ const bar = renderBar(blocks, target, 20);
215
+ line = `${bar} Bitcoin ${blocks.toLocaleString()} / ${target.toLocaleString()} ${progress.message}`;
216
+ break;
217
+ }
190
218
  case "cogcoin_sync": {
191
219
  const current = cogcoinSyncHeight ?? 0;
192
220
  const target = cogcoinSyncTargetHeight ?? current;
@@ -0,0 +1,35 @@
1
+ import type { ProgressOutputMode } from "../types.js";
2
+ export interface TtyRenderStream {
3
+ isTTY?: boolean;
4
+ columns?: number;
5
+ write(chunk: string): boolean | void;
6
+ }
7
+ export interface RenderClock {
8
+ now(): number;
9
+ setTimeout: typeof setTimeout;
10
+ clearTimeout: typeof clearTimeout;
11
+ setInterval: typeof setInterval;
12
+ clearInterval: typeof clearInterval;
13
+ }
14
+ export interface TtyRenderPolicy {
15
+ enabled: boolean;
16
+ linuxHeadlessThrottle: boolean;
17
+ repaintIntervalMs: number;
18
+ }
19
+ export declare const DEFAULT_RENDER_CLOCK: RenderClock;
20
+ export declare function resolveTtyRenderPolicy(progressOutput: ProgressOutputMode, stream: Pick<TtyRenderStream, "isTTY">, options?: {
21
+ platform?: NodeJS.Platform;
22
+ env?: NodeJS.ProcessEnv;
23
+ }): TtyRenderPolicy;
24
+ export declare class TtyRenderThrottle {
25
+ #private;
26
+ constructor(options: {
27
+ clock?: RenderClock;
28
+ intervalMs: number;
29
+ onRender: () => void;
30
+ throttled: boolean;
31
+ });
32
+ request(): void;
33
+ flush(): void;
34
+ cancel(): void;
35
+ }
@@ -0,0 +1,81 @@
1
+ import { HEADLESS_PROGRESS_TICK_MS, PROGRESS_TICK_MS } from "./constants.js";
2
+ export const DEFAULT_RENDER_CLOCK = {
3
+ now: () => Date.now(),
4
+ setTimeout,
5
+ clearTimeout,
6
+ setInterval,
7
+ clearInterval,
8
+ };
9
+ export function resolveTtyRenderPolicy(progressOutput, stream, options = {}) {
10
+ const ttyActive = stream.isTTY === true;
11
+ const enabled = progressOutput === "none"
12
+ ? false
13
+ : progressOutput === "tty"
14
+ ? true
15
+ : ttyActive;
16
+ const env = options.env ?? process.env;
17
+ const linuxHeadlessThrottle = enabled
18
+ && ttyActive
19
+ && (options.platform ?? process.platform) === "linux"
20
+ && (env.DISPLAY?.trim() ?? "").length === 0
21
+ && (env.WAYLAND_DISPLAY?.trim() ?? "").length === 0;
22
+ return {
23
+ enabled,
24
+ linuxHeadlessThrottle,
25
+ repaintIntervalMs: linuxHeadlessThrottle ? HEADLESS_PROGRESS_TICK_MS : PROGRESS_TICK_MS,
26
+ };
27
+ }
28
+ export class TtyRenderThrottle {
29
+ #clock;
30
+ #intervalMs;
31
+ #onRender;
32
+ #throttled;
33
+ #lastRenderAt = null;
34
+ #pendingTimer = null;
35
+ constructor(options) {
36
+ this.#clock = options.clock ?? DEFAULT_RENDER_CLOCK;
37
+ this.#intervalMs = options.intervalMs;
38
+ this.#onRender = options.onRender;
39
+ this.#throttled = options.throttled;
40
+ }
41
+ request() {
42
+ if (!this.#throttled) {
43
+ this.cancel();
44
+ this.#renderNow();
45
+ return;
46
+ }
47
+ const now = this.#clock.now();
48
+ if (this.#lastRenderAt === null || (now - this.#lastRenderAt) >= this.#intervalMs) {
49
+ this.cancel();
50
+ this.#renderNow();
51
+ return;
52
+ }
53
+ if (this.#pendingTimer !== null) {
54
+ return;
55
+ }
56
+ const delayMs = Math.max(0, this.#intervalMs - (now - this.#lastRenderAt));
57
+ this.#pendingTimer = this.#clock.setTimeout(() => {
58
+ this.#pendingTimer = null;
59
+ this.#renderNow();
60
+ }, delayMs);
61
+ }
62
+ flush() {
63
+ if (this.#pendingTimer === null) {
64
+ return;
65
+ }
66
+ this.#clock.clearTimeout(this.#pendingTimer);
67
+ this.#pendingTimer = null;
68
+ this.#renderNow();
69
+ }
70
+ cancel() {
71
+ if (this.#pendingTimer === null) {
72
+ return;
73
+ }
74
+ this.#clock.clearTimeout(this.#pendingTimer);
75
+ this.#pendingTimer = null;
76
+ }
77
+ #renderNow() {
78
+ this.#lastRenderAt = this.#clock.now();
79
+ this.#onRender();
80
+ }
81
+ }
@@ -0,0 +1,11 @@
1
+ export declare const MANAGED_RPC_RETRY_BASE_MS = 1000;
2
+ export declare const MANAGED_RPC_RETRY_MAX_MS = 15000;
3
+ export declare const MANAGED_RPC_RETRY_MESSAGE = "Managed Bitcoin RPC temporarily unavailable; retrying until canceled.";
4
+ export interface ManagedRpcRetryState {
5
+ nextDelayMs: number;
6
+ }
7
+ export declare function createManagedRpcRetryState(): ManagedRpcRetryState;
8
+ export declare function resetManagedRpcRetryState(state: ManagedRpcRetryState): void;
9
+ export declare function consumeManagedRpcRetryDelayMs(state: ManagedRpcRetryState): number;
10
+ export declare function isRetryableManagedRpcError(error: unknown): boolean;
11
+ export declare function describeManagedRpcRetryError(error: unknown): string;
@@ -0,0 +1,30 @@
1
+ export const MANAGED_RPC_RETRY_BASE_MS = 1_000;
2
+ export const MANAGED_RPC_RETRY_MAX_MS = 15_000;
3
+ export const MANAGED_RPC_RETRY_MESSAGE = "Managed Bitcoin RPC temporarily unavailable; retrying until canceled.";
4
+ export function createManagedRpcRetryState() {
5
+ return {
6
+ nextDelayMs: MANAGED_RPC_RETRY_BASE_MS,
7
+ };
8
+ }
9
+ export function resetManagedRpcRetryState(state) {
10
+ state.nextDelayMs = MANAGED_RPC_RETRY_BASE_MS;
11
+ }
12
+ export function consumeManagedRpcRetryDelayMs(state) {
13
+ const delayMs = state.nextDelayMs;
14
+ state.nextDelayMs = Math.min(state.nextDelayMs * 2, MANAGED_RPC_RETRY_MAX_MS);
15
+ return delayMs;
16
+ }
17
+ export function isRetryableManagedRpcError(error) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ if (message === "bitcoind_rpc_timeout") {
20
+ return true;
21
+ }
22
+ if (message.startsWith("The managed Bitcoin RPC request to ")) {
23
+ return message.includes(" failed");
24
+ }
25
+ return message.startsWith("The managed Bitcoin RPC cookie file is unavailable at ")
26
+ || message.startsWith("The managed Bitcoin RPC cookie file could not be read at ");
27
+ }
28
+ export function describeManagedRpcRetryError(error) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
@@ -3,9 +3,6 @@ import { tmpdir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { resolveCogcoinPathsForTesting, resolveDefaultBitcoindDataDirForTesting } from "../app-paths.js";
5
5
  export const UNINITIALIZED_WALLET_ROOT_ID = "wallet-root-uninitialized";
6
- function sanitizeWalletRootId(walletRootId) {
7
- return walletRootId.replace(/[^a-zA-Z0-9._-]+/g, "-");
8
- }
9
6
  function createDataDirSuffix(dataDir) {
10
7
  return createHash("sha256").update(dataDir).digest("hex").slice(0, 12);
11
8
  }
@@ -17,7 +14,6 @@ function resolveIndexerDaemonSocketPath(serviceRootId) {
17
14
  return join(tmpdir(), `cogcoin-indexer-${socketId}.sock`);
18
15
  }
19
16
  export function resolveManagedServicePaths(dataDir, walletRootId = UNINITIALIZED_WALLET_ROOT_ID) {
20
- const normalizedWalletRootId = sanitizeWalletRootId(walletRootId);
21
17
  const defaultPaths = resolveCogcoinPathsForTesting();
22
18
  const defaultBitcoindDataDir = resolveDefaultBitcoindDataDirForTesting();
23
19
  const useDefaultRoots = dataDir === defaultBitcoindDataDir;
@@ -25,8 +21,8 @@ export function resolveManagedServicePaths(dataDir, walletRootId = UNINITIALIZED
25
21
  const runtimeRoot = useDefaultRoots ? defaultPaths.runtimeRoot : join(dataRoot, "runtime");
26
22
  const indexerRoot = useDefaultRoots ? defaultPaths.indexerRoot : join(dataRoot, "indexer");
27
23
  const serviceRootId = useDefaultRoots
28
- ? normalizedWalletRootId
29
- : `${normalizedWalletRootId}-${createDataDirSuffix(dataDir)}`;
24
+ ? "managed"
25
+ : `managed-${createDataDirSuffix(dataDir)}`;
30
26
  const walletRuntimeRoot = join(runtimeRoot, serviceRootId);
31
27
  const indexerServiceRoot = join(indexerRoot, serviceRootId);
32
28
  return {
@@ -1,4 +1,6 @@
1
- import type { InternalManagedBitcoindOptions, ManagedBitcoindObservedStatus, ManagedBitcoindNodeHandle, ManagedCoreWalletReplicaStatus } from "./types.js";
1
+ import { resolveManagedServicePaths } from "./service-paths.js";
2
+ import type { InternalManagedBitcoindOptions, ManagedBitcoindObservedStatus, ManagedBitcoindRuntimeConfig, ManagedBitcoindNodeHandle, ManagedCoreWalletReplicaStatus } from "./types.js";
3
+ export declare function resolveManagedBitcoindDbcacheMiB(totalRamBytes: number): number;
2
4
  interface ManagedWalletReplicaRpc {
3
5
  listWallets(): Promise<string[]>;
4
6
  loadWallet(walletName: string, loadOnStartup?: boolean): Promise<{
@@ -18,7 +20,11 @@ interface ManagedWalletReplicaRpc {
18
20
  }>;
19
21
  walletLock(walletName: string): Promise<null>;
20
22
  }
21
- type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataDir" | "chain" | "startHeight" | "walletRootId" | "rpcPort" | "zmqPort" | "p2pPort" | "pollIntervalMs" | "startupTimeoutMs" | "shutdownTimeoutMs" | "managedWalletPassphrase">;
23
+ type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataDir" | "chain" | "startHeight" | "walletRootId" | "rpcPort" | "zmqPort" | "p2pPort" | "pollIntervalMs" | "startupTimeoutMs" | "shutdownTimeoutMs" | "managedWalletPassphrase"> & {
24
+ getblockArchivePath?: string | null;
25
+ getblockArchiveEndHeight?: number | null;
26
+ getblockArchiveSha256?: string | null;
27
+ };
22
28
  export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
23
29
  export interface ManagedBitcoindServiceProbeResult {
24
30
  compatibility: ManagedBitcoindServiceCompatibility;
@@ -29,9 +35,22 @@ export interface ManagedBitcoindServiceStopResult {
29
35
  status: "stopped" | "not-running";
30
36
  walletRootId: string;
31
37
  }
38
+ export declare function writeBitcoinConfForTesting(filePath: string, options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): Promise<void>;
39
+ export declare function buildManagedServiceArgsForTesting(options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): string[];
32
40
  export declare function createManagedWalletReplica(rpc: ManagedWalletReplicaRpc, walletRootId: string, options?: {
33
41
  managedWalletPassphrase?: string;
34
42
  }): Promise<ManagedCoreWalletReplicaStatus>;
43
+ export declare function stopManagedBitcoindServiceWithLockHeld(options: {
44
+ dataDir: string;
45
+ walletRootId?: string;
46
+ shutdownTimeoutMs?: number;
47
+ paths?: ReturnType<typeof resolveManagedServicePaths>;
48
+ }): Promise<ManagedBitcoindServiceStopResult>;
49
+ export declare function withClaimedUninitializedManagedRuntime<T>(options: {
50
+ dataDir: string;
51
+ walletRootId?: string;
52
+ shutdownTimeoutMs?: number;
53
+ }, callback: () => Promise<T>): Promise<T>;
35
54
  export declare function probeManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindServiceProbeResult>;
36
55
  export declare function attachOrStartManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindNodeHandle>;
37
56
  export declare function stopManagedBitcoindService(options: {