@cogcoin/client 0.5.5 → 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/chainstate.d.ts +2 -1
- package/dist/bitcoind/bootstrap/chainstate.js +4 -1
- package/dist/bitcoind/bootstrap/controller.d.ts +4 -1
- package/dist/bitcoind/bootstrap/controller.js +42 -5
- package/dist/bitcoind/bootstrap/getblock-archive.d.ts +39 -0
- package/dist/bitcoind/bootstrap/getblock-archive.js +548 -0
- package/dist/bitcoind/bootstrap/headers.d.ts +12 -0
- package/dist/bitcoind/bootstrap/headers.js +95 -10
- package/dist/bitcoind/bootstrap.d.ts +1 -0
- package/dist/bitcoind/bootstrap.js +1 -0
- package/dist/bitcoind/client/factory.js +91 -28
- package/dist/bitcoind/client/managed-client.d.ts +1 -1
- package/dist/bitcoind/client/managed-client.js +4 -3
- package/dist/bitcoind/client/sync-engine.js +55 -13
- package/dist/bitcoind/errors.js +18 -0
- package/dist/bitcoind/indexer-daemon-main.js +78 -0
- package/dist/bitcoind/indexer-daemon.d.ts +10 -1
- package/dist/bitcoind/indexer-daemon.js +44 -28
- package/dist/bitcoind/node.js +2 -0
- package/dist/bitcoind/processing-start-height.d.ts +7 -0
- package/dist/bitcoind/processing-start-height.js +9 -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 +49 -23
- package/dist/bitcoind/progress/formatting.js +29 -1
- package/dist/bitcoind/progress/render-policy.d.ts +35 -0
- package/dist/bitcoind/progress/render-policy.js +81 -0
- package/dist/bitcoind/retryable-rpc.d.ts +11 -0
- package/dist/bitcoind/retryable-rpc.js +30 -0
- package/dist/bitcoind/service-paths.js +2 -6
- package/dist/bitcoind/service.d.ts +21 -2
- package/dist/bitcoind/service.js +274 -122
- package/dist/bitcoind/testing.d.ts +2 -2
- package/dist/bitcoind/testing.js +2 -2
- package/dist/bitcoind/types.d.ts +36 -1
- package/dist/cli/commands/follow.js +11 -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/service-runtime.js +150 -134
- package/dist/cli/commands/status.js +2 -0
- package/dist/cli/commands/sync.js +11 -0
- package/dist/cli/commands/wallet-admin.js +106 -24
- package/dist/cli/commands/wallet-mutation.js +57 -4
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.js +8 -4
- 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 +49 -0
- package/dist/cli/mutation-success.d.ts +1 -0
- package/dist/cli/mutation-success.js +2 -2
- package/dist/cli/output.js +86 -1
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +133 -3
- package/dist/cli/preview-json.d.ts +10 -1
- package/dist/cli/preview-json.js +32 -0
- package/dist/cli/prompt.js +1 -1
- package/dist/cli/runner.js +4 -0
- package/dist/cli/types.d.ts +15 -5
- package/dist/cli/types.js +1 -1
- package/dist/cli/wallet-format.js +140 -14
- package/dist/wallet/lifecycle.d.ts +21 -1
- package/dist/wallet/lifecycle.js +252 -116
- package/dist/wallet/mining/visualizer.d.ts +11 -6
- package/dist/wallet/mining/visualizer.js +32 -15
- package/dist/wallet/read/context.js +10 -4
- package/dist/wallet/reset.d.ts +61 -2
- package/dist/wallet/reset.js +246 -89
- package/dist/wallet/root-resolution.d.ts +20 -0
- package/dist/wallet/root-resolution.js +37 -0
- package/dist/wallet/runtime.d.ts +13 -1
- package/dist/wallet/runtime.js +54 -11
- package/dist/wallet/state/crypto.d.ts +3 -0
- package/dist/wallet/state/crypto.js +3 -0
- 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/state/storage.d.ts +7 -1
- package/dist/wallet/state/storage.js +39 -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/dist/wallet/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
|
|
2
|
+
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
2
3
|
interface DaemonRequest {
|
|
3
4
|
id: string;
|
|
4
|
-
method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot";
|
|
5
|
+
method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot" | "PauseBackgroundFollow" | "ResumeBackgroundFollow";
|
|
5
6
|
token?: string;
|
|
6
7
|
}
|
|
7
8
|
interface DaemonResponse {
|
|
@@ -64,6 +65,8 @@ export interface IndexerDaemonClient {
|
|
|
64
65
|
openSnapshot(): Promise<IndexerSnapshotHandle>;
|
|
65
66
|
readSnapshot(token: string): Promise<IndexerSnapshotPayload>;
|
|
66
67
|
closeSnapshot(token: string): Promise<void>;
|
|
68
|
+
pauseBackgroundFollow(): Promise<void>;
|
|
69
|
+
resumeBackgroundFollow(): Promise<void>;
|
|
67
70
|
close(): Promise<void>;
|
|
68
71
|
}
|
|
69
72
|
export type IndexerDaemonCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "schema-mismatch" | "unreachable" | "protocol-error";
|
|
@@ -81,6 +84,12 @@ export interface CoherentIndexerSnapshotLease {
|
|
|
81
84
|
payload: IndexerSnapshotPayload;
|
|
82
85
|
status: ManagedIndexerDaemonStatus;
|
|
83
86
|
}
|
|
87
|
+
export declare function stopIndexerDaemonServiceWithLockHeld(options: {
|
|
88
|
+
dataDir: string;
|
|
89
|
+
walletRootId?: string;
|
|
90
|
+
shutdownTimeoutMs?: number;
|
|
91
|
+
paths?: ReturnType<typeof resolveManagedServicePaths>;
|
|
92
|
+
}): Promise<IndexerDaemonStopResult>;
|
|
84
93
|
export declare function probeIndexerDaemon(options: {
|
|
85
94
|
dataDir: string;
|
|
86
95
|
walletRootId?: string;
|
|
@@ -53,6 +53,33 @@ async function clearIndexerDaemonRuntimeArtifacts(paths) {
|
|
|
53
53
|
await rm(paths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
|
|
54
54
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
55
55
|
}
|
|
56
|
+
export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
57
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
58
|
+
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
59
|
+
const status = await readJsonFile(paths.indexerDaemonStatusPath);
|
|
60
|
+
const processId = status?.processId ?? null;
|
|
61
|
+
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
62
|
+
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
63
|
+
return {
|
|
64
|
+
status: "not-running",
|
|
65
|
+
walletRootId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
process.kill(processId, "SIGTERM");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
|
|
77
|
+
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
78
|
+
return {
|
|
79
|
+
status: "stopped",
|
|
80
|
+
walletRootId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
56
83
|
function createIndexerDaemonClient(socketPath) {
|
|
57
84
|
async function sendRequest(request) {
|
|
58
85
|
return new Promise((resolve, reject) => {
|
|
@@ -141,6 +168,18 @@ function createIndexerDaemonClient(socketPath) {
|
|
|
141
168
|
token,
|
|
142
169
|
});
|
|
143
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
|
+
},
|
|
144
183
|
async close() {
|
|
145
184
|
return;
|
|
146
185
|
},
|
|
@@ -150,9 +189,6 @@ function validateIndexerRuntimeIdentity(identity, expectedWalletRootId) {
|
|
|
150
189
|
if (identity.serviceApiVersion !== INDEXER_DAEMON_SERVICE_API_VERSION) {
|
|
151
190
|
throw new Error("indexer_daemon_service_version_mismatch");
|
|
152
191
|
}
|
|
153
|
-
if (identity.walletRootId !== expectedWalletRootId) {
|
|
154
|
-
throw new Error("indexer_daemon_wallet_root_mismatch");
|
|
155
|
-
}
|
|
156
192
|
if (identity.schemaVersion !== INDEXER_DAEMON_SCHEMA_VERSION || identity.state === "schema-mismatch") {
|
|
157
193
|
throw new Error("indexer_daemon_schema_mismatch");
|
|
158
194
|
}
|
|
@@ -244,9 +280,7 @@ async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
|
|
|
244
280
|
compatibility: error instanceof Error
|
|
245
281
|
? error.message === "indexer_daemon_service_version_mismatch"
|
|
246
282
|
? "service-version-mismatch"
|
|
247
|
-
:
|
|
248
|
-
? "wallet-root-mismatch"
|
|
249
|
-
: "schema-mismatch"
|
|
283
|
+
: "schema-mismatch"
|
|
250
284
|
: "protocol-error",
|
|
251
285
|
status,
|
|
252
286
|
client: null,
|
|
@@ -380,29 +414,11 @@ export async function stopIndexerDaemonService(options) {
|
|
|
380
414
|
dataDir: options.dataDir,
|
|
381
415
|
});
|
|
382
416
|
try {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
386
|
-
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
387
|
-
return {
|
|
388
|
-
status: "not-running",
|
|
389
|
-
walletRootId,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
try {
|
|
393
|
-
process.kill(processId, "SIGTERM");
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
397
|
-
throw error;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
|
|
401
|
-
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
402
|
-
return {
|
|
403
|
-
status: "stopped",
|
|
417
|
+
return await stopIndexerDaemonServiceWithLockHeld({
|
|
418
|
+
...options,
|
|
404
419
|
walletRootId,
|
|
405
|
-
|
|
420
|
+
paths,
|
|
421
|
+
});
|
|
406
422
|
}
|
|
407
423
|
finally {
|
|
408
424
|
await lock.release();
|
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
|
},
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GenesisParameters } from "@cogcoin/indexer/types";
|
|
2
|
+
export declare function resolveCogcoinProcessingStartHeight(genesisParameters: GenesisParameters): number;
|
|
3
|
+
export declare function assertCogcoinProcessingStartHeight(options: {
|
|
4
|
+
chain: "main" | "regtest";
|
|
5
|
+
startHeight: number;
|
|
6
|
+
genesisParameters: GenesisParameters;
|
|
7
|
+
}): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function resolveCogcoinProcessingStartHeight(genesisParameters) {
|
|
2
|
+
return genesisParameters.genesisBlock;
|
|
3
|
+
}
|
|
4
|
+
export function assertCogcoinProcessingStartHeight(options) {
|
|
5
|
+
const processingStartHeight = resolveCogcoinProcessingStartHeight(options.genesisParameters);
|
|
6
|
+
if (options.chain === "main" && options.startHeight < processingStartHeight) {
|
|
7
|
+
throw new Error("cogcoin_processing_start_height_before_genesis");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -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
|
}
|
|
@@ -117,6 +144,7 @@ export class ManagedProgressController {
|
|
|
117
144
|
phase: "cogcoin_sync",
|
|
118
145
|
message: createDefaultMessage("cogcoin_sync"),
|
|
119
146
|
etaSeconds,
|
|
147
|
+
lastError: null,
|
|
120
148
|
updatedAt: Date.now(),
|
|
121
149
|
};
|
|
122
150
|
if (this.#followVisualMode) {
|
|
@@ -142,7 +170,7 @@ export class ManagedProgressController {
|
|
|
142
170
|
if (!this.#started || this.#quoteRotator === null) {
|
|
143
171
|
return;
|
|
144
172
|
}
|
|
145
|
-
const now =
|
|
173
|
+
const now = this.#clock.now();
|
|
146
174
|
if (this.#followVisualMode) {
|
|
147
175
|
advanceFollowSceneState(this.#followScene, now);
|
|
148
176
|
this.#currentQuote = null;
|
|
@@ -169,20 +197,18 @@ export class ManagedProgressController {
|
|
|
169
197
|
catch {
|
|
170
198
|
// User progress callbacks should never break managed sync.
|
|
171
199
|
}
|
|
200
|
+
this.#renderThrottle.request();
|
|
201
|
+
}
|
|
202
|
+
#renderToTty() {
|
|
203
|
+
if (!this.#started || this.#renderer === null) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const now = this.#clock.now();
|
|
172
207
|
const statusFieldText = resolveStatusFieldText(this.#progress, this.#snapshot.height, now);
|
|
173
208
|
if (this.#followVisualMode) {
|
|
174
|
-
this.#renderer
|
|
209
|
+
this.#renderer.renderFollowScene(this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, this.#followScene, statusFieldText);
|
|
175
210
|
return;
|
|
176
211
|
}
|
|
177
|
-
this.#renderer
|
|
178
|
-
}
|
|
179
|
-
#shouldRenderToTty() {
|
|
180
|
-
if (this.#outputMode === "none") {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
if (this.#outputMode === "tty") {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
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);
|
|
187
213
|
}
|
|
188
214
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
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":
|
|
7
|
-
return "
|
|
11
|
+
return "Pre-synchronizing blockheaders.";
|
|
8
12
|
case "load_snapshot":
|
|
9
13
|
return "Loading the UTXO snapshot into bitcoind.";
|
|
10
14
|
case "bitcoin_sync":
|
|
@@ -121,10 +125,17 @@ 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)}`;
|
|
127
135
|
case "wait_headers_for_snapshot":
|
|
136
|
+
return progress.message === "Waiting for Bitcoin headers to reach the snapshot height."
|
|
137
|
+
? `Waiting for Bitcoin headers to reach the snapshot height${animateStatusEllipsis(now)}`
|
|
138
|
+
: `Pre-synchronizing blockheaders${animateStatusEllipsis(now)}`;
|
|
128
139
|
case "load_snapshot":
|
|
129
140
|
case "bitcoin_sync":
|
|
130
141
|
return `Syncing Bitcoin Blocks${animateStatusEllipsis(now)}`;
|
|
@@ -163,6 +174,16 @@ export const formatQuoteLineForTesting = formatQuoteLine;
|
|
|
163
174
|
export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTargetHeight, width = 120, now = Date.now()) {
|
|
164
175
|
let line;
|
|
165
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
|
+
}
|
|
166
187
|
case "snapshot_download": {
|
|
167
188
|
const current = progress.downloadedBytes ?? 0;
|
|
168
189
|
const total = progress.totalBytes ?? 0;
|
|
@@ -187,6 +208,13 @@ export function formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTarge
|
|
|
187
208
|
line = `${bar} Bitcoin ${blocks.toLocaleString()} / ${target.toLocaleString()} ETA ${formatDuration(progress.etaSeconds)} ${progress.message}`;
|
|
188
209
|
break;
|
|
189
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
|
+
}
|
|
190
218
|
case "cogcoin_sync": {
|
|
191
219
|
const current = cogcoinSyncHeight ?? 0;
|
|
192
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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { HEADLESS_PROGRESS_TICK_MS, PROGRESS_TICK_MS } from "./constants.js";
|
|
2
|
+
export const DEFAULT_RENDER_CLOCK = {
|
|
3
|
+
now: () => Date.now(),
|
|
4
|
+
setTimeout,
|
|
5
|
+
clearTimeout,
|
|
6
|
+
setInterval,
|
|
7
|
+
clearInterval,
|
|
8
|
+
};
|
|
9
|
+
export function resolveTtyRenderPolicy(progressOutput, stream, options = {}) {
|
|
10
|
+
const ttyActive = stream.isTTY === true;
|
|
11
|
+
const enabled = progressOutput === "none"
|
|
12
|
+
? false
|
|
13
|
+
: progressOutput === "tty"
|
|
14
|
+
? true
|
|
15
|
+
: ttyActive;
|
|
16
|
+
const env = options.env ?? process.env;
|
|
17
|
+
const linuxHeadlessThrottle = enabled
|
|
18
|
+
&& ttyActive
|
|
19
|
+
&& (options.platform ?? process.platform) === "linux"
|
|
20
|
+
&& (env.DISPLAY?.trim() ?? "").length === 0
|
|
21
|
+
&& (env.WAYLAND_DISPLAY?.trim() ?? "").length === 0;
|
|
22
|
+
return {
|
|
23
|
+
enabled,
|
|
24
|
+
linuxHeadlessThrottle,
|
|
25
|
+
repaintIntervalMs: linuxHeadlessThrottle ? HEADLESS_PROGRESS_TICK_MS : PROGRESS_TICK_MS,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export class TtyRenderThrottle {
|
|
29
|
+
#clock;
|
|
30
|
+
#intervalMs;
|
|
31
|
+
#onRender;
|
|
32
|
+
#throttled;
|
|
33
|
+
#lastRenderAt = null;
|
|
34
|
+
#pendingTimer = null;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.#clock = options.clock ?? DEFAULT_RENDER_CLOCK;
|
|
37
|
+
this.#intervalMs = options.intervalMs;
|
|
38
|
+
this.#onRender = options.onRender;
|
|
39
|
+
this.#throttled = options.throttled;
|
|
40
|
+
}
|
|
41
|
+
request() {
|
|
42
|
+
if (!this.#throttled) {
|
|
43
|
+
this.cancel();
|
|
44
|
+
this.#renderNow();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const now = this.#clock.now();
|
|
48
|
+
if (this.#lastRenderAt === null || (now - this.#lastRenderAt) >= this.#intervalMs) {
|
|
49
|
+
this.cancel();
|
|
50
|
+
this.#renderNow();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (this.#pendingTimer !== null) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const delayMs = Math.max(0, this.#intervalMs - (now - this.#lastRenderAt));
|
|
57
|
+
this.#pendingTimer = this.#clock.setTimeout(() => {
|
|
58
|
+
this.#pendingTimer = null;
|
|
59
|
+
this.#renderNow();
|
|
60
|
+
}, delayMs);
|
|
61
|
+
}
|
|
62
|
+
flush() {
|
|
63
|
+
if (this.#pendingTimer === null) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.#clock.clearTimeout(this.#pendingTimer);
|
|
67
|
+
this.#pendingTimer = null;
|
|
68
|
+
this.#renderNow();
|
|
69
|
+
}
|
|
70
|
+
cancel() {
|
|
71
|
+
if (this.#pendingTimer === null) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.#clock.clearTimeout(this.#pendingTimer);
|
|
75
|
+
this.#pendingTimer = null;
|
|
76
|
+
}
|
|
77
|
+
#renderNow() {
|
|
78
|
+
this.#lastRenderAt = this.#clock.now();
|
|
79
|
+
this.#onRender();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MANAGED_RPC_RETRY_BASE_MS = 1000;
|
|
2
|
+
export declare const MANAGED_RPC_RETRY_MAX_MS = 15000;
|
|
3
|
+
export declare const MANAGED_RPC_RETRY_MESSAGE = "Managed Bitcoin RPC temporarily unavailable; retrying until canceled.";
|
|
4
|
+
export interface ManagedRpcRetryState {
|
|
5
|
+
nextDelayMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function createManagedRpcRetryState(): ManagedRpcRetryState;
|
|
8
|
+
export declare function resetManagedRpcRetryState(state: ManagedRpcRetryState): void;
|
|
9
|
+
export declare function consumeManagedRpcRetryDelayMs(state: ManagedRpcRetryState): number;
|
|
10
|
+
export declare function isRetryableManagedRpcError(error: unknown): boolean;
|
|
11
|
+
export declare function describeManagedRpcRetryError(error: unknown): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const MANAGED_RPC_RETRY_BASE_MS = 1_000;
|
|
2
|
+
export const MANAGED_RPC_RETRY_MAX_MS = 15_000;
|
|
3
|
+
export const MANAGED_RPC_RETRY_MESSAGE = "Managed Bitcoin RPC temporarily unavailable; retrying until canceled.";
|
|
4
|
+
export function createManagedRpcRetryState() {
|
|
5
|
+
return {
|
|
6
|
+
nextDelayMs: MANAGED_RPC_RETRY_BASE_MS,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function resetManagedRpcRetryState(state) {
|
|
10
|
+
state.nextDelayMs = MANAGED_RPC_RETRY_BASE_MS;
|
|
11
|
+
}
|
|
12
|
+
export function consumeManagedRpcRetryDelayMs(state) {
|
|
13
|
+
const delayMs = state.nextDelayMs;
|
|
14
|
+
state.nextDelayMs = Math.min(state.nextDelayMs * 2, MANAGED_RPC_RETRY_MAX_MS);
|
|
15
|
+
return delayMs;
|
|
16
|
+
}
|
|
17
|
+
export function isRetryableManagedRpcError(error) {
|
|
18
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19
|
+
if (message === "bitcoind_rpc_timeout") {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (message.startsWith("The managed Bitcoin RPC request to ")) {
|
|
23
|
+
return message.includes(" failed");
|
|
24
|
+
}
|
|
25
|
+
return message.startsWith("The managed Bitcoin RPC cookie file is unavailable at ")
|
|
26
|
+
|| message.startsWith("The managed Bitcoin RPC cookie file could not be read at ");
|
|
27
|
+
}
|
|
28
|
+
export function describeManagedRpcRetryError(error) {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
}
|
|
@@ -3,9 +3,6 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { resolveCogcoinPathsForTesting, resolveDefaultBitcoindDataDirForTesting } from "../app-paths.js";
|
|
5
5
|
export const UNINITIALIZED_WALLET_ROOT_ID = "wallet-root-uninitialized";
|
|
6
|
-
function sanitizeWalletRootId(walletRootId) {
|
|
7
|
-
return walletRootId.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
8
|
-
}
|
|
9
6
|
function createDataDirSuffix(dataDir) {
|
|
10
7
|
return createHash("sha256").update(dataDir).digest("hex").slice(0, 12);
|
|
11
8
|
}
|
|
@@ -17,7 +14,6 @@ function resolveIndexerDaemonSocketPath(serviceRootId) {
|
|
|
17
14
|
return join(tmpdir(), `cogcoin-indexer-${socketId}.sock`);
|
|
18
15
|
}
|
|
19
16
|
export function resolveManagedServicePaths(dataDir, walletRootId = UNINITIALIZED_WALLET_ROOT_ID) {
|
|
20
|
-
const normalizedWalletRootId = sanitizeWalletRootId(walletRootId);
|
|
21
17
|
const defaultPaths = resolveCogcoinPathsForTesting();
|
|
22
18
|
const defaultBitcoindDataDir = resolveDefaultBitcoindDataDirForTesting();
|
|
23
19
|
const useDefaultRoots = dataDir === defaultBitcoindDataDir;
|
|
@@ -25,8 +21,8 @@ export function resolveManagedServicePaths(dataDir, walletRootId = UNINITIALIZED
|
|
|
25
21
|
const runtimeRoot = useDefaultRoots ? defaultPaths.runtimeRoot : join(dataRoot, "runtime");
|
|
26
22
|
const indexerRoot = useDefaultRoots ? defaultPaths.indexerRoot : join(dataRoot, "indexer");
|
|
27
23
|
const serviceRootId = useDefaultRoots
|
|
28
|
-
?
|
|
29
|
-
:
|
|
24
|
+
? "managed"
|
|
25
|
+
: `managed-${createDataDirSuffix(dataDir)}`;
|
|
30
26
|
const walletRuntimeRoot = join(runtimeRoot, serviceRootId);
|
|
31
27
|
const indexerServiceRoot = join(indexerRoot, serviceRootId);
|
|
32
28
|
return {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
2
|
+
import type { InternalManagedBitcoindOptions, ManagedBitcoindObservedStatus, ManagedBitcoindRuntimeConfig, ManagedBitcoindNodeHandle, ManagedCoreWalletReplicaStatus } from "./types.js";
|
|
3
|
+
export declare function resolveManagedBitcoindDbcacheMiB(totalRamBytes: number): number;
|
|
2
4
|
interface ManagedWalletReplicaRpc {
|
|
3
5
|
listWallets(): Promise<string[]>;
|
|
4
6
|
loadWallet(walletName: string, loadOnStartup?: boolean): Promise<{
|
|
@@ -18,7 +20,11 @@ interface ManagedWalletReplicaRpc {
|
|
|
18
20
|
}>;
|
|
19
21
|
walletLock(walletName: string): Promise<null>;
|
|
20
22
|
}
|
|
21
|
-
type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataDir" | "chain" | "startHeight" | "walletRootId" | "rpcPort" | "zmqPort" | "p2pPort" | "pollIntervalMs" | "startupTimeoutMs" | "shutdownTimeoutMs" | "managedWalletPassphrase"
|
|
23
|
+
type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataDir" | "chain" | "startHeight" | "walletRootId" | "rpcPort" | "zmqPort" | "p2pPort" | "pollIntervalMs" | "startupTimeoutMs" | "shutdownTimeoutMs" | "managedWalletPassphrase"> & {
|
|
24
|
+
getblockArchivePath?: string | null;
|
|
25
|
+
getblockArchiveEndHeight?: number | null;
|
|
26
|
+
getblockArchiveSha256?: string | null;
|
|
27
|
+
};
|
|
22
28
|
export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
|
|
23
29
|
export interface ManagedBitcoindServiceProbeResult {
|
|
24
30
|
compatibility: ManagedBitcoindServiceCompatibility;
|
|
@@ -29,9 +35,22 @@ export interface ManagedBitcoindServiceStopResult {
|
|
|
29
35
|
status: "stopped" | "not-running";
|
|
30
36
|
walletRootId: string;
|
|
31
37
|
}
|
|
38
|
+
export declare function writeBitcoinConfForTesting(filePath: string, options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): Promise<void>;
|
|
39
|
+
export declare function buildManagedServiceArgsForTesting(options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): string[];
|
|
32
40
|
export declare function createManagedWalletReplica(rpc: ManagedWalletReplicaRpc, walletRootId: string, options?: {
|
|
33
41
|
managedWalletPassphrase?: string;
|
|
34
42
|
}): Promise<ManagedCoreWalletReplicaStatus>;
|
|
43
|
+
export declare function stopManagedBitcoindServiceWithLockHeld(options: {
|
|
44
|
+
dataDir: string;
|
|
45
|
+
walletRootId?: string;
|
|
46
|
+
shutdownTimeoutMs?: number;
|
|
47
|
+
paths?: ReturnType<typeof resolveManagedServicePaths>;
|
|
48
|
+
}): Promise<ManagedBitcoindServiceStopResult>;
|
|
49
|
+
export declare function withClaimedUninitializedManagedRuntime<T>(options: {
|
|
50
|
+
dataDir: string;
|
|
51
|
+
walletRootId?: string;
|
|
52
|
+
shutdownTimeoutMs?: number;
|
|
53
|
+
}, callback: () => Promise<T>): Promise<T>;
|
|
35
54
|
export declare function probeManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindServiceProbeResult>;
|
|
36
55
|
export declare function attachOrStartManagedBitcoindService(options: ManagedBitcoindServiceOptions): Promise<ManagedBitcoindNodeHandle>;
|
|
37
56
|
export declare function stopManagedBitcoindService(options: {
|