@cogcoin/client 0.5.4 → 0.5.6

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 (74) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.d.ts +2 -0
  3. package/dist/app-paths.js +4 -0
  4. package/dist/art/wallet.txt +9 -9
  5. package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
  6. package/dist/bitcoind/bootstrap/chainstate.js +4 -1
  7. package/dist/bitcoind/bootstrap/chunk-manifest.d.ts +14 -0
  8. package/dist/bitcoind/bootstrap/chunk-manifest.js +85 -0
  9. package/dist/bitcoind/bootstrap/chunk-recovery.d.ts +4 -0
  10. package/dist/bitcoind/bootstrap/chunk-recovery.js +122 -0
  11. package/dist/bitcoind/bootstrap/constants.d.ts +3 -1
  12. package/dist/bitcoind/bootstrap/constants.js +3 -1
  13. package/dist/bitcoind/bootstrap/controller.d.ts +10 -2
  14. package/dist/bitcoind/bootstrap/controller.js +56 -12
  15. package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.d.ts +2 -0
  16. package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.js +2309 -0
  17. package/dist/bitcoind/bootstrap/download.js +177 -83
  18. package/dist/bitcoind/bootstrap/headers.d.ts +16 -2
  19. package/dist/bitcoind/bootstrap/headers.js +124 -14
  20. package/dist/bitcoind/bootstrap/state.d.ts +11 -1
  21. package/dist/bitcoind/bootstrap/state.js +50 -23
  22. package/dist/bitcoind/bootstrap/types.d.ts +12 -1
  23. package/dist/bitcoind/client/factory.js +11 -2
  24. package/dist/bitcoind/client/internal-types.d.ts +1 -0
  25. package/dist/bitcoind/client/managed-client.d.ts +1 -1
  26. package/dist/bitcoind/client/managed-client.js +29 -15
  27. package/dist/bitcoind/client/sync-engine.js +88 -16
  28. package/dist/bitcoind/errors.js +9 -0
  29. package/dist/bitcoind/indexer-daemon.d.ts +7 -0
  30. package/dist/bitcoind/indexer-daemon.js +31 -22
  31. package/dist/bitcoind/processing-start-height.d.ts +7 -0
  32. package/dist/bitcoind/processing-start-height.js +9 -0
  33. package/dist/bitcoind/progress/controller.js +1 -0
  34. package/dist/bitcoind/progress/formatting.js +4 -1
  35. package/dist/bitcoind/retryable-rpc.d.ts +11 -0
  36. package/dist/bitcoind/retryable-rpc.js +30 -0
  37. package/dist/bitcoind/service.d.ts +16 -1
  38. package/dist/bitcoind/service.js +228 -115
  39. package/dist/bitcoind/testing.d.ts +1 -1
  40. package/dist/bitcoind/testing.js +1 -1
  41. package/dist/bitcoind/types.d.ts +10 -0
  42. package/dist/cli/commands/follow.js +9 -0
  43. package/dist/cli/commands/service-runtime.js +150 -134
  44. package/dist/cli/commands/sync.js +9 -0
  45. package/dist/cli/commands/wallet-admin.js +77 -21
  46. package/dist/cli/context.js +4 -2
  47. package/dist/cli/mutation-json.js +2 -0
  48. package/dist/cli/output.js +3 -1
  49. package/dist/cli/parse.d.ts +1 -1
  50. package/dist/cli/parse.js +6 -0
  51. package/dist/cli/preview-json.js +2 -0
  52. package/dist/cli/runner.js +1 -0
  53. package/dist/cli/types.d.ts +6 -3
  54. package/dist/cli/types.js +1 -1
  55. package/dist/cli/wallet-format.js +134 -14
  56. package/dist/wallet/lifecycle.d.ts +6 -0
  57. package/dist/wallet/lifecycle.js +168 -37
  58. package/dist/wallet/read/context.js +10 -4
  59. package/dist/wallet/reset.d.ts +61 -2
  60. package/dist/wallet/reset.js +208 -63
  61. package/dist/wallet/root-resolution.d.ts +20 -0
  62. package/dist/wallet/root-resolution.js +37 -0
  63. package/dist/wallet/runtime.d.ts +3 -0
  64. package/dist/wallet/runtime.js +3 -0
  65. package/dist/wallet/state/crypto.d.ts +3 -0
  66. package/dist/wallet/state/crypto.js +3 -0
  67. package/dist/wallet/state/pending-init.d.ts +24 -0
  68. package/dist/wallet/state/pending-init.js +59 -0
  69. package/dist/wallet/state/provider.d.ts +1 -0
  70. package/dist/wallet/state/provider.js +7 -1
  71. package/dist/wallet/state/storage.d.ts +7 -1
  72. package/dist/wallet/state/storage.js +39 -0
  73. package/dist/wallet/types.d.ts +9 -0
  74. package/package.json +4 -2
@@ -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
+ }
@@ -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<{
@@ -29,9 +31,22 @@ export interface ManagedBitcoindServiceStopResult {
29
31
  status: "stopped" | "not-running";
30
32
  walletRootId: string;
31
33
  }
34
+ export declare function writeBitcoinConfForTesting(filePath: string, options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): Promise<void>;
35
+ export declare function buildManagedServiceArgsForTesting(options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): string[];
32
36
  export declare function createManagedWalletReplica(rpc: ManagedWalletReplicaRpc, walletRootId: string, options?: {
33
37
  managedWalletPassphrase?: string;
34
38
  }): Promise<ManagedCoreWalletReplicaStatus>;
39
+ export declare function stopManagedBitcoindServiceWithLockHeld(options: {
40
+ dataDir: string;
41
+ walletRootId?: string;
42
+ shutdownTimeoutMs?: number;
43
+ paths?: ReturnType<typeof resolveManagedServicePaths>;
44
+ }): Promise<ManagedBitcoindServiceStopResult>;
45
+ export declare function withClaimedUninitializedManagedRuntime<T>(options: {
46
+ dataDir: string;
47
+ walletRootId?: string;
48
+ shutdownTimeoutMs?: number;
49
+ }, callback: () => Promise<T>): Promise<T>;
35
50
  export declare function probeManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindServiceProbeResult>;
36
51
  export declare function attachOrStartManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindNodeHandle>;
37
52
  export declare function stopManagedBitcoindService(options: {
@@ -2,12 +2,14 @@ import { randomBytes } from "node:crypto";
2
2
  import { execFile, spawn } from "node:child_process";
3
3
  import { access, constants, mkdir, readFile, readdir, rm } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
+ import { totalmem } from "node:os";
5
6
  import { promisify } from "node:util";
6
7
  import net from "node:net";
7
8
  import { getBitcoindPath } from "@cogcoin/bitcoin";
8
9
  import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
9
10
  import { writeFileAtomic } from "../wallet/fs/atomic.js";
10
11
  import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
12
+ import { stopIndexerDaemonServiceWithLockHeld } from "./indexer-daemon.js";
11
13
  import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
12
14
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
13
15
  import { MANAGED_BITCOIND_SERVICE_API_VERSION as MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE } from "./types.js";
@@ -16,6 +18,32 @@ const LOCAL_HOST = "127.0.0.1";
16
18
  const SUPPORTED_BITCOIND_VERSION = "30.2.0";
17
19
  const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
18
20
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 15_000;
21
+ const DEFAULT_DBCACHE_MIB = 450;
22
+ const claimedUninitializedRuntimeKeys = new Set();
23
+ const GIB = 1024 ** 3;
24
+ export function resolveManagedBitcoindDbcacheMiB(totalRamBytes) {
25
+ if (!Number.isFinite(totalRamBytes) || totalRamBytes <= 0) {
26
+ return DEFAULT_DBCACHE_MIB;
27
+ }
28
+ if (totalRamBytes < 8 * GIB) {
29
+ return 450;
30
+ }
31
+ if (totalRamBytes < 16 * GIB) {
32
+ return 768;
33
+ }
34
+ if (totalRamBytes < 32 * GIB) {
35
+ return 1024;
36
+ }
37
+ return 2048;
38
+ }
39
+ function detectManagedBitcoindDbcacheMiB() {
40
+ try {
41
+ return resolveManagedBitcoindDbcacheMiB(totalmem());
42
+ }
43
+ catch {
44
+ return DEFAULT_DBCACHE_MIB;
45
+ }
46
+ }
19
47
  function sleep(ms) {
20
48
  return new Promise((resolve) => {
21
49
  setTimeout(resolve, ms);
@@ -31,6 +59,20 @@ async function waitForProcessExit(pid, timeoutMs, errorCode) {
31
59
  }
32
60
  throw new Error(errorCode);
33
61
  }
62
+ async function acquireFileLockWithRetry(lockPath, metadata, timeoutMs) {
63
+ const deadline = Date.now() + timeoutMs;
64
+ while (true) {
65
+ try {
66
+ return await acquireFileLock(lockPath, metadata);
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof FileLockBusyError) || Date.now() >= deadline) {
70
+ throw error;
71
+ }
72
+ await sleep(250);
73
+ }
74
+ }
75
+ }
34
76
  function getWalletReplicaName(walletRootId) {
35
77
  return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
36
78
  }
@@ -323,6 +365,7 @@ async function resolveRuntimeConfig(statusPath, configPath, options) {
323
365
  },
324
366
  zmqPort,
325
367
  p2pPort,
368
+ dbcacheMiB: detectManagedBitcoindDbcacheMiB(),
326
369
  };
327
370
  }
328
371
  async function writeBitcoinConf(filePath, options, runtimeConfig) {
@@ -334,6 +377,7 @@ async function writeBitcoinConf(filePath, options, runtimeConfig) {
334
377
  "prune=0",
335
378
  "dnsseed=1",
336
379
  "listen=0",
380
+ `dbcache=${runtimeConfig.dbcacheMiB}`,
337
381
  `rpcbind=${LOCAL_HOST}`,
338
382
  `rpcallowip=${LOCAL_HOST}`,
339
383
  `rpcport=${runtimeConfig.rpc.port}`,
@@ -358,12 +402,19 @@ function buildManagedServiceArgs(options, runtimeConfig) {
358
402
  "-prune=0",
359
403
  "-dnsseed=1",
360
404
  "-listen=0",
405
+ `-dbcache=${runtimeConfig.dbcacheMiB}`,
361
406
  ];
362
407
  if (options.chain === "regtest") {
363
408
  args.push("-chain=regtest");
364
409
  }
365
410
  return args;
366
411
  }
412
+ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfig) {
413
+ await writeBitcoinConf(filePath, options, runtimeConfig);
414
+ }
415
+ export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
416
+ return buildManagedServiceArgs(options, runtimeConfig);
417
+ }
367
418
  function isMissingWalletError(message) {
368
419
  return message.includes("bitcoind_rpc_loadwallet_-18_")
369
420
  || message.includes("Path does not exist")
@@ -504,6 +555,86 @@ async function clearManagedBitcoindRuntimeArtifacts(paths) {
504
555
  await rm(paths.bitcoindReadyPath, { force: true }).catch(() => undefined);
505
556
  await rm(paths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
506
557
  }
558
+ export async function stopManagedBitcoindServiceWithLockHeld(options) {
559
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
560
+ const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
561
+ const status = await readJsonFile(paths.bitcoindStatusPath);
562
+ const processId = status?.processId ?? null;
563
+ if (status === null || processId === null || !await isProcessAlive(processId)) {
564
+ await clearManagedBitcoindRuntimeArtifacts(paths);
565
+ return {
566
+ status: "not-running",
567
+ walletRootId,
568
+ };
569
+ }
570
+ const rpc = createRpcClient(status.rpc);
571
+ try {
572
+ await rpc.stop();
573
+ }
574
+ catch {
575
+ try {
576
+ process.kill(processId, "SIGTERM");
577
+ }
578
+ catch (error) {
579
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
580
+ throw error;
581
+ }
582
+ }
583
+ }
584
+ await waitForProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, "managed_bitcoind_service_stop_timeout");
585
+ await clearManagedBitcoindRuntimeArtifacts(paths);
586
+ return {
587
+ status: "stopped",
588
+ walletRootId,
589
+ };
590
+ }
591
+ export async function withClaimedUninitializedManagedRuntime(options, callback) {
592
+ const targetWalletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
593
+ if (targetWalletRootId === UNINITIALIZED_WALLET_ROOT_ID) {
594
+ return callback();
595
+ }
596
+ const claimKey = `${options.dataDir}\n${targetWalletRootId}`;
597
+ if (claimedUninitializedRuntimeKeys.has(claimKey)) {
598
+ return callback();
599
+ }
600
+ claimedUninitializedRuntimeKeys.add(claimKey);
601
+ const uninitializedPaths = resolveManagedServicePaths(options.dataDir, UNINITIALIZED_WALLET_ROOT_ID);
602
+ const lockTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
603
+ const bitcoindLock = await acquireFileLockWithRetry(uninitializedPaths.bitcoindLockPath, {
604
+ purpose: "managed-bitcoind-claim-uninitialized",
605
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
606
+ dataDir: options.dataDir,
607
+ }, lockTimeoutMs);
608
+ try {
609
+ const indexerLock = await acquireFileLockWithRetry(uninitializedPaths.indexerDaemonLockPath, {
610
+ purpose: "managed-indexer-claim-uninitialized",
611
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
612
+ dataDir: options.dataDir,
613
+ }, lockTimeoutMs);
614
+ try {
615
+ await stopIndexerDaemonServiceWithLockHeld({
616
+ dataDir: options.dataDir,
617
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
618
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
619
+ paths: uninitializedPaths,
620
+ });
621
+ await stopManagedBitcoindServiceWithLockHeld({
622
+ dataDir: options.dataDir,
623
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
624
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
625
+ paths: uninitializedPaths,
626
+ });
627
+ return await callback();
628
+ }
629
+ finally {
630
+ await indexerLock.release();
631
+ }
632
+ }
633
+ finally {
634
+ claimedUninitializedRuntimeKeys.delete(claimKey);
635
+ await bitcoindLock.release();
636
+ }
637
+ }
507
638
  async function refreshManagedBitcoindStatus(status, paths, options) {
508
639
  const nowUnixMs = Date.now();
509
640
  const rpc = createRpcClient(status.rpc);
@@ -617,103 +748,109 @@ export async function attachOrStartManagedBitcoindService(options) {
617
748
  dataDir: options.dataDir,
618
749
  walletRootId: options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
619
750
  };
620
- const existingProbe = await probeManagedBitcoindService(resolvedOptions);
621
- if (existingProbe.compatibility === "compatible") {
622
- const existing = await tryAttachExistingManagedBitcoindService(resolvedOptions);
623
- if (existing !== null) {
624
- return existing;
625
- }
626
- }
627
- if (existingProbe.compatibility !== "unreachable") {
628
- throw new Error(existingProbe.error ?? "managed_bitcoind_protocol_error");
629
- }
630
- const paths = resolveManagedServicePaths(resolvedOptions.dataDir ?? "", resolvedOptions.walletRootId);
631
751
  const startupTimeoutMs = resolvedOptions.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
632
- try {
633
- const lock = await acquireFileLock(paths.bitcoindLockPath, {
634
- purpose: "managed-bitcoind-start",
635
- walletRootId: resolvedOptions.walletRootId,
636
- dataDir: resolvedOptions.dataDir,
637
- });
638
- try {
639
- const liveProbe = await probeManagedBitcoindService(resolvedOptions);
640
- if (liveProbe.compatibility === "compatible") {
641
- const reattached = await tryAttachExistingManagedBitcoindService(resolvedOptions);
642
- if (reattached !== null) {
643
- return reattached;
644
- }
645
- }
646
- if (liveProbe.compatibility !== "unreachable") {
647
- throw new Error(liveProbe.error ?? "managed_bitcoind_protocol_error");
752
+ return withClaimedUninitializedManagedRuntime({
753
+ dataDir: resolvedOptions.dataDir ?? "",
754
+ walletRootId: resolvedOptions.walletRootId,
755
+ shutdownTimeoutMs: resolvedOptions.shutdownTimeoutMs,
756
+ }, async () => {
757
+ const existingProbe = await probeManagedBitcoindService(resolvedOptions);
758
+ if (existingProbe.compatibility === "compatible") {
759
+ const existing = await tryAttachExistingManagedBitcoindService(resolvedOptions);
760
+ if (existing !== null) {
761
+ return existing;
648
762
  }
649
- const bitcoindPath = await getBitcoindPath();
650
- await verifyBitcoindVersion(bitcoindPath);
651
- const binaryVersion = SUPPORTED_BITCOIND_VERSION;
652
- await mkdir(resolvedOptions.dataDir ?? "", { recursive: true });
653
- const runtimeConfig = await resolveRuntimeConfig(paths.bitcoindStatusPath, paths.bitcoindRuntimeConfigPath, resolvedOptions);
654
- await writeBitcoinConf(paths.bitcoinConfPath, resolvedOptions, runtimeConfig);
655
- const rpcConfig = runtimeConfig.rpc;
656
- const zmqConfig = {
657
- endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
658
- topic: "hashblock",
659
- port: runtimeConfig.zmqPort,
660
- pollIntervalMs: resolvedOptions.pollIntervalMs ?? 15_000,
661
- };
662
- const child = spawn(bitcoindPath, buildManagedServiceArgs(resolvedOptions, runtimeConfig), {
663
- detached: true,
664
- stdio: "ignore",
763
+ }
764
+ if (existingProbe.compatibility !== "unreachable") {
765
+ throw new Error(existingProbe.error ?? "managed_bitcoind_protocol_error");
766
+ }
767
+ const paths = resolveManagedServicePaths(resolvedOptions.dataDir ?? "", resolvedOptions.walletRootId);
768
+ try {
769
+ const lock = await acquireFileLock(paths.bitcoindLockPath, {
770
+ purpose: "managed-bitcoind-start",
771
+ walletRootId: resolvedOptions.walletRootId,
772
+ dataDir: resolvedOptions.dataDir,
665
773
  });
666
- child.unref();
667
- const rpc = createRpcClient(rpcConfig);
668
774
  try {
669
- await waitForRpcReady(rpc, rpcConfig.cookieFile, resolvedOptions.chain, startupTimeoutMs);
670
- await validateNodeConfigForTesting(rpc, resolvedOptions.chain, zmqConfig.endpoint);
671
- }
672
- catch (error) {
673
- if (child.pid !== undefined) {
674
- try {
675
- process.kill(child.pid, "SIGTERM");
775
+ const liveProbe = await probeManagedBitcoindService(resolvedOptions);
776
+ if (liveProbe.compatibility === "compatible") {
777
+ const reattached = await tryAttachExistingManagedBitcoindService(resolvedOptions);
778
+ if (reattached !== null) {
779
+ return reattached;
676
780
  }
677
- catch {
678
- // ignore kill failures during startup cleanup
781
+ }
782
+ if (liveProbe.compatibility !== "unreachable") {
783
+ throw new Error(liveProbe.error ?? "managed_bitcoind_protocol_error");
784
+ }
785
+ const bitcoindPath = await getBitcoindPath();
786
+ await verifyBitcoindVersion(bitcoindPath);
787
+ const binaryVersion = SUPPORTED_BITCOIND_VERSION;
788
+ 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,
797
+ };
798
+ const child = spawn(bitcoindPath, buildManagedServiceArgs(resolvedOptions, runtimeConfig), {
799
+ detached: true,
800
+ stdio: "ignore",
801
+ });
802
+ child.unref();
803
+ const rpc = createRpcClient(rpcConfig);
804
+ try {
805
+ await waitForRpcReady(rpc, rpcConfig.cookieFile, resolvedOptions.chain, startupTimeoutMs);
806
+ await validateNodeConfigForTesting(rpc, resolvedOptions.chain, zmqConfig.endpoint);
807
+ }
808
+ 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
+ }
679
816
  }
817
+ throw error;
680
818
  }
681
- throw error;
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
+ await writeBitcoindStatus(paths, status);
841
+ return createNodeHandle(status, paths, resolvedOptions);
842
+ }
843
+ finally {
844
+ await lock.release();
682
845
  }
683
- const nowUnixMs = Date.now();
684
- const walletRootId = resolvedOptions.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
685
- const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, walletRootId, resolvedOptions.dataDir ?? "");
686
- const status = createBitcoindServiceStatus({
687
- binaryVersion,
688
- serviceInstanceId: randomBytes(16).toString("hex"),
689
- state: "ready",
690
- processId: child.pid ?? null,
691
- walletRootId,
692
- chain: resolvedOptions.chain,
693
- dataDir: resolvedOptions.dataDir ?? "",
694
- runtimeRoot: paths.walletRuntimeRoot,
695
- startHeight: resolvedOptions.startHeight,
696
- rpc: rpcConfig,
697
- zmq: zmqConfig,
698
- p2pPort: runtimeConfig.p2pPort,
699
- walletReplica,
700
- startedAtUnixMs: nowUnixMs,
701
- heartbeatAtUnixMs: nowUnixMs,
702
- lastError: walletReplica.message ?? null,
703
- });
704
- await writeBitcoindStatus(paths, status);
705
- return createNodeHandle(status, paths, resolvedOptions);
706
- }
707
- finally {
708
- await lock.release();
709
846
  }
710
- }
711
- catch (error) {
712
- if (error instanceof FileLockBusyError) {
713
- return waitForManagedBitcoindService(resolvedOptions, startupTimeoutMs);
847
+ catch (error) {
848
+ if (error instanceof FileLockBusyError) {
849
+ return waitForManagedBitcoindService(resolvedOptions, startupTimeoutMs);
850
+ }
851
+ throw error;
714
852
  }
715
- throw error;
716
- }
853
+ });
717
854
  }
718
855
  export async function stopManagedBitcoindService(options) {
719
856
  const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
@@ -724,35 +861,11 @@ export async function stopManagedBitcoindService(options) {
724
861
  dataDir: options.dataDir,
725
862
  });
726
863
  try {
727
- const status = await readJsonFile(paths.bitcoindStatusPath);
728
- const processId = status?.processId ?? null;
729
- if (status === null || processId === null || !await isProcessAlive(processId)) {
730
- await clearManagedBitcoindRuntimeArtifacts(paths);
731
- return {
732
- status: "not-running",
733
- walletRootId,
734
- };
735
- }
736
- const rpc = createRpcClient(status.rpc);
737
- try {
738
- await rpc.stop();
739
- }
740
- catch {
741
- try {
742
- process.kill(processId, "SIGTERM");
743
- }
744
- catch (error) {
745
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
746
- throw error;
747
- }
748
- }
749
- }
750
- await waitForProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, "managed_bitcoind_service_stop_timeout");
751
- await clearManagedBitcoindRuntimeArtifacts(paths);
752
- return {
753
- status: "stopped",
864
+ return stopManagedBitcoindServiceWithLockHeld({
865
+ ...options,
754
866
  walletRootId,
755
- };
867
+ paths,
868
+ });
756
869
  }
757
870
  finally {
758
871
  await lock.release();
@@ -2,7 +2,7 @@ export { openManagedBitcoindClientInternal } from "./client.js";
2
2
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
3
  export { normalizeRpcBlock } from "./normalize.js";
4
4
  export { BitcoinRpcClient } from "./rpc.js";
5
- export { attachOrStartManagedBitcoindService, readManagedBitcoindServiceStatusForTesting, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, } from "./service.js";
5
+ export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
6
6
  export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, resolveBootstrapPathsForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, 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";
@@ -2,7 +2,7 @@ export { openManagedBitcoindClientInternal } from "./client.js";
2
2
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
3
  export { normalizeRpcBlock } from "./normalize.js";
4
4
  export { BitcoinRpcClient } from "./rpc.js";
5
- export { attachOrStartManagedBitcoindService, readManagedBitcoindServiceStatusForTesting, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, } from "./service.js";
5
+ export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
6
6
  export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, resolveBootstrapPathsForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, 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";
@@ -8,6 +8,15 @@ export interface SnapshotMetadata {
8
8
  sha256: string;
9
9
  sizeBytes: number;
10
10
  }
11
+ export interface SnapshotChunkManifest {
12
+ formatVersion: number;
13
+ chunkSizeBytes: number;
14
+ snapshotFilename: string;
15
+ snapshotHeight: number;
16
+ snapshotSizeBytes: number;
17
+ snapshotSha256: string;
18
+ chunkSha256s: string[];
19
+ }
11
20
  export interface WritingQuote {
12
21
  quote: string;
13
22
  author: string;
@@ -53,6 +62,7 @@ export interface ManagedBitcoindRuntimeConfig {
53
62
  rpc: BitcoindRpcConfig;
54
63
  zmqPort: number;
55
64
  p2pPort: number;
65
+ dbcacheMiB: number;
56
66
  }
57
67
  export declare const MANAGED_BITCOIND_SERVICE_API_VERSION = "cogcoin/bitcoind-service/v1";
58
68
  export type ManagedBitcoindServiceState = "starting" | "ready" | "stopping" | "failed";
@@ -1,10 +1,18 @@
1
1
  import { dirname } from "node:path";
2
+ import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
2
3
  import { usesTtyProgress, writeLine } from "../io.js";
3
4
  import { classifyCliError } from "../output.js";
4
5
  import { createStopSignalWatcher } from "../signals.js";
5
6
  export async function runFollowCommand(parsed, context) {
6
7
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
7
8
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
9
+ const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
10
+ paths: context.resolveWalletRuntimePaths(),
11
+ provider: context.walletSecretProvider,
12
+ loadRawWalletStateEnvelope: context.loadRawWalletStateEnvelope,
13
+ loadUnlockSession: context.loadUnlockSession,
14
+ loadWalletExplicitLock: context.loadWalletExplicitLock,
15
+ });
8
16
  await context.ensureDirectory(dirname(dbPath));
9
17
  const store = await context.openSqliteStore({ filename: dbPath });
10
18
  let storeOwned = true;
@@ -13,6 +21,7 @@ export async function runFollowCommand(parsed, context) {
13
21
  store,
14
22
  databasePath: dbPath,
15
23
  dataDir,
24
+ walletRootId: walletRoot.walletRootId,
16
25
  progressOutput: parsed.progressOutput,
17
26
  });
18
27
  storeOwned = false;