@cogcoin/client 0.5.6 → 0.5.8

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 (71) hide show
  1. package/README.md +11 -2
  2. package/dist/bitcoind/bootstrap/getblock-archive.d.ts +39 -0
  3. package/dist/bitcoind/bootstrap/getblock-archive.js +548 -0
  4. package/dist/bitcoind/bootstrap.d.ts +1 -0
  5. package/dist/bitcoind/bootstrap.js +1 -0
  6. package/dist/bitcoind/client/factory.js +92 -30
  7. package/dist/bitcoind/client/managed-client.d.ts +1 -1
  8. package/dist/bitcoind/client/managed-client.js +22 -2
  9. package/dist/bitcoind/client/sync-engine.js +7 -0
  10. package/dist/bitcoind/errors.js +18 -0
  11. package/dist/bitcoind/indexer-daemon-main.js +78 -0
  12. package/dist/bitcoind/indexer-daemon.d.ts +3 -1
  13. package/dist/bitcoind/indexer-daemon.js +13 -6
  14. package/dist/bitcoind/node.js +2 -0
  15. package/dist/bitcoind/progress/constants.d.ts +1 -0
  16. package/dist/bitcoind/progress/constants.js +1 -0
  17. package/dist/bitcoind/progress/controller.d.ts +22 -0
  18. package/dist/bitcoind/progress/controller.js +48 -23
  19. package/dist/bitcoind/progress/formatting.js +25 -0
  20. package/dist/bitcoind/progress/render-policy.d.ts +35 -0
  21. package/dist/bitcoind/progress/render-policy.js +81 -0
  22. package/dist/bitcoind/service-paths.js +2 -6
  23. package/dist/bitcoind/service.d.ts +5 -1
  24. package/dist/bitcoind/service.js +92 -54
  25. package/dist/bitcoind/testing.d.ts +2 -1
  26. package/dist/bitcoind/testing.js +2 -1
  27. package/dist/bitcoind/types.d.ts +35 -1
  28. package/dist/cli/commands/follow.js +2 -0
  29. package/dist/cli/commands/getblock-archive-restart.d.ts +5 -0
  30. package/dist/cli/commands/getblock-archive-restart.js +15 -0
  31. package/dist/cli/commands/mining-admin.js +4 -0
  32. package/dist/cli/commands/mining-read.js +8 -5
  33. package/dist/cli/commands/mining-runtime.js +4 -0
  34. package/dist/cli/commands/status.js +2 -0
  35. package/dist/cli/commands/sync.js +2 -0
  36. package/dist/cli/commands/wallet-admin.js +29 -3
  37. package/dist/cli/commands/wallet-mutation.js +57 -4
  38. package/dist/cli/commands/wallet-read.js +2 -0
  39. package/dist/cli/context.js +5 -3
  40. package/dist/cli/mutation-command-groups.d.ts +2 -1
  41. package/dist/cli/mutation-command-groups.js +5 -0
  42. package/dist/cli/mutation-json.d.ts +18 -2
  43. package/dist/cli/mutation-json.js +47 -0
  44. package/dist/cli/mutation-success.d.ts +1 -0
  45. package/dist/cli/mutation-success.js +2 -2
  46. package/dist/cli/output.js +97 -1
  47. package/dist/cli/parse.d.ts +1 -1
  48. package/dist/cli/parse.js +127 -3
  49. package/dist/cli/preview-json.d.ts +10 -1
  50. package/dist/cli/preview-json.js +30 -0
  51. package/dist/cli/prompt.js +1 -1
  52. package/dist/cli/runner.js +20 -0
  53. package/dist/cli/signals.js +1 -1
  54. package/dist/cli/types.d.ts +11 -4
  55. package/dist/cli/wallet-format.js +6 -0
  56. package/dist/wallet/lifecycle.d.ts +18 -1
  57. package/dist/wallet/lifecycle.js +170 -81
  58. package/dist/wallet/mining/visualizer.d.ts +11 -6
  59. package/dist/wallet/mining/visualizer.js +32 -15
  60. package/dist/wallet/reset.js +39 -27
  61. package/dist/wallet/runtime.d.ts +12 -1
  62. package/dist/wallet/runtime.js +53 -11
  63. package/dist/wallet/state/provider.d.ts +1 -0
  64. package/dist/wallet/state/provider.js +119 -3
  65. package/dist/wallet/state/seed-index.d.ts +43 -0
  66. package/dist/wallet/state/seed-index.js +151 -0
  67. package/dist/wallet/tx/anchor.d.ts +22 -0
  68. package/dist/wallet/tx/anchor.js +201 -8
  69. package/dist/wallet/tx/index.d.ts +1 -1
  70. package/dist/wallet/tx/index.js +1 -1
  71. package/package.json +1 -1
@@ -1,6 +1,10 @@
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":
@@ -121,6 +125,10 @@ 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)}`;
@@ -166,6 +174,16 @@ export const formatQuoteLineForTesting = formatQuoteLine;
166
174
  export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTargetHeight, width = 120, now = Date.now()) {
167
175
  let line;
168
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
+ }
169
187
  case "snapshot_download": {
170
188
  const current = progress.downloadedBytes ?? 0;
171
189
  const total = progress.totalBytes ?? 0;
@@ -190,6 +208,13 @@ export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTarge
190
208
  line = `${bar} Bitcoin ${blocks.toLocaleString()} / ${target.toLocaleString()} ETA ${formatDuration(progress.etaSeconds)} ${progress.message}`;
191
209
  break;
192
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
+ }
193
218
  case "cogcoin_sync": {
194
219
  const current = cogcoinSyncHeight ?? 0;
195
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
+ }
@@ -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 {
@@ -20,7 +20,11 @@ interface ManagedWalletReplicaRpc {
20
20
  }>;
21
21
  walletLock(walletName: string): Promise<null>;
22
22
  }
23
- 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
+ };
24
28
  export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
25
29
  export interface ManagedBitcoindServiceProbeResult {
26
30
  compatibility: ManagedBitcoindServiceCompatibility;
@@ -226,15 +226,13 @@ async function waitForRpcReady(rpc, cookieFile, expectedChain, timeoutMs) {
226
226
  throw lastError instanceof Error ? lastError : new Error("bitcoind_rpc_timeout");
227
227
  }
228
228
  function validateManagedBitcoindStatus(status, options, runtimeRoot) {
229
+ const legacyRuntimeRoot = join(resolveManagedServicePaths(options.dataDir ?? "", options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID).runtimeRoot, status.walletRootId);
229
230
  if (status.serviceApiVersion !== MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE) {
230
231
  throw new Error("managed_bitcoind_service_version_mismatch");
231
232
  }
232
- if (status.walletRootId !== (options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID)) {
233
- throw new Error("managed_bitcoind_wallet_root_mismatch");
234
- }
235
233
  if (status.chain !== options.chain
236
234
  || status.dataDir !== (options.dataDir ?? "")
237
- || status.runtimeRoot !== runtimeRoot) {
235
+ || (status.runtimeRoot !== runtimeRoot && status.runtimeRoot !== legacyRuntimeRoot)) {
238
236
  throw new Error("managed_bitcoind_runtime_mismatch");
239
237
  }
240
238
  }
@@ -275,6 +273,8 @@ function createBitcoindServiceStatus(options) {
275
273
  rpc: options.rpc,
276
274
  zmq: options.zmq,
277
275
  p2pPort: options.p2pPort,
276
+ getblockArchiveEndHeight: options.getblockArchiveEndHeight,
277
+ getblockArchiveSha256: options.getblockArchiveSha256,
278
278
  walletReplica: options.walletReplica,
279
279
  startedAtUnixMs: options.startedAtUnixMs,
280
280
  heartbeatAtUnixMs: options.heartbeatAtUnixMs,
@@ -287,9 +287,7 @@ function mapManagedBitcoindValidationError(error) {
287
287
  compatibility: error instanceof Error
288
288
  ? error.message === "managed_bitcoind_service_version_mismatch"
289
289
  ? "service-version-mismatch"
290
- : error.message === "managed_bitcoind_wallet_root_mismatch"
291
- ? "wallet-root-mismatch"
292
- : "runtime-mismatch"
290
+ : "runtime-mismatch"
293
291
  : "protocol-error",
294
292
  status: null,
295
293
  error: error instanceof Error ? error.message : "managed_bitcoind_protocol_error",
@@ -366,6 +364,8 @@ async function resolveRuntimeConfig(statusPath, configPath, options) {
366
364
  zmqPort,
367
365
  p2pPort,
368
366
  dbcacheMiB: detectManagedBitcoindDbcacheMiB(),
367
+ getblockArchiveEndHeight: options.getblockArchiveEndHeight ?? null,
368
+ getblockArchiveSha256: options.getblockArchiveSha256 ?? null,
369
369
  };
370
370
  }
371
371
  async function writeBitcoinConf(filePath, options, runtimeConfig) {
@@ -407,6 +407,9 @@ function buildManagedServiceArgs(options, runtimeConfig) {
407
407
  if (options.chain === "regtest") {
408
408
  args.push("-chain=regtest");
409
409
  }
410
+ if (options.getblockArchivePath !== undefined && options.getblockArchivePath !== null) {
411
+ args.push(`-loadblock=${options.getblockArchivePath}`);
412
+ }
410
413
  return args;
411
414
  }
412
415
  export async function writeBitcoinConfForTesting(filePath, options, runtimeConfig) {
@@ -547,6 +550,8 @@ async function writeBitcoindStatus(paths, status) {
547
550
  rpc: status.rpc,
548
551
  zmqPort: status.zmq.port,
549
552
  p2pPort: status.p2pPort,
553
+ getblockArchiveEndHeight: status.getblockArchiveEndHeight,
554
+ getblockArchiveSha256: status.getblockArchiveSha256,
550
555
  });
551
556
  }
552
557
  async function clearManagedBitcoindRuntimeArtifacts(paths) {
@@ -590,6 +595,11 @@ export async function stopManagedBitcoindServiceWithLockHeld(options) {
590
595
  }
591
596
  export async function withClaimedUninitializedManagedRuntime(options, callback) {
592
597
  const targetWalletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
598
+ const targetPaths = resolveManagedServicePaths(options.dataDir, targetWalletRootId);
599
+ const uninitializedPaths = resolveManagedServicePaths(options.dataDir, UNINITIALIZED_WALLET_ROOT_ID);
600
+ if (targetPaths.walletRuntimeRoot === uninitializedPaths.walletRuntimeRoot) {
601
+ return callback();
602
+ }
593
603
  if (targetWalletRootId === UNINITIALIZED_WALLET_ROOT_ID) {
594
604
  return callback();
595
605
  }
@@ -598,7 +608,6 @@ export async function withClaimedUninitializedManagedRuntime(options, callback)
598
608
  return callback();
599
609
  }
600
610
  claimedUninitializedRuntimeKeys.add(claimKey);
601
- const uninitializedPaths = resolveManagedServicePaths(options.dataDir, UNINITIALIZED_WALLET_ROOT_ID);
602
611
  const lockTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
603
612
  const bitcoindLock = await acquireFileLockWithRetry(uninitializedPaths.bitcoindLockPath, {
604
613
  purpose: "managed-bitcoind-claim-uninitialized",
@@ -638,12 +647,15 @@ export async function withClaimedUninitializedManagedRuntime(options, callback)
638
647
  async function refreshManagedBitcoindStatus(status, paths, options) {
639
648
  const nowUnixMs = Date.now();
640
649
  const rpc = createRpcClient(status.rpc);
650
+ const targetWalletRootId = options.walletRootId ?? status.walletRootId;
641
651
  try {
642
652
  await waitForRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS);
643
653
  await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint);
644
- const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, status.walletRootId, status.dataDir);
654
+ const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, targetWalletRootId, status.dataDir);
645
655
  const nextStatus = {
646
656
  ...status,
657
+ walletRootId: targetWalletRootId,
658
+ runtimeRoot: paths.walletRuntimeRoot,
647
659
  state: "ready",
648
660
  processId: await isProcessAlive(status.processId) ? status.processId : null,
649
661
  walletReplica,
@@ -657,6 +669,8 @@ async function refreshManagedBitcoindStatus(status, paths, options) {
657
669
  catch (error) {
658
670
  const nextStatus = {
659
671
  ...status,
672
+ walletRootId: targetWalletRootId,
673
+ runtimeRoot: paths.walletRuntimeRoot,
660
674
  state: "failed",
661
675
  processId: await isProcessAlive(status.processId) ? status.processId : null,
662
676
  heartbeatAtUnixMs: nowUnixMs,
@@ -677,6 +691,8 @@ function createNodeHandle(status, paths, options) {
677
691
  expectedChain: currentStatus.chain,
678
692
  startHeight: currentStatus.startHeight,
679
693
  dataDir: currentStatus.dataDir,
694
+ getblockArchiveEndHeight: currentStatus.getblockArchiveEndHeight ?? null,
695
+ getblockArchiveSha256: currentStatus.getblockArchiveSha256 ?? null,
680
696
  walletRootId: currentStatus.walletRootId,
681
697
  runtimeRoot: paths.walletRuntimeRoot,
682
698
  async validate() {
@@ -684,6 +700,9 @@ function createNodeHandle(status, paths, options) {
684
700
  },
685
701
  async refreshServiceStatus() {
686
702
  currentStatus = await refreshManagedBitcoindStatus(currentStatus, paths, options);
703
+ this.getblockArchiveEndHeight = currentStatus.getblockArchiveEndHeight ?? null;
704
+ this.getblockArchiveSha256 = currentStatus.getblockArchiveSha256 ?? null;
705
+ this.walletRootId = currentStatus.walletRootId;
687
706
  return currentStatus;
688
707
  },
689
708
  async stop() {
@@ -786,57 +805,76 @@ export async function attachOrStartManagedBitcoindService(options) {
786
805
  await verifyBitcoindVersion(bitcoindPath);
787
806
  const binaryVersion = SUPPORTED_BITCOIND_VERSION;
788
807
  await mkdir(resolvedOptions.dataDir ?? "", { recursive: true });
789
- const runtimeConfig = await resolveRuntimeConfig(paths.bitcoindStatusPath, paths.bitcoindRuntimeConfigPath, resolvedOptions);
790
- await writeBitcoinConf(paths.bitcoinConfPath, resolvedOptions, runtimeConfig);
791
- const rpcConfig = runtimeConfig.rpc;
792
- const zmqConfig = {
793
- endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
794
- topic: "hashblock",
795
- port: runtimeConfig.zmqPort,
796
- pollIntervalMs: resolvedOptions.pollIntervalMs ?? 15_000,
808
+ const startManagedProcess = async (startOptions) => {
809
+ const runtimeConfig = await resolveRuntimeConfig(paths.bitcoindStatusPath, paths.bitcoindRuntimeConfigPath, startOptions);
810
+ await writeBitcoinConf(paths.bitcoinConfPath, startOptions, runtimeConfig);
811
+ const rpcConfig = runtimeConfig.rpc;
812
+ const zmqConfig = {
813
+ endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
814
+ topic: "hashblock",
815
+ port: runtimeConfig.zmqPort,
816
+ pollIntervalMs: startOptions.pollIntervalMs ?? 15_000,
817
+ };
818
+ const child = spawn(bitcoindPath, buildManagedServiceArgs(startOptions, runtimeConfig), {
819
+ detached: true,
820
+ stdio: "ignore",
821
+ });
822
+ child.unref();
823
+ const rpc = createRpcClient(rpcConfig);
824
+ try {
825
+ await waitForRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
826
+ await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint);
827
+ }
828
+ catch (error) {
829
+ if (child.pid !== undefined) {
830
+ try {
831
+ process.kill(child.pid, "SIGTERM");
832
+ }
833
+ catch {
834
+ // ignore kill failures during startup cleanup
835
+ }
836
+ }
837
+ throw error;
838
+ }
839
+ const nowUnixMs = Date.now();
840
+ const walletRootId = startOptions.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
841
+ const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, walletRootId, startOptions.dataDir ?? "");
842
+ return createBitcoindServiceStatus({
843
+ binaryVersion,
844
+ serviceInstanceId: randomBytes(16).toString("hex"),
845
+ state: "ready",
846
+ processId: child.pid ?? null,
847
+ walletRootId,
848
+ chain: startOptions.chain,
849
+ dataDir: startOptions.dataDir ?? "",
850
+ runtimeRoot: paths.walletRuntimeRoot,
851
+ startHeight: startOptions.startHeight,
852
+ rpc: rpcConfig,
853
+ zmq: zmqConfig,
854
+ p2pPort: runtimeConfig.p2pPort,
855
+ getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
856
+ getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
857
+ walletReplica,
858
+ startedAtUnixMs: nowUnixMs,
859
+ heartbeatAtUnixMs: nowUnixMs,
860
+ lastError: walletReplica.message ?? null,
861
+ });
797
862
  };
798
- const child = spawn(bitcoindPath, buildManagedServiceArgs(resolvedOptions, runtimeConfig), {
799
- detached: true,
800
- stdio: "ignore",
801
- });
802
- child.unref();
803
- const rpc = createRpcClient(rpcConfig);
863
+ let status;
804
864
  try {
805
- await waitForRpcReady(rpc, rpcConfig.cookieFile, resolvedOptions.chain, startupTimeoutMs);
806
- await validateNodeConfigForTesting(rpc, resolvedOptions.chain, zmqConfig.endpoint);
865
+ status = await startManagedProcess(resolvedOptions);
807
866
  }
808
867
  catch (error) {
809
- if (child.pid !== undefined) {
810
- try {
811
- process.kill(child.pid, "SIGTERM");
812
- }
813
- catch {
814
- // ignore kill failures during startup cleanup
815
- }
868
+ if (resolvedOptions.getblockArchivePath === undefined || resolvedOptions.getblockArchivePath === null) {
869
+ throw error;
816
870
  }
817
- throw error;
871
+ status = await startManagedProcess({
872
+ ...resolvedOptions,
873
+ getblockArchivePath: null,
874
+ getblockArchiveEndHeight: null,
875
+ getblockArchiveSha256: null,
876
+ });
818
877
  }
819
- const nowUnixMs = Date.now();
820
- const walletRootId = resolvedOptions.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
821
- const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, walletRootId, resolvedOptions.dataDir ?? "");
822
- const status = createBitcoindServiceStatus({
823
- binaryVersion,
824
- serviceInstanceId: randomBytes(16).toString("hex"),
825
- state: "ready",
826
- processId: child.pid ?? null,
827
- walletRootId,
828
- chain: resolvedOptions.chain,
829
- dataDir: resolvedOptions.dataDir ?? "",
830
- runtimeRoot: paths.walletRuntimeRoot,
831
- startHeight: resolvedOptions.startHeight,
832
- rpc: rpcConfig,
833
- zmq: zmqConfig,
834
- p2pPort: runtimeConfig.p2pPort,
835
- walletReplica,
836
- startedAtUnixMs: nowUnixMs,
837
- heartbeatAtUnixMs: nowUnixMs,
838
- lastError: walletReplica.message ?? null,
839
- });
840
878
  await writeBitcoindStatus(paths, status);
841
879
  return createNodeHandle(status, paths, resolvedOptions);
842
880
  }
@@ -1,9 +1,10 @@
1
1
  export { openManagedBitcoindClientInternal } from "./client.js";
2
+ export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
2
3
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
4
  export { normalizeRpcBlock } from "./normalize.js";
4
5
  export { BitcoinRpcClient } from "./rpc.js";
5
6
  export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
6
- export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, resolveBootstrapPathsForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
+ export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
8
  export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
8
9
  export { ManagedProgressController, TtyProgressRenderer, advanceFollowSceneStateForTesting, createFollowSceneStateForTesting, createBootstrapProgressForTesting, formatCompactFollowAgeLabelForTesting, loadBannerArtForTesting, loadScrollArtForTesting, loadTrainCarArtForTesting, loadTrainArtForTesting, loadTrainSmokeArtForTesting, formatProgressLineForTesting, formatQuoteLineForTesting, renderArtFrameForTesting, renderCompletionFrameForTesting, renderFollowFrameForTesting, renderIntroFrameForTesting, resolveCompletionMessageForTesting, resolveIntroMessageForTesting, resolveStatusFieldTextForTesting, setFollowBlockTimeForTesting, setFollowBlockTimesForTesting, syncFollowSceneStateForTesting, } from "./progress.js";
9
10
  export { WritingQuoteRotator, loadWritingQuotesForTesting, shuffleIndicesForTesting, } from "./quotes.js";
@@ -1,9 +1,10 @@
1
1
  export { openManagedBitcoindClientInternal } from "./client.js";
2
+ export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
2
3
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
4
  export { normalizeRpcBlock } from "./normalize.js";
4
5
  export { BitcoinRpcClient } from "./rpc.js";
5
6
  export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
6
- export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, resolveBootstrapPathsForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
+ export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
8
  export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
8
9
  export { ManagedProgressController, TtyProgressRenderer, advanceFollowSceneStateForTesting, createFollowSceneStateForTesting, createBootstrapProgressForTesting, formatCompactFollowAgeLabelForTesting, loadBannerArtForTesting, loadScrollArtForTesting, loadTrainCarArtForTesting, loadTrainArtForTesting, loadTrainSmokeArtForTesting, formatProgressLineForTesting, formatQuoteLineForTesting, renderArtFrameForTesting, renderCompletionFrameForTesting, renderFollowFrameForTesting, renderIntroFrameForTesting, resolveCompletionMessageForTesting, resolveIntroMessageForTesting, resolveStatusFieldTextForTesting, setFollowBlockTimeForTesting, setFollowBlockTimesForTesting, syncFollowSceneStateForTesting, } from "./progress.js";
9
10
  export { WritingQuoteRotator, loadWritingQuotesForTesting, shuffleIndicesForTesting, } from "./quotes.js";
@@ -1,5 +1,5 @@
1
1
  import type { BitcoinBlock, Client, ClientOptions, ClientTip } from "../types.js";
2
- export type BootstrapPhase = "snapshot_download" | "wait_headers_for_snapshot" | "load_snapshot" | "bitcoin_sync" | "cogcoin_sync" | "follow_tip" | "paused" | "error" | "complete";
2
+ export type BootstrapPhase = "getblock_archive_download" | "getblock_archive_import" | "snapshot_download" | "wait_headers_for_snapshot" | "load_snapshot" | "bitcoin_sync" | "cogcoin_sync" | "follow_tip" | "paused" | "error" | "complete";
3
3
  export type ProgressOutputMode = "auto" | "tty" | "none";
4
4
  export interface SnapshotMetadata {
5
5
  url: string;
@@ -17,6 +17,28 @@ export interface SnapshotChunkManifest {
17
17
  snapshotSha256: string;
18
18
  chunkSha256s: string[];
19
19
  }
20
+ export interface GetblockArchiveManifestBlockRecord {
21
+ height: number;
22
+ blockHash: string;
23
+ previousBlockHash: string;
24
+ recordOffset: number;
25
+ recordLength: number;
26
+ rawBlockSizeBytes: number;
27
+ }
28
+ export interface GetblockArchiveManifest {
29
+ formatVersion: number;
30
+ chain: "main";
31
+ baseSnapshotHeight: number;
32
+ firstBlockHeight: number;
33
+ endHeight: number;
34
+ blockCount: number;
35
+ artifactFilename: string;
36
+ artifactSizeBytes: number;
37
+ artifactSha256: string;
38
+ chunkSizeBytes: number;
39
+ chunkSha256s: string[];
40
+ blocks: GetblockArchiveManifestBlockRecord[];
41
+ }
20
42
  export interface WritingQuote {
21
43
  quote: string;
22
44
  author: string;
@@ -63,6 +85,8 @@ export interface ManagedBitcoindRuntimeConfig {
63
85
  zmqPort: number;
64
86
  p2pPort: number;
65
87
  dbcacheMiB: number;
88
+ getblockArchiveEndHeight?: number | null;
89
+ getblockArchiveSha256?: string | null;
66
90
  }
67
91
  export declare const MANAGED_BITCOIND_SERVICE_API_VERSION = "cogcoin/bitcoind-service/v1";
68
92
  export type ManagedBitcoindServiceState = "starting" | "ready" | "stopping" | "failed";
@@ -81,6 +105,8 @@ export interface ManagedBitcoindServiceStatus {
81
105
  rpc: BitcoindRpcConfig;
82
106
  zmq: BitcoindZmqConfig;
83
107
  p2pPort: number;
108
+ getblockArchiveEndHeight: number | null;
109
+ getblockArchiveSha256: string | null;
84
110
  walletReplica: ManagedCoreWalletReplicaStatus | null;
85
111
  startedAtUnixMs: number;
86
112
  heartbeatAtUnixMs: number;
@@ -123,6 +149,10 @@ export interface ManagedBitcoindStatus {
123
149
  serviceStatus?: ManagedBitcoindObservedStatus | null;
124
150
  indexerDaemon?: ManagedIndexerDaemonObservedStatus | null;
125
151
  }
152
+ export interface ManagedGetblockArchiveRestartRequest {
153
+ currentArchiveEndHeight: number | null;
154
+ nextArchiveEndHeight: number;
155
+ }
126
156
  export interface ManagedBitcoindOptions extends ClientOptions {
127
157
  dataDir?: string;
128
158
  databasePath?: string;
@@ -137,6 +167,8 @@ export interface ManagedBitcoindOptions extends ClientOptions {
137
167
  managedWalletPassphrase?: string;
138
168
  onProgress?: (event: ManagedBitcoindProgressEvent) => void;
139
169
  progressOutput?: ProgressOutputMode;
170
+ fetchImpl?: typeof fetch;
171
+ confirmGetblockArchiveRestart?: (request: ManagedGetblockArchiveRestartRequest) => Promise<boolean>;
140
172
  }
141
173
  export interface ManagedBitcoindClient extends Client {
142
174
  syncToTip(): Promise<SyncResult>;
@@ -397,6 +429,8 @@ export interface ManagedBitcoindNodeHandle {
397
429
  expectedChain: "main" | "regtest";
398
430
  startHeight: number;
399
431
  dataDir: string;
432
+ getblockArchiveEndHeight: number | null;
433
+ getblockArchiveSha256: string | null;
400
434
  walletRootId?: string;
401
435
  runtimeRoot?: string;
402
436
  validate(): Promise<void>;
@@ -3,6 +3,7 @@ import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolut
3
3
  import { usesTtyProgress, writeLine } from "../io.js";
4
4
  import { classifyCliError } from "../output.js";
5
5
  import { createStopSignalWatcher } from "../signals.js";
6
+ import { confirmGetblockArchiveRestart } from "./getblock-archive-restart.js";
6
7
  export async function runFollowCommand(parsed, context) {
7
8
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
8
9
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
@@ -23,6 +24,7 @@ export async function runFollowCommand(parsed, context) {
23
24
  dataDir,
24
25
  walletRootId: walletRoot.walletRootId,
25
26
  progressOutput: parsed.progressOutput,
27
+ confirmGetblockArchiveRestart: async (options) => confirmGetblockArchiveRestart(parsed, context, options),
26
28
  });
27
29
  storeOwned = false;
28
30
  const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit);
@@ -0,0 +1,5 @@
1
+ import type { ParsedCliArgs, RequiredCliRunnerContext } from "../types.js";
2
+ export declare function confirmGetblockArchiveRestart(parsed: ParsedCliArgs, context: RequiredCliRunnerContext, options: {
3
+ currentArchiveEndHeight: number | null;
4
+ nextArchiveEndHeight: number;
5
+ }): Promise<boolean>;
@@ -0,0 +1,15 @@
1
+ export async function confirmGetblockArchiveRestart(parsed, context, options) {
2
+ if (parsed.assumeYes) {
3
+ return true;
4
+ }
5
+ const prompter = context.createPrompter();
6
+ if (!prompter.isInteractive) {
7
+ return false;
8
+ }
9
+ const currentLabel = options.currentArchiveEndHeight === null
10
+ ? "without a getblock archive"
11
+ : `with a getblock archive through height ${options.currentArchiveEndHeight.toLocaleString()}`;
12
+ prompter.writeLine(`Managed bitcoind is already running ${currentLabel}. A newer getblock archive through height ${options.nextArchiveEndHeight.toLocaleString()} is available.`);
13
+ const answer = (await prompter.prompt(`Restart managed bitcoind to load the getblock archive through height ${options.nextArchiveEndHeight.toLocaleString()}? [y/N]: `)).trim().toLowerCase();
14
+ return answer === "y" || answer === "yes";
15
+ }
@@ -12,11 +12,13 @@ function createCommandPrompter(parsed, context) {
12
12
  export async function runMiningAdminCommand(parsed, context) {
13
13
  try {
14
14
  const provider = context.walletSecretProvider;
15
+ const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
15
16
  if (parsed.command === "hooks-mining-enable") {
16
17
  const prompter = createCommandPrompter(parsed, context);
17
18
  const view = await context.enableMiningHooks({
18
19
  provider,
19
20
  prompter,
21
+ paths: runtimePaths,
20
22
  });
21
23
  const nextSteps = getHooksEnableMiningNextSteps();
22
24
  if (parsed.outputMode === "preview-json") {
@@ -40,6 +42,7 @@ export async function runMiningAdminCommand(parsed, context) {
40
42
  if (parsed.command === "hooks-mining-disable") {
41
43
  const view = await context.disableMiningHooks({
42
44
  provider,
45
+ paths: runtimePaths,
43
46
  });
44
47
  if (parsed.outputMode === "preview-json") {
45
48
  writeJsonValue(context.stdout, createPreviewSuccessEnvelope(resolvePreviewJsonSchema(parsed), describeCanonicalCommand(parsed), "disabled", buildHooksPreviewData("hooks-disable-mining", view)));
@@ -57,6 +60,7 @@ export async function runMiningAdminCommand(parsed, context) {
57
60
  const view = await context.setupBuiltInMining({
58
61
  provider,
59
62
  prompter,
63
+ paths: runtimePaths,
60
64
  });
61
65
  const nextSteps = getMineSetupNextSteps();
62
66
  if (parsed.outputMode === "preview-json") {