@cogcoin/client 1.1.9 → 1.1.11

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