@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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -7
  3. package/dist/bitcoind/indexer-daemon.js +43 -158
  4. package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
  5. package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
  6. package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
  7. package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
  8. package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
  9. package/dist/bitcoind/managed-runtime/status.js +59 -0
  10. package/dist/bitcoind/managed-runtime/types.d.ts +37 -0
  11. package/dist/bitcoind/managed-runtime/types.js +1 -0
  12. package/dist/bitcoind/service.d.ts +2 -7
  13. package/dist/bitcoind/service.js +46 -94
  14. package/dist/wallet/lifecycle/access.d.ts +5 -0
  15. package/dist/wallet/lifecycle/access.js +79 -0
  16. package/dist/wallet/lifecycle/context.d.ts +26 -0
  17. package/dist/wallet/lifecycle/context.js +58 -0
  18. package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
  19. package/dist/wallet/lifecycle/managed-core.js +3 -63
  20. package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
  21. package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
  22. package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
  23. package/dist/wallet/lifecycle/repair-indexer.js +117 -0
  24. package/dist/wallet/lifecycle/repair.d.ts +2 -4
  25. package/dist/wallet/lifecycle/repair.js +77 -318
  26. package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
  27. package/dist/wallet/lifecycle/setup-prompts.js +88 -0
  28. package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
  29. package/dist/wallet/lifecycle/setup-state.js +159 -0
  30. package/dist/wallet/lifecycle/setup.d.ts +3 -4
  31. package/dist/wallet/lifecycle/setup.js +45 -351
  32. package/dist/wallet/lifecycle/types.d.ts +33 -2
  33. package/dist/wallet/read/context.js +13 -188
  34. 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 {};
@@ -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 = 2048;
23
+ const DEFAULT_DBCACHE_MIB = 450;
21
24
  const claimedUninitializedRuntimeKeys = new Set();
25
+ const GIB = 1024 ** 3;
22
26
  export function resolveManagedBitcoindDbcacheMiB(totalRamBytes) {
23
- void totalRamBytes;
24
- return DEFAULT_DBCACHE_MIB;
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
- return DEFAULT_DBCACHE_MIB;
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 readJsonFile(statusPath);
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
- validateManagedBitcoindStatus(status, options, runtimeRoot);
246
+ validateManagedBitcoindObservedStatus(status, {
247
+ chain: options.chain,
248
+ dataDir: options.dataDir ?? "",
249
+ runtimeRoot,
250
+ });
281
251
  }
282
252
  catch (error) {
283
- const mapped = mapManagedBitcoindValidationError(error);
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
- if (isRuntimeMismatchError(error)) {
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 readJsonFile(statusPath);
323
- const previousConfig = await readJsonFile(configPath);
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 readJsonFile(paths.bitcoindStatusPath);
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
- if (existingProbe.compatibility === "compatible") {
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 (existingProbe.compatibility !== "unreachable") {
779
- throw new Error(existingProbe.error ?? "managed_bitcoind_protocol_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
- if (liveProbe.compatibility === "compatible") {
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 (liveProbe.compatibility !== "unreachable") {
797
- throw new Error(liveProbe.error ?? "managed_bitcoind_protocol_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 readJsonFile(paths.bitcoindStatusPath);
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
+ }