@cogcoin/client 1.0.2 → 1.1.1
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 +3 -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/client/sync-engine.js +1 -1
- package/dist/bitcoind/indexer-daemon-main.js +171 -35
- package/dist/bitcoind/indexer-daemon.d.ts +11 -3
- package/dist/bitcoind/indexer-daemon.js +147 -59
- package/dist/bitcoind/indexer-monitor.d.ts +12 -0
- package/dist/bitcoind/indexer-monitor.js +93 -0
- package/dist/bitcoind/progress/controller.js +4 -1
- package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
- package/dist/bitcoind/progress/follow-scene.js +94 -5
- package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
- package/dist/bitcoind/progress/tty-renderer.js +2 -0
- 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 +56 -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/wallet-admin.js +21 -3
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.d.ts +0 -1
- package/dist/cli/context.js +7 -24
- 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 +35 -0
- package/dist/cli/mining-json.d.ts +11 -1
- package/dist/cli/mining-json.js +9 -0
- package/dist/cli/output.js +24 -0
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +23 -0
- package/dist/cli/read-json.d.ts +13 -1
- package/dist/cli/read-json.js +31 -0
- package/dist/cli/runner.js +4 -2
- package/dist/cli/signals.d.ts +12 -0
- package/dist/cli/signals.js +31 -13
- package/dist/cli/types.d.ts +8 -4
- package/dist/cli/update-service.d.ts +2 -12
- package/dist/cli/update-service.js +2 -68
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +17 -0
- package/dist/semver.d.ts +12 -0
- package/dist/semver.js +68 -0
- package/dist/wallet/lifecycle.js +0 -6
- package/dist/wallet/mining/config.js +54 -3
- package/dist/wallet/mining/control.d.ts +5 -2
- package/dist/wallet/mining/control.js +153 -34
- 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/runner.d.ts +58 -2
- package/dist/wallet/mining/runner.js +553 -331
- package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
- package/dist/wallet/mining/sentences.js +7 -4
- package/dist/wallet/mining/types.d.ts +26 -0
- package/dist/wallet/mining/visualizer.d.ts +3 -0
- package/dist/wallet/mining/visualizer.js +106 -12
- package/dist/wallet/read/context.d.ts +1 -0
- package/dist/wallet/read/context.js +19 -10
- package/dist/wallet/reset.js +0 -1
- package/dist/wallet/state/client-password-agent.js +4 -1
- package/dist/wallet/state/client-password.js +15 -8
- package/dist/wallet/tx/anchor.js +0 -1
- package/dist/wallet/tx/bitcoin-transfer.js +0 -1
- package/dist/wallet/tx/cog.js +0 -3
- package/dist/wallet/tx/common.js +1 -1
- package/dist/wallet/tx/domain-admin.js +0 -1
- package/dist/wallet/tx/domain-market.js +0 -3
- package/dist/wallet/tx/field.js +0 -1
- package/dist/wallet/tx/register.js +0 -1
- package/dist/wallet/tx/reputation.js +0 -1
- package/package.json +1 -1
|
@@ -3,11 +3,18 @@ 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, parseSemver } 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 DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
13
|
+
const FORCE_KILL_TIMEOUT_MS = 5_000;
|
|
14
|
+
const INDEXER_DAEMON_REQUEST_TIMEOUT_MS = 15_000;
|
|
15
|
+
const INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS = 35_000;
|
|
16
|
+
const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
|
|
17
|
+
export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
|
|
11
18
|
async function readJsonFile(filePath) {
|
|
12
19
|
try {
|
|
13
20
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
@@ -53,11 +60,16 @@ async function clearIndexerDaemonRuntimeArtifacts(paths) {
|
|
|
53
60
|
await rm(paths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
|
|
54
61
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
55
62
|
}
|
|
63
|
+
function ignoreProcessNotFound(error) {
|
|
64
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
56
68
|
export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
57
69
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
58
70
|
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
59
71
|
const status = await readJsonFile(paths.indexerDaemonStatusPath);
|
|
60
|
-
const processId = status?.processId ?? null;
|
|
72
|
+
const processId = options.processId ?? status?.processId ?? null;
|
|
61
73
|
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
62
74
|
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
63
75
|
return {
|
|
@@ -69,11 +81,23 @@ export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
|
69
81
|
process.kill(processId, "SIGTERM");
|
|
70
82
|
}
|
|
71
83
|
catch (error) {
|
|
72
|
-
|
|
84
|
+
ignoreProcessNotFound(error);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await waitForProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, "indexer_daemon_stop_timeout");
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!(error instanceof Error) || error.message !== "indexer_daemon_stop_timeout") {
|
|
73
91
|
throw error;
|
|
74
92
|
}
|
|
93
|
+
try {
|
|
94
|
+
process.kill(processId, "SIGKILL");
|
|
95
|
+
}
|
|
96
|
+
catch (killError) {
|
|
97
|
+
ignoreProcessNotFound(killError);
|
|
98
|
+
}
|
|
99
|
+
await waitForProcessExit(processId, FORCE_KILL_TIMEOUT_MS, "indexer_daemon_stop_timeout");
|
|
75
100
|
}
|
|
76
|
-
await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
|
|
77
101
|
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
78
102
|
return {
|
|
79
103
|
status: "stopped",
|
|
@@ -95,7 +119,9 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
|
|
|
95
119
|
socket.destroy();
|
|
96
120
|
handler();
|
|
97
121
|
};
|
|
98
|
-
socket.setTimeout(
|
|
122
|
+
socket.setTimeout(request.method === "ResumeBackgroundFollow"
|
|
123
|
+
? INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS
|
|
124
|
+
: INDEXER_DAEMON_REQUEST_TIMEOUT_MS);
|
|
99
125
|
socket.on("connect", () => {
|
|
100
126
|
socket.write(`${JSON.stringify(request)}\n`);
|
|
101
127
|
});
|
|
@@ -169,12 +195,6 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
|
|
|
169
195
|
token,
|
|
170
196
|
});
|
|
171
197
|
},
|
|
172
|
-
async pauseBackgroundFollow() {
|
|
173
|
-
await sendRequest({
|
|
174
|
-
id: randomUUID(),
|
|
175
|
-
method: "PauseBackgroundFollow",
|
|
176
|
-
});
|
|
177
|
-
},
|
|
178
198
|
async resumeBackgroundFollow() {
|
|
179
199
|
await sendRequest({
|
|
180
200
|
id: randomUUID(),
|
|
@@ -271,8 +291,23 @@ function buildStatusFromSnapshotHandle(handle) {
|
|
|
271
291
|
lastAppliedAtUnixMs: handle.lastAppliedAtUnixMs,
|
|
272
292
|
activeSnapshotCount: handle.activeSnapshotCount,
|
|
273
293
|
lastError: handle.lastError,
|
|
294
|
+
backgroundFollowActive: handle.backgroundFollowActive,
|
|
295
|
+
bootstrapPhase: handle.bootstrapPhase,
|
|
296
|
+
bootstrapProgress: handle.bootstrapProgress,
|
|
297
|
+
cogcoinSyncHeight: handle.cogcoinSyncHeight,
|
|
298
|
+
cogcoinSyncTargetHeight: handle.cogcoinSyncTargetHeight,
|
|
274
299
|
};
|
|
275
300
|
}
|
|
301
|
+
function isStaleIndexerDaemonVersion(status, expectedBinaryVersion) {
|
|
302
|
+
if (status === null || expectedBinaryVersion === null || expectedBinaryVersion === undefined) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (parseSemver(expectedBinaryVersion) === null) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const comparison = compareSemver(status.binaryVersion, expectedBinaryVersion);
|
|
309
|
+
return comparison === null || comparison < 0;
|
|
310
|
+
}
|
|
276
311
|
async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
|
|
277
312
|
const client = createIndexerDaemonClient(socketPath);
|
|
278
313
|
try {
|
|
@@ -368,15 +403,84 @@ export async function readObservedIndexerDaemonStatus(options) {
|
|
|
368
403
|
return readJsonFile(paths.indexerDaemonStatusPath);
|
|
369
404
|
}
|
|
370
405
|
export async function attachOrStartIndexerDaemon(options) {
|
|
406
|
+
const requestBackgroundFollow = async (client, observedStatus = null) => {
|
|
407
|
+
if (options.ensureBackgroundFollow !== true) {
|
|
408
|
+
return client;
|
|
409
|
+
}
|
|
410
|
+
if (observedStatus?.backgroundFollowActive === true) {
|
|
411
|
+
return client;
|
|
412
|
+
}
|
|
413
|
+
await client.resumeBackgroundFollow();
|
|
414
|
+
const status = await client.getStatus();
|
|
415
|
+
if (status.backgroundFollowActive !== true) {
|
|
416
|
+
throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE);
|
|
417
|
+
}
|
|
418
|
+
return client;
|
|
419
|
+
};
|
|
371
420
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
372
421
|
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
373
422
|
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
374
423
|
const serviceLifetime = options.serviceLifetime ?? "persistent";
|
|
424
|
+
const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
|
|
425
|
+
const startDaemon = async () => {
|
|
426
|
+
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
427
|
+
const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
|
|
428
|
+
const spawnOptions = serviceLifetime === "ephemeral"
|
|
429
|
+
? {
|
|
430
|
+
stdio: "ignore",
|
|
431
|
+
}
|
|
432
|
+
: {
|
|
433
|
+
detached: true,
|
|
434
|
+
stdio: "ignore",
|
|
435
|
+
};
|
|
436
|
+
const child = spawn(process.execPath, [
|
|
437
|
+
daemonEntryPath,
|
|
438
|
+
`--data-dir=${options.dataDir}`,
|
|
439
|
+
`--database-path=${options.databasePath}`,
|
|
440
|
+
`--wallet-root-id=${walletRootId}`,
|
|
441
|
+
], {
|
|
442
|
+
...spawnOptions,
|
|
443
|
+
});
|
|
444
|
+
if (serviceLifetime !== "ephemeral") {
|
|
445
|
+
child.unref();
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
if (child.pid !== undefined) {
|
|
452
|
+
try {
|
|
453
|
+
process.kill(child.pid, "SIGTERM");
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// ignore shutdown failures while unwinding startup errors
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
|
|
462
|
+
dataDir: options.dataDir,
|
|
463
|
+
walletRootId,
|
|
464
|
+
serviceLifetime,
|
|
465
|
+
ownership: "started",
|
|
466
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
467
|
+
});
|
|
468
|
+
};
|
|
375
469
|
const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
376
470
|
if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
|
|
377
|
-
|
|
471
|
+
if (!isStaleIndexerDaemonVersion(existingProbe.status, expectedBinaryVersion)) {
|
|
472
|
+
try {
|
|
473
|
+
return await requestBackgroundFollow(existingProbe.client, existingProbe.status);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
await existingProbe.client.close().catch(() => undefined);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
await existingProbe.client.close().catch(() => undefined);
|
|
481
|
+
}
|
|
378
482
|
}
|
|
379
|
-
if (existingProbe.compatibility !== "unreachable") {
|
|
483
|
+
if (existingProbe.compatibility !== "unreachable" && existingProbe.compatibility !== "compatible") {
|
|
380
484
|
throw new Error(existingProbe.error ?? "indexer_daemon_protocol_error");
|
|
381
485
|
}
|
|
382
486
|
try {
|
|
@@ -389,53 +493,43 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
389
493
|
try {
|
|
390
494
|
const liveProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
391
495
|
if (liveProbe.compatibility === "compatible" && liveProbe.client !== null) {
|
|
392
|
-
|
|
496
|
+
if (!isStaleIndexerDaemonVersion(liveProbe.status, expectedBinaryVersion)) {
|
|
497
|
+
try {
|
|
498
|
+
return await requestBackgroundFollow(liveProbe.client, liveProbe.status);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
await liveProbe.client.close().catch(() => undefined);
|
|
502
|
+
await stopIndexerDaemonServiceWithLockHeld({
|
|
503
|
+
dataDir: options.dataDir,
|
|
504
|
+
walletRootId,
|
|
505
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
506
|
+
paths,
|
|
507
|
+
processId: liveProbe.status?.processId ?? null,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
await liveProbe.client.close().catch(() => undefined);
|
|
513
|
+
await stopIndexerDaemonServiceWithLockHeld({
|
|
514
|
+
dataDir: options.dataDir,
|
|
515
|
+
walletRootId,
|
|
516
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
517
|
+
paths,
|
|
518
|
+
processId: liveProbe.status?.processId ?? null,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
393
521
|
}
|
|
394
|
-
if (liveProbe.compatibility !== "unreachable") {
|
|
522
|
+
else if (liveProbe.compatibility !== "unreachable") {
|
|
395
523
|
throw new Error(liveProbe.error ?? "indexer_daemon_protocol_error");
|
|
396
524
|
}
|
|
397
|
-
|
|
398
|
-
const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
|
|
399
|
-
const spawnOptions = serviceLifetime === "ephemeral"
|
|
400
|
-
? {
|
|
401
|
-
stdio: "ignore",
|
|
402
|
-
}
|
|
403
|
-
: {
|
|
404
|
-
detached: true,
|
|
405
|
-
stdio: "ignore",
|
|
406
|
-
};
|
|
407
|
-
const child = spawn(process.execPath, [
|
|
408
|
-
daemonEntryPath,
|
|
409
|
-
`--data-dir=${options.dataDir}`,
|
|
410
|
-
`--database-path=${options.databasePath}`,
|
|
411
|
-
`--wallet-root-id=${walletRootId}`,
|
|
412
|
-
], {
|
|
413
|
-
...spawnOptions,
|
|
414
|
-
});
|
|
415
|
-
if (serviceLifetime !== "ephemeral") {
|
|
416
|
-
child.unref();
|
|
417
|
-
}
|
|
525
|
+
const daemon = await startDaemon();
|
|
418
526
|
try {
|
|
419
|
-
await
|
|
527
|
+
return await requestBackgroundFollow(daemon);
|
|
420
528
|
}
|
|
421
529
|
catch (error) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
process.kill(child.pid, "SIGTERM");
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
// ignore shutdown failures while unwinding startup errors
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
throw error;
|
|
530
|
+
await daemon.close().catch(() => undefined);
|
|
531
|
+
throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, { cause: error });
|
|
431
532
|
}
|
|
432
|
-
return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
|
|
433
|
-
dataDir: options.dataDir,
|
|
434
|
-
walletRootId,
|
|
435
|
-
serviceLifetime,
|
|
436
|
-
ownership: "started",
|
|
437
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
438
|
-
});
|
|
439
533
|
}
|
|
440
534
|
finally {
|
|
441
535
|
await lock.release();
|
|
@@ -444,13 +538,7 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
444
538
|
catch (error) {
|
|
445
539
|
if (error instanceof FileLockBusyError) {
|
|
446
540
|
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
447
|
-
return
|
|
448
|
-
dataDir: options.dataDir,
|
|
449
|
-
walletRootId,
|
|
450
|
-
serviceLifetime,
|
|
451
|
-
ownership: "attached",
|
|
452
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
453
|
-
});
|
|
541
|
+
return attachOrStartIndexerDaemon(options);
|
|
454
542
|
}
|
|
455
543
|
throw error;
|
|
456
544
|
}
|
|
@@ -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,93 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
3
|
+
import { readPackageVersionFromDisk } from "../package-version.js";
|
|
4
|
+
import { attachOrStartIndexerDaemon, readObservedIndexerDaemonStatus, } from "./indexer-daemon.js";
|
|
5
|
+
import { resolveCogcoinProcessingStartHeight } from "./processing-start-height.js";
|
|
6
|
+
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
7
|
+
import { attachOrStartManagedBitcoindService } from "./service.js";
|
|
8
|
+
async function readJsonFile(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function resolveStartOptions(options) {
|
|
20
|
+
const paths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
|
|
21
|
+
const observedStatus = await readJsonFile(paths.bitcoindStatusPath).catch(() => null);
|
|
22
|
+
if (observedStatus !== null) {
|
|
23
|
+
return {
|
|
24
|
+
chain: observedStatus.chain,
|
|
25
|
+
startHeight: observedStatus.startHeight,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const genesisParameters = await loadBundledGenesisParameters();
|
|
29
|
+
return {
|
|
30
|
+
chain: "main",
|
|
31
|
+
startHeight: resolveCogcoinProcessingStartHeight(genesisParameters),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function openManagedIndexerMonitor(options) {
|
|
35
|
+
const expectedBinaryVersion = options.expectedBinaryVersion === undefined
|
|
36
|
+
? await readPackageVersionFromDisk()
|
|
37
|
+
: options.expectedBinaryVersion;
|
|
38
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
39
|
+
const startOptions = await resolveStartOptions({
|
|
40
|
+
dataDir: options.dataDir,
|
|
41
|
+
walletRootId,
|
|
42
|
+
});
|
|
43
|
+
await attachOrStartManagedBitcoindService({
|
|
44
|
+
dataDir: options.dataDir,
|
|
45
|
+
chain: startOptions.chain,
|
|
46
|
+
startHeight: startOptions.startHeight,
|
|
47
|
+
walletRootId,
|
|
48
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
49
|
+
});
|
|
50
|
+
const daemon = await attachOrStartIndexerDaemon({
|
|
51
|
+
dataDir: options.dataDir,
|
|
52
|
+
databasePath: options.databasePath,
|
|
53
|
+
walletRootId,
|
|
54
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
55
|
+
ensureBackgroundFollow: true,
|
|
56
|
+
expectedBinaryVersion,
|
|
57
|
+
});
|
|
58
|
+
return createManagedIndexerMonitor({
|
|
59
|
+
daemon,
|
|
60
|
+
dataDir: options.dataDir,
|
|
61
|
+
walletRootId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function createManagedIndexerMonitor(options) {
|
|
65
|
+
let closed = false;
|
|
66
|
+
return {
|
|
67
|
+
async getStatus() {
|
|
68
|
+
if (closed) {
|
|
69
|
+
throw new Error("managed_indexer_monitor_closed");
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return await options.daemon.getStatus();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const observed = await readObservedIndexerDaemonStatus({
|
|
76
|
+
dataDir: options.dataDir,
|
|
77
|
+
walletRootId: options.walletRootId,
|
|
78
|
+
}).catch(() => null);
|
|
79
|
+
if (observed !== null) {
|
|
80
|
+
return observed;
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async close() {
|
|
86
|
+
if (closed) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
closed = true;
|
|
90
|
+
await options.daemon.close().catch(() => undefined);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -128,8 +128,11 @@ export class ManagedProgressController {
|
|
|
128
128
|
this.#cogcoinSyncTargetHeight = null;
|
|
129
129
|
}
|
|
130
130
|
if (this.#followVisualMode) {
|
|
131
|
+
const followIndexedHeight = phase === "follow_tip"
|
|
132
|
+
? this.#cogcoinSyncHeight ?? this.#progress.blocks ?? this.#followScene.indexedHeight
|
|
133
|
+
: undefined;
|
|
131
134
|
syncFollowSceneState(this.#followScene, {
|
|
132
|
-
indexedHeight:
|
|
135
|
+
indexedHeight: followIndexedHeight,
|
|
133
136
|
nodeHeight: this.#progress.blocks,
|
|
134
137
|
liveActivated: phase === "follow_tip" || this.#followScene.liveActivated,
|
|
135
138
|
});
|
|
@@ -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,8 +80,55 @@ 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
|
+
let highest = Math.max(state.indexedHeight ?? Number.NEGATIVE_INFINITY, state.displayedCenterHeight ?? Number.NEGATIVE_INFINITY, state.animation?.height ?? Number.NEGATIVE_INFINITY);
|
|
114
|
+
for (const height of state.queuedHeights) {
|
|
115
|
+
if (height > highest) {
|
|
116
|
+
highest = height;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return highest;
|
|
120
|
+
}
|
|
121
|
+
function resolveLatestAuthoritativeFollowHeight(indexedHeight, nodeHeight) {
|
|
122
|
+
if (indexedHeight === null) {
|
|
123
|
+
return nodeHeight;
|
|
124
|
+
}
|
|
125
|
+
if (nodeHeight === null) {
|
|
126
|
+
return indexedHeight;
|
|
127
|
+
}
|
|
128
|
+
return Math.max(indexedHeight, nodeHeight);
|
|
129
|
+
}
|
|
130
|
+
function resolveDisplayedFollowHeight(state) {
|
|
131
|
+
return state.displayedCenterHeight ?? state.indexedHeight;
|
|
84
132
|
}
|
|
85
133
|
function resetFollowSceneState(state) {
|
|
86
134
|
state.displayedCenterHeight = state.indexedHeight;
|
|
@@ -90,13 +138,33 @@ function resetFollowSceneState(state) {
|
|
|
90
138
|
state.pendingStaticX = null;
|
|
91
139
|
state.animation = null;
|
|
92
140
|
}
|
|
141
|
+
function resyncFollowSceneToLatestHeight(state, latestHeight) {
|
|
142
|
+
state.displayedCenterHeight = latestHeight;
|
|
143
|
+
state.queuedHeights = [];
|
|
144
|
+
state.pendingLabel = null;
|
|
145
|
+
state.pendingStaticX = null;
|
|
146
|
+
state.animation = null;
|
|
147
|
+
}
|
|
148
|
+
function shouldResyncAuthoritativeFollowScene(options) {
|
|
149
|
+
if (options.latestHeight === null) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (options.settleLatest) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
const displayedHeight = resolveDisplayedFollowHeight(options.state);
|
|
156
|
+
if (displayedHeight === null) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return options.latestHeight - displayedHeight > 1;
|
|
160
|
+
}
|
|
93
161
|
function enqueueFollowHeights(state, nodeHeight) {
|
|
94
162
|
const startHeight = Math.max(state.indexedHeight ?? -1, highestTrackedFollowHeight(state));
|
|
95
163
|
for (let height = startHeight + 1; height <= nodeHeight; height += 1) {
|
|
96
164
|
state.queuedHeights.push(height);
|
|
97
165
|
}
|
|
98
166
|
}
|
|
99
|
-
export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, }) {
|
|
167
|
+
export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveActivated, authoritativeTip, settleLatest, }) {
|
|
100
168
|
const wasLive = state.liveActivated;
|
|
101
169
|
const nextLive = liveActivated ?? state.liveActivated;
|
|
102
170
|
let shouldReset = false;
|
|
@@ -119,7 +187,17 @@ export function syncFollowSceneState(state, { indexedHeight, nodeHeight, liveAct
|
|
|
119
187
|
if (shouldReset) {
|
|
120
188
|
resetFollowSceneState(state);
|
|
121
189
|
}
|
|
122
|
-
|
|
190
|
+
const latestAuthoritativeHeight = resolveLatestAuthoritativeFollowHeight(state.indexedHeight, state.observedNodeHeight);
|
|
191
|
+
if (state.liveActivated
|
|
192
|
+
&& authoritativeTip === true
|
|
193
|
+
&& shouldResyncAuthoritativeFollowScene({
|
|
194
|
+
state,
|
|
195
|
+
latestHeight: latestAuthoritativeHeight,
|
|
196
|
+
settleLatest: settleLatest === true,
|
|
197
|
+
})) {
|
|
198
|
+
resyncFollowSceneToLatestHeight(state, latestAuthoritativeHeight);
|
|
199
|
+
}
|
|
200
|
+
else if (state.liveActivated && state.observedNodeHeight !== null) {
|
|
123
201
|
enqueueFollowHeights(state, state.observedNodeHeight);
|
|
124
202
|
}
|
|
125
203
|
if (!wasLive && state.liveActivated && state.displayedCenterHeight === null) {
|
|
@@ -363,7 +441,18 @@ export function renderFollowFrame(state, statusFieldText, now, options = {}) {
|
|
|
363
441
|
if (headerRow !== undefined) {
|
|
364
442
|
frame[MESSAGE_FIELD_ROW] = replaceSegment(headerRow, FIELD_LEFT, FIELD_WIDTH, renderFollowHeaderField(options));
|
|
365
443
|
}
|
|
366
|
-
|
|
444
|
+
if (statusFieldText.length > 0
|
|
445
|
+
|| (options.artworkStatusLeftText !== null
|
|
446
|
+
&& options.artworkStatusLeftText !== undefined
|
|
447
|
+
&& options.artworkStatusLeftText.length > 0)
|
|
448
|
+
|| (options.artworkStatusRightText !== null
|
|
449
|
+
&& options.artworkStatusRightText !== undefined
|
|
450
|
+
&& options.artworkStatusRightText.length > 0)) {
|
|
451
|
+
const statusRow = frame[STATUS_FIELD_ROW];
|
|
452
|
+
if (statusRow !== undefined) {
|
|
453
|
+
frame[STATUS_FIELD_ROW] = replaceSegment(statusRow, FIELD_LEFT, FIELD_WIDTH, renderFollowStatusField(statusFieldText, options));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
367
456
|
return frame;
|
|
368
457
|
}
|
|
369
458
|
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");
|
|
@@ -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";
|
package/dist/bitcoind/testing.js
CHANGED
|
@@ -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";
|
package/dist/bitcoind/types.d.ts
CHANGED
|
@@ -154,7 +154,6 @@ export interface ManagedGetblockArchiveRestartRequest {
|
|
|
154
154
|
}
|
|
155
155
|
export interface ManagedBitcoindOptions extends ClientOptions {
|
|
156
156
|
dataDir?: string;
|
|
157
|
-
databasePath?: string;
|
|
158
157
|
rpcPort?: number;
|
|
159
158
|
zmqPort?: number;
|
|
160
159
|
p2pPort?: number;
|
|
@@ -173,7 +172,6 @@ export interface ManagedBitcoindClient extends Client {
|
|
|
173
172
|
syncToTip(): Promise<SyncResult>;
|
|
174
173
|
startFollowingTip(): Promise<void>;
|
|
175
174
|
getNodeStatus(): Promise<ManagedBitcoindStatus>;
|
|
176
|
-
detachToBackgroundFollow(): Promise<void>;
|
|
177
175
|
close(): Promise<void>;
|
|
178
176
|
}
|
|
179
177
|
export interface InternalManagedBitcoindOptions extends ManagedBitcoindOptions {
|
|
@@ -235,6 +233,11 @@ export interface ManagedIndexerDaemonStatus {
|
|
|
235
233
|
lastAppliedAtUnixMs: number | null;
|
|
236
234
|
activeSnapshotCount: number;
|
|
237
235
|
lastError: string | null;
|
|
236
|
+
backgroundFollowActive?: boolean;
|
|
237
|
+
bootstrapPhase?: BootstrapPhase | null;
|
|
238
|
+
bootstrapProgress?: BootstrapProgress | null;
|
|
239
|
+
cogcoinSyncHeight?: number | null;
|
|
240
|
+
cogcoinSyncTargetHeight?: number | null;
|
|
238
241
|
}
|
|
239
242
|
export interface ManagedIndexerDaemonObservedStatus extends Omit<ManagedIndexerDaemonStatus, "serviceApiVersion" | "schemaVersion"> {
|
|
240
243
|
serviceApiVersion: string;
|