@cogcoin/client 1.0.2 → 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 +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/indexer-daemon-main.js +173 -28
- package/dist/bitcoind/indexer-daemon.d.ts +11 -3
- package/dist/bitcoind/indexer-daemon.js +123 -57
- 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/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.js +5 -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 +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/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 +15 -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,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 {
|
|
@@ -95,7 +100,9 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
|
|
|
95
100
|
socket.destroy();
|
|
96
101
|
handler();
|
|
97
102
|
};
|
|
98
|
-
socket.setTimeout(
|
|
103
|
+
socket.setTimeout(request.method === "ResumeBackgroundFollow"
|
|
104
|
+
? INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS
|
|
105
|
+
: INDEXER_DAEMON_REQUEST_TIMEOUT_MS);
|
|
99
106
|
socket.on("connect", () => {
|
|
100
107
|
socket.write(`${JSON.stringify(request)}\n`);
|
|
101
108
|
});
|
|
@@ -169,12 +176,6 @@ function createIndexerDaemonClient(socketPath, closeOptions = null) {
|
|
|
169
176
|
token,
|
|
170
177
|
});
|
|
171
178
|
},
|
|
172
|
-
async pauseBackgroundFollow() {
|
|
173
|
-
await sendRequest({
|
|
174
|
-
id: randomUUID(),
|
|
175
|
-
method: "PauseBackgroundFollow",
|
|
176
|
-
});
|
|
177
|
-
},
|
|
178
179
|
async resumeBackgroundFollow() {
|
|
179
180
|
await sendRequest({
|
|
180
181
|
id: randomUUID(),
|
|
@@ -271,8 +272,20 @@ function buildStatusFromSnapshotHandle(handle) {
|
|
|
271
272
|
lastAppliedAtUnixMs: handle.lastAppliedAtUnixMs,
|
|
272
273
|
activeSnapshotCount: handle.activeSnapshotCount,
|
|
273
274
|
lastError: handle.lastError,
|
|
275
|
+
backgroundFollowActive: handle.backgroundFollowActive,
|
|
276
|
+
bootstrapPhase: handle.bootstrapPhase,
|
|
277
|
+
bootstrapProgress: handle.bootstrapProgress,
|
|
278
|
+
cogcoinSyncHeight: handle.cogcoinSyncHeight,
|
|
279
|
+
cogcoinSyncTargetHeight: handle.cogcoinSyncTargetHeight,
|
|
274
280
|
};
|
|
275
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
|
+
}
|
|
276
289
|
async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
|
|
277
290
|
const client = createIndexerDaemonClient(socketPath);
|
|
278
291
|
try {
|
|
@@ -368,15 +381,84 @@ export async function readObservedIndexerDaemonStatus(options) {
|
|
|
368
381
|
return readJsonFile(paths.indexerDaemonStatusPath);
|
|
369
382
|
}
|
|
370
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
|
+
};
|
|
371
398
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
372
399
|
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
373
400
|
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
374
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
|
+
};
|
|
375
447
|
const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
376
448
|
if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
|
|
377
|
-
|
|
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
|
+
}
|
|
378
460
|
}
|
|
379
|
-
if (existingProbe.compatibility !== "unreachable") {
|
|
461
|
+
if (existingProbe.compatibility !== "unreachable" && existingProbe.compatibility !== "compatible") {
|
|
380
462
|
throw new Error(existingProbe.error ?? "indexer_daemon_protocol_error");
|
|
381
463
|
}
|
|
382
464
|
try {
|
|
@@ -389,53 +471,43 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
389
471
|
try {
|
|
390
472
|
const liveProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
391
473
|
if (liveProbe.compatibility === "compatible" && liveProbe.client !== null) {
|
|
392
|
-
|
|
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
|
+
}
|
|
393
499
|
}
|
|
394
|
-
if (liveProbe.compatibility !== "unreachable") {
|
|
500
|
+
else if (liveProbe.compatibility !== "unreachable") {
|
|
395
501
|
throw new Error(liveProbe.error ?? "indexer_daemon_protocol_error");
|
|
396
502
|
}
|
|
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
|
-
}
|
|
503
|
+
const daemon = await startDaemon();
|
|
418
504
|
try {
|
|
419
|
-
await
|
|
505
|
+
return await requestBackgroundFollow(daemon);
|
|
420
506
|
}
|
|
421
507
|
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;
|
|
508
|
+
await daemon.close().catch(() => undefined);
|
|
509
|
+
throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, { cause: error });
|
|
431
510
|
}
|
|
432
|
-
return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
|
|
433
|
-
dataDir: options.dataDir,
|
|
434
|
-
walletRootId,
|
|
435
|
-
serviceLifetime,
|
|
436
|
-
ownership: "started",
|
|
437
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
438
|
-
});
|
|
439
511
|
}
|
|
440
512
|
finally {
|
|
441
513
|
await lock.release();
|
|
@@ -444,13 +516,7 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
444
516
|
catch (error) {
|
|
445
517
|
if (error instanceof FileLockBusyError) {
|
|
446
518
|
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
447
|
-
return
|
|
448
|
-
dataDir: options.dataDir,
|
|
449
|
-
walletRootId,
|
|
450
|
-
serviceLifetime,
|
|
451
|
-
ownership: "attached",
|
|
452
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
453
|
-
});
|
|
519
|
+
return attachOrStartIndexerDaemon(options);
|
|
454
520
|
}
|
|
455
521
|
throw error;
|
|
456
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");
|
|
@@ -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;
|