@cogcoin/client 1.1.5 → 1.1.7

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 (132) hide show
  1. package/README.md +2 -2
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -7
  3. package/dist/bitcoind/indexer-daemon.js +39 -204
  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/bitcoind-runtime.d.ts +20 -0
  7. package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
  8. package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
  9. package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
  10. package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
  11. package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
  12. package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
  13. package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
  14. package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
  15. package/dist/bitcoind/managed-runtime/status.js +59 -0
  16. package/dist/bitcoind/managed-runtime/types.d.ts +77 -0
  17. package/dist/bitcoind/node.d.ts +2 -2
  18. package/dist/bitcoind/node.js +2 -2
  19. package/dist/bitcoind/rpc.d.ts +2 -1
  20. package/dist/bitcoind/rpc.js +53 -3
  21. package/dist/bitcoind/service.d.ts +2 -7
  22. package/dist/bitcoind/service.js +79 -207
  23. package/dist/cli/command-registry.d.ts +1 -1
  24. package/dist/cli/command-registry.js +2 -64
  25. package/dist/cli/commands/client-admin.js +3 -18
  26. package/dist/cli/commands/mining-runtime.js +4 -60
  27. package/dist/cli/commands/wallet-admin.js +6 -6
  28. package/dist/cli/context.js +1 -3
  29. package/dist/cli/mining-json.d.ts +1 -22
  30. package/dist/cli/mining-json.js +0 -23
  31. package/dist/cli/output.js +16 -2
  32. package/dist/cli/parse.js +0 -2
  33. package/dist/cli/preview-json.d.ts +1 -22
  34. package/dist/cli/preview-json.js +0 -19
  35. package/dist/cli/types.d.ts +1 -3
  36. package/dist/cli/wallet-format.js +1 -1
  37. package/dist/cli/workflow-hints.d.ts +1 -2
  38. package/dist/cli/workflow-hints.js +5 -8
  39. package/dist/wallet/lifecycle/access.d.ts +5 -0
  40. package/dist/wallet/lifecycle/access.js +79 -0
  41. package/dist/wallet/lifecycle/context.d.ts +26 -0
  42. package/dist/wallet/lifecycle/context.js +57 -0
  43. package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
  44. package/dist/wallet/lifecycle/managed-core.js +3 -63
  45. package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
  46. package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
  47. package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
  48. package/dist/wallet/lifecycle/repair-indexer.js +117 -0
  49. package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
  50. package/dist/wallet/lifecycle/repair-mining.js +5 -39
  51. package/dist/wallet/lifecycle/repair.d.ts +2 -4
  52. package/dist/wallet/lifecycle/repair.js +74 -318
  53. package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
  54. package/dist/wallet/lifecycle/setup-prompts.js +88 -0
  55. package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
  56. package/dist/wallet/lifecycle/setup-state.js +159 -0
  57. package/dist/wallet/lifecycle/setup.d.ts +3 -4
  58. package/dist/wallet/lifecycle/setup.js +47 -351
  59. package/dist/wallet/lifecycle/types.d.ts +33 -5
  60. package/dist/wallet/managed-core-wallet.d.ts +2 -0
  61. package/dist/wallet/managed-core-wallet.js +27 -1
  62. package/dist/wallet/mining/candidate.d.ts +1 -0
  63. package/dist/wallet/mining/candidate.js +38 -6
  64. package/dist/wallet/mining/competitiveness.d.ts +1 -0
  65. package/dist/wallet/mining/competitiveness.js +6 -0
  66. package/dist/wallet/mining/cycle.d.ts +2 -0
  67. package/dist/wallet/mining/cycle.js +14 -4
  68. package/dist/wallet/mining/engine-types.d.ts +1 -0
  69. package/dist/wallet/mining/index.d.ts +1 -1
  70. package/dist/wallet/mining/index.js +1 -1
  71. package/dist/wallet/mining/publish.d.ts +3 -0
  72. package/dist/wallet/mining/publish.js +78 -6
  73. package/dist/wallet/mining/runner.d.ts +0 -32
  74. package/dist/wallet/mining/runner.js +59 -104
  75. package/dist/wallet/mining/stop.d.ts +7 -0
  76. package/dist/wallet/mining/stop.js +23 -0
  77. package/dist/wallet/mining/supervisor.d.ts +2 -36
  78. package/dist/wallet/mining/supervisor.js +139 -246
  79. package/dist/wallet/read/context.d.ts +1 -5
  80. package/dist/wallet/read/context.js +20 -379
  81. package/dist/wallet/read/managed-services.d.ts +33 -0
  82. package/dist/wallet/read/managed-services.js +222 -0
  83. package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
  84. package/dist/wallet/state/client-password/bootstrap.js +3 -0
  85. package/dist/wallet/state/client-password/context.d.ts +10 -0
  86. package/dist/wallet/state/client-password/context.js +46 -0
  87. package/dist/wallet/state/client-password/crypto.d.ts +34 -0
  88. package/dist/wallet/state/client-password/crypto.js +117 -0
  89. package/dist/wallet/state/client-password/files.d.ts +10 -0
  90. package/dist/wallet/state/client-password/files.js +109 -0
  91. package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
  92. package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
  93. package/dist/wallet/state/client-password/messages.d.ts +3 -0
  94. package/dist/wallet/state/client-password/messages.js +9 -0
  95. package/dist/wallet/state/client-password/migration.d.ts +4 -0
  96. package/dist/wallet/state/client-password/migration.js +32 -0
  97. package/dist/wallet/state/client-password/prompts.d.ts +12 -0
  98. package/dist/wallet/state/client-password/prompts.js +79 -0
  99. package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
  100. package/dist/wallet/state/client-password/protected-secrets.js +90 -0
  101. package/dist/wallet/state/client-password/readiness.d.ts +4 -0
  102. package/dist/wallet/state/client-password/readiness.js +48 -0
  103. package/dist/wallet/state/client-password/references.d.ts +1 -0
  104. package/dist/wallet/state/client-password/references.js +56 -0
  105. package/dist/wallet/state/client-password/rotation.d.ts +6 -0
  106. package/dist/wallet/state/client-password/rotation.js +98 -0
  107. package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
  108. package/dist/wallet/state/client-password/session-policy.js +28 -0
  109. package/dist/wallet/state/client-password/session.d.ts +19 -0
  110. package/dist/wallet/state/client-password/session.js +170 -0
  111. package/dist/wallet/state/client-password/setup.d.ts +8 -0
  112. package/dist/wallet/state/client-password/setup.js +49 -0
  113. package/dist/wallet/state/client-password/types.d.ts +82 -0
  114. package/dist/wallet/state/client-password/types.js +5 -0
  115. package/dist/wallet/state/client-password.d.ts +7 -38
  116. package/dist/wallet/state/client-password.js +52 -937
  117. package/dist/wallet/tx/anchor.js +123 -216
  118. package/dist/wallet/tx/cog.js +294 -489
  119. package/dist/wallet/tx/common.d.ts +2 -0
  120. package/dist/wallet/tx/common.js +2 -0
  121. package/dist/wallet/tx/domain-admin.js +111 -220
  122. package/dist/wallet/tx/domain-market.js +401 -681
  123. package/dist/wallet/tx/executor.d.ts +176 -0
  124. package/dist/wallet/tx/executor.js +302 -0
  125. package/dist/wallet/tx/field.js +109 -215
  126. package/dist/wallet/tx/register.js +158 -269
  127. package/dist/wallet/tx/reputation.js +120 -227
  128. package/package.json +1 -1
  129. package/dist/wallet/mining/worker-main.js +0 -17
  130. package/dist/wallet/state/client-password-agent.d.ts +0 -1
  131. package/dist/wallet/state/client-password-agent.js +0 -211
  132. /package/dist/{wallet/mining/worker-main.d.ts → bitcoind/managed-runtime/types.js} +0 -0
@@ -0,0 +1,44 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { resolveManagedServicePaths } from "../service-paths.js";
4
+ import { readJsonFileIfPresent } from "./status.js";
5
+ export async function listManagedBitcoindStatusCandidates(options) {
6
+ const candidates = new Map();
7
+ const addCandidate = async (statusPath, allowDataDirMismatch = false) => {
8
+ const status = await readJsonFileIfPresent(statusPath);
9
+ if (status === null) {
10
+ return;
11
+ }
12
+ if (!allowDataDirMismatch && status.dataDir !== options.dataDir) {
13
+ return;
14
+ }
15
+ candidates.set(statusPath, status);
16
+ };
17
+ await addCandidate(options.expectedStatusPath, true);
18
+ try {
19
+ const entries = await readdir(options.runtimeRoot, {
20
+ withFileTypes: true,
21
+ });
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory()) {
24
+ continue;
25
+ }
26
+ const statusPath = join(options.runtimeRoot, entry.name, "bitcoind-status.json");
27
+ if (statusPath === options.expectedStatusPath) {
28
+ continue;
29
+ }
30
+ await addCandidate(statusPath);
31
+ }
32
+ }
33
+ catch {
34
+ // Missing runtime roots are handled by returning no candidates.
35
+ }
36
+ return [...candidates.entries()].map(([statusPath, status]) => ({
37
+ statusPath,
38
+ status,
39
+ }));
40
+ }
41
+ export async function readManagedBitcoindObservedStatus(options) {
42
+ const paths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
43
+ return readJsonFileIfPresent(paths.bitcoindStatusPath);
44
+ }
@@ -0,0 +1,34 @@
1
+ import type { WalletIndexerStatus } from "../../wallet/read/types.js";
2
+ import type { IndexerSnapshotHandle, IndexerSnapshotPayload } from "../indexer-daemon.js";
3
+ import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus, type ManagedIndexerTruthSource } from "../types.js";
4
+ import { buildManagedIndexerStatusFromSnapshotHandle } from "./status.js";
5
+ import type { IndexerDaemonProbeDecision, ManagedIndexerDaemonProbeResult, ManagedIndexerSnapshotLike } from "./types.js";
6
+ type IndexerRuntimeIdentityLike = {
7
+ serviceApiVersion: string;
8
+ schemaVersion: string;
9
+ walletRootId: string;
10
+ daemonInstanceId: string;
11
+ processId: number | null;
12
+ startedAtUnixMs: number;
13
+ state?: ManagedIndexerDaemonStatus["state"] | string;
14
+ };
15
+ export declare function validateIndexerRuntimeIdentity(identity: IndexerRuntimeIdentityLike, expectedWalletRootId: string): void;
16
+ export declare function validateIndexerDaemonStatus(status: ManagedIndexerDaemonObservedStatus, expectedWalletRootId: string): void;
17
+ export declare function validateIndexerSnapshotHandle(handle: IndexerSnapshotHandle, expectedWalletRootId: string): void;
18
+ export declare function validateIndexerSnapshotPayload(payload: IndexerSnapshotPayload, handle: IndexerSnapshotHandle, expectedWalletRootId: string): void;
19
+ export declare function mapIndexerDaemonValidationError<TClient>(error: unknown, status: ManagedIndexerDaemonObservedStatus): ManagedIndexerDaemonProbeResult<TClient>;
20
+ export declare function mapIndexerDaemonTransportError<TClient>(error: unknown): ManagedIndexerDaemonProbeResult<TClient>;
21
+ export declare function isStaleIndexerDaemonVersion(status: ManagedIndexerDaemonObservedStatus | null, expectedBinaryVersion: string | null | undefined): boolean;
22
+ export declare function resolveIndexerDaemonProbeDecision<TClient>(options: {
23
+ probe: ManagedIndexerDaemonProbeResult<TClient>;
24
+ expectedBinaryVersion: string | null | undefined;
25
+ }): IndexerDaemonProbeDecision;
26
+ export declare function deriveManagedIndexerWalletStatus(options: {
27
+ daemonStatus: ManagedIndexerDaemonStatus | null;
28
+ observedStatus?: ManagedIndexerDaemonObservedStatus | null;
29
+ snapshot: ManagedIndexerSnapshotLike | null;
30
+ source: ManagedIndexerTruthSource;
31
+ now: number;
32
+ startupError: string | null;
33
+ }): WalletIndexerStatus;
34
+ export { buildManagedIndexerStatusFromSnapshotHandle };
@@ -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,15 @@
1
+ import type { ManagedIndexerDaemonObservedStatus } from "../types.js";
2
+ import type { ManagedIndexerDaemonProbeResult, ManagedIndexerRuntimeOptionsLike, ManagedIndexerRuntimePathsLike, ManagedRuntimeLockLike } from "./types.js";
3
+ type ManagedIndexerRuntimeDependencies<TOptions, TClient> = {
4
+ getPaths(options: TOptions): ManagedIndexerRuntimePathsLike;
5
+ probeDaemon(options: TOptions, paths: ManagedIndexerRuntimePathsLike): Promise<ManagedIndexerDaemonProbeResult<TClient>>;
6
+ requestBackgroundFollow(client: TClient, observedStatus: ManagedIndexerDaemonObservedStatus | null): Promise<TClient>;
7
+ closeClient(client: TClient): Promise<void>;
8
+ acquireStartLock(options: TOptions, paths: ManagedIndexerRuntimePathsLike): Promise<ManagedRuntimeLockLike>;
9
+ startDaemon(options: TOptions, paths: ManagedIndexerRuntimePathsLike): Promise<TClient>;
10
+ stopWithLockHeld(options: TOptions, paths: ManagedIndexerRuntimePathsLike, processId: number | null): Promise<unknown>;
11
+ isLockBusyError(error: unknown): boolean;
12
+ sleep(ms: number): Promise<void>;
13
+ };
14
+ export declare function attachOrStartManagedIndexerRuntime<TOptions extends ManagedIndexerRuntimeOptionsLike, TClient>(options: TOptions, dependencies: ManagedIndexerRuntimeDependencies<TOptions, TClient>): Promise<TClient>;
15
+ export {};
@@ -0,0 +1,82 @@
1
+ import { resolveIndexerDaemonProbeDecision } from "./indexer-policy.js";
2
+ async function waitForManagedIndexerRuntime(options, dependencies, paths) {
3
+ const deadline = Date.now() + options.startupTimeoutMs;
4
+ while (Date.now() < deadline) {
5
+ const probe = await dependencies.probeDaemon(options, paths);
6
+ if (probe.compatibility === "compatible" && probe.client !== null) {
7
+ await dependencies.closeClient(probe.client).catch(() => undefined);
8
+ return;
9
+ }
10
+ if (probe.compatibility !== "unreachable") {
11
+ throw new Error(probe.error ?? "indexer_daemon_protocol_error");
12
+ }
13
+ await dependencies.sleep(250);
14
+ }
15
+ throw new Error("indexer_daemon_start_timeout");
16
+ }
17
+ export async function attachOrStartManagedIndexerRuntime(options, dependencies) {
18
+ const paths = dependencies.getPaths(options);
19
+ const existingProbe = await dependencies.probeDaemon(options, paths);
20
+ const existingDecision = resolveIndexerDaemonProbeDecision({
21
+ probe: existingProbe,
22
+ expectedBinaryVersion: options.expectedBinaryVersion ?? null,
23
+ });
24
+ if (existingDecision.action === "attach" && existingProbe.client !== null) {
25
+ try {
26
+ return await dependencies.requestBackgroundFollow(existingProbe.client, existingProbe.status);
27
+ }
28
+ catch {
29
+ await dependencies.closeClient(existingProbe.client).catch(() => undefined);
30
+ }
31
+ }
32
+ if (existingDecision.action === "replace" && existingProbe.client !== null) {
33
+ await dependencies.closeClient(existingProbe.client).catch(() => undefined);
34
+ }
35
+ if (existingDecision.action === "reject") {
36
+ throw new Error(existingDecision.error ?? "indexer_daemon_protocol_error");
37
+ }
38
+ try {
39
+ const lock = await dependencies.acquireStartLock(options, paths);
40
+ try {
41
+ const liveProbe = await dependencies.probeDaemon(options, paths);
42
+ const liveDecision = resolveIndexerDaemonProbeDecision({
43
+ probe: liveProbe,
44
+ expectedBinaryVersion: options.expectedBinaryVersion ?? null,
45
+ });
46
+ if (liveDecision.action === "attach" && liveProbe.client !== null) {
47
+ try {
48
+ return await dependencies.requestBackgroundFollow(liveProbe.client, liveProbe.status);
49
+ }
50
+ catch {
51
+ await dependencies.closeClient(liveProbe.client).catch(() => undefined);
52
+ await dependencies.stopWithLockHeld(options, paths, liveProbe.status?.processId ?? null);
53
+ }
54
+ }
55
+ else if (liveDecision.action === "replace" && liveProbe.client !== null) {
56
+ await dependencies.closeClient(liveProbe.client).catch(() => undefined);
57
+ await dependencies.stopWithLockHeld(options, paths, liveProbe.status?.processId ?? null);
58
+ }
59
+ else if (liveDecision.action === "reject") {
60
+ throw new Error(liveDecision.error ?? "indexer_daemon_protocol_error");
61
+ }
62
+ const daemon = await dependencies.startDaemon(options, paths);
63
+ try {
64
+ return await dependencies.requestBackgroundFollow(daemon, null);
65
+ }
66
+ catch (error) {
67
+ await dependencies.closeClient(daemon).catch(() => undefined);
68
+ throw new Error("indexer_daemon_background_follow_recovery_failed", { cause: error });
69
+ }
70
+ }
71
+ finally {
72
+ await lock.release();
73
+ }
74
+ }
75
+ catch (error) {
76
+ if (dependencies.isLockBusyError(error)) {
77
+ await waitForManagedIndexerRuntime(options, dependencies, paths);
78
+ return attachOrStartManagedIndexerRuntime(options, dependencies);
79
+ }
80
+ throw error;
81
+ }
82
+ }
@@ -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,77 @@
1
+ import type { ClientTip } from "../../types.js";
2
+ import type { ManagedServicePaths } from "../service-paths.js";
3
+ import type { ManagedBitcoindObservedStatus, ManagedIndexerDaemonObservedStatus, ManagedIndexerTruthSource } from "../types.js";
4
+ import type { WalletBitcoindStatus, WalletIndexerStatus, WalletNodeStatus, WalletServiceHealth, WalletSnapshotView } from "../../wallet/read/types.js";
5
+ export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
6
+ export interface ManagedBitcoindServiceProbeResult {
7
+ compatibility: ManagedBitcoindServiceCompatibility;
8
+ status: ManagedBitcoindObservedStatus | null;
9
+ error: string | null;
10
+ }
11
+ export interface ManagedBitcoindStatusCandidate {
12
+ status: ManagedBitcoindObservedStatus;
13
+ statusPath: string;
14
+ }
15
+ export type IndexerDaemonCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "schema-mismatch" | "unreachable" | "protocol-error";
16
+ export interface ManagedIndexerDaemonProbeResult<TClient> {
17
+ compatibility: IndexerDaemonCompatibility;
18
+ status: ManagedIndexerDaemonObservedStatus | null;
19
+ client: TClient | null;
20
+ error: string | null;
21
+ }
22
+ export interface ManagedBitcoindProbeDecision {
23
+ action: "attach" | "start" | "reject";
24
+ error: string | null;
25
+ }
26
+ export interface IndexerDaemonProbeDecision {
27
+ action: "attach" | "replace" | "start" | "reject";
28
+ error: string | null;
29
+ }
30
+ export interface ManagedRuntimeLockLike {
31
+ release(): Promise<void>;
32
+ }
33
+ export type ManagedBitcoindRuntimePathsLike = ManagedServicePaths;
34
+ export type ManagedIndexerRuntimePathsLike = ManagedServicePaths;
35
+ export interface ManagedBitcoindRuntimeOptionsLike {
36
+ dataDir: string;
37
+ walletRootId: string;
38
+ startupTimeoutMs: number;
39
+ }
40
+ export interface ManagedIndexerRuntimeOptionsLike {
41
+ dataDir: string;
42
+ walletRootId: string;
43
+ startupTimeoutMs: number;
44
+ shutdownTimeoutMs?: number;
45
+ expectedBinaryVersion?: string | null;
46
+ }
47
+ export interface ManagedIndexerSnapshotLike {
48
+ tip: ClientTip | null;
49
+ daemonInstanceId?: string | null;
50
+ snapshotSeq?: string | null;
51
+ openedAtUnixMs?: number | null;
52
+ }
53
+ export interface ManagedIndexerStatusProjection {
54
+ status: ManagedIndexerDaemonObservedStatus | null;
55
+ source: ManagedIndexerTruthSource;
56
+ snapshotTip: ClientTip | null;
57
+ daemonInstanceId: string | null;
58
+ snapshotSeq: string | null;
59
+ openedAtUnixMs: number | null;
60
+ }
61
+ export interface ManagedWalletNodeConnection<TNodeHandle, TRpc> {
62
+ handle: TNodeHandle | null;
63
+ rpc: TRpc | null;
64
+ status: WalletNodeStatus | null;
65
+ observedStatus: ManagedBitcoindObservedStatus | null;
66
+ error: string | null;
67
+ }
68
+ export interface ManagedWalletReadServiceBundle<TNodeHandle, TRpc, TDaemonClient> {
69
+ node: ManagedWalletNodeConnection<TNodeHandle, TRpc>;
70
+ bitcoind: WalletBitcoindStatus;
71
+ nodeHealth: WalletServiceHealth;
72
+ nodeMessage: string | null;
73
+ daemonClient: TDaemonClient | null;
74
+ indexer: WalletIndexerStatus;
75
+ snapshot: WalletSnapshotView | null;
76
+ close(): Promise<void>;
77
+ }
@@ -1,8 +1,8 @@
1
1
  import { resolveDefaultBitcoindDataDirForTesting } from "../app-paths.js";
2
- import { BitcoinRpcClient } from "./rpc.js";
2
+ import { BitcoinRpcClient, type RpcTransportOptions } from "./rpc.js";
3
3
  import type { BitcoindRpcConfig, InternalManagedBitcoindOptions, ManagedBitcoindNodeHandle } from "./types.js";
4
4
  export { resolveDefaultBitcoindDataDirForTesting };
5
5
  export declare function buildBitcoindArgsForTesting(options: InternalManagedBitcoindOptions, rpcPort: number, zmqPort: number, p2pPort: number): string[];
6
6
  export declare function validateNodeConfigForTesting(rpcClient: BitcoinRpcClient, expectedChain: "main" | "regtest", zmqEndpoint: string): Promise<void>;
7
7
  export declare function launchManagedBitcoindNode(options: InternalManagedBitcoindOptions): Promise<ManagedBitcoindNodeHandle>;
8
- export declare function createRpcClient(config: BitcoindRpcConfig): BitcoinRpcClient;
8
+ export declare function createRpcClient(config: BitcoindRpcConfig, options?: RpcTransportOptions): BitcoinRpcClient;
@@ -217,6 +217,6 @@ export async function launchManagedBitcoindNode(options) {
217
217
  },
218
218
  };
219
219
  }
220
- export function createRpcClient(config) {
221
- return new BitcoinRpcClient(config.url, config.cookieFile);
220
+ export function createRpcClient(config, options = {}) {
221
+ return new BitcoinRpcClient(config.url, config.cookieFile, options);
222
222
  }
@@ -7,9 +7,10 @@ interface RpcResponsePayload {
7
7
  readonly statusCode: number;
8
8
  readonly bodyText: string;
9
9
  }
10
- interface RpcTransportOptions {
10
+ export interface RpcTransportOptions {
11
11
  fetchImpl?: typeof fetch;
12
12
  requestTimeoutMs?: number;
13
+ abortSignal?: AbortSignal;
13
14
  requestImpl?: (request: {
14
15
  url: URL;
15
16
  payload: RpcRequestPayload;
@@ -1,17 +1,20 @@
1
1
  import { request as httpRequest } from "node:http";
2
2
  import { request as httpsRequest } from "node:https";
3
3
  import { readFile } from "node:fs/promises";
4
+ const DEFAULT_MANAGED_RPC_REQUEST_TIMEOUT_MS = 30_000;
4
5
  export class BitcoinRpcClient {
5
6
  #url;
6
7
  #cookieFile;
7
8
  #fetchImpl;
8
9
  #requestTimeoutMs;
10
+ #abortSignal;
9
11
  #requestImpl;
10
12
  constructor(url, cookieFile, options = {}) {
11
13
  this.#url = url;
12
14
  this.#cookieFile = cookieFile;
13
15
  this.#fetchImpl = options.fetchImpl ?? fetch;
14
- this.#requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
16
+ this.#requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_MANAGED_RPC_REQUEST_TIMEOUT_MS;
17
+ this.#abortSignal = options.abortSignal;
15
18
  this.#requestImpl = options.requestImpl ?? this.#sendNodeRequest.bind(this);
16
19
  }
17
20
  async call(method, params = []) {
@@ -25,20 +28,67 @@ export class BitcoinRpcClient {
25
28
  async #callAtUrl(urlString, method, params = []) {
26
29
  const payload = await this.#buildRequestPayload(method, params);
27
30
  let response;
28
- const abortSignal = AbortSignal.timeout(this.#requestTimeoutMs);
31
+ const requestSignal = this.#createRequestSignal();
29
32
  try {
30
33
  response = await this.#fetchImpl(urlString, {
31
34
  method: "POST",
32
35
  headers: payload.headers,
33
36
  body: payload.body,
34
- signal: abortSignal,
37
+ signal: requestSignal.signal,
35
38
  });
36
39
  }
37
40
  catch (error) {
41
+ if (this.#abortSignal?.aborted) {
42
+ const reason = this.#abortSignal.reason;
43
+ if (reason instanceof Error) {
44
+ throw reason;
45
+ }
46
+ }
38
47
  throw new Error(this.#describeTransportError(urlString, method, error), { cause: error });
39
48
  }
49
+ finally {
50
+ requestSignal.dispose();
51
+ }
40
52
  return this.#parseResponse(method, response.status, await response.text());
41
53
  }
54
+ #createRequestSignal() {
55
+ const timeoutSignal = AbortSignal.timeout(this.#requestTimeoutMs);
56
+ if (this.#abortSignal === undefined) {
57
+ return {
58
+ signal: timeoutSignal,
59
+ dispose() { },
60
+ };
61
+ }
62
+ const controller = new AbortController();
63
+ const handleAbort = (source) => {
64
+ controller.abort(source.reason);
65
+ };
66
+ const forwardTimeout = () => {
67
+ handleAbort(timeoutSignal);
68
+ };
69
+ const forwardAbort = () => {
70
+ handleAbort(this.#abortSignal);
71
+ };
72
+ if (timeoutSignal.aborted) {
73
+ handleAbort(timeoutSignal);
74
+ }
75
+ else {
76
+ timeoutSignal.addEventListener("abort", forwardTimeout, { once: true });
77
+ }
78
+ if (this.#abortSignal.aborted) {
79
+ handleAbort(this.#abortSignal);
80
+ }
81
+ else {
82
+ this.#abortSignal.addEventListener("abort", forwardAbort, { once: true });
83
+ }
84
+ return {
85
+ signal: controller.signal,
86
+ dispose: () => {
87
+ timeoutSignal.removeEventListener("abort", forwardTimeout);
88
+ this.#abortSignal?.removeEventListener("abort", forwardAbort);
89
+ },
90
+ };
91
+ }
42
92
  async #buildRequestPayload(method, params) {
43
93
  let cookie;
44
94
  try {