@cogcoin/client 1.1.9 → 1.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bitcoind/client/managed-client.d.ts +2 -0
- package/dist/bitcoind/client/managed-client.js +6 -0
- package/dist/bitcoind/indexer-daemon/background-follow.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/background-follow.js +132 -0
- package/dist/bitcoind/indexer-daemon/client.d.ts +12 -0
- package/dist/bitcoind/indexer-daemon/client.js +137 -0
- package/dist/bitcoind/indexer-daemon/lifecycle.d.ts +30 -0
- package/dist/bitcoind/indexer-daemon/lifecycle.js +153 -0
- package/dist/bitcoind/indexer-daemon/process.d.ts +35 -0
- package/dist/bitcoind/indexer-daemon/process.js +140 -0
- package/dist/bitcoind/indexer-daemon/runtime.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/runtime.js +204 -0
- package/dist/bitcoind/indexer-daemon/server.d.ts +12 -0
- package/dist/bitcoind/indexer-daemon/server.js +87 -0
- package/dist/bitcoind/indexer-daemon/snapshot-leases.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/snapshot-leases.js +139 -0
- package/dist/bitcoind/indexer-daemon/status.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/status.js +282 -0
- package/dist/bitcoind/indexer-daemon/types.d.ts +141 -0
- package/dist/bitcoind/indexer-daemon/types.js +1 -0
- package/dist/bitcoind/indexer-daemon-main.js +14 -665
- package/dist/bitcoind/indexer-daemon.d.ts +4 -132
- package/dist/bitcoind/indexer-daemon.js +2 -417
- package/dist/bitcoind/managed-bitcoind-service-config.d.ts +30 -0
- package/dist/bitcoind/managed-bitcoind-service-config.js +202 -0
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.d.ts +28 -0
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +296 -0
- package/dist/bitcoind/managed-bitcoind-service-process.d.ts +8 -0
- package/dist/bitcoind/managed-bitcoind-service-process.js +48 -0
- package/dist/bitcoind/managed-bitcoind-service-replica.d.ts +8 -0
- package/dist/bitcoind/managed-bitcoind-service-replica.js +142 -0
- package/dist/bitcoind/managed-bitcoind-service-status.d.ts +42 -0
- package/dist/bitcoind/managed-bitcoind-service-status.js +170 -0
- package/dist/bitcoind/managed-bitcoind-service-types.d.ts +36 -0
- package/dist/bitcoind/managed-bitcoind-service-types.js +1 -0
- package/dist/bitcoind/service.d.ts +7 -63
- package/dist/bitcoind/service.js +7 -797
- package/dist/cli/mining-format.js +6 -1
- package/dist/cli/wallet-format/balance.js +1 -1
- package/dist/client/default-client.d.ts +3 -1
- package/dist/client/default-client.js +22 -0
- package/dist/types.d.ts +13 -1
- package/dist/wallet/fs/atomic.d.ts +11 -2
- package/dist/wallet/fs/atomic.js +45 -5
- package/dist/wallet/mining/cycle.js +4 -4
- package/dist/wallet/mining/engine-types.d.ts +1 -0
- package/dist/wallet/mining/engine-types.js +9 -1
- package/dist/wallet/mining/projection.d.ts +1 -0
- package/dist/wallet/mining/projection.js +15 -1
- package/dist/wallet/mining/publish.js +3 -6
- package/dist/wallet/mining/runner.js +30 -18
- package/dist/wallet/mining/visualizer-sync.js +7 -9
- package/dist/wallet/mining/visualizer.js +9 -7
- package/dist/wallet/read/context.d.ts +4 -10
- package/dist/wallet/read/context.js +6 -228
- package/dist/wallet/read/local-state.d.ts +36 -0
- package/dist/wallet/read/local-state.js +259 -0
- package/dist/wallet/read/managed-bitcoind.d.ts +30 -0
- package/dist/wallet/read/managed-bitcoind.js +138 -0
- package/dist/wallet/read/managed-indexer.d.ts +23 -0
- package/dist/wallet/read/managed-indexer.js +87 -0
- package/dist/wallet/read/managed-services.d.ts +6 -21
- package/dist/wallet/read/managed-services.js +23 -196
- package/dist/wallet/read/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,23 +1,7 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import net from "node:net";
|
|
3
|
-
import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
|
|
4
|
-
import { loadBundledGenesisParameters, serializeIndexerState } from "@cogcoin/indexer";
|
|
5
|
-
import { openManagedBitcoindClientInternal } from "./client.js";
|
|
6
|
-
import { DEFAULT_SNAPSHOT_METADATA } from "./bootstrap.js";
|
|
7
|
-
import { openClient } from "../client.js";
|
|
8
1
|
import { readPackageVersionFromDisk } from "../package-version.js";
|
|
9
|
-
import {
|
|
10
|
-
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
11
|
-
import { createRpcClient } from "./node.js";
|
|
12
|
-
import { normalizeCogcoinProcessingStartHeight } from "./processing-start-height.js";
|
|
13
|
-
import { createBootstrapProgress } from "./progress/formatting.js";
|
|
2
|
+
import { loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
14
3
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
15
|
-
import {
|
|
16
|
-
const SNAPSHOT_TTL_MS = 30_000;
|
|
17
|
-
const HEARTBEAT_INTERVAL_MS = 1_000;
|
|
18
|
-
const FORCE_RESUME_ERROR_ENV = "COGCOIN_TEST_INDEXER_DAEMON_FORCE_RESUME_ERROR";
|
|
19
|
-
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS = 30_000;
|
|
20
|
-
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR = "indexer_daemon_background_follow_resume_timeout";
|
|
4
|
+
import { createIndexerDaemonRuntime } from "./indexer-daemon/runtime.js";
|
|
21
5
|
function parseArg(name) {
|
|
22
6
|
const prefix = `--${name}=`;
|
|
23
7
|
const value = process.argv.find((entry) => entry.startsWith(prefix));
|
|
@@ -26,653 +10,26 @@ function parseArg(name) {
|
|
|
26
10
|
}
|
|
27
11
|
return value.slice(prefix.length);
|
|
28
12
|
}
|
|
29
|
-
async function readJsonFile(filePath) {
|
|
30
|
-
try {
|
|
31
|
-
return JSON.parse(await readFile(filePath, "utf8"));
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async function readManagedBitcoindStatus(paths) {
|
|
41
|
-
return readJsonFile(paths.bitcoindStatusPath);
|
|
42
|
-
}
|
|
43
|
-
async function withTimeout(promise, timeoutMs, errorCode) {
|
|
44
|
-
let timeoutId = null;
|
|
45
|
-
try {
|
|
46
|
-
return await Promise.race([
|
|
47
|
-
promise,
|
|
48
|
-
new Promise((_, reject) => {
|
|
49
|
-
timeoutId = setTimeout(() => reject(new Error(errorCode)), timeoutMs);
|
|
50
|
-
}),
|
|
51
|
-
]);
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
if (timeoutId !== null) {
|
|
55
|
-
clearTimeout(timeoutId);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function createSnapshotKey(appliedTip) {
|
|
60
|
-
return appliedTip === null
|
|
61
|
-
? "__null__"
|
|
62
|
-
: [
|
|
63
|
-
appliedTip.height,
|
|
64
|
-
appliedTip.blockHashHex,
|
|
65
|
-
appliedTip.stateHashHex ?? "",
|
|
66
|
-
].join(":");
|
|
67
|
-
}
|
|
68
|
-
function createManagedBitcoindCookieUnavailableMessage(cookieFile) {
|
|
69
|
-
return `The managed Bitcoin RPC cookie file is unavailable at ${cookieFile} while preparing getblockchaininfo. The managed node is not running or is shutting down.`;
|
|
70
|
-
}
|
|
71
|
-
async function readCoreTipStatus(paths) {
|
|
72
|
-
const runtimeConfig = await readJsonFile(paths.bitcoindRuntimeConfigPath).catch(() => null);
|
|
73
|
-
if (runtimeConfig?.rpc === undefined || runtimeConfig.rpc === null) {
|
|
74
|
-
return {
|
|
75
|
-
rpcReachable: false,
|
|
76
|
-
coreBestHeight: null,
|
|
77
|
-
coreBestHash: null,
|
|
78
|
-
error: "managed_bitcoind_runtime_config_unavailable",
|
|
79
|
-
prerequisiteUnavailable: true,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
try {
|
|
83
|
-
await access(runtimeConfig.rpc.cookieFile, constants.R_OK);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return {
|
|
87
|
-
rpcReachable: false,
|
|
88
|
-
coreBestHeight: null,
|
|
89
|
-
coreBestHash: null,
|
|
90
|
-
error: createManagedBitcoindCookieUnavailableMessage(runtimeConfig.rpc.cookieFile),
|
|
91
|
-
prerequisiteUnavailable: true,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
const rpc = createRpcClient(runtimeConfig.rpc);
|
|
96
|
-
const info = await rpc.getBlockchainInfo();
|
|
97
|
-
return {
|
|
98
|
-
rpcReachable: true,
|
|
99
|
-
coreBestHeight: info.blocks,
|
|
100
|
-
coreBestHash: info.bestblockhash,
|
|
101
|
-
error: null,
|
|
102
|
-
prerequisiteUnavailable: false,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
return {
|
|
107
|
-
rpcReachable: false,
|
|
108
|
-
coreBestHeight: null,
|
|
109
|
-
coreBestHash: null,
|
|
110
|
-
error: error instanceof Error ? error.message : String(error),
|
|
111
|
-
prerequisiteUnavailable: false,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async function readAppliedTipStatus(databasePath) {
|
|
116
|
-
try {
|
|
117
|
-
const store = await openSqliteStore({ filename: databasePath });
|
|
118
|
-
try {
|
|
119
|
-
const client = await openClient({ store });
|
|
120
|
-
try {
|
|
121
|
-
return {
|
|
122
|
-
appliedTip: await client.getTip(),
|
|
123
|
-
error: null,
|
|
124
|
-
schemaMismatch: false,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
finally {
|
|
128
|
-
await client.close();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
await store.close().catch(() => undefined);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
-
return {
|
|
138
|
-
appliedTip: null,
|
|
139
|
-
error: message,
|
|
140
|
-
schemaMismatch: message === "sqlite_store_schema_version_unsupported",
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async function loadSnapshot(databasePath) {
|
|
145
|
-
const store = await openSqliteStore({ filename: databasePath });
|
|
146
|
-
try {
|
|
147
|
-
const client = await openClient({ store });
|
|
148
|
-
try {
|
|
149
|
-
const [tip, state] = await Promise.all([client.getTip(), client.getState()]);
|
|
150
|
-
return {
|
|
151
|
-
token: randomUUID(),
|
|
152
|
-
stateBase64: Buffer.from(serializeIndexerState(state)).toString("base64"),
|
|
153
|
-
tip,
|
|
154
|
-
expiresAtUnixMs: Date.now() + SNAPSHOT_TTL_MS,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
finally {
|
|
158
|
-
await client.close();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
await store.close().catch(() => undefined);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
13
|
async function main() {
|
|
166
14
|
const dataDir = parseArg("data-dir");
|
|
167
15
|
const databasePath = parseArg("database-path");
|
|
168
16
|
const walletRootId = parseArg("wallet-root-id") || UNINITIALIZED_WALLET_ROOT_ID;
|
|
169
17
|
const paths = resolveManagedServicePaths(dataDir, walletRootId);
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const startedAtUnixMs = Date.now();
|
|
174
|
-
const snapshots = new Map();
|
|
175
|
-
let state = "starting";
|
|
176
|
-
let heartbeatAtUnixMs = startedAtUnixMs;
|
|
177
|
-
let updatedAtUnixMs = startedAtUnixMs;
|
|
178
|
-
let rpcReachable = false;
|
|
179
|
-
let coreBestHeight = null;
|
|
180
|
-
let coreBestHash = null;
|
|
181
|
-
let appliedTipHeight = null;
|
|
182
|
-
let appliedTipHash = null;
|
|
183
|
-
let snapshotSeqCounter = 0;
|
|
184
|
-
let snapshotSeq = null;
|
|
185
|
-
let lastSnapshotKey;
|
|
186
|
-
let lastAppliedAtUnixMs = null;
|
|
187
|
-
let lastError = null;
|
|
188
|
-
let hasSuccessfulCoreTipRefresh = false;
|
|
189
|
-
let backgroundStore = null;
|
|
190
|
-
let backgroundClient = null;
|
|
191
|
-
let backgroundResumePromise = null;
|
|
192
|
-
let backgroundFollowError = null;
|
|
193
|
-
let backgroundFollowActive = false;
|
|
194
|
-
let bootstrapPhase = "paused";
|
|
195
|
-
let bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
|
|
196
|
-
let cogcoinSyncHeight = null;
|
|
197
|
-
let cogcoinSyncTargetHeight = null;
|
|
198
|
-
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
199
|
-
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
200
|
-
const observeAppliedTip = (appliedTip, now) => {
|
|
201
|
-
appliedTipHeight = appliedTip?.height ?? null;
|
|
202
|
-
appliedTipHash = appliedTip?.blockHashHex ?? null;
|
|
203
|
-
const snapshotKey = createSnapshotKey(appliedTip);
|
|
204
|
-
if (lastSnapshotKey !== snapshotKey) {
|
|
205
|
-
snapshotSeqCounter += 1;
|
|
206
|
-
snapshotSeq = String(snapshotSeqCounter);
|
|
207
|
-
lastSnapshotKey = snapshotKey;
|
|
208
|
-
lastAppliedAtUnixMs = now;
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
const deriveLeaseState = (coreStatus, appliedTip) => {
|
|
212
|
-
if (coreStatus.error !== null) {
|
|
213
|
-
return {
|
|
214
|
-
state: coreStatus.prerequisiteUnavailable && !hasSuccessfulCoreTipRefresh ? "starting" : "failed",
|
|
215
|
-
lastError: coreStatus.error,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
hasSuccessfulCoreTipRefresh = true;
|
|
219
|
-
if (coreStatus.coreBestHeight !== null
|
|
220
|
-
&& appliedTip?.height !== undefined
|
|
221
|
-
&& coreStatus.coreBestHash !== null
|
|
222
|
-
&& appliedTip?.blockHashHex !== undefined) {
|
|
223
|
-
return {
|
|
224
|
-
state: coreStatus.coreBestHeight === appliedTip.height && coreStatus.coreBestHash === appliedTip.blockHashHex
|
|
225
|
-
? "synced"
|
|
226
|
-
: "catching-up",
|
|
227
|
-
lastError: null,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
return {
|
|
231
|
-
state: "starting",
|
|
232
|
-
lastError: null,
|
|
233
|
-
};
|
|
234
|
-
};
|
|
235
|
-
const buildStatus = () => ({
|
|
236
|
-
serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
|
|
237
|
-
binaryVersion,
|
|
238
|
-
buildId: null,
|
|
239
|
-
updatedAtUnixMs,
|
|
18
|
+
const runtime = createIndexerDaemonRuntime({
|
|
19
|
+
dataDir,
|
|
20
|
+
databasePath,
|
|
240
21
|
walletRootId,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
processId: process.pid ?? null,
|
|
245
|
-
startedAtUnixMs,
|
|
246
|
-
heartbeatAtUnixMs,
|
|
247
|
-
ipcReady: true,
|
|
248
|
-
rpcReachable,
|
|
249
|
-
coreBestHeight,
|
|
250
|
-
coreBestHash,
|
|
251
|
-
appliedTipHeight,
|
|
252
|
-
appliedTipHash,
|
|
253
|
-
snapshotSeq,
|
|
254
|
-
backlogBlocks: coreBestHeight === null || appliedTipHeight === null
|
|
255
|
-
? null
|
|
256
|
-
: Math.max(coreBestHeight - appliedTipHeight, 0),
|
|
257
|
-
reorgDepth: null,
|
|
258
|
-
lastAppliedAtUnixMs,
|
|
259
|
-
activeSnapshotCount: snapshots.size,
|
|
260
|
-
lastError,
|
|
261
|
-
backgroundFollowActive,
|
|
262
|
-
bootstrapPhase,
|
|
263
|
-
bootstrapProgress: { ...bootstrapProgress },
|
|
264
|
-
cogcoinSyncHeight,
|
|
265
|
-
cogcoinSyncTargetHeight,
|
|
22
|
+
paths,
|
|
23
|
+
binaryVersion: await readPackageVersionFromDisk().catch(() => "0.0.0"),
|
|
24
|
+
genesisParameters: await loadBundledGenesisParameters(),
|
|
266
25
|
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return status;
|
|
271
|
-
};
|
|
272
|
-
const recordBackgroundFollowFailure = async (message) => {
|
|
273
|
-
const now = Date.now();
|
|
274
|
-
heartbeatAtUnixMs = now;
|
|
275
|
-
updatedAtUnixMs = now;
|
|
276
|
-
state = "failed";
|
|
277
|
-
lastError = message;
|
|
278
|
-
backgroundFollowError = message;
|
|
279
|
-
backgroundFollowActive = false;
|
|
280
|
-
bootstrapPhase = "error";
|
|
281
|
-
bootstrapProgress = {
|
|
282
|
-
...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
|
|
283
|
-
blocks: coreBestHeight,
|
|
284
|
-
headers: coreBestHeight,
|
|
285
|
-
targetHeight: coreBestHeight,
|
|
286
|
-
message,
|
|
287
|
-
lastError: message,
|
|
288
|
-
updatedAt: now,
|
|
289
|
-
};
|
|
290
|
-
cogcoinSyncHeight = appliedTipHeight;
|
|
291
|
-
cogcoinSyncTargetHeight = coreBestHeight;
|
|
292
|
-
await writeStatus();
|
|
293
|
-
};
|
|
294
|
-
const refreshStatus = async () => {
|
|
295
|
-
const now = Date.now();
|
|
296
|
-
heartbeatAtUnixMs = now;
|
|
297
|
-
updatedAtUnixMs = now;
|
|
298
|
-
const [coreStatus, indexedStatus] = await Promise.all([
|
|
299
|
-
readCoreTipStatus(paths),
|
|
300
|
-
readAppliedTipStatus(databasePath),
|
|
301
|
-
]);
|
|
302
|
-
const backgroundStatus = await backgroundClient?.getNodeStatus().catch(() => null) ?? null;
|
|
303
|
-
if (backgroundStatus?.following === true) {
|
|
304
|
-
backgroundFollowError = null;
|
|
305
|
-
}
|
|
306
|
-
rpcReachable = coreStatus.rpcReachable;
|
|
307
|
-
coreBestHeight = coreStatus.coreBestHeight;
|
|
308
|
-
coreBestHash = coreStatus.coreBestHash;
|
|
309
|
-
observeAppliedTip(indexedStatus.appliedTip, now);
|
|
310
|
-
backgroundFollowActive = backgroundStatus?.following ?? (backgroundClient !== null);
|
|
311
|
-
bootstrapPhase = backgroundStatus?.bootstrapPhase ?? (backgroundFollowActive ? "follow_tip" : "paused");
|
|
312
|
-
bootstrapProgress = backgroundStatus?.bootstrapProgress ?? createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA);
|
|
313
|
-
cogcoinSyncHeight = backgroundStatus?.cogcoinSyncHeight ?? indexedStatus.appliedTip?.height ?? null;
|
|
314
|
-
cogcoinSyncTargetHeight = backgroundStatus?.cogcoinSyncTargetHeight ?? coreStatus.coreBestHeight;
|
|
315
|
-
if (backgroundStatus === null && backgroundFollowError !== null) {
|
|
316
|
-
state = "failed";
|
|
317
|
-
lastError = backgroundFollowError;
|
|
318
|
-
backgroundFollowActive = false;
|
|
319
|
-
bootstrapPhase = "error";
|
|
320
|
-
bootstrapProgress = {
|
|
321
|
-
...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
|
|
322
|
-
blocks: coreStatus.coreBestHeight,
|
|
323
|
-
headers: coreStatus.coreBestHeight,
|
|
324
|
-
targetHeight: coreStatus.coreBestHeight,
|
|
325
|
-
message: backgroundFollowError,
|
|
326
|
-
lastError: backgroundFollowError,
|
|
327
|
-
updatedAt: now,
|
|
328
|
-
};
|
|
329
|
-
cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
|
|
330
|
-
cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
|
|
331
|
-
return writeStatus();
|
|
332
|
-
}
|
|
333
|
-
if (indexedStatus.schemaMismatch) {
|
|
334
|
-
state = "schema-mismatch";
|
|
335
|
-
lastError = indexedStatus.error;
|
|
336
|
-
bootstrapPhase = "error";
|
|
337
|
-
bootstrapProgress = {
|
|
338
|
-
...bootstrapProgress,
|
|
339
|
-
phase: "error",
|
|
340
|
-
message: indexedStatus.error ?? "Indexer schema mismatch.",
|
|
341
|
-
lastError: indexedStatus.error,
|
|
342
|
-
updatedAt: now,
|
|
343
|
-
};
|
|
344
|
-
return writeStatus();
|
|
345
|
-
}
|
|
346
|
-
if (indexedStatus.error !== null) {
|
|
347
|
-
state = "failed";
|
|
348
|
-
lastError = indexedStatus.error;
|
|
349
|
-
bootstrapPhase = "error";
|
|
350
|
-
bootstrapProgress = {
|
|
351
|
-
...bootstrapProgress,
|
|
352
|
-
phase: "error",
|
|
353
|
-
message: indexedStatus.error,
|
|
354
|
-
lastError: indexedStatus.error,
|
|
355
|
-
updatedAt: now,
|
|
356
|
-
};
|
|
357
|
-
return writeStatus();
|
|
358
|
-
}
|
|
359
|
-
const leaseState = deriveLeaseState(coreStatus, indexedStatus.appliedTip);
|
|
360
|
-
state = leaseState.state;
|
|
361
|
-
lastError = leaseState.lastError;
|
|
362
|
-
if (lastError !== null) {
|
|
363
|
-
bootstrapPhase = leaseState.state === "starting" ? "paused" : "error";
|
|
364
|
-
bootstrapProgress = {
|
|
365
|
-
...bootstrapProgress,
|
|
366
|
-
phase: bootstrapPhase,
|
|
367
|
-
message: lastError,
|
|
368
|
-
lastError,
|
|
369
|
-
updatedAt: now,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
else if (backgroundStatus === null) {
|
|
373
|
-
bootstrapPhase = leaseState.state === "synced" ? "follow_tip" : "paused";
|
|
374
|
-
bootstrapProgress = {
|
|
375
|
-
...createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA),
|
|
376
|
-
blocks: coreStatus.coreBestHeight,
|
|
377
|
-
headers: coreStatus.coreBestHeight,
|
|
378
|
-
targetHeight: coreStatus.coreBestHeight,
|
|
379
|
-
updatedAt: now,
|
|
380
|
-
};
|
|
381
|
-
cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
|
|
382
|
-
cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
|
|
383
|
-
}
|
|
384
|
-
return writeStatus();
|
|
385
|
-
};
|
|
386
|
-
const pauseBackgroundFollow = async () => {
|
|
387
|
-
const pendingResume = backgroundResumePromise;
|
|
388
|
-
backgroundResumePromise = null;
|
|
389
|
-
await pendingResume?.catch(() => undefined);
|
|
390
|
-
const client = backgroundClient;
|
|
391
|
-
const store = backgroundStore;
|
|
392
|
-
backgroundClient = null;
|
|
393
|
-
backgroundStore = null;
|
|
394
|
-
await client?.close().catch(() => undefined);
|
|
395
|
-
await store?.close().catch(() => undefined);
|
|
396
|
-
backgroundFollowError = null;
|
|
397
|
-
backgroundFollowActive = false;
|
|
398
|
-
bootstrapPhase = "paused";
|
|
399
|
-
bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
|
|
400
|
-
cogcoinSyncHeight = appliedTipHeight;
|
|
401
|
-
cogcoinSyncTargetHeight = coreBestHeight;
|
|
402
|
-
};
|
|
403
|
-
const resumeBackgroundFollow = async () => {
|
|
404
|
-
if (backgroundClient !== null) {
|
|
26
|
+
let shuttingDown = false;
|
|
27
|
+
const shutdown = async () => {
|
|
28
|
+
if (shuttingDown) {
|
|
405
29
|
return;
|
|
406
30
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
backgroundResumePromise = (async () => {
|
|
411
|
-
let store = null;
|
|
412
|
-
try {
|
|
413
|
-
const forcedResumeError = process.env[FORCE_RESUME_ERROR_ENV]?.trim();
|
|
414
|
-
if (forcedResumeError) {
|
|
415
|
-
throw new Error(forcedResumeError);
|
|
416
|
-
}
|
|
417
|
-
const bitcoindStatus = await readManagedBitcoindStatus(paths);
|
|
418
|
-
store = await openSqliteStore({ filename: databasePath });
|
|
419
|
-
const openedStore = store;
|
|
420
|
-
const chain = bitcoindStatus?.chain ?? "main";
|
|
421
|
-
const startHeight = normalizeCogcoinProcessingStartHeight({
|
|
422
|
-
chain,
|
|
423
|
-
startHeight: bitcoindStatus?.startHeight,
|
|
424
|
-
genesisParameters,
|
|
425
|
-
});
|
|
426
|
-
const client = await openManagedBitcoindClientInternal({
|
|
427
|
-
store: openedStore,
|
|
428
|
-
dataDir,
|
|
429
|
-
chain,
|
|
430
|
-
startHeight,
|
|
431
|
-
walletRootId,
|
|
432
|
-
progressOutput: "none",
|
|
433
|
-
});
|
|
434
|
-
backgroundStore = openedStore;
|
|
435
|
-
backgroundClient = client;
|
|
436
|
-
backgroundFollowError = null;
|
|
437
|
-
backgroundFollowActive = true;
|
|
438
|
-
void client.startFollowingTip().catch(async (error) => {
|
|
439
|
-
if (backgroundClient !== client || backgroundStore !== openedStore) {
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
backgroundClient = null;
|
|
443
|
-
backgroundStore = null;
|
|
444
|
-
backgroundFollowActive = false;
|
|
445
|
-
await client.close().catch(() => undefined);
|
|
446
|
-
await openedStore.close().catch(() => undefined);
|
|
447
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
448
|
-
await recordBackgroundFollowFailure(message).catch(() => undefined);
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
catch (error) {
|
|
452
|
-
await store?.close().catch(() => undefined);
|
|
453
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
454
|
-
await recordBackgroundFollowFailure(message).catch(() => undefined);
|
|
455
|
-
throw error;
|
|
456
|
-
}
|
|
457
|
-
})();
|
|
458
|
-
try {
|
|
459
|
-
await backgroundResumePromise;
|
|
460
|
-
}
|
|
461
|
-
finally {
|
|
462
|
-
backgroundResumePromise = null;
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
const heartbeat = setInterval(() => {
|
|
466
|
-
void refreshStatus().catch(() => undefined);
|
|
467
|
-
const now = Date.now();
|
|
468
|
-
for (const [token, snapshot] of snapshots.entries()) {
|
|
469
|
-
if (snapshot.expiresAtUnixMs <= now) {
|
|
470
|
-
snapshots.delete(token);
|
|
471
|
-
void writeStatus();
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
475
|
-
heartbeat.unref();
|
|
476
|
-
const server = net.createServer((socket) => {
|
|
477
|
-
let buffer = "";
|
|
478
|
-
const writeResponse = (response) => {
|
|
479
|
-
socket.write(`${JSON.stringify(response)}\n`);
|
|
480
|
-
};
|
|
481
|
-
socket.on("data", (chunk) => {
|
|
482
|
-
buffer += chunk.toString("utf8");
|
|
483
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
484
|
-
while (newlineIndex >= 0) {
|
|
485
|
-
const line = buffer.slice(0, newlineIndex);
|
|
486
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
487
|
-
if (line.trim().length === 0) {
|
|
488
|
-
newlineIndex = buffer.indexOf("\n");
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
let request;
|
|
492
|
-
try {
|
|
493
|
-
request = JSON.parse(line);
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
writeResponse({
|
|
497
|
-
id: "invalid",
|
|
498
|
-
ok: false,
|
|
499
|
-
error: error instanceof Error ? error.message : String(error),
|
|
500
|
-
});
|
|
501
|
-
newlineIndex = buffer.indexOf("\n");
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
void (async () => {
|
|
505
|
-
try {
|
|
506
|
-
if (request.method === "GetStatus") {
|
|
507
|
-
writeResponse({
|
|
508
|
-
id: request.id,
|
|
509
|
-
ok: true,
|
|
510
|
-
result: buildStatus(),
|
|
511
|
-
});
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
if (request.method === "OpenSnapshot") {
|
|
515
|
-
const [snapshotMaterial, coreStatus] = await Promise.all([
|
|
516
|
-
loadSnapshot(databasePath),
|
|
517
|
-
readCoreTipStatus(paths),
|
|
518
|
-
]);
|
|
519
|
-
const now = Date.now();
|
|
520
|
-
heartbeatAtUnixMs = now;
|
|
521
|
-
updatedAtUnixMs = now;
|
|
522
|
-
rpcReachable = coreStatus.rpcReachable;
|
|
523
|
-
coreBestHeight = coreStatus.coreBestHeight;
|
|
524
|
-
coreBestHash = coreStatus.coreBestHash;
|
|
525
|
-
observeAppliedTip(snapshotMaterial.tip, now);
|
|
526
|
-
const leaseState = deriveLeaseState(coreStatus, snapshotMaterial.tip);
|
|
527
|
-
state = leaseState.state;
|
|
528
|
-
lastError = leaseState.lastError;
|
|
529
|
-
const snapshot = {
|
|
530
|
-
...snapshotMaterial,
|
|
531
|
-
serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
|
|
532
|
-
schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
|
|
533
|
-
walletRootId,
|
|
534
|
-
daemonInstanceId,
|
|
535
|
-
processId: process.pid ?? null,
|
|
536
|
-
startedAtUnixMs,
|
|
537
|
-
snapshotSeq,
|
|
538
|
-
tipHeight: snapshotMaterial.tip?.height ?? null,
|
|
539
|
-
tipHash: snapshotMaterial.tip?.blockHashHex ?? null,
|
|
540
|
-
openedAtUnixMs: now,
|
|
541
|
-
};
|
|
542
|
-
snapshots.set(snapshot.token, snapshot);
|
|
543
|
-
const leaseStatus = await writeStatus();
|
|
544
|
-
const result = {
|
|
545
|
-
token: snapshot.token,
|
|
546
|
-
expiresAtUnixMs: snapshot.expiresAtUnixMs,
|
|
547
|
-
serviceApiVersion: snapshot.serviceApiVersion,
|
|
548
|
-
binaryVersion,
|
|
549
|
-
buildId: null,
|
|
550
|
-
walletRootId: snapshot.walletRootId,
|
|
551
|
-
daemonInstanceId: snapshot.daemonInstanceId,
|
|
552
|
-
schemaVersion: snapshot.schemaVersion,
|
|
553
|
-
processId: snapshot.processId,
|
|
554
|
-
startedAtUnixMs: snapshot.startedAtUnixMs,
|
|
555
|
-
state: leaseStatus.state,
|
|
556
|
-
heartbeatAtUnixMs: leaseStatus.heartbeatAtUnixMs,
|
|
557
|
-
rpcReachable: leaseStatus.rpcReachable,
|
|
558
|
-
coreBestHeight: leaseStatus.coreBestHeight,
|
|
559
|
-
coreBestHash: leaseStatus.coreBestHash,
|
|
560
|
-
appliedTipHeight: leaseStatus.appliedTipHeight,
|
|
561
|
-
appliedTipHash: leaseStatus.appliedTipHash,
|
|
562
|
-
snapshotSeq: snapshot.snapshotSeq,
|
|
563
|
-
backlogBlocks: leaseStatus.backlogBlocks,
|
|
564
|
-
reorgDepth: leaseStatus.reorgDepth,
|
|
565
|
-
lastAppliedAtUnixMs: leaseStatus.lastAppliedAtUnixMs,
|
|
566
|
-
activeSnapshotCount: leaseStatus.activeSnapshotCount,
|
|
567
|
-
lastError: leaseStatus.lastError,
|
|
568
|
-
backgroundFollowActive: leaseStatus.backgroundFollowActive ?? false,
|
|
569
|
-
bootstrapPhase: leaseStatus.bootstrapPhase ?? null,
|
|
570
|
-
bootstrapProgress: leaseStatus.bootstrapProgress ?? null,
|
|
571
|
-
cogcoinSyncHeight: leaseStatus.cogcoinSyncHeight ?? null,
|
|
572
|
-
cogcoinSyncTargetHeight: leaseStatus.cogcoinSyncTargetHeight ?? null,
|
|
573
|
-
tipHeight: snapshot.tipHeight,
|
|
574
|
-
tipHash: snapshot.tipHash,
|
|
575
|
-
openedAtUnixMs: snapshot.openedAtUnixMs,
|
|
576
|
-
};
|
|
577
|
-
writeResponse({
|
|
578
|
-
id: request.id,
|
|
579
|
-
ok: true,
|
|
580
|
-
result,
|
|
581
|
-
});
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
if (request.method === "ReadSnapshot") {
|
|
585
|
-
const snapshot = request.token ? snapshots.get(request.token) : null;
|
|
586
|
-
if (!snapshot || snapshot.expiresAtUnixMs <= Date.now()) {
|
|
587
|
-
if (request.token) {
|
|
588
|
-
snapshots.delete(request.token);
|
|
589
|
-
await writeStatus();
|
|
590
|
-
}
|
|
591
|
-
throw new Error("indexer_daemon_snapshot_invalid");
|
|
592
|
-
}
|
|
593
|
-
if (snapshot.snapshotSeq !== snapshotSeq) {
|
|
594
|
-
snapshots.delete(snapshot.token);
|
|
595
|
-
await writeStatus();
|
|
596
|
-
throw new Error("indexer_daemon_snapshot_rotated");
|
|
597
|
-
}
|
|
598
|
-
const result = {
|
|
599
|
-
token: snapshot.token,
|
|
600
|
-
stateBase64: snapshot.stateBase64,
|
|
601
|
-
serviceApiVersion: snapshot.serviceApiVersion,
|
|
602
|
-
schemaVersion: snapshot.schemaVersion,
|
|
603
|
-
walletRootId: snapshot.walletRootId,
|
|
604
|
-
daemonInstanceId: snapshot.daemonInstanceId,
|
|
605
|
-
processId: snapshot.processId,
|
|
606
|
-
startedAtUnixMs: snapshot.startedAtUnixMs,
|
|
607
|
-
snapshotSeq: snapshot.snapshotSeq,
|
|
608
|
-
tipHeight: snapshot.tipHeight,
|
|
609
|
-
tipHash: snapshot.tipHash,
|
|
610
|
-
openedAtUnixMs: snapshot.openedAtUnixMs,
|
|
611
|
-
tip: snapshot.tip,
|
|
612
|
-
expiresAtUnixMs: snapshot.expiresAtUnixMs,
|
|
613
|
-
};
|
|
614
|
-
writeResponse({
|
|
615
|
-
id: request.id,
|
|
616
|
-
ok: true,
|
|
617
|
-
result,
|
|
618
|
-
});
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
if (request.method === "CloseSnapshot") {
|
|
622
|
-
if (request.token) {
|
|
623
|
-
snapshots.delete(request.token);
|
|
624
|
-
await writeStatus();
|
|
625
|
-
}
|
|
626
|
-
writeResponse({
|
|
627
|
-
id: request.id,
|
|
628
|
-
ok: true,
|
|
629
|
-
result: null,
|
|
630
|
-
});
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
if (request.method === "ResumeBackgroundFollow") {
|
|
634
|
-
try {
|
|
635
|
-
await withTimeout(resumeBackgroundFollow(), BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS, BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR);
|
|
636
|
-
}
|
|
637
|
-
catch (error) {
|
|
638
|
-
if (error instanceof Error
|
|
639
|
-
&& error.message === BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR) {
|
|
640
|
-
await recordBackgroundFollowFailure(error.message).catch(() => undefined);
|
|
641
|
-
}
|
|
642
|
-
throw error;
|
|
643
|
-
}
|
|
644
|
-
writeResponse({
|
|
645
|
-
id: request.id,
|
|
646
|
-
ok: true,
|
|
647
|
-
result: null,
|
|
648
|
-
});
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
throw new Error(`indexer_daemon_unknown_method_${request.method}`);
|
|
652
|
-
}
|
|
653
|
-
catch (error) {
|
|
654
|
-
writeResponse({
|
|
655
|
-
id: request.id,
|
|
656
|
-
ok: false,
|
|
657
|
-
error: error instanceof Error ? error.message : String(error),
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
})();
|
|
661
|
-
newlineIndex = buffer.indexOf("\n");
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
});
|
|
665
|
-
const shutdown = async () => {
|
|
666
|
-
clearInterval(heartbeat);
|
|
667
|
-
await pauseBackgroundFollow().catch(() => undefined);
|
|
668
|
-
state = "stopping";
|
|
669
|
-
heartbeatAtUnixMs = Date.now();
|
|
670
|
-
updatedAtUnixMs = heartbeatAtUnixMs;
|
|
671
|
-
await writeStatus().catch(() => undefined);
|
|
672
|
-
await new Promise((resolve) => {
|
|
673
|
-
server.close(() => resolve());
|
|
674
|
-
});
|
|
675
|
-
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
31
|
+
shuttingDown = true;
|
|
32
|
+
await runtime.shutdown().catch(() => undefined);
|
|
676
33
|
process.exit(0);
|
|
677
34
|
};
|
|
678
35
|
process.on("SIGTERM", () => {
|
|
@@ -681,14 +38,6 @@ async function main() {
|
|
|
681
38
|
process.on("SIGINT", () => {
|
|
682
39
|
void shutdown();
|
|
683
40
|
});
|
|
684
|
-
await
|
|
685
|
-
server.once("error", reject);
|
|
686
|
-
server.listen(paths.indexerDaemonSocketPath, async () => {
|
|
687
|
-
server.off("error", reject);
|
|
688
|
-
await writeStatus();
|
|
689
|
-
await refreshStatus().catch(() => undefined);
|
|
690
|
-
resolve();
|
|
691
|
-
});
|
|
692
|
-
});
|
|
41
|
+
await runtime.start();
|
|
693
42
|
}
|
|
694
43
|
await main();
|