@cogcoin/client 1.1.9 → 1.1.10

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 (31) hide show
  1. package/README.md +1 -1
  2. package/dist/bitcoind/managed-bitcoind-service-config.d.ts +13 -0
  3. package/dist/bitcoind/managed-bitcoind-service-config.js +165 -0
  4. package/dist/bitcoind/managed-bitcoind-service-lifecycle.d.ts +28 -0
  5. package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +290 -0
  6. package/dist/bitcoind/managed-bitcoind-service-process.d.ts +8 -0
  7. package/dist/bitcoind/managed-bitcoind-service-process.js +48 -0
  8. package/dist/bitcoind/managed-bitcoind-service-replica.d.ts +8 -0
  9. package/dist/bitcoind/managed-bitcoind-service-replica.js +142 -0
  10. package/dist/bitcoind/managed-bitcoind-service-status.d.ts +42 -0
  11. package/dist/bitcoind/managed-bitcoind-service-status.js +178 -0
  12. package/dist/bitcoind/managed-bitcoind-service-types.d.ts +36 -0
  13. package/dist/bitcoind/managed-bitcoind-service-types.js +1 -0
  14. package/dist/bitcoind/service.d.ts +7 -63
  15. package/dist/bitcoind/service.js +7 -797
  16. package/dist/wallet/mining/engine-types.d.ts +1 -0
  17. package/dist/wallet/mining/engine-types.js +9 -1
  18. package/dist/wallet/mining/publish.js +3 -6
  19. package/dist/wallet/mining/runner.js +30 -18
  20. package/dist/wallet/mining/visualizer.js +7 -6
  21. package/dist/wallet/read/context.d.ts +4 -10
  22. package/dist/wallet/read/context.js +4 -227
  23. package/dist/wallet/read/local-state.d.ts +28 -0
  24. package/dist/wallet/read/local-state.js +233 -0
  25. package/dist/wallet/read/managed-bitcoind.d.ts +30 -0
  26. package/dist/wallet/read/managed-bitcoind.js +138 -0
  27. package/dist/wallet/read/managed-indexer.d.ts +23 -0
  28. package/dist/wallet/read/managed-indexer.js +87 -0
  29. package/dist/wallet/read/managed-services.d.ts +6 -21
  30. package/dist/wallet/read/managed-services.js +23 -196
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.9` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
3
+ `@cogcoin/client@1.1.10` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
4
4
 
5
5
  Use Node 22 or newer.
6
6
 
@@ -0,0 +1,13 @@
1
+ import type { BitcoindRpcConfig, BitcoindZmqConfig, ManagedBitcoindRuntimeConfig } from "./types.js";
2
+ import type { ManagedBitcoindServiceOptions } from "./managed-bitcoind-service-types.js";
3
+ export declare const LOCAL_HOST = "127.0.0.1";
4
+ export declare const SUPPORTED_BITCOIND_VERSION = "30.2.0";
5
+ export declare function resolveManagedBitcoindDbcacheMiB(totalRamBytes: number): number;
6
+ export declare function detectManagedBitcoindDbcacheMiB(): number;
7
+ export declare function verifyManagedBitcoindVersion(bitcoindPath: string): Promise<void>;
8
+ export declare function getManagedBitcoindCookieFile(dataDir: string, chain: "main" | "regtest"): string;
9
+ export declare function resolveManagedBitcoindRuntimeConfig(statusPath: string, configPath: string, options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindRuntimeConfig>;
10
+ export declare function writeBitcoinConfForTesting(filePath: string, options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): Promise<void>;
11
+ export declare function buildManagedServiceArgsForTesting(options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): string[];
12
+ export declare function waitForManagedBitcoindCookie(cookieFile: string, timeoutMs: number, sleepImpl: (ms: number) => Promise<void>): Promise<void>;
13
+ export type { BitcoindRpcConfig, BitcoindZmqConfig, };
@@ -0,0 +1,165 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, constants, mkdir } from "node:fs/promises";
3
+ import { totalmem } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import net from "node:net";
7
+ import { writeFileAtomic } from "../wallet/fs/atomic.js";
8
+ import { readJsonFileIfPresent } from "./managed-runtime/status.js";
9
+ const execFileAsync = promisify(execFile);
10
+ export const LOCAL_HOST = "127.0.0.1";
11
+ export const SUPPORTED_BITCOIND_VERSION = "30.2.0";
12
+ const DEFAULT_DBCACHE_MIB = 450;
13
+ const GIB = 1024 ** 3;
14
+ export function resolveManagedBitcoindDbcacheMiB(totalRamBytes) {
15
+ if (!Number.isFinite(totalRamBytes) || totalRamBytes <= 0) {
16
+ return DEFAULT_DBCACHE_MIB;
17
+ }
18
+ if (totalRamBytes < 8 * GIB) {
19
+ return 450;
20
+ }
21
+ if (totalRamBytes < 16 * GIB) {
22
+ return 768;
23
+ }
24
+ if (totalRamBytes < 32 * GIB) {
25
+ return 1024;
26
+ }
27
+ return 2048;
28
+ }
29
+ export function detectManagedBitcoindDbcacheMiB() {
30
+ try {
31
+ return resolveManagedBitcoindDbcacheMiB(totalmem());
32
+ }
33
+ catch {
34
+ return DEFAULT_DBCACHE_MIB;
35
+ }
36
+ }
37
+ async function allocatePort() {
38
+ return new Promise((resolve, reject) => {
39
+ const server = net.createServer();
40
+ server.listen(0, LOCAL_HOST, () => {
41
+ const address = server.address();
42
+ if (!address || typeof address === "string") {
43
+ server.close();
44
+ reject(new Error("bitcoind_port_allocation_failed"));
45
+ return;
46
+ }
47
+ const { port } = address;
48
+ server.close((error) => {
49
+ if (error) {
50
+ reject(error);
51
+ return;
52
+ }
53
+ resolve(port);
54
+ });
55
+ });
56
+ server.on("error", reject);
57
+ });
58
+ }
59
+ async function allocateDistinctPort(reserved) {
60
+ while (true) {
61
+ const port = await allocatePort();
62
+ if (!reserved.has(port)) {
63
+ reserved.add(port);
64
+ return port;
65
+ }
66
+ }
67
+ }
68
+ export async function verifyManagedBitcoindVersion(bitcoindPath) {
69
+ const { stdout } = await execFileAsync(bitcoindPath, ["-nosettings=1", "-version"]);
70
+ if (!stdout.includes("Bitcoin Core") || !stdout.includes(`v${SUPPORTED_BITCOIND_VERSION}`)) {
71
+ throw new Error("bitcoind_version_unsupported");
72
+ }
73
+ }
74
+ export function getManagedBitcoindCookieFile(dataDir, chain) {
75
+ return chain === "main" ? join(dataDir, ".cookie") : join(dataDir, chain, ".cookie");
76
+ }
77
+ export async function resolveManagedBitcoindRuntimeConfig(statusPath, configPath, options) {
78
+ const previousStatus = await readJsonFileIfPresent(statusPath);
79
+ const previousConfig = await readJsonFileIfPresent(configPath);
80
+ const reserved = new Set();
81
+ const rpcPort = options.rpcPort
82
+ ?? previousStatus?.rpc.port
83
+ ?? previousConfig?.rpc?.port
84
+ ?? await allocateDistinctPort(reserved);
85
+ reserved.add(rpcPort);
86
+ const zmqPort = options.zmqPort
87
+ ?? previousStatus?.zmq.port
88
+ ?? previousConfig?.zmqPort
89
+ ?? await allocateDistinctPort(reserved);
90
+ reserved.add(zmqPort);
91
+ const p2pPort = options.p2pPort
92
+ ?? previousStatus?.p2pPort
93
+ ?? previousConfig?.p2pPort
94
+ ?? await allocateDistinctPort(reserved);
95
+ return {
96
+ chain: options.chain,
97
+ rpc: {
98
+ url: `http://${LOCAL_HOST}:${rpcPort}`,
99
+ cookieFile: getManagedBitcoindCookieFile(options.dataDir ?? "", options.chain),
100
+ port: rpcPort,
101
+ },
102
+ zmqPort,
103
+ p2pPort,
104
+ dbcacheMiB: detectManagedBitcoindDbcacheMiB(),
105
+ getblockArchiveEndHeight: options.getblockArchiveEndHeight ?? null,
106
+ getblockArchiveSha256: options.getblockArchiveSha256 ?? null,
107
+ };
108
+ }
109
+ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfig) {
110
+ const walletDir = join(options.dataDir ?? "", "wallets");
111
+ await mkdir(dirname(filePath), { recursive: true });
112
+ await mkdir(walletDir, { recursive: true });
113
+ const lines = [
114
+ "server=1",
115
+ "prune=0",
116
+ "dnsseed=1",
117
+ "listen=0",
118
+ `dbcache=${runtimeConfig.dbcacheMiB}`,
119
+ `rpcbind=${LOCAL_HOST}`,
120
+ `rpcallowip=${LOCAL_HOST}`,
121
+ `rpcport=${runtimeConfig.rpc.port}`,
122
+ `port=${runtimeConfig.p2pPort}`,
123
+ `zmqpubhashblock=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
124
+ `walletdir=${walletDir}`,
125
+ ];
126
+ await writeFileAtomic(filePath, `${lines.join("\n")}\n`, { mode: 0o600 });
127
+ }
128
+ export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
129
+ const walletDir = join(options.dataDir ?? "", "wallets");
130
+ const args = [
131
+ "-nosettings=1",
132
+ `-datadir=${options.dataDir}`,
133
+ `-rpcbind=${LOCAL_HOST}`,
134
+ `-rpcallowip=${LOCAL_HOST}`,
135
+ `-rpcport=${runtimeConfig.rpc.port}`,
136
+ `-port=${runtimeConfig.p2pPort}`,
137
+ `-zmqpubhashblock=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
138
+ `-walletdir=${walletDir}`,
139
+ "-server=1",
140
+ "-prune=0",
141
+ "-dnsseed=1",
142
+ "-listen=0",
143
+ `-dbcache=${runtimeConfig.dbcacheMiB}`,
144
+ ];
145
+ if (options.chain === "regtest") {
146
+ args.push("-chain=regtest");
147
+ }
148
+ if (options.getblockArchivePath !== undefined && options.getblockArchivePath !== null) {
149
+ args.push(`-loadblock=${options.getblockArchivePath}`);
150
+ }
151
+ return args;
152
+ }
153
+ export async function waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleepImpl) {
154
+ const deadline = Date.now() + timeoutMs;
155
+ while (Date.now() < deadline) {
156
+ try {
157
+ await access(cookieFile, constants.R_OK);
158
+ return;
159
+ }
160
+ catch {
161
+ await sleepImpl(250);
162
+ }
163
+ }
164
+ throw new Error("bitcoind_cookie_timeout");
165
+ }
@@ -0,0 +1,28 @@
1
+ import type { ManagedBitcoindServiceProbeResult } from "./managed-runtime/types.js";
2
+ import { resolveManagedServicePaths } from "./service-paths.js";
3
+ import { type ManagedBitcoindNodeHandle } from "./types.js";
4
+ import type { ManagedBitcoindServiceOptions, ManagedBitcoindServiceStopResult } from "./managed-bitcoind-service-types.js";
5
+ export declare function stopManagedBitcoindServiceWithLockHeld(options: {
6
+ dataDir: string;
7
+ walletRootId?: string;
8
+ shutdownTimeoutMs?: number;
9
+ paths?: ReturnType<typeof resolveManagedServicePaths>;
10
+ }): Promise<ManagedBitcoindServiceStopResult>;
11
+ export declare function withClaimedUninitializedManagedRuntime<T>(options: {
12
+ dataDir: string;
13
+ walletRootId?: string;
14
+ shutdownTimeoutMs?: number;
15
+ }, callback: () => Promise<T>): Promise<T>;
16
+ export declare function probeManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindServiceProbeResult>;
17
+ export declare function attachOrStartManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindNodeHandle>;
18
+ export declare function stopManagedBitcoindService(options: {
19
+ dataDir: string;
20
+ walletRootId?: string;
21
+ shutdownTimeoutMs?: number;
22
+ }): Promise<ManagedBitcoindServiceStopResult>;
23
+ export declare function shutdownManagedBitcoindServiceForTesting(options: {
24
+ dataDir: string;
25
+ chain?: "main" | "regtest";
26
+ walletRootId?: string;
27
+ shutdownTimeoutMs?: number;
28
+ }): Promise<void>;
@@ -0,0 +1,290 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir, rm } from "node:fs/promises";
4
+ import { getBitcoindPath } from "@cogcoin/bitcoin";
5
+ import { acquireFileLock } from "../wallet/fs/lock.js";
6
+ import { stopIndexerDaemonServiceWithLockHeld } from "./indexer-daemon.js";
7
+ import { readManagedBitcoindObservedStatus, listManagedBitcoindStatusCandidates } from "./managed-runtime/bitcoind-status.js";
8
+ import { attachOrStartManagedBitcoindRuntime, probeManagedBitcoindRuntime } from "./managed-runtime/bitcoind-runtime.js";
9
+ import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
10
+ import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
11
+ import { DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS, } from "./types.js";
12
+ import { buildManagedServiceArgsForTesting, LOCAL_HOST, resolveManagedBitcoindRuntimeConfig, SUPPORTED_BITCOIND_VERSION, verifyManagedBitcoindVersion, writeBitcoinConfForTesting, } from "./managed-bitcoind-service-config.js";
13
+ import { DEFAULT_MANAGED_BITCOIND_SHUTDOWN_TIMEOUT_MS, DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS, FileLockBusyError, acquireManagedBitcoindFileLockWithRetry, isManagedBitcoindProcessAlive, waitForManagedBitcoindProcessExit, sleep, } from "./managed-bitcoind-service-process.js";
14
+ import { createManagedWalletReplica, loadManagedWalletReplicaIfPresent, } from "./managed-bitcoind-service-replica.js";
15
+ import { clearManagedBitcoindRuntimeArtifacts, createBitcoindServiceStatus, createManagedBitcoindNodeHandle, probeManagedBitcoindStatusCandidate, refreshManagedBitcoindStatus, waitForManagedBitcoindRpcReady, writeManagedBitcoindStatus, } from "./managed-bitcoind-service-status.js";
16
+ const claimedUninitializedRuntimeKeys = new Set();
17
+ export async function stopManagedBitcoindServiceWithLockHeld(options) {
18
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
19
+ const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
20
+ const status = await readManagedBitcoindObservedStatus({
21
+ dataDir: options.dataDir,
22
+ walletRootId,
23
+ });
24
+ const processId = status?.processId ?? null;
25
+ if (status === null || processId === null || !await isManagedBitcoindProcessAlive(processId)) {
26
+ await clearManagedBitcoindRuntimeArtifacts(paths);
27
+ return {
28
+ status: "not-running",
29
+ walletRootId,
30
+ };
31
+ }
32
+ const rpc = createRpcClient(status.rpc);
33
+ try {
34
+ await rpc.stop();
35
+ }
36
+ catch {
37
+ try {
38
+ process.kill(processId, "SIGTERM");
39
+ }
40
+ catch (error) {
41
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
42
+ throw error;
43
+ }
44
+ }
45
+ }
46
+ await waitForManagedBitcoindProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_SHUTDOWN_TIMEOUT_MS, "managed_bitcoind_service_stop_timeout");
47
+ await clearManagedBitcoindRuntimeArtifacts(paths);
48
+ return {
49
+ status: "stopped",
50
+ walletRootId,
51
+ };
52
+ }
53
+ export async function withClaimedUninitializedManagedRuntime(options, callback) {
54
+ const targetWalletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
55
+ const targetPaths = resolveManagedServicePaths(options.dataDir, targetWalletRootId);
56
+ const uninitializedPaths = resolveManagedServicePaths(options.dataDir, UNINITIALIZED_WALLET_ROOT_ID);
57
+ if (targetPaths.walletRuntimeRoot === uninitializedPaths.walletRuntimeRoot) {
58
+ return callback();
59
+ }
60
+ if (targetWalletRootId === UNINITIALIZED_WALLET_ROOT_ID) {
61
+ return callback();
62
+ }
63
+ const claimKey = `${options.dataDir}\n${targetWalletRootId}`;
64
+ if (claimedUninitializedRuntimeKeys.has(claimKey)) {
65
+ return callback();
66
+ }
67
+ claimedUninitializedRuntimeKeys.add(claimKey);
68
+ const lockTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS;
69
+ const bitcoindLock = await acquireManagedBitcoindFileLockWithRetry(uninitializedPaths.bitcoindLockPath, {
70
+ purpose: "managed-bitcoind-claim-uninitialized",
71
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
72
+ dataDir: options.dataDir,
73
+ }, lockTimeoutMs);
74
+ try {
75
+ const indexerLock = await acquireManagedBitcoindFileLockWithRetry(uninitializedPaths.indexerDaemonLockPath, {
76
+ purpose: "managed-indexer-claim-uninitialized",
77
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
78
+ dataDir: options.dataDir,
79
+ }, lockTimeoutMs);
80
+ try {
81
+ await stopIndexerDaemonServiceWithLockHeld({
82
+ dataDir: options.dataDir,
83
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
84
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
85
+ paths: uninitializedPaths,
86
+ });
87
+ await stopManagedBitcoindServiceWithLockHeld({
88
+ dataDir: options.dataDir,
89
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
90
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
91
+ paths: uninitializedPaths,
92
+ });
93
+ return await callback();
94
+ }
95
+ finally {
96
+ await indexerLock.release();
97
+ }
98
+ }
99
+ finally {
100
+ claimedUninitializedRuntimeKeys.delete(claimKey);
101
+ await bitcoindLock.release();
102
+ }
103
+ }
104
+ async function tryAttachExistingManagedBitcoindService(options) {
105
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
106
+ const paths = resolveManagedServicePaths(options.dataDir ?? "", walletRootId);
107
+ const probe = await probeManagedBitcoindService(options);
108
+ if (probe.compatibility !== "compatible" || probe.status === null) {
109
+ return null;
110
+ }
111
+ const refreshed = await refreshManagedBitcoindStatus(probe.status, paths, options);
112
+ return createManagedBitcoindNodeHandle({
113
+ status: refreshed,
114
+ paths,
115
+ serviceOptions: options,
116
+ ownership: "attached",
117
+ stopService: stopManagedBitcoindService,
118
+ });
119
+ }
120
+ export async function probeManagedBitcoindService(options) {
121
+ const resolvedOptions = {
122
+ ...options,
123
+ dataDir: options.dataDir ?? "",
124
+ walletRootId: options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
125
+ startupTimeoutMs: options.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS,
126
+ };
127
+ return probeManagedBitcoindRuntime(resolvedOptions, {
128
+ getPaths: (runtimeOptions) => resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
129
+ listStatusCandidates: listManagedBitcoindStatusCandidates,
130
+ isProcessAlive: isManagedBitcoindProcessAlive,
131
+ probeStatusCandidate: probeManagedBitcoindStatusCandidate,
132
+ });
133
+ }
134
+ export async function attachOrStartManagedBitcoindService(options) {
135
+ const resolvedOptions = {
136
+ ...options,
137
+ dataDir: options.dataDir,
138
+ serviceLifetime: options.serviceLifetime ?? "persistent",
139
+ walletRootId: options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
140
+ };
141
+ const startupTimeoutMs = resolvedOptions.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS;
142
+ return withClaimedUninitializedManagedRuntime({
143
+ dataDir: resolvedOptions.dataDir ?? "",
144
+ walletRootId: resolvedOptions.walletRootId,
145
+ shutdownTimeoutMs: resolvedOptions.shutdownTimeoutMs,
146
+ }, async () => {
147
+ return attachOrStartManagedBitcoindRuntime({
148
+ ...resolvedOptions,
149
+ dataDir: resolvedOptions.dataDir ?? "",
150
+ walletRootId: resolvedOptions.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
151
+ startupTimeoutMs,
152
+ }, {
153
+ getPaths: (runtimeOptions) => resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
154
+ listStatusCandidates: listManagedBitcoindStatusCandidates,
155
+ isProcessAlive: isManagedBitcoindProcessAlive,
156
+ probeStatusCandidate: probeManagedBitcoindStatusCandidate,
157
+ attachExisting: tryAttachExistingManagedBitcoindService,
158
+ acquireStartLock: async (runtimeOptions, paths) => acquireFileLock(paths.bitcoindLockPath, {
159
+ purpose: "managed-bitcoind-start",
160
+ walletRootId: runtimeOptions.walletRootId,
161
+ dataDir: runtimeOptions.dataDir,
162
+ }),
163
+ startService: async (runtimeOptions, paths) => {
164
+ const bitcoindPath = await getBitcoindPath();
165
+ await verifyManagedBitcoindVersion(bitcoindPath);
166
+ const binaryVersion = SUPPORTED_BITCOIND_VERSION;
167
+ await mkdir(runtimeOptions.dataDir, { recursive: true });
168
+ const startManagedProcess = async (startOptions) => {
169
+ const runtimeConfig = await resolveManagedBitcoindRuntimeConfig(paths.bitcoindStatusPath, paths.bitcoindRuntimeConfigPath, startOptions);
170
+ await writeBitcoinConfForTesting(paths.bitcoinConfPath, startOptions, runtimeConfig);
171
+ const rpcConfig = runtimeConfig.rpc;
172
+ const zmqConfig = {
173
+ endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
174
+ topic: "hashblock",
175
+ port: runtimeConfig.zmqPort,
176
+ pollIntervalMs: startOptions.pollIntervalMs ?? DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS,
177
+ };
178
+ const spawnOptions = startOptions.serviceLifetime === "ephemeral"
179
+ ? {
180
+ stdio: "ignore",
181
+ }
182
+ : {
183
+ detached: true,
184
+ stdio: "ignore",
185
+ };
186
+ const child = spawn(bitcoindPath, buildManagedServiceArgsForTesting(startOptions, runtimeConfig), {
187
+ ...spawnOptions,
188
+ });
189
+ if (startOptions.serviceLifetime !== "ephemeral") {
190
+ child.unref();
191
+ }
192
+ const rpc = createRpcClient(rpcConfig);
193
+ try {
194
+ await waitForManagedBitcoindRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
195
+ await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint);
196
+ }
197
+ catch (error) {
198
+ if (child.pid !== undefined) {
199
+ try {
200
+ process.kill(child.pid, "SIGTERM");
201
+ }
202
+ catch {
203
+ // ignore kill failures during startup cleanup
204
+ }
205
+ }
206
+ throw error;
207
+ }
208
+ const nowUnixMs = Date.now();
209
+ const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, startOptions.walletRootId, startOptions.dataDir);
210
+ return createBitcoindServiceStatus({
211
+ binaryVersion,
212
+ serviceInstanceId: randomBytes(16).toString("hex"),
213
+ state: "ready",
214
+ processId: child.pid ?? null,
215
+ walletRootId: startOptions.walletRootId,
216
+ chain: startOptions.chain,
217
+ dataDir: startOptions.dataDir,
218
+ runtimeRoot: paths.walletRuntimeRoot,
219
+ startHeight: startOptions.startHeight,
220
+ rpc: rpcConfig,
221
+ zmq: zmqConfig,
222
+ p2pPort: runtimeConfig.p2pPort,
223
+ getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
224
+ getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
225
+ walletReplica,
226
+ startedAtUnixMs: nowUnixMs,
227
+ heartbeatAtUnixMs: nowUnixMs,
228
+ lastError: walletReplica.message ?? null,
229
+ });
230
+ };
231
+ let status;
232
+ try {
233
+ status = await startManagedProcess(runtimeOptions);
234
+ }
235
+ catch (error) {
236
+ if (runtimeOptions.getblockArchivePath === undefined || runtimeOptions.getblockArchivePath === null) {
237
+ throw error;
238
+ }
239
+ status = await startManagedProcess({
240
+ ...runtimeOptions,
241
+ getblockArchivePath: null,
242
+ getblockArchiveEndHeight: null,
243
+ getblockArchiveSha256: null,
244
+ });
245
+ }
246
+ await writeManagedBitcoindStatus(paths, status);
247
+ return createManagedBitcoindNodeHandle({
248
+ status,
249
+ paths: resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
250
+ serviceOptions: runtimeOptions,
251
+ ownership: "started",
252
+ stopService: stopManagedBitcoindService,
253
+ });
254
+ },
255
+ isLockBusyError: (error) => error instanceof FileLockBusyError,
256
+ sleep,
257
+ });
258
+ });
259
+ }
260
+ export async function stopManagedBitcoindService(options) {
261
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
262
+ const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
263
+ const lock = await acquireFileLock(paths.bitcoindLockPath, {
264
+ purpose: "managed-bitcoind-stop",
265
+ walletRootId,
266
+ dataDir: options.dataDir,
267
+ });
268
+ try {
269
+ return stopManagedBitcoindServiceWithLockHeld({
270
+ ...options,
271
+ walletRootId,
272
+ paths,
273
+ });
274
+ }
275
+ finally {
276
+ await lock.release();
277
+ }
278
+ }
279
+ export async function shutdownManagedBitcoindServiceForTesting(options) {
280
+ await stopManagedBitcoindService({
281
+ dataDir: options.dataDir,
282
+ walletRootId: options.walletRootId,
283
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
284
+ }).catch(async (error) => {
285
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
286
+ const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
287
+ await rm(paths.bitcoindReadyPath, { force: true }).catch(() => undefined);
288
+ throw error;
289
+ });
290
+ }
@@ -0,0 +1,8 @@
1
+ import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
2
+ export declare const DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS = 60000;
3
+ export declare const DEFAULT_MANAGED_BITCOIND_SHUTDOWN_TIMEOUT_MS = 15000;
4
+ export declare function sleep(ms: number): Promise<void>;
5
+ export declare function isManagedBitcoindProcessAlive(pid: number | null): Promise<boolean>;
6
+ export declare function waitForManagedBitcoindProcessExit(pid: number, timeoutMs: number, errorCode: string): Promise<void>;
7
+ export declare function acquireManagedBitcoindFileLockWithRetry(lockPath: string, metadata: Parameters<typeof acquireFileLock>[1], timeoutMs: number): Promise<Awaited<ReturnType<typeof acquireFileLock>>>;
8
+ export { FileLockBusyError };
@@ -0,0 +1,48 @@
1
+ import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
2
+ export const DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS = 60_000;
3
+ export const DEFAULT_MANAGED_BITCOIND_SHUTDOWN_TIMEOUT_MS = 15_000;
4
+ export function sleep(ms) {
5
+ return new Promise((resolve) => {
6
+ setTimeout(resolve, ms);
7
+ });
8
+ }
9
+ export async function isManagedBitcoindProcessAlive(pid) {
10
+ if (pid === null) {
11
+ return false;
12
+ }
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ }
17
+ catch (error) {
18
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ }
24
+ export async function waitForManagedBitcoindProcessExit(pid, timeoutMs, errorCode) {
25
+ const deadline = Date.now() + timeoutMs;
26
+ while (Date.now() < deadline) {
27
+ if (!await isManagedBitcoindProcessAlive(pid)) {
28
+ return;
29
+ }
30
+ await sleep(250);
31
+ }
32
+ throw new Error(errorCode);
33
+ }
34
+ export async function acquireManagedBitcoindFileLockWithRetry(lockPath, metadata, timeoutMs) {
35
+ const deadline = Date.now() + timeoutMs;
36
+ while (true) {
37
+ try {
38
+ return await acquireFileLock(lockPath, metadata);
39
+ }
40
+ catch (error) {
41
+ if (!(error instanceof FileLockBusyError) || Date.now() >= deadline) {
42
+ throw error;
43
+ }
44
+ await sleep(250);
45
+ }
46
+ }
47
+ }
48
+ export { FileLockBusyError };
@@ -0,0 +1,8 @@
1
+ import type { ManagedCoreWalletReplicaStatus } from "./types.js";
2
+ import type { ManagedWalletReplicaRpc } from "./managed-bitcoind-service-types.js";
3
+ export declare function getManagedBitcoindWalletReplicaName(walletRootId: string): string;
4
+ export declare function createMissingManagedWalletReplicaStatus(walletRootId: string, message: string): ManagedCoreWalletReplicaStatus;
5
+ export declare function loadManagedWalletReplicaIfPresent(rpc: ManagedWalletReplicaRpc, walletRootId: string, dataDir: string): Promise<ManagedCoreWalletReplicaStatus>;
6
+ export declare function createManagedWalletReplica(rpc: ManagedWalletReplicaRpc, walletRootId: string, options?: {
7
+ managedWalletPassphrase?: string;
8
+ }): Promise<ManagedCoreWalletReplicaStatus>;