@cogcoin/client 1.1.5 → 1.1.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/bitcoind/indexer-daemon.d.ts +3 -7
- package/dist/bitcoind/indexer-daemon.js +43 -158
- package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
- package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/status.js +59 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +37 -0
- package/dist/bitcoind/managed-runtime/types.js +1 -0
- package/dist/bitcoind/service.d.ts +2 -7
- package/dist/bitcoind/service.js +46 -94
- package/dist/wallet/lifecycle/access.d.ts +5 -0
- package/dist/wallet/lifecycle/access.js +79 -0
- package/dist/wallet/lifecycle/context.d.ts +26 -0
- package/dist/wallet/lifecycle/context.js +58 -0
- package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
- package/dist/wallet/lifecycle/managed-core.js +3 -63
- package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
- package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
- package/dist/wallet/lifecycle/repair-indexer.js +117 -0
- package/dist/wallet/lifecycle/repair.d.ts +2 -4
- package/dist/wallet/lifecycle/repair.js +77 -318
- package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
- package/dist/wallet/lifecycle/setup-prompts.js +88 -0
- package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
- package/dist/wallet/lifecycle/setup-state.js +159 -0
- package/dist/wallet/lifecycle/setup.d.ts +3 -4
- package/dist/wallet/lifecycle/setup.js +45 -351
- package/dist/wallet/lifecycle/types.d.ts +33 -2
- package/dist/wallet/read/context.js +13 -188
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { compareSemver, parseSemver } from "../../semver.js";
|
|
2
|
+
import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "../types.js";
|
|
3
|
+
import { buildManagedIndexerStatusFromSnapshotHandle, resolveManagedIndexerStatusProjection } from "./status.js";
|
|
4
|
+
const STALE_HEARTBEAT_THRESHOLD_MS = 15_000;
|
|
5
|
+
function isUnreachableIndexerDaemonError(error) {
|
|
6
|
+
if (error instanceof Error) {
|
|
7
|
+
if (error.message === "indexer_daemon_connection_closed"
|
|
8
|
+
|| error.message === "indexer_daemon_request_timeout"
|
|
9
|
+
|| error.message === "indexer_daemon_protocol_error") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if ("code" in error) {
|
|
13
|
+
const code = error.code;
|
|
14
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export function validateIndexerRuntimeIdentity(identity, expectedWalletRootId) {
|
|
20
|
+
void expectedWalletRootId;
|
|
21
|
+
if (identity.serviceApiVersion !== INDEXER_DAEMON_SERVICE_API_VERSION) {
|
|
22
|
+
throw new Error("indexer_daemon_service_version_mismatch");
|
|
23
|
+
}
|
|
24
|
+
// Managed indexer daemons are adopted across wallet roots when the runtime
|
|
25
|
+
// is otherwise compatible. Wallet-root ownership remains advisory here.
|
|
26
|
+
if (identity.schemaVersion !== INDEXER_DAEMON_SCHEMA_VERSION || identity.state === "schema-mismatch") {
|
|
27
|
+
throw new Error("indexer_daemon_schema_mismatch");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function validateIndexerDaemonStatus(status, expectedWalletRootId) {
|
|
31
|
+
validateIndexerRuntimeIdentity(status, expectedWalletRootId);
|
|
32
|
+
}
|
|
33
|
+
export function validateIndexerSnapshotHandle(handle, expectedWalletRootId) {
|
|
34
|
+
validateIndexerRuntimeIdentity(handle, expectedWalletRootId);
|
|
35
|
+
}
|
|
36
|
+
export function validateIndexerSnapshotPayload(payload, handle, expectedWalletRootId) {
|
|
37
|
+
validateIndexerRuntimeIdentity(payload, expectedWalletRootId);
|
|
38
|
+
if (payload.token !== handle.token
|
|
39
|
+
|| payload.daemonInstanceId !== handle.daemonInstanceId
|
|
40
|
+
|| payload.processId !== handle.processId
|
|
41
|
+
|| payload.startedAtUnixMs !== handle.startedAtUnixMs
|
|
42
|
+
|| payload.snapshotSeq !== handle.snapshotSeq
|
|
43
|
+
|| payload.tipHeight !== handle.tipHeight
|
|
44
|
+
|| payload.tipHash !== handle.tipHash
|
|
45
|
+
|| payload.openedAtUnixMs !== handle.openedAtUnixMs) {
|
|
46
|
+
throw new Error("indexer_daemon_snapshot_identity_mismatch");
|
|
47
|
+
}
|
|
48
|
+
if (payload.tip === null) {
|
|
49
|
+
if (payload.tipHeight !== null || payload.tipHash !== null) {
|
|
50
|
+
throw new Error("indexer_daemon_snapshot_identity_mismatch");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (payload.tip.height !== payload.tipHeight || payload.tip.blockHashHex !== payload.tipHash) {
|
|
54
|
+
throw new Error("indexer_daemon_snapshot_identity_mismatch");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function mapIndexerDaemonValidationError(error, status) {
|
|
58
|
+
return {
|
|
59
|
+
compatibility: error instanceof Error
|
|
60
|
+
? error.message === "indexer_daemon_service_version_mismatch"
|
|
61
|
+
? "service-version-mismatch"
|
|
62
|
+
: "schema-mismatch"
|
|
63
|
+
: "protocol-error",
|
|
64
|
+
status,
|
|
65
|
+
client: null,
|
|
66
|
+
error: error instanceof Error ? error.message : "indexer_daemon_protocol_error",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function mapIndexerDaemonTransportError(error) {
|
|
70
|
+
return {
|
|
71
|
+
compatibility: isUnreachableIndexerDaemonError(error) ? "unreachable" : "protocol-error",
|
|
72
|
+
status: null,
|
|
73
|
+
client: null,
|
|
74
|
+
error: isUnreachableIndexerDaemonError(error)
|
|
75
|
+
? null
|
|
76
|
+
: error instanceof Error
|
|
77
|
+
? "indexer_daemon_protocol_error"
|
|
78
|
+
: "indexer_daemon_protocol_error",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function isStaleIndexerDaemonVersion(status, expectedBinaryVersion) {
|
|
82
|
+
if (status === null || expectedBinaryVersion === null || expectedBinaryVersion === undefined) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (parseSemver(expectedBinaryVersion) === null) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const comparison = compareSemver(status.binaryVersion, expectedBinaryVersion);
|
|
89
|
+
return comparison === null || comparison < 0;
|
|
90
|
+
}
|
|
91
|
+
export function resolveIndexerDaemonProbeDecision(options) {
|
|
92
|
+
if (options.probe.compatibility === "compatible") {
|
|
93
|
+
return {
|
|
94
|
+
action: isStaleIndexerDaemonVersion(options.probe.status, options.expectedBinaryVersion)
|
|
95
|
+
? "replace"
|
|
96
|
+
: "attach",
|
|
97
|
+
error: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (options.probe.compatibility === "unreachable") {
|
|
101
|
+
return {
|
|
102
|
+
action: "start",
|
|
103
|
+
error: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
action: "reject",
|
|
108
|
+
error: options.probe.error ?? "indexer_daemon_protocol_error",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function mapIndexerStartupError(message) {
|
|
112
|
+
switch (message) {
|
|
113
|
+
case "indexer_daemon_start_timeout":
|
|
114
|
+
return {
|
|
115
|
+
health: "starting",
|
|
116
|
+
message: "Indexer daemon is still starting.",
|
|
117
|
+
};
|
|
118
|
+
case "indexer_daemon_service_version_mismatch":
|
|
119
|
+
return {
|
|
120
|
+
health: "service-version-mismatch",
|
|
121
|
+
message: "The live indexer daemon is running an incompatible service API version.",
|
|
122
|
+
};
|
|
123
|
+
case "indexer_daemon_schema_mismatch":
|
|
124
|
+
return {
|
|
125
|
+
health: "schema-mismatch",
|
|
126
|
+
message: "The live indexer daemon is using an incompatible sqlite schema.",
|
|
127
|
+
};
|
|
128
|
+
case "indexer_daemon_wallet_root_mismatch":
|
|
129
|
+
return {
|
|
130
|
+
health: "wallet-root-mismatch",
|
|
131
|
+
message: "The live indexer daemon belongs to a different wallet root.",
|
|
132
|
+
};
|
|
133
|
+
case "indexer_daemon_protocol_error":
|
|
134
|
+
return {
|
|
135
|
+
health: "unavailable",
|
|
136
|
+
message: "The live indexer daemon socket responded with an invalid or incomplete protocol exchange.",
|
|
137
|
+
};
|
|
138
|
+
case "indexer_daemon_background_follow_recovery_failed":
|
|
139
|
+
return {
|
|
140
|
+
health: "failed",
|
|
141
|
+
message: "The managed indexer daemon could not recover automatic background follow.",
|
|
142
|
+
};
|
|
143
|
+
default:
|
|
144
|
+
return {
|
|
145
|
+
health: "unavailable",
|
|
146
|
+
message,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function deriveManagedIndexerWalletStatus(options) {
|
|
151
|
+
const projection = resolveManagedIndexerStatusProjection({
|
|
152
|
+
daemonStatus: options.daemonStatus,
|
|
153
|
+
observedStatus: options.observedStatus,
|
|
154
|
+
snapshot: options.snapshot,
|
|
155
|
+
source: options.source,
|
|
156
|
+
});
|
|
157
|
+
const createResult = (health, message) => ({
|
|
158
|
+
health,
|
|
159
|
+
status: projection.status,
|
|
160
|
+
message,
|
|
161
|
+
snapshotTip: projection.snapshotTip,
|
|
162
|
+
source: projection.source,
|
|
163
|
+
daemonInstanceId: projection.daemonInstanceId,
|
|
164
|
+
snapshotSeq: projection.snapshotSeq,
|
|
165
|
+
openedAtUnixMs: projection.openedAtUnixMs,
|
|
166
|
+
});
|
|
167
|
+
if (options.startupError !== null) {
|
|
168
|
+
const mapped = mapIndexerStartupError(options.startupError);
|
|
169
|
+
return createResult(mapped.health, mapped.message);
|
|
170
|
+
}
|
|
171
|
+
if (projection.status === null) {
|
|
172
|
+
return createResult("unavailable", "Indexer daemon is unavailable.");
|
|
173
|
+
}
|
|
174
|
+
if ((options.now - projection.status.heartbeatAtUnixMs) > STALE_HEARTBEAT_THRESHOLD_MS) {
|
|
175
|
+
return createResult("stale-heartbeat", "Indexer daemon heartbeat is stale.");
|
|
176
|
+
}
|
|
177
|
+
if (projection.status.state === "schema-mismatch") {
|
|
178
|
+
return createResult("schema-mismatch", projection.status.lastError ?? "Indexer daemon sqlite schema is incompatible.");
|
|
179
|
+
}
|
|
180
|
+
if (projection.status.state === "failed") {
|
|
181
|
+
return createResult("failed", projection.status.lastError ?? "Indexer daemon refresh failed.");
|
|
182
|
+
}
|
|
183
|
+
if (projection.status.state === "service-version-mismatch") {
|
|
184
|
+
return createResult("service-version-mismatch", "Indexer daemon service API is incompatible.");
|
|
185
|
+
}
|
|
186
|
+
if (options.snapshot === null) {
|
|
187
|
+
if (projection.status.state === "reorging") {
|
|
188
|
+
return createResult("reorging", "Indexer daemon is replaying a reorg and refreshing the coherent snapshot.");
|
|
189
|
+
}
|
|
190
|
+
return createResult(projection.status.state === "catching-up" ? "catching-up" : "starting", "Indexer snapshot is not ready yet.");
|
|
191
|
+
}
|
|
192
|
+
if (projection.status.state === "catching-up") {
|
|
193
|
+
return createResult("catching-up", "Indexer daemon is still catching up to the managed Bitcoin tip.");
|
|
194
|
+
}
|
|
195
|
+
if (projection.status.state === "reorging") {
|
|
196
|
+
return createResult("reorging", "Indexer daemon is replaying a reorg and refreshing the coherent snapshot.");
|
|
197
|
+
}
|
|
198
|
+
return createResult("synced", null);
|
|
199
|
+
}
|
|
200
|
+
export { buildManagedIndexerStatusFromSnapshotHandle };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IndexerSnapshotHandle } from "../indexer-daemon.js";
|
|
2
|
+
import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus, type ManagedIndexerTruthSource } from "../types.js";
|
|
3
|
+
import type { ManagedIndexerSnapshotLike, ManagedIndexerStatusProjection } from "./types.js";
|
|
4
|
+
export declare function readJsonFileIfPresent<T>(filePath: string): Promise<T | null>;
|
|
5
|
+
export declare function buildManagedIndexerStatusFromSnapshotHandle(handle: IndexerSnapshotHandle): ManagedIndexerDaemonStatus;
|
|
6
|
+
export declare function resolveManagedIndexerStatusProjection(options: {
|
|
7
|
+
daemonStatus: ManagedIndexerDaemonStatus | null;
|
|
8
|
+
observedStatus?: ManagedIndexerDaemonObservedStatus | null;
|
|
9
|
+
snapshot: ManagedIndexerSnapshotLike | null;
|
|
10
|
+
source: ManagedIndexerTruthSource;
|
|
11
|
+
}): ManagedIndexerStatusProjection;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "../types.js";
|
|
3
|
+
export async function readJsonFileIfPresent(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function buildManagedIndexerStatusFromSnapshotHandle(handle) {
|
|
15
|
+
return {
|
|
16
|
+
serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
|
|
17
|
+
binaryVersion: handle.binaryVersion,
|
|
18
|
+
buildId: handle.buildId,
|
|
19
|
+
updatedAtUnixMs: Math.max(handle.heartbeatAtUnixMs, handle.openedAtUnixMs),
|
|
20
|
+
walletRootId: handle.walletRootId,
|
|
21
|
+
daemonInstanceId: handle.daemonInstanceId,
|
|
22
|
+
schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
|
|
23
|
+
state: handle.state,
|
|
24
|
+
processId: handle.processId,
|
|
25
|
+
startedAtUnixMs: handle.startedAtUnixMs,
|
|
26
|
+
heartbeatAtUnixMs: handle.heartbeatAtUnixMs,
|
|
27
|
+
ipcReady: true,
|
|
28
|
+
rpcReachable: handle.rpcReachable,
|
|
29
|
+
coreBestHeight: handle.coreBestHeight,
|
|
30
|
+
coreBestHash: handle.coreBestHash,
|
|
31
|
+
appliedTipHeight: handle.appliedTipHeight,
|
|
32
|
+
appliedTipHash: handle.appliedTipHash,
|
|
33
|
+
snapshotSeq: handle.snapshotSeq,
|
|
34
|
+
backlogBlocks: handle.backlogBlocks,
|
|
35
|
+
reorgDepth: handle.reorgDepth,
|
|
36
|
+
lastAppliedAtUnixMs: handle.lastAppliedAtUnixMs,
|
|
37
|
+
activeSnapshotCount: handle.activeSnapshotCount,
|
|
38
|
+
lastError: handle.lastError,
|
|
39
|
+
backgroundFollowActive: handle.backgroundFollowActive,
|
|
40
|
+
bootstrapPhase: handle.bootstrapPhase,
|
|
41
|
+
bootstrapProgress: handle.bootstrapProgress,
|
|
42
|
+
cogcoinSyncHeight: handle.cogcoinSyncHeight,
|
|
43
|
+
cogcoinSyncTargetHeight: handle.cogcoinSyncTargetHeight,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function resolveManagedIndexerStatusProjection(options) {
|
|
47
|
+
const status = options.source === "lease"
|
|
48
|
+
? options.daemonStatus
|
|
49
|
+
: options.observedStatus ?? options.daemonStatus;
|
|
50
|
+
const source = status === null && options.snapshot === null ? "none" : options.source;
|
|
51
|
+
return {
|
|
52
|
+
status,
|
|
53
|
+
source,
|
|
54
|
+
snapshotTip: options.snapshot?.tip ?? null,
|
|
55
|
+
daemonInstanceId: options.snapshot?.daemonInstanceId ?? status?.daemonInstanceId ?? null,
|
|
56
|
+
snapshotSeq: options.snapshot?.snapshotSeq ?? status?.snapshotSeq ?? null,
|
|
57
|
+
openedAtUnixMs: options.snapshot?.openedAtUnixMs ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ClientTip } from "../../types.js";
|
|
2
|
+
import type { ManagedBitcoindObservedStatus, ManagedIndexerDaemonObservedStatus, ManagedIndexerTruthSource } from "../types.js";
|
|
3
|
+
export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
|
|
4
|
+
export interface ManagedBitcoindServiceProbeResult {
|
|
5
|
+
compatibility: ManagedBitcoindServiceCompatibility;
|
|
6
|
+
status: ManagedBitcoindObservedStatus | null;
|
|
7
|
+
error: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type IndexerDaemonCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "schema-mismatch" | "unreachable" | "protocol-error";
|
|
10
|
+
export interface ManagedIndexerDaemonProbeResult<TClient> {
|
|
11
|
+
compatibility: IndexerDaemonCompatibility;
|
|
12
|
+
status: ManagedIndexerDaemonObservedStatus | null;
|
|
13
|
+
client: TClient | null;
|
|
14
|
+
error: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ManagedBitcoindProbeDecision {
|
|
17
|
+
action: "attach" | "start" | "reject";
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface IndexerDaemonProbeDecision {
|
|
21
|
+
action: "attach" | "replace" | "start" | "reject";
|
|
22
|
+
error: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface ManagedIndexerSnapshotLike {
|
|
25
|
+
tip: ClientTip | null;
|
|
26
|
+
daemonInstanceId?: string | null;
|
|
27
|
+
snapshotSeq?: string | null;
|
|
28
|
+
openedAtUnixMs?: number | null;
|
|
29
|
+
}
|
|
30
|
+
export interface ManagedIndexerStatusProjection {
|
|
31
|
+
status: ManagedIndexerDaemonObservedStatus | null;
|
|
32
|
+
source: ManagedIndexerTruthSource;
|
|
33
|
+
snapshotTip: ClientTip | null;
|
|
34
|
+
daemonInstanceId: string | null;
|
|
35
|
+
snapshotSeq: string | null;
|
|
36
|
+
openedAtUnixMs: number | null;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { ManagedBitcoindServiceProbeResult } from "./managed-runtime/types.js";
|
|
1
2
|
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
2
3
|
import type { InternalManagedBitcoindOptions, ManagedBitcoindObservedStatus, ManagedBitcoindRuntimeConfig, ManagedBitcoindNodeHandle, ManagedCoreWalletReplicaStatus } from "./types.js";
|
|
4
|
+
export type { ManagedBitcoindServiceCompatibility, ManagedBitcoindServiceProbeResult, } from "./managed-runtime/types.js";
|
|
3
5
|
export declare function resolveManagedBitcoindDbcacheMiB(totalRamBytes: number): number;
|
|
4
6
|
interface ManagedWalletReplicaRpc {
|
|
5
7
|
listWallets(): Promise<string[]>;
|
|
@@ -26,12 +28,6 @@ type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataD
|
|
|
26
28
|
getblockArchiveSha256?: string | null;
|
|
27
29
|
serviceLifetime?: "persistent" | "ephemeral";
|
|
28
30
|
};
|
|
29
|
-
export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
|
|
30
|
-
export interface ManagedBitcoindServiceProbeResult {
|
|
31
|
-
compatibility: ManagedBitcoindServiceCompatibility;
|
|
32
|
-
status: ManagedBitcoindObservedStatus | null;
|
|
33
|
-
error: string | null;
|
|
34
|
-
}
|
|
35
31
|
export interface ManagedBitcoindServiceStopResult {
|
|
36
32
|
status: "stopped" | "not-running";
|
|
37
33
|
walletRootId: string;
|
|
@@ -66,4 +62,3 @@ export declare function shutdownManagedBitcoindServiceForTesting(options: {
|
|
|
66
62
|
walletRootId?: string;
|
|
67
63
|
shutdownTimeoutMs?: number;
|
|
68
64
|
}): Promise<void>;
|
|
69
|
-
export {};
|
package/dist/bitcoind/service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { totalmem } from "node:os";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
import net from "node:net";
|
|
@@ -9,6 +10,8 @@ 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";
|
|
11
12
|
import { stopIndexerDaemonServiceWithLockHeld } from "./indexer-daemon.js";
|
|
13
|
+
import { mapManagedBitcoindRuntimeProbeFailure, mapManagedBitcoindValidationError, resolveManagedBitcoindProbeDecision, validateManagedBitcoindObservedStatus, } from "./managed-runtime/bitcoind-policy.js";
|
|
14
|
+
import { readJsonFileIfPresent } from "./managed-runtime/status.js";
|
|
12
15
|
import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
|
|
13
16
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
14
17
|
import { DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS, MANAGED_BITCOIND_SERVICE_API_VERSION as MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE, } from "./types.js";
|
|
@@ -17,14 +20,31 @@ const LOCAL_HOST = "127.0.0.1";
|
|
|
17
20
|
const SUPPORTED_BITCOIND_VERSION = "30.2.0";
|
|
18
21
|
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
19
22
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 15_000;
|
|
20
|
-
const DEFAULT_DBCACHE_MIB =
|
|
23
|
+
const DEFAULT_DBCACHE_MIB = 450;
|
|
21
24
|
const claimedUninitializedRuntimeKeys = new Set();
|
|
25
|
+
const GIB = 1024 ** 3;
|
|
22
26
|
export function resolveManagedBitcoindDbcacheMiB(totalRamBytes) {
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
if (!Number.isFinite(totalRamBytes) || totalRamBytes <= 0) {
|
|
28
|
+
return DEFAULT_DBCACHE_MIB;
|
|
29
|
+
}
|
|
30
|
+
if (totalRamBytes < 8 * GIB) {
|
|
31
|
+
return 450;
|
|
32
|
+
}
|
|
33
|
+
if (totalRamBytes < 16 * GIB) {
|
|
34
|
+
return 768;
|
|
35
|
+
}
|
|
36
|
+
if (totalRamBytes < 32 * GIB) {
|
|
37
|
+
return 1024;
|
|
38
|
+
}
|
|
39
|
+
return 2048;
|
|
25
40
|
}
|
|
26
41
|
function detectManagedBitcoindDbcacheMiB() {
|
|
27
|
-
|
|
42
|
+
try {
|
|
43
|
+
return resolveManagedBitcoindDbcacheMiB(totalmem());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return DEFAULT_DBCACHE_MIB;
|
|
47
|
+
}
|
|
28
48
|
}
|
|
29
49
|
function sleep(ms) {
|
|
30
50
|
return new Promise((resolve) => {
|
|
@@ -58,21 +78,10 @@ async function acquireFileLockWithRetry(lockPath, metadata, timeoutMs) {
|
|
|
58
78
|
function getWalletReplicaName(walletRootId) {
|
|
59
79
|
return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
|
|
60
80
|
}
|
|
61
|
-
async function readJsonFile(filePath) {
|
|
62
|
-
try {
|
|
63
|
-
return JSON.parse(await readFile(filePath, "utf8"));
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
81
|
async function listManagedBitcoindStatusCandidates(options) {
|
|
73
82
|
const candidates = new Map();
|
|
74
83
|
const addCandidate = async (statusPath, allowDataDirMismatch = false) => {
|
|
75
|
-
const status = await
|
|
84
|
+
const status = await readJsonFileIfPresent(statusPath);
|
|
76
85
|
if (status === null) {
|
|
77
86
|
return;
|
|
78
87
|
}
|
|
@@ -207,38 +216,6 @@ async function waitForRpcReady(rpc, cookieFile, expectedChain, timeoutMs) {
|
|
|
207
216
|
}
|
|
208
217
|
throw lastError instanceof Error ? lastError : new Error("bitcoind_rpc_timeout");
|
|
209
218
|
}
|
|
210
|
-
function validateManagedBitcoindStatus(status, options, runtimeRoot) {
|
|
211
|
-
const legacyRuntimeRoot = join(resolveManagedServicePaths(options.dataDir ?? "", options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID).runtimeRoot, status.walletRootId);
|
|
212
|
-
if (status.serviceApiVersion !== MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE) {
|
|
213
|
-
throw new Error("managed_bitcoind_service_version_mismatch");
|
|
214
|
-
}
|
|
215
|
-
if (status.chain !== options.chain
|
|
216
|
-
|| status.dataDir !== (options.dataDir ?? "")
|
|
217
|
-
|| (status.runtimeRoot !== runtimeRoot && status.runtimeRoot !== legacyRuntimeRoot)) {
|
|
218
|
-
throw new Error("managed_bitcoind_runtime_mismatch");
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function isRuntimeMismatchError(error) {
|
|
222
|
-
if (!(error instanceof Error)) {
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
return error.message.startsWith("bitcoind_chain_expected_")
|
|
226
|
-
|| error.message === "managed_bitcoind_runtime_mismatch";
|
|
227
|
-
}
|
|
228
|
-
function isUnreachableManagedBitcoindError(error) {
|
|
229
|
-
if (error instanceof Error) {
|
|
230
|
-
if ("code" in error) {
|
|
231
|
-
const code = error.code;
|
|
232
|
-
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET";
|
|
233
|
-
}
|
|
234
|
-
return error.message === "bitcoind_cookie_timeout"
|
|
235
|
-
|| error.message.includes("cookie file is unavailable")
|
|
236
|
-
|| error.message.includes("ECONNREFUSED")
|
|
237
|
-
|| error.message.includes("ECONNRESET")
|
|
238
|
-
|| error.message.includes("socket hang up");
|
|
239
|
-
}
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
219
|
function createBitcoindServiceStatus(options) {
|
|
243
220
|
return {
|
|
244
221
|
serviceApiVersion: MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE,
|
|
@@ -264,27 +241,16 @@ function createBitcoindServiceStatus(options) {
|
|
|
264
241
|
lastError: options.lastError,
|
|
265
242
|
};
|
|
266
243
|
}
|
|
267
|
-
function mapManagedBitcoindValidationError(error) {
|
|
268
|
-
return {
|
|
269
|
-
compatibility: error instanceof Error
|
|
270
|
-
? error.message === "managed_bitcoind_service_version_mismatch"
|
|
271
|
-
? "service-version-mismatch"
|
|
272
|
-
: "runtime-mismatch"
|
|
273
|
-
: "protocol-error",
|
|
274
|
-
status: null,
|
|
275
|
-
error: error instanceof Error ? error.message : "managed_bitcoind_protocol_error",
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
244
|
async function probeManagedBitcoindStatusCandidate(status, options, runtimeRoot) {
|
|
279
245
|
try {
|
|
280
|
-
|
|
246
|
+
validateManagedBitcoindObservedStatus(status, {
|
|
247
|
+
chain: options.chain,
|
|
248
|
+
dataDir: options.dataDir ?? "",
|
|
249
|
+
runtimeRoot,
|
|
250
|
+
});
|
|
281
251
|
}
|
|
282
252
|
catch (error) {
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
...mapped,
|
|
286
|
-
status,
|
|
287
|
-
};
|
|
253
|
+
return mapManagedBitcoindValidationError(error, status);
|
|
288
254
|
}
|
|
289
255
|
const rpc = createRpcClient(status.rpc);
|
|
290
256
|
try {
|
|
@@ -297,30 +263,12 @@ async function probeManagedBitcoindStatusCandidate(status, options, runtimeRoot)
|
|
|
297
263
|
};
|
|
298
264
|
}
|
|
299
265
|
catch (error) {
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
compatibility: "runtime-mismatch",
|
|
303
|
-
status,
|
|
304
|
-
error: "managed_bitcoind_runtime_mismatch",
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
if (isUnreachableManagedBitcoindError(error)) {
|
|
308
|
-
return {
|
|
309
|
-
compatibility: "unreachable",
|
|
310
|
-
status,
|
|
311
|
-
error: null,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
compatibility: "protocol-error",
|
|
316
|
-
status,
|
|
317
|
-
error: "managed_bitcoind_protocol_error",
|
|
318
|
-
};
|
|
266
|
+
return mapManagedBitcoindRuntimeProbeFailure(error, status);
|
|
319
267
|
}
|
|
320
268
|
}
|
|
321
269
|
async function resolveRuntimeConfig(statusPath, configPath, options) {
|
|
322
|
-
const previousStatus = await
|
|
323
|
-
const previousConfig = await
|
|
270
|
+
const previousStatus = await readJsonFileIfPresent(statusPath);
|
|
271
|
+
const previousConfig = await readJsonFileIfPresent(configPath);
|
|
324
272
|
const reserved = new Set();
|
|
325
273
|
const rpcPort = options.rpcPort
|
|
326
274
|
?? previousStatus?.rpc.port
|
|
@@ -545,7 +493,7 @@ async function clearManagedBitcoindRuntimeArtifacts(paths) {
|
|
|
545
493
|
export async function stopManagedBitcoindServiceWithLockHeld(options) {
|
|
546
494
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
547
495
|
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
548
|
-
const status = await
|
|
496
|
+
const status = await readJsonFileIfPresent(paths.bitcoindStatusPath);
|
|
549
497
|
const processId = status?.processId ?? null;
|
|
550
498
|
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
551
499
|
await clearManagedBitcoindRuntimeArtifacts(paths);
|
|
@@ -769,14 +717,16 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
769
717
|
shutdownTimeoutMs: resolvedOptions.shutdownTimeoutMs,
|
|
770
718
|
}, async () => {
|
|
771
719
|
const existingProbe = await probeManagedBitcoindService(resolvedOptions);
|
|
772
|
-
|
|
720
|
+
const existingDecision = resolveManagedBitcoindProbeDecision(existingProbe);
|
|
721
|
+
if (existingDecision.action === "attach") {
|
|
773
722
|
const existing = await tryAttachExistingManagedBitcoindService(resolvedOptions);
|
|
774
723
|
if (existing !== null) {
|
|
775
724
|
return existing;
|
|
776
725
|
}
|
|
726
|
+
throw new Error("managed_bitcoind_protocol_error");
|
|
777
727
|
}
|
|
778
|
-
if (
|
|
779
|
-
throw new Error(
|
|
728
|
+
if (existingDecision.action === "reject") {
|
|
729
|
+
throw new Error(existingDecision.error ?? "managed_bitcoind_protocol_error");
|
|
780
730
|
}
|
|
781
731
|
const paths = resolveManagedServicePaths(resolvedOptions.dataDir ?? "", resolvedOptions.walletRootId);
|
|
782
732
|
try {
|
|
@@ -787,14 +737,16 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
787
737
|
});
|
|
788
738
|
try {
|
|
789
739
|
const liveProbe = await probeManagedBitcoindService(resolvedOptions);
|
|
790
|
-
|
|
740
|
+
const liveDecision = resolveManagedBitcoindProbeDecision(liveProbe);
|
|
741
|
+
if (liveDecision.action === "attach") {
|
|
791
742
|
const reattached = await tryAttachExistingManagedBitcoindService(resolvedOptions);
|
|
792
743
|
if (reattached !== null) {
|
|
793
744
|
return reattached;
|
|
794
745
|
}
|
|
746
|
+
throw new Error("managed_bitcoind_protocol_error");
|
|
795
747
|
}
|
|
796
|
-
if (
|
|
797
|
-
throw new Error(
|
|
748
|
+
if (liveDecision.action === "reject") {
|
|
749
|
+
throw new Error(liveDecision.error ?? "managed_bitcoind_protocol_error");
|
|
798
750
|
}
|
|
799
751
|
const bitcoindPath = await getBitcoindPath();
|
|
800
752
|
await verifyBitcoindVersion(bitcoindPath);
|
|
@@ -915,7 +867,7 @@ export async function stopManagedBitcoindService(options) {
|
|
|
915
867
|
}
|
|
916
868
|
export async function readManagedBitcoindServiceStatusForTesting(dataDir, walletRootId = UNINITIALIZED_WALLET_ROOT_ID) {
|
|
917
869
|
const paths = resolveManagedServicePaths(dataDir, walletRootId);
|
|
918
|
-
return
|
|
870
|
+
return readJsonFileIfPresent(paths.bitcoindStatusPath);
|
|
919
871
|
}
|
|
920
872
|
export async function shutdownManagedBitcoindServiceForTesting(options) {
|
|
921
873
|
await stopManagedBitcoindService({
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { WalletAccessContext, WalletLoadedState } from "./types.js";
|
|
2
|
+
export declare function isWalletSecretAccessError(error: unknown): boolean;
|
|
3
|
+
export declare function mapWalletReadAccessError(error: unknown): Error;
|
|
4
|
+
export declare function normalizeLoadedWalletStateIfNeeded(options: WalletAccessContext & WalletLoadedState): Promise<WalletLoadedState>;
|
|
5
|
+
export declare function loadWalletStateForAccess(options: WalletAccessContext): Promise<WalletLoadedState>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { normalizeWalletStateRecord, persistWalletCoinControlStateIfNeeded, } from "../coin-control.js";
|
|
2
|
+
import { persistNormalizedWalletDescriptorStateIfNeeded } from "../descriptor-normalization.js";
|
|
3
|
+
import { normalizeMiningStateRecord } from "../mining/state.js";
|
|
4
|
+
import { createWalletSecretReference } from "../state/provider.js";
|
|
5
|
+
import { loadWalletState } from "../state/storage.js";
|
|
6
|
+
export function isWalletSecretAccessError(error) {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
return message.startsWith("wallet_secret_missing_")
|
|
9
|
+
|| message.startsWith("wallet_secret_provider_");
|
|
10
|
+
}
|
|
11
|
+
export function mapWalletReadAccessError(error) {
|
|
12
|
+
if (isWalletSecretAccessError(error)) {
|
|
13
|
+
return new Error("wallet_secret_provider_unavailable");
|
|
14
|
+
}
|
|
15
|
+
return new Error("local-state-corrupt");
|
|
16
|
+
}
|
|
17
|
+
export async function normalizeLoadedWalletStateIfNeeded(options) {
|
|
18
|
+
let state = options.state;
|
|
19
|
+
let source = options.source;
|
|
20
|
+
if (options.dataDir !== undefined) {
|
|
21
|
+
const node = await options.attachService({
|
|
22
|
+
dataDir: options.dataDir,
|
|
23
|
+
chain: "main",
|
|
24
|
+
startHeight: 0,
|
|
25
|
+
walletRootId: state.walletRootId,
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
const normalizedDescriptorState = await persistNormalizedWalletDescriptorStateIfNeeded({
|
|
29
|
+
state,
|
|
30
|
+
access: {
|
|
31
|
+
provider: options.provider,
|
|
32
|
+
secretReference: createWalletSecretReference(state.walletRootId),
|
|
33
|
+
},
|
|
34
|
+
paths: options.paths,
|
|
35
|
+
nowUnixMs: options.nowUnixMs,
|
|
36
|
+
replacePrimary: source === "backup",
|
|
37
|
+
rpc: options.rpcFactory(node.rpc),
|
|
38
|
+
});
|
|
39
|
+
state = normalizedDescriptorState.state;
|
|
40
|
+
source = normalizedDescriptorState.changed ? "primary" : source;
|
|
41
|
+
const reconciledCoinControl = await persistWalletCoinControlStateIfNeeded({
|
|
42
|
+
state,
|
|
43
|
+
access: {
|
|
44
|
+
provider: options.provider,
|
|
45
|
+
secretReference: createWalletSecretReference(state.walletRootId),
|
|
46
|
+
},
|
|
47
|
+
paths: options.paths,
|
|
48
|
+
nowUnixMs: options.nowUnixMs,
|
|
49
|
+
replacePrimary: source === "backup",
|
|
50
|
+
rpc: options.rpcFactory(node.rpc),
|
|
51
|
+
});
|
|
52
|
+
state = reconciledCoinControl.state;
|
|
53
|
+
source = reconciledCoinControl.changed ? "primary" : source;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await node.stop?.().catch(() => undefined);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
state: normalizeWalletStateRecord({
|
|
61
|
+
...state,
|
|
62
|
+
miningState: normalizeMiningStateRecord(state.miningState),
|
|
63
|
+
}),
|
|
64
|
+
source,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function loadWalletStateForAccess(options) {
|
|
68
|
+
const loaded = await loadWalletState({
|
|
69
|
+
primaryPath: options.paths.walletStatePath,
|
|
70
|
+
backupPath: options.paths.walletStateBackupPath,
|
|
71
|
+
}, {
|
|
72
|
+
provider: options.provider,
|
|
73
|
+
});
|
|
74
|
+
return await normalizeLoadedWalletStateIfNeeded({
|
|
75
|
+
...options,
|
|
76
|
+
state: loaded.state,
|
|
77
|
+
source: loaded.source,
|
|
78
|
+
});
|
|
79
|
+
}
|