@cogcoin/client 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/bitcoind/bootstrap/getblock-archive.d.ts +39 -0
- package/dist/bitcoind/bootstrap/getblock-archive.js +548 -0
- package/dist/bitcoind/bootstrap.d.ts +1 -0
- package/dist/bitcoind/bootstrap.js +1 -0
- package/dist/bitcoind/client/factory.js +84 -30
- package/dist/bitcoind/client/managed-client.js +2 -1
- package/dist/bitcoind/client/sync-engine.js +7 -0
- package/dist/bitcoind/errors.js +18 -0
- package/dist/bitcoind/indexer-daemon-main.js +78 -0
- package/dist/bitcoind/indexer-daemon.d.ts +3 -1
- package/dist/bitcoind/indexer-daemon.js +13 -6
- package/dist/bitcoind/node.js +2 -0
- package/dist/bitcoind/progress/constants.d.ts +1 -0
- package/dist/bitcoind/progress/constants.js +1 -0
- package/dist/bitcoind/progress/controller.d.ts +22 -0
- package/dist/bitcoind/progress/controller.js +48 -23
- package/dist/bitcoind/progress/formatting.js +25 -0
- package/dist/bitcoind/progress/render-policy.d.ts +35 -0
- package/dist/bitcoind/progress/render-policy.js +81 -0
- package/dist/bitcoind/service-paths.js +2 -6
- package/dist/bitcoind/service.d.ts +5 -1
- package/dist/bitcoind/service.js +93 -54
- package/dist/bitcoind/testing.d.ts +1 -1
- package/dist/bitcoind/testing.js +1 -1
- package/dist/bitcoind/types.d.ts +35 -1
- package/dist/cli/commands/follow.js +2 -0
- package/dist/cli/commands/getblock-archive-restart.d.ts +5 -0
- package/dist/cli/commands/getblock-archive-restart.js +15 -0
- package/dist/cli/commands/mining-admin.js +4 -0
- package/dist/cli/commands/mining-read.js +8 -5
- package/dist/cli/commands/mining-runtime.js +4 -0
- package/dist/cli/commands/status.js +2 -0
- package/dist/cli/commands/sync.js +2 -0
- package/dist/cli/commands/wallet-admin.js +29 -3
- package/dist/cli/commands/wallet-mutation.js +57 -4
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.js +5 -3
- package/dist/cli/mutation-command-groups.d.ts +2 -1
- package/dist/cli/mutation-command-groups.js +5 -0
- package/dist/cli/mutation-json.d.ts +18 -2
- package/dist/cli/mutation-json.js +47 -0
- package/dist/cli/mutation-success.d.ts +1 -0
- package/dist/cli/mutation-success.js +2 -2
- package/dist/cli/output.js +84 -1
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +127 -3
- package/dist/cli/preview-json.d.ts +10 -1
- package/dist/cli/preview-json.js +30 -0
- package/dist/cli/prompt.js +1 -1
- package/dist/cli/runner.js +3 -0
- package/dist/cli/types.d.ts +11 -4
- package/dist/cli/wallet-format.js +6 -0
- package/dist/wallet/lifecycle.d.ts +15 -1
- package/dist/wallet/lifecycle.js +147 -83
- package/dist/wallet/mining/visualizer.d.ts +11 -6
- package/dist/wallet/mining/visualizer.js +32 -15
- package/dist/wallet/reset.js +39 -27
- package/dist/wallet/runtime.d.ts +12 -1
- package/dist/wallet/runtime.js +53 -11
- package/dist/wallet/state/provider.d.ts +1 -0
- package/dist/wallet/state/provider.js +119 -3
- package/dist/wallet/state/seed-index.d.ts +43 -0
- package/dist/wallet/state/seed-index.js +151 -0
- package/dist/wallet/tx/anchor.d.ts +22 -0
- package/dist/wallet/tx/anchor.js +215 -8
- package/dist/wallet/tx/index.d.ts +1 -1
- package/dist/wallet/tx/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
2
2
|
import { resolveDefaultBitcoindDataDirForTesting } from "../../app-paths.js";
|
|
3
3
|
import { openClient } from "../../client.js";
|
|
4
|
-
import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting } from "../bootstrap.js";
|
|
4
|
+
import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, } from "../bootstrap.js";
|
|
5
5
|
import { attachOrStartIndexerDaemon } from "../indexer-daemon.js";
|
|
6
6
|
import { createRpcClient } from "../node.js";
|
|
7
7
|
import { assertCogcoinProcessingStartHeight, resolveCogcoinProcessingStartHeight, } from "../processing-start-height.js";
|
|
8
8
|
import { ManagedProgressController } from "../progress.js";
|
|
9
|
-
import { attachOrStartManagedBitcoindService } from "../service.js";
|
|
9
|
+
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../service.js";
|
|
10
10
|
import { DefaultManagedBitcoindClient } from "./managed-client.js";
|
|
11
11
|
const DEFAULT_SYNC_DEBOUNCE_MS = 250;
|
|
12
12
|
async function createManagedBitcoindClient(options) {
|
|
@@ -17,40 +17,94 @@ async function createManagedBitcoindClient(options) {
|
|
|
17
17
|
genesisParameters,
|
|
18
18
|
});
|
|
19
19
|
const dataDir = options.dataDir ?? resolveDefaultBitcoindDataDirForTesting();
|
|
20
|
-
const node = await attachOrStartManagedBitcoindService({
|
|
21
|
-
...options,
|
|
22
|
-
dataDir,
|
|
23
|
-
});
|
|
24
|
-
const rpc = createRpcClient(node.rpc);
|
|
25
20
|
const progress = new ManagedProgressController({
|
|
26
21
|
onProgress: options.onProgress,
|
|
27
22
|
progressOutput: options.progressOutput,
|
|
28
|
-
quoteStatePath: resolveBootstrapPathsForTesting(
|
|
29
|
-
snapshot: DEFAULT_SNAPSHOT_METADATA,
|
|
30
|
-
});
|
|
31
|
-
const bootstrap = new AssumeUtxoBootstrapController({
|
|
32
|
-
rpc,
|
|
33
|
-
dataDir: node.dataDir,
|
|
34
|
-
progress,
|
|
23
|
+
quoteStatePath: resolveBootstrapPathsForTesting(dataDir, DEFAULT_SNAPSHOT_METADATA).quoteStatePath,
|
|
35
24
|
snapshot: DEFAULT_SNAPSHOT_METADATA,
|
|
36
25
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
let progressStarted = false;
|
|
27
|
+
try {
|
|
28
|
+
await progress.start();
|
|
29
|
+
progressStarted = true;
|
|
30
|
+
let getblockArchive = options.chain === "main"
|
|
31
|
+
? await prepareLatestGetblockArchiveForTesting({
|
|
32
|
+
dataDir,
|
|
33
|
+
progress,
|
|
34
|
+
fetchImpl: options.fetchImpl,
|
|
35
|
+
})
|
|
36
|
+
: null;
|
|
37
|
+
if (options.chain === "main" && getblockArchive !== null) {
|
|
38
|
+
const existingProbe = await probeManagedBitcoindService({
|
|
39
|
+
...options,
|
|
40
|
+
dataDir,
|
|
41
|
+
});
|
|
42
|
+
if (existingProbe.compatibility === "compatible" && existingProbe.status !== null) {
|
|
43
|
+
const currentArchiveEndHeight = existingProbe.status.getblockArchiveEndHeight ?? null;
|
|
44
|
+
const currentArchiveSha256 = existingProbe.status.getblockArchiveSha256 ?? null;
|
|
45
|
+
const nextArchiveEndHeight = getblockArchive.manifest.endHeight;
|
|
46
|
+
const nextArchiveSha256 = getblockArchive.manifest.artifactSha256;
|
|
47
|
+
const needsRestart = currentArchiveEndHeight !== nextArchiveEndHeight
|
|
48
|
+
|| currentArchiveSha256 !== nextArchiveSha256;
|
|
49
|
+
if (needsRestart) {
|
|
50
|
+
const restartApproved = options.confirmGetblockArchiveRestart === undefined
|
|
51
|
+
? false
|
|
52
|
+
: await options.confirmGetblockArchiveRestart({
|
|
53
|
+
currentArchiveEndHeight,
|
|
54
|
+
nextArchiveEndHeight,
|
|
55
|
+
});
|
|
56
|
+
if (restartApproved) {
|
|
57
|
+
await stopManagedBitcoindService({
|
|
58
|
+
dataDir,
|
|
59
|
+
walletRootId: options.walletRootId,
|
|
60
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
getblockArchive = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const node = await attachOrStartManagedBitcoindService({
|
|
70
|
+
...options,
|
|
44
71
|
dataDir,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
getblockArchivePath: getblockArchive?.artifactPath ?? null,
|
|
73
|
+
getblockArchiveEndHeight: getblockArchive?.manifest.endHeight ?? null,
|
|
74
|
+
getblockArchiveSha256: getblockArchive?.manifest.artifactSha256 ?? null,
|
|
75
|
+
});
|
|
76
|
+
const rpc = createRpcClient(node.rpc);
|
|
77
|
+
const bootstrap = new AssumeUtxoBootstrapController({
|
|
78
|
+
rpc,
|
|
79
|
+
dataDir: node.dataDir,
|
|
80
|
+
progress,
|
|
81
|
+
snapshot: DEFAULT_SNAPSHOT_METADATA,
|
|
82
|
+
});
|
|
83
|
+
const client = await openClient({
|
|
84
|
+
store: options.store,
|
|
85
|
+
genesisParameters,
|
|
86
|
+
snapshotInterval: options.snapshotInterval,
|
|
87
|
+
});
|
|
88
|
+
const indexerDaemon = options.databasePath
|
|
89
|
+
? await attachOrStartIndexerDaemon({
|
|
90
|
+
dataDir,
|
|
91
|
+
databasePath: options.databasePath,
|
|
92
|
+
walletRootId: options.walletRootId,
|
|
93
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
94
|
+
})
|
|
95
|
+
: null;
|
|
96
|
+
await indexerDaemon?.pauseBackgroundFollow();
|
|
97
|
+
// The persistent service may already exist from a non-processing attach path
|
|
98
|
+
// that used startHeight 0. Cogcoin replay still begins at the requested
|
|
99
|
+
// processing boundary for this managed client.
|
|
100
|
+
return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, indexerDaemon, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (progressStarted) {
|
|
104
|
+
await progress.close().catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
54
108
|
}
|
|
55
109
|
export async function openManagedBitcoindClient(options) {
|
|
56
110
|
const genesisParameters = options.genesisParameters ?? await loadBundledGenesisParameters();
|
|
@@ -176,8 +176,9 @@ export class DefaultManagedBitcoindClient {
|
|
|
176
176
|
await this.#syncPromise.catch(() => undefined);
|
|
177
177
|
await this.#progress.close();
|
|
178
178
|
await this.#node.stop();
|
|
179
|
-
await this.#indexerDaemon?.close();
|
|
180
179
|
await this.#client.close();
|
|
180
|
+
await this.#indexerDaemon?.resumeBackgroundFollow().catch(() => undefined);
|
|
181
|
+
await this.#indexerDaemon?.close();
|
|
181
182
|
}
|
|
182
183
|
async playSyncCompletionScene() {
|
|
183
184
|
this.#assertOpen();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { waitForGetblockArchiveImport } from "../bootstrap.js";
|
|
1
2
|
import { formatManagedSyncErrorMessage } from "../errors.js";
|
|
2
3
|
import { normalizeRpcBlock } from "../normalize.js";
|
|
3
4
|
import { MANAGED_RPC_RETRY_MESSAGE, consumeManagedRpcRetryDelayMs, createManagedRpcRetryState, describeManagedRpcRetryError, isRetryableManagedRpcError, resetManagedRpcRetryState, } from "../retryable-rpc.js";
|
|
@@ -151,6 +152,12 @@ export async function syncToTip(dependencies) {
|
|
|
151
152
|
signal: dependencies.abortSignal,
|
|
152
153
|
retryState,
|
|
153
154
|
}));
|
|
155
|
+
if (dependencies.node.expectedChain === "main"
|
|
156
|
+
&& dependencies.node.getblockArchiveEndHeight !== null) {
|
|
157
|
+
await waitForGetblockArchiveImport({
|
|
158
|
+
getBlockchainInfo: () => runRpc(() => dependencies.rpc.getBlockchainInfo()),
|
|
159
|
+
}, dependencies.progress, dependencies.node.getblockArchiveEndHeight, dependencies.abortSignal);
|
|
160
|
+
}
|
|
154
161
|
const startTip = await dependencies.client.getTip();
|
|
155
162
|
const aggregate = {
|
|
156
163
|
appliedBlocks: 0,
|
package/dist/bitcoind/errors.js
CHANGED
|
@@ -5,6 +5,24 @@ function appendNextStep(message, nextStep) {
|
|
|
5
5
|
return `${message} Next: ${nextStep}`;
|
|
6
6
|
}
|
|
7
7
|
export function formatManagedSyncErrorMessage(message) {
|
|
8
|
+
if (message.startsWith("managed_getblock_archive_manifest_http_")) {
|
|
9
|
+
return appendNextStep(`Getblock archive manifest request failed (${message.replace("managed_getblock_archive_manifest_http_", "HTTP ")}).`, "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
10
|
+
}
|
|
11
|
+
if (message.startsWith("managed_getblock_archive_http_")) {
|
|
12
|
+
return appendNextStep(`Getblock archive request failed (${message.replace("managed_getblock_archive_http_", "HTTP ")}).`, "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
13
|
+
}
|
|
14
|
+
if (message === "managed_getblock_archive_response_body_missing") {
|
|
15
|
+
return appendNextStep("Getblock archive server returned an empty response body.", "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
16
|
+
}
|
|
17
|
+
if (message === "managed_getblock_archive_resume_requires_partial_content") {
|
|
18
|
+
return appendNextStep("Getblock archive server ignored the resume request for a partial download.", "Wait a moment and rerun sync. If this keeps happening, confirm the snapshot host supports HTTP range requests.");
|
|
19
|
+
}
|
|
20
|
+
if (message.startsWith("managed_getblock_archive_chunk_sha256_mismatch_")) {
|
|
21
|
+
return appendNextStep("A downloaded getblock archive chunk was corrupted and was rolled back to the last verified checkpoint.", "Wait a moment and rerun sync. If this keeps happening, check local disk health and the stability of the archive download.");
|
|
22
|
+
}
|
|
23
|
+
if (message === "managed_getblock_archive_sha256_mismatch" || message === "managed_getblock_archive_truncated") {
|
|
24
|
+
return appendNextStep("The downloaded getblock archive did not match the published manifest.", "Rerun sync so the archive can be downloaded again. If this keeps happening, check local disk health and the snapshot host.");
|
|
25
|
+
}
|
|
8
26
|
if (message === "bitcoind_no_peers_for_header_sync_check_internet_or_firewall") {
|
|
9
27
|
return appendNextStep("No Bitcoin peers were available for header sync.", "Check your internet access and firewall rules for outbound Bitcoin connections, then rerun sync.");
|
|
10
28
|
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { serializeIndexerState } from "@cogcoin/indexer";
|
|
5
|
+
import { openManagedBitcoindClientInternal } from "./client.js";
|
|
5
6
|
import { openClient } from "../client.js";
|
|
6
7
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
7
8
|
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
@@ -29,6 +30,9 @@ async function readJsonFile(filePath) {
|
|
|
29
30
|
throw error;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
async function readManagedBitcoindStatus(paths) {
|
|
34
|
+
return readJsonFile(paths.bitcoindStatusPath);
|
|
35
|
+
}
|
|
32
36
|
async function readPackageVersionFromDisk() {
|
|
33
37
|
try {
|
|
34
38
|
const raw = await readFile(new URL("../../package.json", import.meta.url), "utf8");
|
|
@@ -168,6 +172,9 @@ async function main() {
|
|
|
168
172
|
let lastAppliedAtUnixMs = null;
|
|
169
173
|
let lastError = null;
|
|
170
174
|
let hasSuccessfulCoreTipRefresh = false;
|
|
175
|
+
let backgroundStore = null;
|
|
176
|
+
let backgroundClient = null;
|
|
177
|
+
let backgroundResumePromise = null;
|
|
171
178
|
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
172
179
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
173
180
|
const observeAppliedTip = (appliedTip, now) => {
|
|
@@ -264,6 +271,58 @@ async function main() {
|
|
|
264
271
|
lastError = leaseState.lastError;
|
|
265
272
|
return writeStatus();
|
|
266
273
|
};
|
|
274
|
+
const pauseBackgroundFollow = async () => {
|
|
275
|
+
const pendingResume = backgroundResumePromise;
|
|
276
|
+
backgroundResumePromise = null;
|
|
277
|
+
await pendingResume?.catch(() => undefined);
|
|
278
|
+
const client = backgroundClient;
|
|
279
|
+
const store = backgroundStore;
|
|
280
|
+
backgroundClient = null;
|
|
281
|
+
backgroundStore = null;
|
|
282
|
+
await client?.close().catch(() => undefined);
|
|
283
|
+
await store?.close().catch(() => undefined);
|
|
284
|
+
};
|
|
285
|
+
const resumeBackgroundFollow = async () => {
|
|
286
|
+
if (backgroundClient !== null) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (backgroundResumePromise !== null) {
|
|
290
|
+
return backgroundResumePromise;
|
|
291
|
+
}
|
|
292
|
+
backgroundResumePromise = (async () => {
|
|
293
|
+
const bitcoindStatus = await readManagedBitcoindStatus(paths);
|
|
294
|
+
const store = await openSqliteStore({ filename: databasePath });
|
|
295
|
+
try {
|
|
296
|
+
const client = await openManagedBitcoindClientInternal({
|
|
297
|
+
store,
|
|
298
|
+
dataDir,
|
|
299
|
+
chain: bitcoindStatus?.chain ?? "main",
|
|
300
|
+
startHeight: bitcoindStatus?.startHeight ?? 0,
|
|
301
|
+
walletRootId,
|
|
302
|
+
progressOutput: "none",
|
|
303
|
+
});
|
|
304
|
+
try {
|
|
305
|
+
await client.startFollowingTip();
|
|
306
|
+
backgroundStore = store;
|
|
307
|
+
backgroundClient = client;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
await client.close().catch(() => undefined);
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
await store.close().catch(() => undefined);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
try {
|
|
320
|
+
await backgroundResumePromise;
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
backgroundResumePromise = null;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
267
326
|
const heartbeat = setInterval(() => {
|
|
268
327
|
void refreshStatus().catch(() => undefined);
|
|
269
328
|
const now = Date.now();
|
|
@@ -427,6 +486,24 @@ async function main() {
|
|
|
427
486
|
});
|
|
428
487
|
return;
|
|
429
488
|
}
|
|
489
|
+
if (request.method === "PauseBackgroundFollow") {
|
|
490
|
+
await pauseBackgroundFollow();
|
|
491
|
+
writeResponse({
|
|
492
|
+
id: request.id,
|
|
493
|
+
ok: true,
|
|
494
|
+
result: null,
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (request.method === "ResumeBackgroundFollow") {
|
|
499
|
+
await resumeBackgroundFollow();
|
|
500
|
+
writeResponse({
|
|
501
|
+
id: request.id,
|
|
502
|
+
ok: true,
|
|
503
|
+
result: null,
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
430
507
|
throw new Error(`indexer_daemon_unknown_method_${request.method}`);
|
|
431
508
|
}
|
|
432
509
|
catch (error) {
|
|
@@ -443,6 +520,7 @@ async function main() {
|
|
|
443
520
|
});
|
|
444
521
|
const shutdown = async () => {
|
|
445
522
|
clearInterval(heartbeat);
|
|
523
|
+
await pauseBackgroundFollow().catch(() => undefined);
|
|
446
524
|
state = "stopping";
|
|
447
525
|
heartbeatAtUnixMs = Date.now();
|
|
448
526
|
updatedAtUnixMs = heartbeatAtUnixMs;
|
|
@@ -2,7 +2,7 @@ import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatu
|
|
|
2
2
|
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
3
3
|
interface DaemonRequest {
|
|
4
4
|
id: string;
|
|
5
|
-
method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot";
|
|
5
|
+
method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot" | "PauseBackgroundFollow" | "ResumeBackgroundFollow";
|
|
6
6
|
token?: string;
|
|
7
7
|
}
|
|
8
8
|
interface DaemonResponse {
|
|
@@ -65,6 +65,8 @@ export interface IndexerDaemonClient {
|
|
|
65
65
|
openSnapshot(): Promise<IndexerSnapshotHandle>;
|
|
66
66
|
readSnapshot(token: string): Promise<IndexerSnapshotPayload>;
|
|
67
67
|
closeSnapshot(token: string): Promise<void>;
|
|
68
|
+
pauseBackgroundFollow(): Promise<void>;
|
|
69
|
+
resumeBackgroundFollow(): Promise<void>;
|
|
68
70
|
close(): Promise<void>;
|
|
69
71
|
}
|
|
70
72
|
export type IndexerDaemonCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "schema-mismatch" | "unreachable" | "protocol-error";
|
|
@@ -168,6 +168,18 @@ function createIndexerDaemonClient(socketPath) {
|
|
|
168
168
|
token,
|
|
169
169
|
});
|
|
170
170
|
},
|
|
171
|
+
async pauseBackgroundFollow() {
|
|
172
|
+
await sendRequest({
|
|
173
|
+
id: randomUUID(),
|
|
174
|
+
method: "PauseBackgroundFollow",
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
async resumeBackgroundFollow() {
|
|
178
|
+
await sendRequest({
|
|
179
|
+
id: randomUUID(),
|
|
180
|
+
method: "ResumeBackgroundFollow",
|
|
181
|
+
});
|
|
182
|
+
},
|
|
171
183
|
async close() {
|
|
172
184
|
return;
|
|
173
185
|
},
|
|
@@ -177,9 +189,6 @@ function validateIndexerRuntimeIdentity(identity, expectedWalletRootId) {
|
|
|
177
189
|
if (identity.serviceApiVersion !== INDEXER_DAEMON_SERVICE_API_VERSION) {
|
|
178
190
|
throw new Error("indexer_daemon_service_version_mismatch");
|
|
179
191
|
}
|
|
180
|
-
if (identity.walletRootId !== expectedWalletRootId) {
|
|
181
|
-
throw new Error("indexer_daemon_wallet_root_mismatch");
|
|
182
|
-
}
|
|
183
192
|
if (identity.schemaVersion !== INDEXER_DAEMON_SCHEMA_VERSION || identity.state === "schema-mismatch") {
|
|
184
193
|
throw new Error("indexer_daemon_schema_mismatch");
|
|
185
194
|
}
|
|
@@ -271,9 +280,7 @@ async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
|
|
|
271
280
|
compatibility: error instanceof Error
|
|
272
281
|
? error.message === "indexer_daemon_service_version_mismatch"
|
|
273
282
|
? "service-version-mismatch"
|
|
274
|
-
:
|
|
275
|
-
? "wallet-root-mismatch"
|
|
276
|
-
: "schema-mismatch"
|
|
283
|
+
: "schema-mismatch"
|
|
277
284
|
: "protocol-error",
|
|
278
285
|
status,
|
|
279
286
|
client: null,
|
package/dist/bitcoind/node.js
CHANGED
|
@@ -182,6 +182,8 @@ export async function launchManagedBitcoindNode(options) {
|
|
|
182
182
|
expectedChain: resolvedOptions.chain,
|
|
183
183
|
startHeight: resolvedOptions.startHeight,
|
|
184
184
|
dataDir: resolvedOptions.dataDir,
|
|
185
|
+
getblockArchiveEndHeight: null,
|
|
186
|
+
getblockArchiveSha256: null,
|
|
185
187
|
async validate() {
|
|
186
188
|
await validateNodeConfigForTesting(rpcClient, resolvedOptions.chain, zmqEndpoint);
|
|
187
189
|
},
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
import type { QuoteDisplayPhase } from "../quotes.js";
|
|
1
2
|
import type { BootstrapPhase, BootstrapProgress, ManagedBitcoindProgressEvent, ProgressOutputMode, SnapshotMetadata, WritingQuote } from "../types.js";
|
|
3
|
+
import { type FollowSceneStateForTesting } from "./follow-scene.js";
|
|
4
|
+
import { type RenderClock, type TtyRenderStream } from "./render-policy.js";
|
|
5
|
+
interface QuoteRotatorLike {
|
|
6
|
+
current(now?: number): Promise<{
|
|
7
|
+
displayPhase: QuoteDisplayPhase;
|
|
8
|
+
currentQuote: WritingQuote | null;
|
|
9
|
+
displayStartedAt: number;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
interface ProgressRendererLike {
|
|
13
|
+
render(displayPhase: QuoteDisplayPhase, quote: WritingQuote | null, progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, introElapsedMs?: number, statusFieldText?: string): void;
|
|
14
|
+
renderTrainScene(kind: "intro" | "completion", progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, elapsedMs: number, statusFieldText?: string): void;
|
|
15
|
+
renderFollowScene(progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, followScene: FollowSceneStateForTesting, statusFieldText?: string): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
2
18
|
interface ProgressControllerOptions {
|
|
3
19
|
onProgress?: (event: ManagedBitcoindProgressEvent) => void;
|
|
4
20
|
progressOutput?: ProgressOutputMode;
|
|
5
21
|
quoteStatePath: string;
|
|
6
22
|
snapshot: SnapshotMetadata;
|
|
23
|
+
quoteRotator?: QuoteRotatorLike;
|
|
24
|
+
rendererFactory?: (stream: TtyRenderStream) => ProgressRendererLike;
|
|
25
|
+
stream?: TtyRenderStream;
|
|
26
|
+
platform?: NodeJS.Platform;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
clock?: RenderClock;
|
|
7
29
|
}
|
|
8
30
|
export declare class ManagedProgressController {
|
|
9
31
|
#private;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { WritingQuoteRotator as QuoteRotator } from "../quotes.js";
|
|
2
|
-
import { INTRO_TOTAL_MS
|
|
2
|
+
import { INTRO_TOTAL_MS } from "./constants.js";
|
|
3
3
|
import { advanceFollowSceneState, createFollowSceneState, replaceFollowBlockTimes, setFollowBlockTime, syncFollowSceneState, } from "./follow-scene.js";
|
|
4
4
|
import { createBootstrapProgress, createDefaultMessage, resolveStatusFieldText, } from "./formatting.js";
|
|
5
|
+
import { DEFAULT_RENDER_CLOCK, resolveTtyRenderPolicy, TtyRenderThrottle, } from "./render-policy.js";
|
|
5
6
|
import { TtyProgressRenderer } from "./tty-renderer.js";
|
|
6
7
|
export class ManagedProgressController {
|
|
7
8
|
#options;
|
|
8
9
|
#snapshot;
|
|
9
10
|
#outputMode;
|
|
11
|
+
#clock;
|
|
12
|
+
#renderStream;
|
|
13
|
+
#renderThrottle;
|
|
14
|
+
#renderIntervalMs;
|
|
10
15
|
#quoteRotator = null;
|
|
11
16
|
#renderer = null;
|
|
12
17
|
#ticker = null;
|
|
@@ -24,26 +29,47 @@ export class ManagedProgressController {
|
|
|
24
29
|
this.#snapshot = options.snapshot;
|
|
25
30
|
this.#outputMode = options.progressOutput ?? "auto";
|
|
26
31
|
this.#progress = createBootstrapProgress("paused", options.snapshot);
|
|
32
|
+
this.#clock = options.clock ?? DEFAULT_RENDER_CLOCK;
|
|
33
|
+
this.#renderStream = options.stream ?? process.stderr;
|
|
34
|
+
const renderPolicy = resolveTtyRenderPolicy(this.#outputMode, this.#renderStream, {
|
|
35
|
+
platform: options.platform,
|
|
36
|
+
env: options.env,
|
|
37
|
+
});
|
|
38
|
+
this.#renderIntervalMs = renderPolicy.repaintIntervalMs;
|
|
39
|
+
this.#renderThrottle = new TtyRenderThrottle({
|
|
40
|
+
clock: this.#clock,
|
|
41
|
+
intervalMs: this.#renderIntervalMs,
|
|
42
|
+
onRender: () => {
|
|
43
|
+
this.#renderToTty();
|
|
44
|
+
},
|
|
45
|
+
throttled: renderPolicy.linuxHeadlessThrottle,
|
|
46
|
+
});
|
|
27
47
|
}
|
|
28
48
|
async start() {
|
|
29
49
|
if (this.#started) {
|
|
30
50
|
return;
|
|
31
51
|
}
|
|
32
52
|
this.#started = true;
|
|
33
|
-
this.#quoteRotator =
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
this.#quoteRotator = this.#options.quoteRotator
|
|
54
|
+
?? await QuoteRotator.create(this.#options.quoteStatePath);
|
|
55
|
+
if (resolveTtyRenderPolicy(this.#outputMode, this.#renderStream, {
|
|
56
|
+
platform: this.#options.platform,
|
|
57
|
+
env: this.#options.env,
|
|
58
|
+
}).enabled) {
|
|
59
|
+
this.#renderer = this.#options.rendererFactory?.(this.#renderStream)
|
|
60
|
+
?? new TtyProgressRenderer(this.#renderStream);
|
|
36
61
|
}
|
|
37
62
|
await this.#refresh();
|
|
38
|
-
this.#ticker = setInterval(() => {
|
|
63
|
+
this.#ticker = this.#clock.setInterval(() => {
|
|
39
64
|
void this.#refresh();
|
|
40
|
-
},
|
|
65
|
+
}, this.#renderIntervalMs);
|
|
41
66
|
}
|
|
42
67
|
async close() {
|
|
43
68
|
if (this.#ticker !== null) {
|
|
44
|
-
clearInterval(this.#ticker);
|
|
69
|
+
this.#clock.clearInterval(this.#ticker);
|
|
45
70
|
this.#ticker = null;
|
|
46
71
|
}
|
|
72
|
+
this.#renderThrottle.flush();
|
|
47
73
|
this.#renderer?.close();
|
|
48
74
|
this.#renderer = null;
|
|
49
75
|
this.#started = false;
|
|
@@ -72,19 +98,20 @@ export class ManagedProgressController {
|
|
|
72
98
|
if (!this.#started || this.#renderer === null) {
|
|
73
99
|
return;
|
|
74
100
|
}
|
|
101
|
+
this.#renderThrottle.flush();
|
|
75
102
|
if (this.#ticker !== null) {
|
|
76
|
-
clearInterval(this.#ticker);
|
|
103
|
+
this.#clock.clearInterval(this.#ticker);
|
|
77
104
|
this.#ticker = null;
|
|
78
105
|
}
|
|
79
|
-
const startedAt =
|
|
106
|
+
const startedAt = this.#clock.now();
|
|
80
107
|
while (true) {
|
|
81
|
-
const elapsedMs = Math.min(INTRO_TOTAL_MS,
|
|
108
|
+
const elapsedMs = Math.min(INTRO_TOTAL_MS, this.#clock.now() - startedAt);
|
|
82
109
|
this.#renderer.renderTrainScene("completion", this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, elapsedMs);
|
|
83
110
|
if (elapsedMs >= INTRO_TOTAL_MS) {
|
|
84
111
|
break;
|
|
85
112
|
}
|
|
86
113
|
await new Promise((resolve) => {
|
|
87
|
-
setTimeout(resolve,
|
|
114
|
+
this.#clock.setTimeout(resolve, this.#renderIntervalMs);
|
|
88
115
|
});
|
|
89
116
|
}
|
|
90
117
|
}
|
|
@@ -143,7 +170,7 @@ export class ManagedProgressController {
|
|
|
143
170
|
if (!this.#started || this.#quoteRotator === null) {
|
|
144
171
|
return;
|
|
145
172
|
}
|
|
146
|
-
const now =
|
|
173
|
+
const now = this.#clock.now();
|
|
147
174
|
if (this.#followVisualMode) {
|
|
148
175
|
advanceFollowSceneState(this.#followScene, now);
|
|
149
176
|
this.#currentQuote = null;
|
|
@@ -170,20 +197,18 @@ export class ManagedProgressController {
|
|
|
170
197
|
catch {
|
|
171
198
|
// User progress callbacks should never break managed sync.
|
|
172
199
|
}
|
|
200
|
+
this.#renderThrottle.request();
|
|
201
|
+
}
|
|
202
|
+
#renderToTty() {
|
|
203
|
+
if (!this.#started || this.#renderer === null) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const now = this.#clock.now();
|
|
173
207
|
const statusFieldText = resolveStatusFieldText(this.#progress, this.#snapshot.height, now);
|
|
174
208
|
if (this.#followVisualMode) {
|
|
175
|
-
this.#renderer
|
|
209
|
+
this.#renderer.renderFollowScene(this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, this.#followScene, statusFieldText);
|
|
176
210
|
return;
|
|
177
211
|
}
|
|
178
|
-
this.#renderer
|
|
179
|
-
}
|
|
180
|
-
#shouldRenderToTty() {
|
|
181
|
-
if (this.#outputMode === "none") {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
if (this.#outputMode === "tty") {
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
return process.stderr.isTTY === true;
|
|
212
|
+
this.#renderer.render(this.#currentDisplayPhase, this.#currentQuote, this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, Math.max(0, now - this.#currentDisplayStartedAt), statusFieldText);
|
|
188
213
|
}
|
|
189
214
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { FIELD_LEFT, FIELD_WIDTH, PREPARING_SYNC_LINE, PROGRESS_TICK_MS, SCROLL_WINDOW_LEFT, SCROLL_WINDOW_WIDTH, STATUS_ELLIPSIS_TICK_MS, STATUS_ELLIPSIS_WIDTH, } from "./constants.js";
|
|
2
2
|
export function createDefaultMessage(phase) {
|
|
3
3
|
switch (phase) {
|
|
4
|
+
case "getblock_archive_download":
|
|
5
|
+
return "Downloading getblock archive.";
|
|
6
|
+
case "getblock_archive_import":
|
|
7
|
+
return "Bitcoin Core is importing getblock archive blocks.";
|
|
4
8
|
case "snapshot_download":
|
|
5
9
|
return "Downloading UTXO snapshot.";
|
|
6
10
|
case "wait_headers_for_snapshot":
|
|
@@ -121,6 +125,10 @@ function animateStatusEllipsis(now) {
|
|
|
121
125
|
}
|
|
122
126
|
export function resolveStatusFieldText(progress, snapshotHeight, now = 0) {
|
|
123
127
|
switch (progress.phase) {
|
|
128
|
+
case "getblock_archive_download":
|
|
129
|
+
return `Downloading getblock archive${animateStatusEllipsis(now)}`;
|
|
130
|
+
case "getblock_archive_import":
|
|
131
|
+
return `Importing getblock archive${animateStatusEllipsis(now)}`;
|
|
124
132
|
case "paused":
|
|
125
133
|
case "snapshot_download":
|
|
126
134
|
return `Downloading snapshot to ${snapshotHeight}${animateStatusEllipsis(now)}`;
|
|
@@ -166,6 +174,16 @@ export const formatQuoteLineForTesting = formatQuoteLine;
|
|
|
166
174
|
export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTargetHeight, width = 120, now = Date.now()) {
|
|
167
175
|
let line;
|
|
168
176
|
switch (progress.phase) {
|
|
177
|
+
case "getblock_archive_download": {
|
|
178
|
+
const current = progress.downloadedBytes ?? 0;
|
|
179
|
+
const total = progress.totalBytes ?? 0;
|
|
180
|
+
const bar = renderBar(current, total, 20);
|
|
181
|
+
const percent = progress.percent ?? (total > 0 ? (current / total) * 100 : 0);
|
|
182
|
+
const speed = progress.bytesPerSecond === null ? "--" : `${formatBytes(progress.bytesPerSecond)}/s`;
|
|
183
|
+
const resumed = progress.resumed ? " resumed" : "";
|
|
184
|
+
line = `${bar} ${percent.toFixed(2)}% ${formatBytes(current)} / ${formatBytes(total)} ${speed} ETA ${formatDuration(progress.etaSeconds)}${resumed}`;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
169
187
|
case "snapshot_download": {
|
|
170
188
|
const current = progress.downloadedBytes ?? 0;
|
|
171
189
|
const total = progress.totalBytes ?? 0;
|
|
@@ -190,6 +208,13 @@ export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTarge
|
|
|
190
208
|
line = `${bar} Bitcoin ${blocks.toLocaleString()} / ${target.toLocaleString()} ETA ${formatDuration(progress.etaSeconds)} ${progress.message}`;
|
|
191
209
|
break;
|
|
192
210
|
}
|
|
211
|
+
case "getblock_archive_import": {
|
|
212
|
+
const blocks = progress.blocks ?? 0;
|
|
213
|
+
const target = progress.targetHeight ?? blocks;
|
|
214
|
+
const bar = renderBar(blocks, target, 20);
|
|
215
|
+
line = `${bar} Bitcoin ${blocks.toLocaleString()} / ${target.toLocaleString()} ${progress.message}`;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
193
218
|
case "cogcoin_sync": {
|
|
194
219
|
const current = cogcoinSyncHeight ?? 0;
|
|
195
220
|
const target = cogcoinSyncTargetHeight ?? current;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ProgressOutputMode } from "../types.js";
|
|
2
|
+
export interface TtyRenderStream {
|
|
3
|
+
isTTY?: boolean;
|
|
4
|
+
columns?: number;
|
|
5
|
+
write(chunk: string): boolean | void;
|
|
6
|
+
}
|
|
7
|
+
export interface RenderClock {
|
|
8
|
+
now(): number;
|
|
9
|
+
setTimeout: typeof setTimeout;
|
|
10
|
+
clearTimeout: typeof clearTimeout;
|
|
11
|
+
setInterval: typeof setInterval;
|
|
12
|
+
clearInterval: typeof clearInterval;
|
|
13
|
+
}
|
|
14
|
+
export interface TtyRenderPolicy {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
linuxHeadlessThrottle: boolean;
|
|
17
|
+
repaintIntervalMs: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_RENDER_CLOCK: RenderClock;
|
|
20
|
+
export declare function resolveTtyRenderPolicy(progressOutput: ProgressOutputMode, stream: Pick<TtyRenderStream, "isTTY">, options?: {
|
|
21
|
+
platform?: NodeJS.Platform;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
}): TtyRenderPolicy;
|
|
24
|
+
export declare class TtyRenderThrottle {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(options: {
|
|
27
|
+
clock?: RenderClock;
|
|
28
|
+
intervalMs: number;
|
|
29
|
+
onRender: () => void;
|
|
30
|
+
throttled: boolean;
|
|
31
|
+
});
|
|
32
|
+
request(): void;
|
|
33
|
+
flush(): void;
|
|
34
|
+
cancel(): void;
|
|
35
|
+
}
|