@cogcoin/client 0.5.6 → 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 (69) 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 +84 -30
  7. package/dist/bitcoind/client/managed-client.js +2 -1
  8. package/dist/bitcoind/client/sync-engine.js +7 -0
  9. package/dist/bitcoind/errors.js +18 -0
  10. package/dist/bitcoind/indexer-daemon-main.js +78 -0
  11. package/dist/bitcoind/indexer-daemon.d.ts +3 -1
  12. package/dist/bitcoind/indexer-daemon.js +13 -6
  13. package/dist/bitcoind/node.js +2 -0
  14. package/dist/bitcoind/progress/constants.d.ts +1 -0
  15. package/dist/bitcoind/progress/constants.js +1 -0
  16. package/dist/bitcoind/progress/controller.d.ts +22 -0
  17. package/dist/bitcoind/progress/controller.js +48 -23
  18. package/dist/bitcoind/progress/formatting.js +25 -0
  19. package/dist/bitcoind/progress/render-policy.d.ts +35 -0
  20. package/dist/bitcoind/progress/render-policy.js +81 -0
  21. package/dist/bitcoind/service-paths.js +2 -6
  22. package/dist/bitcoind/service.d.ts +5 -1
  23. package/dist/bitcoind/service.js +93 -54
  24. package/dist/bitcoind/testing.d.ts +1 -1
  25. package/dist/bitcoind/testing.js +1 -1
  26. package/dist/bitcoind/types.d.ts +35 -1
  27. package/dist/cli/commands/follow.js +2 -0
  28. package/dist/cli/commands/getblock-archive-restart.d.ts +5 -0
  29. package/dist/cli/commands/getblock-archive-restart.js +15 -0
  30. package/dist/cli/commands/mining-admin.js +4 -0
  31. package/dist/cli/commands/mining-read.js +8 -5
  32. package/dist/cli/commands/mining-runtime.js +4 -0
  33. package/dist/cli/commands/status.js +2 -0
  34. package/dist/cli/commands/sync.js +2 -0
  35. package/dist/cli/commands/wallet-admin.js +29 -3
  36. package/dist/cli/commands/wallet-mutation.js +57 -4
  37. package/dist/cli/commands/wallet-read.js +2 -0
  38. package/dist/cli/context.js +5 -3
  39. package/dist/cli/mutation-command-groups.d.ts +2 -1
  40. package/dist/cli/mutation-command-groups.js +5 -0
  41. package/dist/cli/mutation-json.d.ts +18 -2
  42. package/dist/cli/mutation-json.js +47 -0
  43. package/dist/cli/mutation-success.d.ts +1 -0
  44. package/dist/cli/mutation-success.js +2 -2
  45. package/dist/cli/output.js +84 -1
  46. package/dist/cli/parse.d.ts +1 -1
  47. package/dist/cli/parse.js +127 -3
  48. package/dist/cli/preview-json.d.ts +10 -1
  49. package/dist/cli/preview-json.js +30 -0
  50. package/dist/cli/prompt.js +1 -1
  51. package/dist/cli/runner.js +3 -0
  52. package/dist/cli/types.d.ts +11 -4
  53. package/dist/cli/wallet-format.js +6 -0
  54. package/dist/wallet/lifecycle.d.ts +15 -1
  55. package/dist/wallet/lifecycle.js +147 -83
  56. package/dist/wallet/mining/visualizer.d.ts +11 -6
  57. package/dist/wallet/mining/visualizer.js +32 -15
  58. package/dist/wallet/reset.js +39 -27
  59. package/dist/wallet/runtime.d.ts +12 -1
  60. package/dist/wallet/runtime.js +53 -11
  61. package/dist/wallet/state/provider.d.ts +1 -0
  62. package/dist/wallet/state/provider.js +119 -3
  63. package/dist/wallet/state/seed-index.d.ts +43 -0
  64. package/dist/wallet/state/seed-index.js +151 -0
  65. package/dist/wallet/tx/anchor.d.ts +22 -0
  66. package/dist/wallet/tx/anchor.js +215 -8
  67. package/dist/wallet/tx/index.d.ts +1 -1
  68. package/dist/wallet/tx/index.js +1 -1
  69. package/package.json +1 -1
@@ -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,14 @@ 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 walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
230
+ const legacyRuntimeRoot = join(resolveManagedServicePaths(options.dataDir ?? "", walletRootId).runtimeRoot, walletRootId);
229
231
  if (status.serviceApiVersion !== MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE) {
230
232
  throw new Error("managed_bitcoind_service_version_mismatch");
231
233
  }
232
- if (status.walletRootId !== (options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID)) {
233
- throw new Error("managed_bitcoind_wallet_root_mismatch");
234
- }
235
234
  if (status.chain !== options.chain
236
235
  || status.dataDir !== (options.dataDir ?? "")
237
- || status.runtimeRoot !== runtimeRoot) {
236
+ || (status.runtimeRoot !== runtimeRoot && status.runtimeRoot !== legacyRuntimeRoot)) {
238
237
  throw new Error("managed_bitcoind_runtime_mismatch");
239
238
  }
240
239
  }
@@ -275,6 +274,8 @@ function createBitcoindServiceStatus(options) {
275
274
  rpc: options.rpc,
276
275
  zmq: options.zmq,
277
276
  p2pPort: options.p2pPort,
277
+ getblockArchiveEndHeight: options.getblockArchiveEndHeight,
278
+ getblockArchiveSha256: options.getblockArchiveSha256,
278
279
  walletReplica: options.walletReplica,
279
280
  startedAtUnixMs: options.startedAtUnixMs,
280
281
  heartbeatAtUnixMs: options.heartbeatAtUnixMs,
@@ -287,9 +288,7 @@ function mapManagedBitcoindValidationError(error) {
287
288
  compatibility: error instanceof Error
288
289
  ? error.message === "managed_bitcoind_service_version_mismatch"
289
290
  ? "service-version-mismatch"
290
- : error.message === "managed_bitcoind_wallet_root_mismatch"
291
- ? "wallet-root-mismatch"
292
- : "runtime-mismatch"
291
+ : "runtime-mismatch"
293
292
  : "protocol-error",
294
293
  status: null,
295
294
  error: error instanceof Error ? error.message : "managed_bitcoind_protocol_error",
@@ -366,6 +365,8 @@ async function resolveRuntimeConfig(statusPath, configPath, options) {
366
365
  zmqPort,
367
366
  p2pPort,
368
367
  dbcacheMiB: detectManagedBitcoindDbcacheMiB(),
368
+ getblockArchiveEndHeight: options.getblockArchiveEndHeight ?? null,
369
+ getblockArchiveSha256: options.getblockArchiveSha256 ?? null,
369
370
  };
370
371
  }
371
372
  async function writeBitcoinConf(filePath, options, runtimeConfig) {
@@ -407,6 +408,9 @@ function buildManagedServiceArgs(options, runtimeConfig) {
407
408
  if (options.chain === "regtest") {
408
409
  args.push("-chain=regtest");
409
410
  }
411
+ if (options.getblockArchivePath !== undefined && options.getblockArchivePath !== null) {
412
+ args.push(`-loadblock=${options.getblockArchivePath}`);
413
+ }
410
414
  return args;
411
415
  }
412
416
  export async function writeBitcoinConfForTesting(filePath, options, runtimeConfig) {
@@ -547,6 +551,8 @@ async function writeBitcoindStatus(paths, status) {
547
551
  rpc: status.rpc,
548
552
  zmqPort: status.zmq.port,
549
553
  p2pPort: status.p2pPort,
554
+ getblockArchiveEndHeight: status.getblockArchiveEndHeight,
555
+ getblockArchiveSha256: status.getblockArchiveSha256,
550
556
  });
551
557
  }
552
558
  async function clearManagedBitcoindRuntimeArtifacts(paths) {
@@ -590,6 +596,11 @@ export async function stopManagedBitcoindServiceWithLockHeld(options) {
590
596
  }
591
597
  export async function withClaimedUninitializedManagedRuntime(options, callback) {
592
598
  const targetWalletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
599
+ const targetPaths = resolveManagedServicePaths(options.dataDir, targetWalletRootId);
600
+ const uninitializedPaths = resolveManagedServicePaths(options.dataDir, UNINITIALIZED_WALLET_ROOT_ID);
601
+ if (targetPaths.walletRuntimeRoot === uninitializedPaths.walletRuntimeRoot) {
602
+ return callback();
603
+ }
593
604
  if (targetWalletRootId === UNINITIALIZED_WALLET_ROOT_ID) {
594
605
  return callback();
595
606
  }
@@ -598,7 +609,6 @@ export async function withClaimedUninitializedManagedRuntime(options, callback)
598
609
  return callback();
599
610
  }
600
611
  claimedUninitializedRuntimeKeys.add(claimKey);
601
- const uninitializedPaths = resolveManagedServicePaths(options.dataDir, UNINITIALIZED_WALLET_ROOT_ID);
602
612
  const lockTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
603
613
  const bitcoindLock = await acquireFileLockWithRetry(uninitializedPaths.bitcoindLockPath, {
604
614
  purpose: "managed-bitcoind-claim-uninitialized",
@@ -638,12 +648,15 @@ export async function withClaimedUninitializedManagedRuntime(options, callback)
638
648
  async function refreshManagedBitcoindStatus(status, paths, options) {
639
649
  const nowUnixMs = Date.now();
640
650
  const rpc = createRpcClient(status.rpc);
651
+ const targetWalletRootId = options.walletRootId ?? status.walletRootId;
641
652
  try {
642
653
  await waitForRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS);
643
654
  await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint);
644
- const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, status.walletRootId, status.dataDir);
655
+ const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, targetWalletRootId, status.dataDir);
645
656
  const nextStatus = {
646
657
  ...status,
658
+ walletRootId: targetWalletRootId,
659
+ runtimeRoot: paths.walletRuntimeRoot,
647
660
  state: "ready",
648
661
  processId: await isProcessAlive(status.processId) ? status.processId : null,
649
662
  walletReplica,
@@ -657,6 +670,8 @@ async function refreshManagedBitcoindStatus(status, paths, options) {
657
670
  catch (error) {
658
671
  const nextStatus = {
659
672
  ...status,
673
+ walletRootId: targetWalletRootId,
674
+ runtimeRoot: paths.walletRuntimeRoot,
660
675
  state: "failed",
661
676
  processId: await isProcessAlive(status.processId) ? status.processId : null,
662
677
  heartbeatAtUnixMs: nowUnixMs,
@@ -677,6 +692,8 @@ function createNodeHandle(status, paths, options) {
677
692
  expectedChain: currentStatus.chain,
678
693
  startHeight: currentStatus.startHeight,
679
694
  dataDir: currentStatus.dataDir,
695
+ getblockArchiveEndHeight: currentStatus.getblockArchiveEndHeight ?? null,
696
+ getblockArchiveSha256: currentStatus.getblockArchiveSha256 ?? null,
680
697
  walletRootId: currentStatus.walletRootId,
681
698
  runtimeRoot: paths.walletRuntimeRoot,
682
699
  async validate() {
@@ -684,6 +701,9 @@ function createNodeHandle(status, paths, options) {
684
701
  },
685
702
  async refreshServiceStatus() {
686
703
  currentStatus = await refreshManagedBitcoindStatus(currentStatus, paths, options);
704
+ this.getblockArchiveEndHeight = currentStatus.getblockArchiveEndHeight ?? null;
705
+ this.getblockArchiveSha256 = currentStatus.getblockArchiveSha256 ?? null;
706
+ this.walletRootId = currentStatus.walletRootId;
687
707
  return currentStatus;
688
708
  },
689
709
  async stop() {
@@ -786,57 +806,76 @@ export async function attachOrStartManagedBitcoindService(options) {
786
806
  await verifyBitcoindVersion(bitcoindPath);
787
807
  const binaryVersion = SUPPORTED_BITCOIND_VERSION;
788
808
  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,
809
+ const startManagedProcess = async (startOptions) => {
810
+ const runtimeConfig = await resolveRuntimeConfig(paths.bitcoindStatusPath, paths.bitcoindRuntimeConfigPath, startOptions);
811
+ await writeBitcoinConf(paths.bitcoinConfPath, startOptions, runtimeConfig);
812
+ const rpcConfig = runtimeConfig.rpc;
813
+ const zmqConfig = {
814
+ endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
815
+ topic: "hashblock",
816
+ port: runtimeConfig.zmqPort,
817
+ pollIntervalMs: startOptions.pollIntervalMs ?? 15_000,
818
+ };
819
+ const child = spawn(bitcoindPath, buildManagedServiceArgs(startOptions, runtimeConfig), {
820
+ detached: true,
821
+ stdio: "ignore",
822
+ });
823
+ child.unref();
824
+ const rpc = createRpcClient(rpcConfig);
825
+ try {
826
+ await waitForRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
827
+ await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint);
828
+ }
829
+ catch (error) {
830
+ if (child.pid !== undefined) {
831
+ try {
832
+ process.kill(child.pid, "SIGTERM");
833
+ }
834
+ catch {
835
+ // ignore kill failures during startup cleanup
836
+ }
837
+ }
838
+ throw error;
839
+ }
840
+ const nowUnixMs = Date.now();
841
+ const walletRootId = startOptions.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
842
+ const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, walletRootId, startOptions.dataDir ?? "");
843
+ return createBitcoindServiceStatus({
844
+ binaryVersion,
845
+ serviceInstanceId: randomBytes(16).toString("hex"),
846
+ state: "ready",
847
+ processId: child.pid ?? null,
848
+ walletRootId,
849
+ chain: startOptions.chain,
850
+ dataDir: startOptions.dataDir ?? "",
851
+ runtimeRoot: paths.walletRuntimeRoot,
852
+ startHeight: startOptions.startHeight,
853
+ rpc: rpcConfig,
854
+ zmq: zmqConfig,
855
+ p2pPort: runtimeConfig.p2pPort,
856
+ getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
857
+ getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
858
+ walletReplica,
859
+ startedAtUnixMs: nowUnixMs,
860
+ heartbeatAtUnixMs: nowUnixMs,
861
+ lastError: walletReplica.message ?? null,
862
+ });
797
863
  };
798
- const child = spawn(bitcoindPath, buildManagedServiceArgs(resolvedOptions, runtimeConfig), {
799
- detached: true,
800
- stdio: "ignore",
801
- });
802
- child.unref();
803
- const rpc = createRpcClient(rpcConfig);
864
+ let status;
804
865
  try {
805
- await waitForRpcReady(rpc, rpcConfig.cookieFile, resolvedOptions.chain, startupTimeoutMs);
806
- await validateNodeConfigForTesting(rpc, resolvedOptions.chain, zmqConfig.endpoint);
866
+ status = await startManagedProcess(resolvedOptions);
807
867
  }
808
868
  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
- }
869
+ if (resolvedOptions.getblockArchivePath === undefined || resolvedOptions.getblockArchivePath === null) {
870
+ throw error;
816
871
  }
817
- throw error;
872
+ status = await startManagedProcess({
873
+ ...resolvedOptions,
874
+ getblockArchivePath: null,
875
+ getblockArchiveEndHeight: null,
876
+ getblockArchiveSha256: null,
877
+ });
818
878
  }
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
879
  await writeBitcoindStatus(paths, status);
841
880
  return createNodeHandle(status, paths, resolvedOptions);
842
881
  }
@@ -3,7 +3,7 @@ export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopInde
3
3
  export { normalizeRpcBlock } from "./normalize.js";
4
4
  export { BitcoinRpcClient } from "./rpc.js";
5
5
  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";
6
+ export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
7
  export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
8
8
  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
9
  export { WritingQuoteRotator, loadWritingQuotesForTesting, shuffleIndicesForTesting, } from "./quotes.js";
@@ -3,7 +3,7 @@ export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopInde
3
3
  export { normalizeRpcBlock } from "./normalize.js";
4
4
  export { BitcoinRpcClient } from "./rpc.js";
5
5
  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";
6
+ export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
7
  export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
8
8
  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
9
  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") {
@@ -5,9 +5,7 @@ import { writeLine } from "../io.js";
5
5
  import { inspectWalletLocalState } from "../../wallet/read/index.js";
6
6
  import { createErrorEnvelope, createSuccessEnvelope, describeCanonicalCommand, normalizeListPage, writeJsonValue, } from "../output.js";
7
7
  import { buildHooksStatusJson, buildMineLogJson, buildMineStatusJson } from "../read-json.js";
8
- import { resolveWalletRuntimePathsForTesting } from "../../wallet/runtime.js";
9
- async function readRotationIndices() {
10
- const paths = resolveWalletRuntimePathsForTesting();
8
+ async function readRotationIndices(paths) {
11
9
  const rotation = [];
12
10
  for (let index = 1; index <= 4; index += 1) {
13
11
  try {
@@ -26,10 +24,12 @@ export async function runMiningReadCommand(parsed, context) {
26
24
  try {
27
25
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
28
26
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
27
+ const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
29
28
  await context.ensureDirectory(dirname(dbPath));
30
29
  if (parsed.command === "hooks-mining-status") {
31
30
  const localState = await inspectWalletLocalState({
32
31
  secretProvider: context.walletSecretProvider,
32
+ paths: runtimePaths,
33
33
  });
34
34
  const view = await context.inspectMiningControlPlane({
35
35
  provider: context.walletSecretProvider,
@@ -52,6 +52,7 @@ export async function runMiningReadCommand(parsed, context) {
52
52
  openedAtUnixMs: null,
53
53
  },
54
54
  verify: parsed.verify,
55
+ paths: runtimePaths,
55
56
  });
56
57
  if (parsed.outputMode === "json") {
57
58
  const result = buildHooksStatusJson(view);
@@ -79,7 +80,7 @@ export async function runMiningReadCommand(parsed, context) {
79
80
  const events = normalized.items.slice().reverse();
80
81
  if (events.length === 0) {
81
82
  if (parsed.outputMode === "json") {
82
- const result = buildMineLogJson(events, normalized.page, await readRotationIndices());
83
+ const result = buildMineLogJson(events, normalized.page, await readRotationIndices(runtimePaths));
83
84
  writeJsonValue(context.stdout, createSuccessEnvelope("cogcoin/mine-log/v1", describeCanonicalCommand(parsed), result.data, {
84
85
  warnings: result.warnings,
85
86
  explanations: result.explanations,
@@ -91,7 +92,7 @@ export async function runMiningReadCommand(parsed, context) {
91
92
  return 0;
92
93
  }
93
94
  if (parsed.outputMode === "json") {
94
- const result = buildMineLogJson(events, normalized.page, await readRotationIndices());
95
+ const result = buildMineLogJson(events, normalized.page, await readRotationIndices(runtimePaths));
95
96
  writeJsonValue(context.stdout, createSuccessEnvelope("cogcoin/mine-log/v1", describeCanonicalCommand(parsed), result.data, {
96
97
  warnings: result.warnings,
97
98
  explanations: result.explanations,
@@ -131,6 +132,7 @@ export async function runMiningReadCommand(parsed, context) {
131
132
  dataDir,
132
133
  databasePath: dbPath,
133
134
  secretProvider: context.walletSecretProvider,
135
+ paths: runtimePaths,
134
136
  });
135
137
  try {
136
138
  const mining = readContext.mining ?? await context.inspectMiningControlPlane({
@@ -140,6 +142,7 @@ export async function runMiningReadCommand(parsed, context) {
140
142
  nodeStatus: readContext.nodeStatus,
141
143
  nodeHealth: readContext.nodeHealth,
142
144
  indexer: readContext.indexer,
145
+ paths: runtimePaths,
143
146
  });
144
147
  if (parsed.outputMode === "json") {
145
148
  const result = buildMineStatusJson(mining);
@@ -15,6 +15,7 @@ export async function runMiningRuntimeCommand(parsed, context) {
15
15
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
16
16
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
17
17
  const provider = context.walletSecretProvider;
18
+ const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
18
19
  await context.ensureDirectory(dirname(dbPath));
19
20
  if (parsed.command === "mine") {
20
21
  const abortController = new AbortController();
@@ -33,6 +34,7 @@ export async function runMiningRuntimeCommand(parsed, context) {
33
34
  stdout: context.stdout,
34
35
  stderr: context.stderr,
35
36
  progressOutput: parsed.progressOutput,
37
+ paths: runtimePaths,
36
38
  });
37
39
  }
38
40
  finally {
@@ -47,6 +49,7 @@ export async function runMiningRuntimeCommand(parsed, context) {
47
49
  databasePath: dbPath,
48
50
  provider,
49
51
  prompter: createCommandPrompter(parsed, context),
52
+ paths: runtimePaths,
50
53
  });
51
54
  if (parsed.outputMode === "preview-json") {
52
55
  writeJsonValue(context.stdout, createPreviewSuccessEnvelope(resolvePreviewJsonSchema(parsed), describeCanonicalCommand(parsed), result.started ? "started" : "already-active", buildMineStartPreviewData(result)));
@@ -74,6 +77,7 @@ export async function runMiningRuntimeCommand(parsed, context) {
74
77
  dataDir,
75
78
  databasePath: dbPath,
76
79
  provider,
80
+ paths: runtimePaths,
77
81
  });
78
82
  const nextSteps = getMineStopNextSteps();
79
83
  if (parsed.outputMode === "preview-json") {