@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.
- package/README.md +1 -1
- package/dist/app-paths.d.ts +2 -0
- package/dist/app-paths.js +4 -0
- package/dist/art/wallet.txt +9 -9
- package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
- package/dist/bitcoind/bootstrap/chainstate.js +4 -1
- package/dist/bitcoind/bootstrap/chunk-manifest.d.ts +14 -0
- package/dist/bitcoind/bootstrap/chunk-manifest.js +85 -0
- package/dist/bitcoind/bootstrap/chunk-recovery.d.ts +4 -0
- package/dist/bitcoind/bootstrap/chunk-recovery.js +122 -0
- package/dist/bitcoind/bootstrap/constants.d.ts +3 -1
- package/dist/bitcoind/bootstrap/constants.js +3 -1
- package/dist/bitcoind/bootstrap/controller.d.ts +10 -2
- package/dist/bitcoind/bootstrap/controller.js +56 -12
- package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.d.ts +2 -0
- package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.js +2309 -0
- package/dist/bitcoind/bootstrap/download.js +177 -83
- package/dist/bitcoind/bootstrap/headers.d.ts +16 -2
- package/dist/bitcoind/bootstrap/headers.js +124 -14
- package/dist/bitcoind/bootstrap/state.d.ts +11 -1
- package/dist/bitcoind/bootstrap/state.js +50 -23
- package/dist/bitcoind/bootstrap/types.d.ts +12 -1
- package/dist/bitcoind/client/factory.js +11 -2
- package/dist/bitcoind/client/internal-types.d.ts +1 -0
- package/dist/bitcoind/client/managed-client.d.ts +1 -1
- package/dist/bitcoind/client/managed-client.js +29 -15
- package/dist/bitcoind/client/sync-engine.js +88 -16
- package/dist/bitcoind/errors.js +9 -0
- package/dist/bitcoind/indexer-daemon.d.ts +7 -0
- package/dist/bitcoind/indexer-daemon.js +31 -22
- package/dist/bitcoind/processing-start-height.d.ts +7 -0
- package/dist/bitcoind/processing-start-height.js +9 -0
- package/dist/bitcoind/progress/controller.js +1 -0
- package/dist/bitcoind/progress/formatting.js +4 -1
- package/dist/bitcoind/retryable-rpc.d.ts +11 -0
- package/dist/bitcoind/retryable-rpc.js +30 -0
- package/dist/bitcoind/service.d.ts +16 -1
- package/dist/bitcoind/service.js +228 -115
- package/dist/bitcoind/testing.d.ts +1 -1
- package/dist/bitcoind/testing.js +1 -1
- package/dist/bitcoind/types.d.ts +10 -0
- package/dist/cli/commands/follow.js +9 -0
- package/dist/cli/commands/service-runtime.js +150 -134
- package/dist/cli/commands/sync.js +9 -0
- package/dist/cli/commands/wallet-admin.js +77 -21
- package/dist/cli/context.js +4 -2
- package/dist/cli/mutation-json.js +2 -0
- package/dist/cli/output.js +3 -1
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +6 -0
- package/dist/cli/preview-json.js +2 -0
- package/dist/cli/runner.js +1 -0
- package/dist/cli/types.d.ts +6 -3
- package/dist/cli/types.js +1 -1
- package/dist/cli/wallet-format.js +134 -14
- package/dist/wallet/lifecycle.d.ts +6 -0
- package/dist/wallet/lifecycle.js +168 -37
- package/dist/wallet/read/context.js +10 -4
- package/dist/wallet/reset.d.ts +61 -2
- package/dist/wallet/reset.js +208 -63
- package/dist/wallet/root-resolution.d.ts +20 -0
- package/dist/wallet/root-resolution.js +37 -0
- package/dist/wallet/runtime.d.ts +3 -0
- package/dist/wallet/runtime.js +3 -0
- package/dist/wallet/state/crypto.d.ts +3 -0
- package/dist/wallet/state/crypto.js +3 -0
- package/dist/wallet/state/pending-init.d.ts +24 -0
- package/dist/wallet/state/pending-init.js +59 -0
- package/dist/wallet/state/provider.d.ts +1 -0
- package/dist/wallet/state/provider.js +7 -1
- package/dist/wallet/state/storage.d.ts +7 -1
- package/dist/wallet/state/storage.js +39 -0
- package/dist/wallet/types.d.ts +9 -0
- 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
|
|
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: {
|
package/dist/bitcoind/service.js
CHANGED
|
@@ -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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
if (
|
|
641
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
847
|
+
catch (error) {
|
|
848
|
+
if (error instanceof FileLockBusyError) {
|
|
849
|
+
return waitForManagedBitcoindService(resolvedOptions, startupTimeoutMs);
|
|
850
|
+
}
|
|
851
|
+
throw error;
|
|
714
852
|
}
|
|
715
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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";
|
package/dist/bitcoind/testing.js
CHANGED
|
@@ -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";
|
package/dist/bitcoind/types.d.ts
CHANGED
|
@@ -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;
|