@cogcoin/client 1.0.1 → 1.1.0
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 +4 -2
- package/dist/bitcoind/client/factory.d.ts +0 -8
- package/dist/bitcoind/client/factory.js +1 -59
- package/dist/bitcoind/client/managed-client.d.ts +1 -3
- package/dist/bitcoind/client/managed-client.js +3 -47
- package/dist/bitcoind/indexer-daemon-main.js +173 -28
- package/dist/bitcoind/indexer-daemon.d.ts +14 -3
- package/dist/bitcoind/indexer-daemon.js +145 -29
- package/dist/bitcoind/indexer-monitor.d.ts +12 -0
- package/dist/bitcoind/indexer-monitor.js +89 -0
- package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
- package/dist/bitcoind/progress/follow-scene.js +87 -4
- package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
- package/dist/bitcoind/progress/tty-renderer.js +2 -0
- package/dist/bitcoind/retryable-rpc.js +3 -0
- package/dist/bitcoind/service.d.ts +1 -0
- package/dist/bitcoind/service.js +31 -9
- package/dist/bitcoind/testing.d.ts +0 -1
- package/dist/bitcoind/testing.js +0 -1
- package/dist/bitcoind/types.d.ts +5 -2
- package/dist/cli/commands/follow.js +44 -49
- package/dist/cli/commands/mining-admin.js +65 -2
- package/dist/cli/commands/mining-read.js +43 -3
- package/dist/cli/commands/mining-runtime.js +91 -73
- package/dist/cli/commands/service-runtime.js +42 -2
- package/dist/cli/commands/status.js +3 -1
- package/dist/cli/commands/sync.js +50 -90
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/commands/wallet-admin.js +21 -3
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.js +36 -1
- package/dist/cli/managed-indexer-observer.d.ts +33 -0
- package/dist/cli/managed-indexer-observer.js +163 -0
- package/dist/cli/mining-format.d.ts +3 -1
- package/dist/cli/mining-format.js +63 -0
- package/dist/cli/mining-json.d.ts +11 -1
- package/dist/cli/mining-json.js +15 -0
- package/dist/cli/output.js +74 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +28 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/read-json.d.ts +26 -1
- package/dist/cli/read-json.js +48 -0
- package/dist/cli/runner.js +8 -2
- package/dist/cli/signals.d.ts +12 -0
- package/dist/cli/signals.js +31 -13
- package/dist/cli/types.d.ts +13 -4
- package/dist/cli/update-notifier.js +7 -222
- package/dist/cli/update-service.d.ts +34 -0
- package/dist/cli/update-service.js +152 -0
- package/dist/client/initialization.js +5 -0
- package/dist/semver.d.ts +12 -0
- package/dist/semver.js +68 -0
- package/dist/wallet/lifecycle.d.ts +10 -0
- package/dist/wallet/mining/config.js +64 -3
- package/dist/wallet/mining/control.d.ts +5 -1
- package/dist/wallet/mining/control.js +269 -26
- package/dist/wallet/mining/domain-prompts.d.ts +17 -0
- package/dist/wallet/mining/domain-prompts.js +130 -0
- package/dist/wallet/mining/index.d.ts +2 -1
- package/dist/wallet/mining/index.js +1 -0
- package/dist/wallet/mining/provider-model.d.ts +30 -0
- package/dist/wallet/mining/provider-model.js +134 -0
- package/dist/wallet/mining/runner.d.ts +156 -5
- package/dist/wallet/mining/runner.js +1019 -399
- package/dist/wallet/mining/runtime-artifacts.js +1 -0
- package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
- package/dist/wallet/mining/sentences.d.ts +2 -2
- package/dist/wallet/mining/sentences.js +32 -6
- package/dist/wallet/mining/types.d.ts +35 -1
- package/dist/wallet/mining/visualizer.d.ts +3 -0
- package/dist/wallet/mining/visualizer.js +132 -15
- package/dist/wallet/read/context.d.ts +1 -0
- package/dist/wallet/read/context.js +15 -7
- package/dist/wallet/state/client-password-agent.js +4 -1
- package/dist/wallet/state/client-password.js +15 -8
- package/dist/wallet/tx/common.js +1 -1
- package/package.json +3 -2
|
@@ -3,11 +3,16 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import net from "node:net";
|
|
6
|
+
import { compareSemver } from "../semver.js";
|
|
6
7
|
import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
|
|
7
8
|
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
8
9
|
import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
|
|
9
10
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
10
11
|
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
12
|
+
const INDEXER_DAEMON_REQUEST_TIMEOUT_MS = 15_000;
|
|
13
|
+
const INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS = 35_000;
|
|
14
|
+
const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
|
|
15
|
+
export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
|
|
11
16
|
async function readJsonFile(filePath) {
|
|
12
17
|
try {
|
|
13
18
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
@@ -57,7 +62,7 @@ export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
|
57
62
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
58
63
|
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
59
64
|
const status = await readJsonFile(paths.indexerDaemonStatusPath);
|
|
60
|
-
const processId = status?.processId ?? null;
|
|
65
|
+
const processId = options.processId ?? status?.processId ?? null;
|
|
61
66
|
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
62
67
|
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
63
68
|
return {
|
|
@@ -80,7 +85,8 @@ export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
|
80
85
|
walletRootId,
|
|
81
86
|
};
|
|
82
87
|
}
|
|
83
|
-
function createIndexerDaemonClient(socketPath) {
|
|
88
|
+
function createIndexerDaemonClient(socketPath, closeOptions = null) {
|
|
89
|
+
let closed = false;
|
|
84
90
|
async function sendRequest(request) {
|
|
85
91
|
return new Promise((resolve, reject) => {
|
|
86
92
|
const socket = net.createConnection(socketPath);
|
|
@@ -94,7 +100,9 @@ function createIndexerDaemonClient(socketPath) {
|
|
|
94
100
|
socket.destroy();
|
|
95
101
|
handler();
|
|
96
102
|
};
|
|
97
|
-
socket.setTimeout(
|
|
103
|
+
socket.setTimeout(request.method === "ResumeBackgroundFollow"
|
|
104
|
+
? INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS
|
|
105
|
+
: INDEXER_DAEMON_REQUEST_TIMEOUT_MS);
|
|
98
106
|
socket.on("connect", () => {
|
|
99
107
|
socket.write(`${JSON.stringify(request)}\n`);
|
|
100
108
|
});
|
|
@@ -168,12 +176,6 @@ function createIndexerDaemonClient(socketPath) {
|
|
|
168
176
|
token,
|
|
169
177
|
});
|
|
170
178
|
},
|
|
171
|
-
async pauseBackgroundFollow() {
|
|
172
|
-
await sendRequest({
|
|
173
|
-
id: randomUUID(),
|
|
174
|
-
method: "PauseBackgroundFollow",
|
|
175
|
-
});
|
|
176
|
-
},
|
|
177
179
|
async resumeBackgroundFollow() {
|
|
178
180
|
await sendRequest({
|
|
179
181
|
id: randomUUID(),
|
|
@@ -181,7 +183,18 @@ function createIndexerDaemonClient(socketPath) {
|
|
|
181
183
|
});
|
|
182
184
|
},
|
|
183
185
|
async close() {
|
|
184
|
-
|
|
186
|
+
if (closed) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
closed = true;
|
|
190
|
+
if (closeOptions === null || closeOptions.serviceLifetime !== "ephemeral" || closeOptions.ownership === "attached") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
await stopIndexerDaemonService({
|
|
194
|
+
dataDir: closeOptions.dataDir,
|
|
195
|
+
walletRootId: closeOptions.walletRootId,
|
|
196
|
+
shutdownTimeoutMs: closeOptions.shutdownTimeoutMs,
|
|
197
|
+
});
|
|
185
198
|
},
|
|
186
199
|
};
|
|
187
200
|
}
|
|
@@ -259,8 +272,20 @@ function buildStatusFromSnapshotHandle(handle) {
|
|
|
259
272
|
lastAppliedAtUnixMs: handle.lastAppliedAtUnixMs,
|
|
260
273
|
activeSnapshotCount: handle.activeSnapshotCount,
|
|
261
274
|
lastError: handle.lastError,
|
|
275
|
+
backgroundFollowActive: handle.backgroundFollowActive,
|
|
276
|
+
bootstrapPhase: handle.bootstrapPhase,
|
|
277
|
+
bootstrapProgress: handle.bootstrapProgress,
|
|
278
|
+
cogcoinSyncHeight: handle.cogcoinSyncHeight,
|
|
279
|
+
cogcoinSyncTargetHeight: handle.cogcoinSyncTargetHeight,
|
|
262
280
|
};
|
|
263
281
|
}
|
|
282
|
+
function isStaleIndexerDaemonVersion(status, expectedBinaryVersion) {
|
|
283
|
+
if (status === null || expectedBinaryVersion === null || expectedBinaryVersion === undefined) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const comparison = compareSemver(status.binaryVersion, expectedBinaryVersion);
|
|
287
|
+
return comparison !== null && comparison < 0;
|
|
288
|
+
}
|
|
264
289
|
async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
|
|
265
290
|
const client = createIndexerDaemonClient(socketPath);
|
|
266
291
|
try {
|
|
@@ -308,7 +333,8 @@ async function waitForIndexerDaemon(dataDir, walletRootId, timeoutMs) {
|
|
|
308
333
|
while (Date.now() < deadline) {
|
|
309
334
|
const probe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
310
335
|
if (probe.compatibility === "compatible" && probe.client !== null) {
|
|
311
|
-
|
|
336
|
+
await probe.client.close().catch(() => undefined);
|
|
337
|
+
return;
|
|
312
338
|
}
|
|
313
339
|
if (probe.compatibility !== "unreachable") {
|
|
314
340
|
throw new Error(probe.error ?? "indexer_daemon_protocol_error");
|
|
@@ -355,14 +381,84 @@ export async function readObservedIndexerDaemonStatus(options) {
|
|
|
355
381
|
return readJsonFile(paths.indexerDaemonStatusPath);
|
|
356
382
|
}
|
|
357
383
|
export async function attachOrStartIndexerDaemon(options) {
|
|
384
|
+
const requestBackgroundFollow = async (client, observedStatus = null) => {
|
|
385
|
+
if (options.ensureBackgroundFollow !== true) {
|
|
386
|
+
return client;
|
|
387
|
+
}
|
|
388
|
+
if (observedStatus?.backgroundFollowActive === true) {
|
|
389
|
+
return client;
|
|
390
|
+
}
|
|
391
|
+
await client.resumeBackgroundFollow();
|
|
392
|
+
const status = await client.getStatus();
|
|
393
|
+
if (status.backgroundFollowActive !== true) {
|
|
394
|
+
throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE);
|
|
395
|
+
}
|
|
396
|
+
return client;
|
|
397
|
+
};
|
|
358
398
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
359
399
|
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
360
400
|
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
401
|
+
const serviceLifetime = options.serviceLifetime ?? "persistent";
|
|
402
|
+
const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
|
|
403
|
+
const startDaemon = async () => {
|
|
404
|
+
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
405
|
+
const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
|
|
406
|
+
const spawnOptions = serviceLifetime === "ephemeral"
|
|
407
|
+
? {
|
|
408
|
+
stdio: "ignore",
|
|
409
|
+
}
|
|
410
|
+
: {
|
|
411
|
+
detached: true,
|
|
412
|
+
stdio: "ignore",
|
|
413
|
+
};
|
|
414
|
+
const child = spawn(process.execPath, [
|
|
415
|
+
daemonEntryPath,
|
|
416
|
+
`--data-dir=${options.dataDir}`,
|
|
417
|
+
`--database-path=${options.databasePath}`,
|
|
418
|
+
`--wallet-root-id=${walletRootId}`,
|
|
419
|
+
], {
|
|
420
|
+
...spawnOptions,
|
|
421
|
+
});
|
|
422
|
+
if (serviceLifetime !== "ephemeral") {
|
|
423
|
+
child.unref();
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (child.pid !== undefined) {
|
|
430
|
+
try {
|
|
431
|
+
process.kill(child.pid, "SIGTERM");
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// ignore shutdown failures while unwinding startup errors
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
|
|
440
|
+
dataDir: options.dataDir,
|
|
441
|
+
walletRootId,
|
|
442
|
+
serviceLifetime,
|
|
443
|
+
ownership: "started",
|
|
444
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
445
|
+
});
|
|
446
|
+
};
|
|
361
447
|
const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
362
448
|
if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
|
|
363
|
-
|
|
449
|
+
if (!isStaleIndexerDaemonVersion(existingProbe.status, expectedBinaryVersion)) {
|
|
450
|
+
try {
|
|
451
|
+
return await requestBackgroundFollow(existingProbe.client, existingProbe.status);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
await existingProbe.client.close().catch(() => undefined);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
await existingProbe.client.close().catch(() => undefined);
|
|
459
|
+
}
|
|
364
460
|
}
|
|
365
|
-
if (existingProbe.compatibility !== "unreachable") {
|
|
461
|
+
if (existingProbe.compatibility !== "unreachable" && existingProbe.compatibility !== "compatible") {
|
|
366
462
|
throw new Error(existingProbe.error ?? "indexer_daemon_protocol_error");
|
|
367
463
|
}
|
|
368
464
|
try {
|
|
@@ -375,24 +471,43 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
375
471
|
try {
|
|
376
472
|
const liveProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
377
473
|
if (liveProbe.compatibility === "compatible" && liveProbe.client !== null) {
|
|
378
|
-
|
|
474
|
+
if (!isStaleIndexerDaemonVersion(liveProbe.status, expectedBinaryVersion)) {
|
|
475
|
+
try {
|
|
476
|
+
return await requestBackgroundFollow(liveProbe.client, liveProbe.status);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
await liveProbe.client.close().catch(() => undefined);
|
|
480
|
+
await stopIndexerDaemonServiceWithLockHeld({
|
|
481
|
+
dataDir: options.dataDir,
|
|
482
|
+
walletRootId,
|
|
483
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
484
|
+
paths,
|
|
485
|
+
processId: liveProbe.status?.processId ?? null,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
await liveProbe.client.close().catch(() => undefined);
|
|
491
|
+
await stopIndexerDaemonServiceWithLockHeld({
|
|
492
|
+
dataDir: options.dataDir,
|
|
493
|
+
walletRootId,
|
|
494
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
495
|
+
paths,
|
|
496
|
+
processId: liveProbe.status?.processId ?? null,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
379
499
|
}
|
|
380
|
-
if (liveProbe.compatibility !== "unreachable") {
|
|
500
|
+
else if (liveProbe.compatibility !== "unreachable") {
|
|
381
501
|
throw new Error(liveProbe.error ?? "indexer_daemon_protocol_error");
|
|
382
502
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
detached: true,
|
|
392
|
-
stdio: "ignore",
|
|
393
|
-
});
|
|
394
|
-
child.unref();
|
|
395
|
-
return await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
503
|
+
const daemon = await startDaemon();
|
|
504
|
+
try {
|
|
505
|
+
return await requestBackgroundFollow(daemon);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
await daemon.close().catch(() => undefined);
|
|
509
|
+
throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, { cause: error });
|
|
510
|
+
}
|
|
396
511
|
}
|
|
397
512
|
finally {
|
|
398
513
|
await lock.release();
|
|
@@ -400,7 +515,8 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
400
515
|
}
|
|
401
516
|
catch (error) {
|
|
402
517
|
if (error instanceof FileLockBusyError) {
|
|
403
|
-
|
|
518
|
+
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
519
|
+
return attachOrStartIndexerDaemon(options);
|
|
404
520
|
}
|
|
405
521
|
throw error;
|
|
406
522
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ManagedIndexerDaemonObservedStatus } from "./types.js";
|
|
2
|
+
export interface ManagedIndexerMonitor {
|
|
3
|
+
getStatus(): Promise<ManagedIndexerDaemonObservedStatus>;
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function openManagedIndexerMonitor(options: {
|
|
7
|
+
dataDir: string;
|
|
8
|
+
databasePath: string;
|
|
9
|
+
walletRootId?: string;
|
|
10
|
+
startupTimeoutMs?: number;
|
|
11
|
+
expectedBinaryVersion?: string | null;
|
|
12
|
+
}): Promise<ManagedIndexerMonitor>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
3
|
+
import { attachOrStartIndexerDaemon, readObservedIndexerDaemonStatus, } from "./indexer-daemon.js";
|
|
4
|
+
import { resolveCogcoinProcessingStartHeight } from "./processing-start-height.js";
|
|
5
|
+
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
6
|
+
import { attachOrStartManagedBitcoindService } from "./service.js";
|
|
7
|
+
async function readJsonFile(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function resolveStartOptions(options) {
|
|
19
|
+
const paths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
|
|
20
|
+
const observedStatus = await readJsonFile(paths.bitcoindStatusPath).catch(() => null);
|
|
21
|
+
if (observedStatus !== null) {
|
|
22
|
+
return {
|
|
23
|
+
chain: observedStatus.chain,
|
|
24
|
+
startHeight: observedStatus.startHeight,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const genesisParameters = await loadBundledGenesisParameters();
|
|
28
|
+
return {
|
|
29
|
+
chain: "main",
|
|
30
|
+
startHeight: resolveCogcoinProcessingStartHeight(genesisParameters),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function openManagedIndexerMonitor(options) {
|
|
34
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
35
|
+
const startOptions = await resolveStartOptions({
|
|
36
|
+
dataDir: options.dataDir,
|
|
37
|
+
walletRootId,
|
|
38
|
+
});
|
|
39
|
+
await attachOrStartManagedBitcoindService({
|
|
40
|
+
dataDir: options.dataDir,
|
|
41
|
+
chain: startOptions.chain,
|
|
42
|
+
startHeight: startOptions.startHeight,
|
|
43
|
+
walletRootId,
|
|
44
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
45
|
+
});
|
|
46
|
+
const daemon = await attachOrStartIndexerDaemon({
|
|
47
|
+
dataDir: options.dataDir,
|
|
48
|
+
databasePath: options.databasePath,
|
|
49
|
+
walletRootId,
|
|
50
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
51
|
+
ensureBackgroundFollow: true,
|
|
52
|
+
expectedBinaryVersion: options.expectedBinaryVersion,
|
|
53
|
+
});
|
|
54
|
+
return createManagedIndexerMonitor({
|
|
55
|
+
daemon,
|
|
56
|
+
dataDir: options.dataDir,
|
|
57
|
+
walletRootId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function createManagedIndexerMonitor(options) {
|
|
61
|
+
let closed = false;
|
|
62
|
+
return {
|
|
63
|
+
async getStatus() {
|
|
64
|
+
if (closed) {
|
|
65
|
+
throw new Error("managed_indexer_monitor_closed");
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return await options.daemon.getStatus();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const observed = await readObservedIndexerDaemonStatus({
|
|
72
|
+
dataDir: options.dataDir,
|
|
73
|
+
walletRootId: options.walletRootId,
|
|
74
|
+
}).catch(() => null);
|
|
75
|
+
if (observed !== null) {
|
|
76
|
+
return observed;
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async close() {
|
|
82
|
+
if (closed) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
closed = true;
|
|
86
|
+
await options.daemon.close().catch(() => undefined);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -19,16 +19,20 @@ export interface FollowSceneStateForTesting {
|
|
|
19
19
|
export interface FollowFrameRenderOptions {
|
|
20
20
|
artworkCogText?: string | null;
|
|
21
21
|
artworkSatText?: string | null;
|
|
22
|
+
artworkStatusLeftText?: string | null;
|
|
23
|
+
artworkStatusRightText?: string | null;
|
|
22
24
|
}
|
|
23
25
|
export declare function createFollowSceneState(indexedHeight?: number | null, blockTimesByHeight?: Record<number, number>): FollowSceneStateForTesting;
|
|
24
26
|
export declare function setFollowBlockTime(state: FollowSceneStateForTesting, height: number, blockTime: number): void;
|
|
25
27
|
export declare function replaceFollowBlockTimes(state: FollowSceneStateForTesting, blockTimesByHeight: Record<number, number>): void;
|
|
26
28
|
export declare function formatCompactFollowAgeLabel(blockTime: number, now: number): string;
|
|
27
29
|
export declare const formatCompactFollowAgeLabelForTesting: typeof formatCompactFollowAgeLabel;
|
|
28
|
-
export declare function syncFollowSceneState(state: FollowSceneStateForTesting, { indexedHeight, nodeHeight, liveActivated, }: {
|
|
30
|
+
export declare function syncFollowSceneState(state: FollowSceneStateForTesting, { indexedHeight, nodeHeight, liveActivated, authoritativeTip, settleLatest, }: {
|
|
29
31
|
indexedHeight?: number | null;
|
|
30
32
|
nodeHeight?: number | null;
|
|
31
33
|
liveActivated?: boolean;
|
|
34
|
+
authoritativeTip?: boolean;
|
|
35
|
+
settleLatest?: boolean;
|
|
32
36
|
}): void;
|
|
33
37
|
export declare function advanceFollowSceneState(state: FollowSceneStateForTesting, now: number): void;
|
|
34
38
|
export declare function renderFollowFrame(state: FollowSceneStateForTesting, statusFieldText: string, now: number, options?: FollowFrameRenderOptions): string[];
|
|
@@ -39,6 +43,8 @@ export declare function syncFollowSceneStateForTesting(state: FollowSceneStateFo
|
|
|
39
43
|
indexedHeight?: number | null;
|
|
40
44
|
nodeHeight?: number | null;
|
|
41
45
|
liveActivated?: boolean;
|
|
46
|
+
authoritativeTip?: boolean;
|
|
47
|
+
settleLatest?: boolean;
|
|
42
48
|
}): FollowSceneStateForTesting;
|
|
43
49
|
export declare function advanceFollowSceneStateForTesting(state: FollowSceneStateForTesting, now: number): FollowSceneStateForTesting;
|
|
44
50
|
export declare function renderFollowFrameForTesting(state: FollowSceneStateForTesting, statusFieldText?: string, now?: number, options?: FollowFrameRenderOptions): string[];
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { loadArtTemplate, loadFollowCarTemplate } from "./assets.js";
|
|
2
2
|
import { FIELD_LEFT, FIELD_WIDTH, FOLLOW_AGE_ROW, FOLLOW_APPROACH_MS, FOLLOW_CAR_HEIGHT, FOLLOW_CAR_PITCH, FOLLOW_CAR_TOP, FOLLOW_CAR_WIDTH, FOLLOW_CENTER_SLOT_X, FOLLOW_CLIP_MAX_COLUMN, FOLLOW_CLIP_MIN_COLUMN, FOLLOW_CONNECTION_SLOT_X, FOLLOW_FAST_APPROACH_MS, FOLLOW_FAST_SHIFT_MS, FOLLOW_PENDING_ENTER_MS, FOLLOW_PENDING_LABEL, FOLLOW_PENDING_OFFSCREEN_LEFT_X, FOLLOW_PENDING_SLOT_X, FOLLOW_RIGHT_SLOT_XS, FOLLOW_SHIFT_MS, FOLLOW_WINDOW_LEFT, MESSAGE_FIELD_ROW, NEUTRAL_MESSAGE_TITLE, STATUS_FIELD_ROW, } from "./constants.js";
|
|
3
|
-
import { centerLine, computeCenteredLeftPadding,
|
|
3
|
+
import { centerLine, computeCenteredLeftPadding, replaceSegment, rightAlignLine, truncateLine, } from "./formatting.js";
|
|
4
4
|
const FOLLOW_TITLE_LEFT = computeCenteredLeftPadding(NEUTRAL_MESSAGE_TITLE, FIELD_WIDTH);
|
|
5
5
|
const FOLLOW_TITLE_WIDTH = NEUTRAL_MESSAGE_TITLE.length;
|
|
6
6
|
const FOLLOW_COG_LEFT = 0;
|
|
7
7
|
const FOLLOW_COG_WIDTH = FOLLOW_TITLE_LEFT;
|
|
8
8
|
const FOLLOW_SAT_LEFT = FOLLOW_TITLE_LEFT + FOLLOW_TITLE_WIDTH;
|
|
9
9
|
const FOLLOW_SAT_WIDTH = FIELD_WIDTH - FOLLOW_SAT_LEFT;
|
|
10
|
+
const FOLLOW_STATUS_VERSION_GAP = 2;
|
|
10
11
|
export function createFollowSceneState(indexedHeight = null, blockTimesByHeight = {}) {
|
|
11
12
|
return {
|
|
12
13
|
liveActivated: false,
|
|
@@ -79,9 +80,50 @@ function renderFollowHeaderField(options) {
|
|
|
79
80
|
}
|
|
80
81
|
return field;
|
|
81
82
|
}
|
|
83
|
+
function renderFollowStatusField(statusFieldText, options) {
|
|
84
|
+
const leftText = options.artworkStatusLeftText === null || options.artworkStatusLeftText === undefined
|
|
85
|
+
? ""
|
|
86
|
+
: truncateLine(options.artworkStatusLeftText, FIELD_WIDTH);
|
|
87
|
+
const rightText = options.artworkStatusRightText === null || options.artworkStatusRightText === undefined
|
|
88
|
+
? ""
|
|
89
|
+
: truncateLine(options.artworkStatusRightText, FIELD_WIDTH);
|
|
90
|
+
const leftWidth = Math.min(FIELD_WIDTH, leftText.length);
|
|
91
|
+
const rightWidth = Math.min(FIELD_WIDTH, rightText.length);
|
|
92
|
+
if (leftWidth === 0 && rightWidth === 0) {
|
|
93
|
+
return centerLine(statusFieldText, FIELD_WIDTH);
|
|
94
|
+
}
|
|
95
|
+
let field = centerLine(statusFieldText, FIELD_WIDTH);
|
|
96
|
+
if (leftWidth > 0) {
|
|
97
|
+
field = replaceSegment(field, 0, leftWidth, leftAlignLane(leftText, leftWidth));
|
|
98
|
+
if (leftWidth < FIELD_WIDTH) {
|
|
99
|
+
field = replaceSegment(field, leftWidth, Math.min(FOLLOW_STATUS_VERSION_GAP, FIELD_WIDTH - leftWidth), "".padEnd(Math.min(FOLLOW_STATUS_VERSION_GAP, FIELD_WIDTH - leftWidth), " "));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (rightWidth > 0) {
|
|
103
|
+
if (rightWidth < FIELD_WIDTH) {
|
|
104
|
+
const gapStart = Math.max(0, FIELD_WIDTH - rightWidth - FOLLOW_STATUS_VERSION_GAP);
|
|
105
|
+
const gapWidth = Math.min(FOLLOW_STATUS_VERSION_GAP, FIELD_WIDTH - rightWidth);
|
|
106
|
+
field = replaceSegment(field, gapStart, gapWidth, "".padEnd(gapWidth, " "));
|
|
107
|
+
}
|
|
108
|
+
field = replaceSegment(field, FIELD_WIDTH - rightWidth, rightWidth, rightAlignLine(rightText, rightWidth));
|
|
109
|
+
}
|
|
110
|
+
return field;
|
|
111
|
+
}
|
|
82
112
|
function highestTrackedFollowHeight(state) {
|
|
83
113
|
return Math.max(state.indexedHeight ?? Number.NEGATIVE_INFINITY, state.displayedCenterHeight ?? Number.NEGATIVE_INFINITY, state.animation?.height ?? Number.NEGATIVE_INFINITY, ...state.queuedHeights);
|
|
84
114
|
}
|
|
115
|
+
function resolveLatestAuthoritativeFollowHeight(indexedHeight, nodeHeight) {
|
|
116
|
+
if (indexedHeight === null) {
|
|
117
|
+
return nodeHeight;
|
|
118
|
+
}
|
|
119
|
+
if (nodeHeight === null) {
|
|
120
|
+
return indexedHeight;
|
|
121
|
+
}
|
|
122
|
+
return Math.max(indexedHeight, nodeHeight);
|
|
123
|
+
}
|
|
124
|
+
function resolveDisplayedFollowHeight(state) {
|
|
125
|
+
return state.displayedCenterHeight ?? state.indexedHeight;
|
|
126
|
+
}
|
|
85
127
|
function resetFollowSceneState(state) {
|
|
86
128
|
state.displayedCenterHeight = state.indexedHeight;
|
|
87
129
|
state.blockTimesByHeight = {};
|
|
@@ -90,13 +132,33 @@ function resetFollowSceneState(state) {
|
|
|
90
132
|
state.pendingStaticX = null;
|
|
91
133
|
state.animation = null;
|
|
92
134
|
}
|
|
135
|
+
function resyncFollowSceneToLatestHeight(state, latestHeight) {
|
|
136
|
+
state.displayedCenterHeight = latestHeight;
|
|
137
|
+
state.queuedHeights = [];
|
|
138
|
+
state.pendingLabel = null;
|
|
139
|
+
state.pendingStaticX = null;
|
|
140
|
+
state.animation = null;
|
|
141
|
+
}
|
|
142
|
+
function shouldResyncAuthoritativeFollowScene(options) {
|
|
143
|
+
if (options.latestHeight === null) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (options.settleLatest) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const displayedHeight = resolveDisplayedFollowHeight(options.state);
|
|
150
|
+
if (displayedHeight === null) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return options.latestHeight - displayedHeight > 1;
|
|
154
|
+
}
|
|
93
155
|
function enqueueFollowHeights(state, nodeHeight) {
|
|
94
156
|
const startHeight = Math.max(state.indexedHeight ?? -1, highestTrackedFollowHeight(state));
|
|
95
157
|
for (let height = startHeight + 1; height <= nodeHeight; height += 1) {
|
|
96
158
|
state.queuedHeights.push(height);
|
|
97
159
|
}
|
|
98
160
|
}
|
|
99
|
-
export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, }) {
|
|
161
|
+
export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, authoritativeTip, settleLatest, }) {
|
|
100
162
|
const wasLive = state.liveActivated;
|
|
101
163
|
const nextLive = liveActivated ?? state.liveActivated;
|
|
102
164
|
let shouldReset = false;
|
|
@@ -119,7 +181,17 @@ export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveAct
|
|
|
119
181
|
if (shouldReset) {
|
|
120
182
|
resetFollowSceneState(state);
|
|
121
183
|
}
|
|
122
|
-
|
|
184
|
+
const latestAuthoritativeHeight = resolveLatestAuthoritativeFollowHeight(state.indexedHeight, state.observedNodeHeight);
|
|
185
|
+
if (state.liveActivated
|
|
186
|
+
&& authoritativeTip === true
|
|
187
|
+
&& shouldResyncAuthoritativeFollowScene({
|
|
188
|
+
state,
|
|
189
|
+
latestHeight: latestAuthoritativeHeight,
|
|
190
|
+
settleLatest: settleLatest === true,
|
|
191
|
+
})) {
|
|
192
|
+
resyncFollowSceneToLatestHeight(state, latestAuthoritativeHeight);
|
|
193
|
+
}
|
|
194
|
+
else if (state.liveActivated && state.observedNodeHeight !== null) {
|
|
123
195
|
enqueueFollowHeights(state, state.observedNodeHeight);
|
|
124
196
|
}
|
|
125
197
|
if (!wasLive && state.liveActivated && state.displayedCenterHeight === null) {
|
|
@@ -363,7 +435,18 @@ export function renderFollowFrame(state, statusFieldText, now, options = {}) {
|
|
|
363
435
|
if (headerRow !== undefined) {
|
|
364
436
|
frame[MESSAGE_FIELD_ROW] = replaceSegment(headerRow, FIELD_LEFT, FIELD_WIDTH, renderFollowHeaderField(options));
|
|
365
437
|
}
|
|
366
|
-
|
|
438
|
+
if (statusFieldText.length > 0
|
|
439
|
+
|| (options.artworkStatusLeftText !== null
|
|
440
|
+
&& options.artworkStatusLeftText !== undefined
|
|
441
|
+
&& options.artworkStatusLeftText.length > 0)
|
|
442
|
+
|| (options.artworkStatusRightText !== null
|
|
443
|
+
&& options.artworkStatusRightText !== undefined
|
|
444
|
+
&& options.artworkStatusRightText.length > 0)) {
|
|
445
|
+
const statusRow = frame[STATUS_FIELD_ROW];
|
|
446
|
+
if (statusRow !== undefined) {
|
|
447
|
+
frame[STATUS_FIELD_ROW] = replaceSegment(statusRow, FIELD_LEFT, FIELD_WIDTH, renderFollowStatusField(statusFieldText, options));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
367
450
|
return frame;
|
|
368
451
|
}
|
|
369
452
|
export function createFollowSceneStateForTesting(indexedHeight = null, blockTimesByHeight = {}) {
|
|
@@ -10,6 +10,8 @@ interface RenderStream {
|
|
|
10
10
|
export interface FollowSceneRenderOptions {
|
|
11
11
|
artworkCogText?: string | null;
|
|
12
12
|
artworkSatText?: string | null;
|
|
13
|
+
artworkStatusLeftText?: string | null;
|
|
14
|
+
artworkStatusRightText?: string | null;
|
|
13
15
|
extraLines?: string[];
|
|
14
16
|
}
|
|
15
17
|
export declare class TtyProgressRenderer {
|
|
@@ -94,6 +94,8 @@ export class TtyProgressRenderer {
|
|
|
94
94
|
? [...renderFollowFrame(followScene, statusFieldText, now, {
|
|
95
95
|
artworkCogText: renderOptions.artworkCogText ?? null,
|
|
96
96
|
artworkSatText: renderOptions.artworkSatText ?? null,
|
|
97
|
+
artworkStatusLeftText: renderOptions.artworkStatusLeftText ?? null,
|
|
98
|
+
artworkStatusRightText: renderOptions.artworkStatusRightText ?? null,
|
|
97
99
|
}), "", progressLine, "", ...extraLines]
|
|
98
100
|
: [truncateLine(NEUTRAL_MESSAGE_TITLE, width), progressLine, "", ...extraLines];
|
|
99
101
|
const frame = lines.join("\n");
|
|
@@ -19,6 +19,9 @@ export function isRetryableManagedRpcError(error) {
|
|
|
19
19
|
if (message === "bitcoind_rpc_timeout") {
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
|
+
if (/^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
22
25
|
if (message.startsWith("The managed Bitcoin RPC request to ")) {
|
|
23
26
|
return message.includes(" failed");
|
|
24
27
|
}
|
|
@@ -24,6 +24,7 @@ type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataD
|
|
|
24
24
|
getblockArchivePath?: string | null;
|
|
25
25
|
getblockArchiveEndHeight?: number | null;
|
|
26
26
|
getblockArchiveSha256?: string | null;
|
|
27
|
+
serviceLifetime?: "persistent" | "ephemeral";
|
|
27
28
|
};
|
|
28
29
|
export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
|
|
29
30
|
export interface ManagedBitcoindServiceProbeResult {
|
package/dist/bitcoind/service.js
CHANGED
|
@@ -681,9 +681,10 @@ async function refreshManagedBitcoindStatus(status, paths, options) {
|
|
|
681
681
|
return nextStatus;
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
-
function createNodeHandle(status, paths, options) {
|
|
684
|
+
function createNodeHandle(status, paths, options, ownership) {
|
|
685
685
|
let currentStatus = status;
|
|
686
686
|
const rpc = createRpcClient(currentStatus.rpc);
|
|
687
|
+
let stopped = false;
|
|
687
688
|
return {
|
|
688
689
|
rpc: currentStatus.rpc,
|
|
689
690
|
zmq: currentStatus.zmq,
|
|
@@ -706,9 +707,20 @@ function createNodeHandle(status, paths, options) {
|
|
|
706
707
|
return currentStatus;
|
|
707
708
|
},
|
|
708
709
|
async stop() {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
710
|
+
if (stopped) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
stopped = true;
|
|
714
|
+
if (options.serviceLifetime !== "ephemeral" || ownership === "attached") {
|
|
715
|
+
// Public managed clients detach from persistent services, and ephemeral
|
|
716
|
+
// attach callers must not shut down services they did not launch.
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
await stopManagedBitcoindService({
|
|
720
|
+
dataDir: currentStatus.dataDir,
|
|
721
|
+
walletRootId: currentStatus.walletRootId,
|
|
722
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
723
|
+
});
|
|
712
724
|
},
|
|
713
725
|
};
|
|
714
726
|
}
|
|
@@ -720,7 +732,7 @@ async function tryAttachExistingManagedBitcoindService(options) {
|
|
|
720
732
|
return null;
|
|
721
733
|
}
|
|
722
734
|
const refreshed = await refreshManagedBitcoindStatus(probe.status, paths, options);
|
|
723
|
-
return createNodeHandle(refreshed, paths, options);
|
|
735
|
+
return createNodeHandle(refreshed, paths, options, "attached");
|
|
724
736
|
}
|
|
725
737
|
async function waitForManagedBitcoindService(options, timeoutMs) {
|
|
726
738
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -765,6 +777,7 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
765
777
|
const resolvedOptions = {
|
|
766
778
|
...options,
|
|
767
779
|
dataDir: options.dataDir,
|
|
780
|
+
serviceLifetime: options.serviceLifetime ?? "persistent",
|
|
768
781
|
walletRootId: options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
|
|
769
782
|
};
|
|
770
783
|
const startupTimeoutMs = resolvedOptions.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
@@ -815,11 +828,20 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
815
828
|
port: runtimeConfig.zmqPort,
|
|
816
829
|
pollIntervalMs: startOptions.pollIntervalMs ?? 15_000,
|
|
817
830
|
};
|
|
831
|
+
const spawnOptions = startOptions.serviceLifetime === "ephemeral"
|
|
832
|
+
? {
|
|
833
|
+
stdio: "ignore",
|
|
834
|
+
}
|
|
835
|
+
: {
|
|
836
|
+
detached: true,
|
|
837
|
+
stdio: "ignore",
|
|
838
|
+
};
|
|
818
839
|
const child = spawn(bitcoindPath, buildManagedServiceArgs(startOptions, runtimeConfig), {
|
|
819
|
-
|
|
820
|
-
stdio: "ignore",
|
|
840
|
+
...spawnOptions,
|
|
821
841
|
});
|
|
822
|
-
|
|
842
|
+
if (startOptions.serviceLifetime !== "ephemeral") {
|
|
843
|
+
child.unref();
|
|
844
|
+
}
|
|
823
845
|
const rpc = createRpcClient(rpcConfig);
|
|
824
846
|
try {
|
|
825
847
|
await waitForRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
|
|
@@ -876,7 +898,7 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
876
898
|
});
|
|
877
899
|
}
|
|
878
900
|
await writeBitcoindStatus(paths, status);
|
|
879
|
-
return createNodeHandle(status, paths, resolvedOptions);
|
|
901
|
+
return createNodeHandle(status, paths, resolvedOptions, "started");
|
|
880
902
|
}
|
|
881
903
|
finally {
|
|
882
904
|
await lock.release();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { openManagedBitcoindClientInternal } from "./client.js";
|
|
2
2
|
export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
|
|
3
|
-
export { pauseIndexerDaemonForForegroundClientForTesting } from "./client/factory.js";
|
|
4
3
|
export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
|
|
5
4
|
export { normalizeRpcBlock } from "./normalize.js";
|
|
6
5
|
export { BitcoinRpcClient } from "./rpc.js";
|