@cogcoin/client 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/bitcoind/client/factory.d.ts +0 -8
- package/dist/bitcoind/client/factory.js +1 -59
- package/dist/bitcoind/client/managed-client.d.ts +1 -3
- package/dist/bitcoind/client/managed-client.js +3 -47
- package/dist/bitcoind/indexer-daemon-main.js +173 -28
- package/dist/bitcoind/indexer-daemon.d.ts +14 -3
- package/dist/bitcoind/indexer-daemon.js +145 -29
- package/dist/bitcoind/indexer-monitor.d.ts +12 -0
- package/dist/bitcoind/indexer-monitor.js +89 -0
- package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
- package/dist/bitcoind/progress/follow-scene.js +87 -4
- package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
- package/dist/bitcoind/progress/tty-renderer.js +2 -0
- package/dist/bitcoind/retryable-rpc.js +3 -0
- package/dist/bitcoind/service.d.ts +1 -0
- package/dist/bitcoind/service.js +31 -9
- package/dist/bitcoind/testing.d.ts +0 -1
- package/dist/bitcoind/testing.js +0 -1
- package/dist/bitcoind/types.d.ts +5 -2
- package/dist/cli/commands/follow.js +44 -49
- package/dist/cli/commands/mining-admin.js +65 -2
- package/dist/cli/commands/mining-read.js +43 -3
- package/dist/cli/commands/mining-runtime.js +91 -73
- package/dist/cli/commands/service-runtime.js +42 -2
- package/dist/cli/commands/status.js +3 -1
- package/dist/cli/commands/sync.js +50 -90
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/commands/wallet-admin.js +21 -3
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.js +36 -1
- package/dist/cli/managed-indexer-observer.d.ts +33 -0
- package/dist/cli/managed-indexer-observer.js +163 -0
- package/dist/cli/mining-format.d.ts +3 -1
- package/dist/cli/mining-format.js +63 -0
- package/dist/cli/mining-json.d.ts +11 -1
- package/dist/cli/mining-json.js +15 -0
- package/dist/cli/output.js +74 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +28 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/read-json.d.ts +26 -1
- package/dist/cli/read-json.js +48 -0
- package/dist/cli/runner.js +8 -2
- package/dist/cli/signals.d.ts +12 -0
- package/dist/cli/signals.js +31 -13
- package/dist/cli/types.d.ts +13 -4
- package/dist/cli/update-notifier.js +7 -222
- package/dist/cli/update-service.d.ts +34 -0
- package/dist/cli/update-service.js +152 -0
- package/dist/client/initialization.js +5 -0
- package/dist/semver.d.ts +12 -0
- package/dist/semver.js +68 -0
- package/dist/wallet/lifecycle.d.ts +10 -0
- package/dist/wallet/mining/config.js +64 -3
- package/dist/wallet/mining/control.d.ts +5 -1
- package/dist/wallet/mining/control.js +269 -26
- package/dist/wallet/mining/domain-prompts.d.ts +17 -0
- package/dist/wallet/mining/domain-prompts.js +130 -0
- package/dist/wallet/mining/index.d.ts +2 -1
- package/dist/wallet/mining/index.js +1 -0
- package/dist/wallet/mining/provider-model.d.ts +30 -0
- package/dist/wallet/mining/provider-model.js +134 -0
- package/dist/wallet/mining/runner.d.ts +156 -5
- package/dist/wallet/mining/runner.js +1019 -399
- package/dist/wallet/mining/runtime-artifacts.js +1 -0
- package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
- package/dist/wallet/mining/sentences.d.ts +2 -2
- package/dist/wallet/mining/sentences.js +32 -6
- package/dist/wallet/mining/types.d.ts +35 -1
- package/dist/wallet/mining/visualizer.d.ts +3 -0
- package/dist/wallet/mining/visualizer.js +132 -15
- package/dist/wallet/read/context.d.ts +1 -0
- package/dist/wallet/read/context.js +15 -7
- package/dist/wallet/state/client-password-agent.js +4 -1
- package/dist/wallet/state/client-password.js +15 -8
- package/dist/wallet/tx/common.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.
|
|
3
|
+
`@cogcoin/client@1.1.0` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
@@ -130,13 +130,15 @@ Managed node subpath:
|
|
|
130
130
|
|
|
131
131
|
The installed `cogcoin` command covers the first-party local wallet and node workflow:
|
|
132
132
|
|
|
133
|
+
- update commands such as `update` to compare the current CLI version with the latest npm release and install it
|
|
133
134
|
- wallet lifecycle commands such as `init`, `restore`, `wallet delete`, `wallet show-mnemonic`, and `repair`
|
|
134
135
|
- sync and service commands such as `status`, `sync`, `follow`, `bitcoin start`, `bitcoin stop`, `bitcoin status`, `indexer start`, `indexer stop`, and `indexer status`
|
|
135
136
|
- domain and field commands such as `register`, `anchor`, `show`, `domains`, `fields`, `buy`, `sell`, and `transfer`
|
|
136
137
|
- COG and reputation commands such as `send`, `cog lock`, `claim`, `reclaim`, `rep give`, and `rep revoke`
|
|
137
|
-
- mining commands such as `mine`, `mine start`, `mine stop`, `mine status`, `mine log`, and `mine
|
|
138
|
+
- mining commands such as `mine`, `mine start`, `mine stop`, `mine status`, `mine log`, `mine setup`, `mine prompt`, and `mine prompt list`
|
|
138
139
|
|
|
139
140
|
The CLI also supports stable `--output json` and `--output preview-json` envelopes on the commands that advertise machine-readable output.
|
|
141
|
+
Use `cogcoin mine prompt <domain>` to set or clear a per-domain mining prompt override for one anchored root domain, and `cogcoin mine prompt list` to inspect the current per-domain prompt state alongside the global fallback prompt.
|
|
140
142
|
Interactive text invocations periodically check the npm registry for newer `@cogcoin/client` releases and print `npm install -g @cogcoin/client` when a newer version is available.
|
|
141
143
|
Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
|
|
142
144
|
Ordinary `sync`, `follow`, and wallet-aware read/status flows detach from the managed Bitcoin and indexer services on exit instead of stopping them.
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
import { stopIndexerDaemonService, type IndexerDaemonClient } from "../indexer-daemon.js";
|
|
2
1
|
import type { InternalManagedBitcoindOptions, ManagedBitcoindClient } from "../types.js";
|
|
3
|
-
export declare function pauseIndexerDaemonForForegroundClientForTesting(options: {
|
|
4
|
-
daemon: IndexerDaemonClient;
|
|
5
|
-
dataDir: string;
|
|
6
|
-
walletRootId: string;
|
|
7
|
-
shutdownTimeoutMs?: number;
|
|
8
|
-
stopDaemon?: typeof stopIndexerDaemonService;
|
|
9
|
-
}): Promise<IndexerDaemonClient | null>;
|
|
10
2
|
export declare function openManagedBitcoindClient(options: Omit<InternalManagedBitcoindOptions, "chain" | "startHeight">): Promise<ManagedBitcoindClient>;
|
|
11
3
|
export declare function openManagedBitcoindClientInternal(options: InternalManagedBitcoindOptions): Promise<ManagedBitcoindClient>;
|
|
@@ -2,46 +2,12 @@ import { loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
|
2
2
|
import { resolveDefaultBitcoindDataDirForTesting } from "../../app-paths.js";
|
|
3
3
|
import { openClient } from "../../client.js";
|
|
4
4
|
import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting, } from "../bootstrap.js";
|
|
5
|
-
import { attachOrStartIndexerDaemon, stopIndexerDaemonService, } from "../indexer-daemon.js";
|
|
6
5
|
import { createRpcClient } from "../node.js";
|
|
7
6
|
import { assertCogcoinProcessingStartHeight, resolveCogcoinProcessingStartHeight, } from "../processing-start-height.js";
|
|
8
7
|
import { ManagedProgressController } from "../progress.js";
|
|
9
8
|
import { attachOrStartManagedBitcoindService, } from "../service.js";
|
|
10
9
|
import { DefaultManagedBitcoindClient } from "./managed-client.js";
|
|
11
10
|
const DEFAULT_SYNC_DEBOUNCE_MS = 250;
|
|
12
|
-
function isRecoverableIndexerDaemonPauseError(error) {
|
|
13
|
-
if (!(error instanceof Error)) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
if (error.message === "indexer_daemon_request_timeout"
|
|
17
|
-
|| error.message === "indexer_daemon_connection_closed"
|
|
18
|
-
|| error.message === "indexer_daemon_protocol_error") {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
if ("code" in error) {
|
|
22
|
-
const code = error.code;
|
|
23
|
-
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET";
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
export async function pauseIndexerDaemonForForegroundClientForTesting(options) {
|
|
28
|
-
try {
|
|
29
|
-
await options.daemon.pauseBackgroundFollow();
|
|
30
|
-
return options.daemon;
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
await options.daemon.close().catch(() => undefined);
|
|
34
|
-
if (!isRecoverableIndexerDaemonPauseError(error)) {
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
await (options.stopDaemon ?? stopIndexerDaemonService)({
|
|
38
|
-
dataDir: options.dataDir,
|
|
39
|
-
walletRootId: options.walletRootId,
|
|
40
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
41
|
-
});
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
11
|
async function createManagedBitcoindClient(options) {
|
|
46
12
|
const genesisParameters = options.genesisParameters ?? await loadBundledGenesisParameters();
|
|
47
13
|
assertCogcoinProcessingStartHeight({
|
|
@@ -83,31 +49,7 @@ async function createManagedBitcoindClient(options) {
|
|
|
83
49
|
genesisParameters,
|
|
84
50
|
snapshotInterval: options.snapshotInterval,
|
|
85
51
|
});
|
|
86
|
-
|
|
87
|
-
? await pauseIndexerDaemonForForegroundClientForTesting({
|
|
88
|
-
daemon: await attachOrStartIndexerDaemon({
|
|
89
|
-
dataDir,
|
|
90
|
-
databasePath: options.databasePath,
|
|
91
|
-
walletRootId: options.walletRootId,
|
|
92
|
-
startupTimeoutMs: options.startupTimeoutMs,
|
|
93
|
-
}),
|
|
94
|
-
dataDir,
|
|
95
|
-
walletRootId,
|
|
96
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
97
|
-
})
|
|
98
|
-
: null;
|
|
99
|
-
// The persistent service may already exist from a non-processing attach path
|
|
100
|
-
// that used startHeight 0. Cogcoin replay still begins at the requested
|
|
101
|
-
// processing boundary for this managed client.
|
|
102
|
-
const databasePath = options.databasePath ?? null;
|
|
103
|
-
return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, indexerDaemon, databasePath
|
|
104
|
-
? async () => attachOrStartIndexerDaemon({
|
|
105
|
-
dataDir,
|
|
106
|
-
databasePath,
|
|
107
|
-
walletRootId: options.walletRootId,
|
|
108
|
-
startupTimeoutMs: options.startupTimeoutMs,
|
|
109
|
-
})
|
|
110
|
-
: null, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS, dataDir, walletRootId, options.startupTimeoutMs, options.shutdownTimeoutMs, options.fetchImpl);
|
|
52
|
+
return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS, dataDir, walletRootId, options.startupTimeoutMs, options.shutdownTimeoutMs, options.fetchImpl);
|
|
111
53
|
}
|
|
112
54
|
catch (error) {
|
|
113
55
|
if (progressStarted) {
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { BitcoinBlock } from "@cogcoin/indexer/types";
|
|
2
2
|
import type { ClientStoreAdapter } from "../../types.js";
|
|
3
3
|
import { AssumeUtxoBootstrapController } from "../bootstrap.js";
|
|
4
|
-
import type { IndexerDaemonClient } from "../indexer-daemon.js";
|
|
5
4
|
import type { ManagedProgressController } from "../progress.js";
|
|
6
5
|
import type { BitcoinRpcClient } from "../rpc.js";
|
|
7
6
|
import type { ManagedBitcoindClient, ManagedBitcoindNodeHandle, ManagedBitcoindStatus, SyncResult } from "../types.js";
|
|
8
7
|
import { type SyncRecoveryClient } from "./internal-types.js";
|
|
9
8
|
export declare class DefaultManagedBitcoindClient implements ManagedBitcoindClient {
|
|
10
9
|
#private;
|
|
11
|
-
constructor(client: SyncRecoveryClient, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController,
|
|
10
|
+
constructor(client: SyncRecoveryClient, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, startHeight: number, syncDebounceMs: number, dataDir: string, walletRootId: string, startupTimeoutMs: number | undefined, shutdownTimeoutMs: number | undefined, fetchImpl: typeof fetch | undefined);
|
|
12
11
|
getTip(): Promise<import("../../types.js").ClientTip | null>;
|
|
13
12
|
getState(): Promise<import("@cogcoin/indexer/types").IndexerState>;
|
|
14
13
|
applyBlock(block: BitcoinBlock): Promise<import("../../types.js").ApplyBlockResult>;
|
|
@@ -18,5 +17,4 @@ export declare class DefaultManagedBitcoindClient implements ManagedBitcoindClie
|
|
|
18
17
|
getNodeStatus(): Promise<ManagedBitcoindStatus>;
|
|
19
18
|
close(): Promise<void>;
|
|
20
19
|
playSyncCompletionScene(): Promise<void>;
|
|
21
|
-
detachToBackgroundFollow(): Promise<void>;
|
|
22
20
|
}
|
|
@@ -26,8 +26,6 @@ export class DefaultManagedBitcoindClient {
|
|
|
26
26
|
#rpc;
|
|
27
27
|
#progress;
|
|
28
28
|
#bootstrap;
|
|
29
|
-
#indexerDaemon;
|
|
30
|
-
#reattachIndexerDaemon;
|
|
31
29
|
#startHeight;
|
|
32
30
|
#syncDebounceMs;
|
|
33
31
|
#dataDir;
|
|
@@ -45,16 +43,13 @@ export class DefaultManagedBitcoindClient {
|
|
|
45
43
|
#syncPromise = Promise.resolve(createInitialSyncResult());
|
|
46
44
|
#debounceTimer = null;
|
|
47
45
|
#syncAbortControllers = new Set();
|
|
48
|
-
|
|
49
|
-
constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, reattachIndexerDaemon, startHeight, syncDebounceMs, dataDir, walletRootId, startupTimeoutMs, shutdownTimeoutMs, fetchImpl) {
|
|
46
|
+
constructor(client, store, node, rpc, progress, bootstrap, startHeight, syncDebounceMs, dataDir, walletRootId, startupTimeoutMs, shutdownTimeoutMs, fetchImpl) {
|
|
50
47
|
this.#client = client;
|
|
51
48
|
this.#store = store;
|
|
52
49
|
this.#node = node;
|
|
53
50
|
this.#rpc = rpc;
|
|
54
51
|
this.#progress = progress;
|
|
55
52
|
this.#bootstrap = bootstrap;
|
|
56
|
-
this.#indexerDaemon = indexerDaemon;
|
|
57
|
-
this.#reattachIndexerDaemon = reattachIndexerDaemon;
|
|
58
53
|
this.#startHeight = startHeight;
|
|
59
54
|
this.#syncDebounceMs = syncDebounceMs;
|
|
60
55
|
this.#dataDir = dataDir;
|
|
@@ -119,7 +114,6 @@ export class DefaultManagedBitcoindClient {
|
|
|
119
114
|
const indexedTip = await this.#client.getTip();
|
|
120
115
|
const progressStatus = this.#progress.getStatusSnapshot();
|
|
121
116
|
const serviceStatus = await this.#node.refreshServiceStatus?.();
|
|
122
|
-
const daemonStatus = await this.#indexerDaemon?.getStatus().catch(() => null);
|
|
123
117
|
try {
|
|
124
118
|
const info = await this.#rpc.getBlockchainInfo();
|
|
125
119
|
return {
|
|
@@ -143,7 +137,7 @@ export class DefaultManagedBitcoindClient {
|
|
|
143
137
|
serviceUpdatedAtUnixMs: serviceStatus?.updatedAtUnixMs ?? null,
|
|
144
138
|
walletReplica: serviceStatus?.walletReplica ?? null,
|
|
145
139
|
serviceStatus: serviceStatus ?? null,
|
|
146
|
-
indexerDaemon:
|
|
140
|
+
indexerDaemon: null,
|
|
147
141
|
};
|
|
148
142
|
}
|
|
149
143
|
catch {
|
|
@@ -168,7 +162,7 @@ export class DefaultManagedBitcoindClient {
|
|
|
168
162
|
serviceUpdatedAtUnixMs: serviceStatus?.updatedAtUnixMs ?? null,
|
|
169
163
|
walletReplica: serviceStatus?.walletReplica ?? null,
|
|
170
164
|
serviceStatus: serviceStatus ?? null,
|
|
171
|
-
indexerDaemon:
|
|
165
|
+
indexerDaemon: null,
|
|
172
166
|
};
|
|
173
167
|
}
|
|
174
168
|
}
|
|
@@ -197,20 +191,11 @@ export class DefaultManagedBitcoindClient {
|
|
|
197
191
|
await this.#progress.close();
|
|
198
192
|
await this.#node.stop();
|
|
199
193
|
await this.#client.close();
|
|
200
|
-
await this.#resumeIndexerBackgroundFollow();
|
|
201
|
-
await this.#indexerDaemon?.close();
|
|
202
|
-
this.#indexerDaemon = null;
|
|
203
194
|
}
|
|
204
195
|
async playSyncCompletionScene() {
|
|
205
196
|
this.#assertOpen();
|
|
206
197
|
await this.#progress.playCompletionScene();
|
|
207
198
|
}
|
|
208
|
-
async detachToBackgroundFollow() {
|
|
209
|
-
this.#assertOpen();
|
|
210
|
-
await this.#resumeIndexerBackgroundFollow();
|
|
211
|
-
await this.#indexerDaemon?.close();
|
|
212
|
-
this.#indexerDaemon = null;
|
|
213
|
-
}
|
|
214
199
|
async #setGetblockStatusMessage(currentHeight, message, targetHeight = currentHeight) {
|
|
215
200
|
const safeTargetHeight = Math.max(currentHeight, targetHeight);
|
|
216
201
|
await this.#progress.setPhase("bitcoin_sync", {
|
|
@@ -404,33 +389,4 @@ export class DefaultManagedBitcoindClient {
|
|
|
404
389
|
throw new Error("managed_bitcoind_client_closed");
|
|
405
390
|
}
|
|
406
391
|
}
|
|
407
|
-
async #resumeIndexerBackgroundFollow() {
|
|
408
|
-
if (this.#backgroundFollowResumed) {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
if (this.#indexerDaemon === null && this.#reattachIndexerDaemon === null) {
|
|
412
|
-
this.#backgroundFollowResumed = true;
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
if (this.#indexerDaemon !== null) {
|
|
416
|
-
try {
|
|
417
|
-
await this.#indexerDaemon.resumeBackgroundFollow();
|
|
418
|
-
this.#backgroundFollowResumed = true;
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
if (this.#reattachIndexerDaemon === null) {
|
|
423
|
-
throw error;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
const reattachIndexerDaemon = this.#reattachIndexerDaemon;
|
|
428
|
-
if (reattachIndexerDaemon === null) {
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
const replacementDaemon = await reattachIndexerDaemon();
|
|
432
|
-
this.#indexerDaemon = replacementDaemon;
|
|
433
|
-
await replacementDaemon?.resumeBackgroundFollow();
|
|
434
|
-
this.#backgroundFollowResumed = true;
|
|
435
|
-
}
|
|
436
392
|
}
|
|
@@ -3,15 +3,20 @@ import net from "node:net";
|
|
|
3
3
|
import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { loadBundledGenesisParameters, serializeIndexerState } from "@cogcoin/indexer";
|
|
5
5
|
import { openManagedBitcoindClientInternal } from "./client.js";
|
|
6
|
+
import { DEFAULT_SNAPSHOT_METADATA } from "./bootstrap.js";
|
|
6
7
|
import { openClient } from "../client.js";
|
|
7
8
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
8
9
|
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
9
10
|
import { createRpcClient } from "./node.js";
|
|
10
11
|
import { normalizeCogcoinProcessingStartHeight } from "./processing-start-height.js";
|
|
12
|
+
import { createBootstrapProgress } from "./progress/formatting.js";
|
|
11
13
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
12
14
|
import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
|
|
13
15
|
const SNAPSHOT_TTL_MS = 30_000;
|
|
14
16
|
const HEARTBEAT_INTERVAL_MS = 1_000;
|
|
17
|
+
const FORCE_RESUME_ERROR_ENV = "COGCOIN_TEST_INDEXER_DAEMON_FORCE_RESUME_ERROR";
|
|
18
|
+
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS = 30_000;
|
|
19
|
+
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR = "indexer_daemon_background_follow_resume_timeout";
|
|
15
20
|
function parseArg(name) {
|
|
16
21
|
const prefix = `--${name}=`;
|
|
17
22
|
const value = process.argv.find((entry) => entry.startsWith(prefix));
|
|
@@ -34,6 +39,22 @@ async function readJsonFile(filePath) {
|
|
|
34
39
|
async function readManagedBitcoindStatus(paths) {
|
|
35
40
|
return readJsonFile(paths.bitcoindStatusPath);
|
|
36
41
|
}
|
|
42
|
+
async function withTimeout(promise, timeoutMs, errorCode) {
|
|
43
|
+
let timeoutId = null;
|
|
44
|
+
try {
|
|
45
|
+
return await Promise.race([
|
|
46
|
+
promise,
|
|
47
|
+
new Promise((_, reject) => {
|
|
48
|
+
timeoutId = setTimeout(() => reject(new Error(errorCode)), timeoutMs);
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (timeoutId !== null) {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
37
58
|
async function readPackageVersionFromDisk() {
|
|
38
59
|
try {
|
|
39
60
|
const raw = await readFile(new URL("../../package.json", import.meta.url), "utf8");
|
|
@@ -177,6 +198,12 @@ async function main() {
|
|
|
177
198
|
let backgroundStore = null;
|
|
178
199
|
let backgroundClient = null;
|
|
179
200
|
let backgroundResumePromise = null;
|
|
201
|
+
let backgroundFollowError = null;
|
|
202
|
+
let backgroundFollowActive = false;
|
|
203
|
+
let bootstrapPhase = "paused";
|
|
204
|
+
let bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
|
|
205
|
+
let cogcoinSyncHeight = null;
|
|
206
|
+
let cogcoinSyncTargetHeight = null;
|
|
180
207
|
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
181
208
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
182
209
|
const observeAppliedTip = (appliedTip, now) => {
|
|
@@ -240,12 +267,39 @@ async function main() {
|
|
|
240
267
|
lastAppliedAtUnixMs,
|
|
241
268
|
activeSnapshotCount: snapshots.size,
|
|
242
269
|
lastError,
|
|
270
|
+
backgroundFollowActive,
|
|
271
|
+
bootstrapPhase,
|
|
272
|
+
bootstrapProgress: { ...bootstrapProgress },
|
|
273
|
+
cogcoinSyncHeight,
|
|
274
|
+
cogcoinSyncTargetHeight,
|
|
243
275
|
});
|
|
244
276
|
const writeStatus = async () => {
|
|
245
277
|
const status = buildStatus();
|
|
246
278
|
await writeRuntimeStatusFile(paths.indexerDaemonStatusPath, status);
|
|
247
279
|
return status;
|
|
248
280
|
};
|
|
281
|
+
const recordBackgroundFollowFailure = async (message) => {
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
heartbeatAtUnixMs = now;
|
|
284
|
+
updatedAtUnixMs = now;
|
|
285
|
+
state = "failed";
|
|
286
|
+
lastError = message;
|
|
287
|
+
backgroundFollowError = message;
|
|
288
|
+
backgroundFollowActive = false;
|
|
289
|
+
bootstrapPhase = "error";
|
|
290
|
+
bootstrapProgress = {
|
|
291
|
+
...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
|
|
292
|
+
blocks: coreBestHeight,
|
|
293
|
+
headers: coreBestHeight,
|
|
294
|
+
targetHeight: coreBestHeight,
|
|
295
|
+
message,
|
|
296
|
+
lastError: message,
|
|
297
|
+
updatedAt: now,
|
|
298
|
+
};
|
|
299
|
+
cogcoinSyncHeight = appliedTipHeight;
|
|
300
|
+
cogcoinSyncTargetHeight = coreBestHeight;
|
|
301
|
+
await writeStatus();
|
|
302
|
+
};
|
|
249
303
|
const refreshStatus = async () => {
|
|
250
304
|
const now = Date.now();
|
|
251
305
|
heartbeatAtUnixMs = now;
|
|
@@ -254,23 +308,88 @@ async function main() {
|
|
|
254
308
|
readCoreTipStatus(paths),
|
|
255
309
|
readAppliedTipStatus(databasePath),
|
|
256
310
|
]);
|
|
311
|
+
const backgroundStatus = await backgroundClient?.getNodeStatus().catch(() => null) ?? null;
|
|
312
|
+
if (backgroundStatus?.following === true) {
|
|
313
|
+
backgroundFollowError = null;
|
|
314
|
+
}
|
|
257
315
|
rpcReachable = coreStatus.rpcReachable;
|
|
258
316
|
coreBestHeight = coreStatus.coreBestHeight;
|
|
259
317
|
coreBestHash = coreStatus.coreBestHash;
|
|
260
318
|
observeAppliedTip(indexedStatus.appliedTip, now);
|
|
319
|
+
backgroundFollowActive = backgroundStatus?.following ?? (backgroundClient !== null);
|
|
320
|
+
bootstrapPhase = backgroundStatus?.bootstrapPhase ?? (backgroundFollowActive ? "follow_tip" : "paused");
|
|
321
|
+
bootstrapProgress = backgroundStatus?.bootstrapProgress ?? createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA);
|
|
322
|
+
cogcoinSyncHeight = backgroundStatus?.cogcoinSyncHeight ?? indexedStatus.appliedTip?.height ?? null;
|
|
323
|
+
cogcoinSyncTargetHeight = backgroundStatus?.cogcoinSyncTargetHeight ?? coreStatus.coreBestHeight;
|
|
324
|
+
if (backgroundStatus === null && backgroundFollowError !== null) {
|
|
325
|
+
state = "failed";
|
|
326
|
+
lastError = backgroundFollowError;
|
|
327
|
+
backgroundFollowActive = false;
|
|
328
|
+
bootstrapPhase = "error";
|
|
329
|
+
bootstrapProgress = {
|
|
330
|
+
...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
|
|
331
|
+
blocks: coreStatus.coreBestHeight,
|
|
332
|
+
headers: coreStatus.coreBestHeight,
|
|
333
|
+
targetHeight: coreStatus.coreBestHeight,
|
|
334
|
+
message: backgroundFollowError,
|
|
335
|
+
lastError: backgroundFollowError,
|
|
336
|
+
updatedAt: now,
|
|
337
|
+
};
|
|
338
|
+
cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
|
|
339
|
+
cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
|
|
340
|
+
return writeStatus();
|
|
341
|
+
}
|
|
261
342
|
if (indexedStatus.schemaMismatch) {
|
|
262
343
|
state = "schema-mismatch";
|
|
263
344
|
lastError = indexedStatus.error;
|
|
345
|
+
bootstrapPhase = "error";
|
|
346
|
+
bootstrapProgress = {
|
|
347
|
+
...bootstrapProgress,
|
|
348
|
+
phase: "error",
|
|
349
|
+
message: indexedStatus.error ?? "Indexer schema mismatch.",
|
|
350
|
+
lastError: indexedStatus.error,
|
|
351
|
+
updatedAt: now,
|
|
352
|
+
};
|
|
264
353
|
return writeStatus();
|
|
265
354
|
}
|
|
266
355
|
if (indexedStatus.error !== null) {
|
|
267
356
|
state = "failed";
|
|
268
357
|
lastError = indexedStatus.error;
|
|
358
|
+
bootstrapPhase = "error";
|
|
359
|
+
bootstrapProgress = {
|
|
360
|
+
...bootstrapProgress,
|
|
361
|
+
phase: "error",
|
|
362
|
+
message: indexedStatus.error,
|
|
363
|
+
lastError: indexedStatus.error,
|
|
364
|
+
updatedAt: now,
|
|
365
|
+
};
|
|
269
366
|
return writeStatus();
|
|
270
367
|
}
|
|
271
368
|
const leaseState = deriveLeaseState(coreStatus, indexedStatus.appliedTip);
|
|
272
369
|
state = leaseState.state;
|
|
273
370
|
lastError = leaseState.lastError;
|
|
371
|
+
if (lastError !== null) {
|
|
372
|
+
bootstrapPhase = leaseState.state === "starting" ? "paused" : "error";
|
|
373
|
+
bootstrapProgress = {
|
|
374
|
+
...bootstrapProgress,
|
|
375
|
+
phase: bootstrapPhase,
|
|
376
|
+
message: lastError,
|
|
377
|
+
lastError,
|
|
378
|
+
updatedAt: now,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
else if (backgroundStatus === null) {
|
|
382
|
+
bootstrapPhase = leaseState.state === "synced" ? "follow_tip" : "paused";
|
|
383
|
+
bootstrapProgress = {
|
|
384
|
+
...createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA),
|
|
385
|
+
blocks: coreStatus.coreBestHeight,
|
|
386
|
+
headers: coreStatus.coreBestHeight,
|
|
387
|
+
targetHeight: coreStatus.coreBestHeight,
|
|
388
|
+
updatedAt: now,
|
|
389
|
+
};
|
|
390
|
+
cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
|
|
391
|
+
cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
|
|
392
|
+
}
|
|
274
393
|
return writeStatus();
|
|
275
394
|
};
|
|
276
395
|
const pauseBackgroundFollow = async () => {
|
|
@@ -283,6 +402,12 @@ async function main() {
|
|
|
283
402
|
backgroundStore = null;
|
|
284
403
|
await client?.close().catch(() => undefined);
|
|
285
404
|
await store?.close().catch(() => undefined);
|
|
405
|
+
backgroundFollowError = null;
|
|
406
|
+
backgroundFollowActive = false;
|
|
407
|
+
bootstrapPhase = "paused";
|
|
408
|
+
bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
|
|
409
|
+
cogcoinSyncHeight = appliedTipHeight;
|
|
410
|
+
cogcoinSyncTargetHeight = coreBestHeight;
|
|
286
411
|
};
|
|
287
412
|
const resumeBackgroundFollow = async () => {
|
|
288
413
|
if (backgroundClient !== null) {
|
|
@@ -292,35 +417,50 @@ async function main() {
|
|
|
292
417
|
return backgroundResumePromise;
|
|
293
418
|
}
|
|
294
419
|
backgroundResumePromise = (async () => {
|
|
295
|
-
|
|
296
|
-
const store = await openSqliteStore({ filename: databasePath });
|
|
297
|
-
const chain = bitcoindStatus?.chain ?? "main";
|
|
298
|
-
const startHeight = normalizeCogcoinProcessingStartHeight({
|
|
299
|
-
chain,
|
|
300
|
-
startHeight: bitcoindStatus?.startHeight,
|
|
301
|
-
genesisParameters,
|
|
302
|
-
});
|
|
420
|
+
let store = null;
|
|
303
421
|
try {
|
|
422
|
+
const forcedResumeError = process.env[FORCE_RESUME_ERROR_ENV]?.trim();
|
|
423
|
+
if (forcedResumeError) {
|
|
424
|
+
throw new Error(forcedResumeError);
|
|
425
|
+
}
|
|
426
|
+
const bitcoindStatus = await readManagedBitcoindStatus(paths);
|
|
427
|
+
store = await openSqliteStore({ filename: databasePath });
|
|
428
|
+
const openedStore = store;
|
|
429
|
+
const chain = bitcoindStatus?.chain ?? "main";
|
|
430
|
+
const startHeight = normalizeCogcoinProcessingStartHeight({
|
|
431
|
+
chain,
|
|
432
|
+
startHeight: bitcoindStatus?.startHeight,
|
|
433
|
+
genesisParameters,
|
|
434
|
+
});
|
|
304
435
|
const client = await openManagedBitcoindClientInternal({
|
|
305
|
-
store,
|
|
436
|
+
store: openedStore,
|
|
306
437
|
dataDir,
|
|
307
438
|
chain,
|
|
308
439
|
startHeight,
|
|
309
440
|
walletRootId,
|
|
310
441
|
progressOutput: "none",
|
|
311
442
|
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
443
|
+
backgroundStore = openedStore;
|
|
444
|
+
backgroundClient = client;
|
|
445
|
+
backgroundFollowError = null;
|
|
446
|
+
backgroundFollowActive = true;
|
|
447
|
+
void client.startFollowingTip().catch(async (error) => {
|
|
448
|
+
if (backgroundClient !== client || backgroundStore !== openedStore) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
backgroundClient = null;
|
|
452
|
+
backgroundStore = null;
|
|
453
|
+
backgroundFollowActive = false;
|
|
318
454
|
await client.close().catch(() => undefined);
|
|
319
|
-
|
|
320
|
-
|
|
455
|
+
await openedStore.close().catch(() => undefined);
|
|
456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
457
|
+
await recordBackgroundFollowFailure(message).catch(() => undefined);
|
|
458
|
+
});
|
|
321
459
|
}
|
|
322
460
|
catch (error) {
|
|
323
|
-
await store
|
|
461
|
+
await store?.close().catch(() => undefined);
|
|
462
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
463
|
+
await recordBackgroundFollowFailure(message).catch(() => undefined);
|
|
324
464
|
throw error;
|
|
325
465
|
}
|
|
326
466
|
})();
|
|
@@ -434,6 +574,11 @@ async function main() {
|
|
|
434
574
|
lastAppliedAtUnixMs: leaseStatus.lastAppliedAtUnixMs,
|
|
435
575
|
activeSnapshotCount: leaseStatus.activeSnapshotCount,
|
|
436
576
|
lastError: leaseStatus.lastError,
|
|
577
|
+
backgroundFollowActive: leaseStatus.backgroundFollowActive ?? false,
|
|
578
|
+
bootstrapPhase: leaseStatus.bootstrapPhase ?? null,
|
|
579
|
+
bootstrapProgress: leaseStatus.bootstrapProgress ?? null,
|
|
580
|
+
cogcoinSyncHeight: leaseStatus.cogcoinSyncHeight ?? null,
|
|
581
|
+
cogcoinSyncTargetHeight: leaseStatus.cogcoinSyncTargetHeight ?? null,
|
|
437
582
|
tipHeight: snapshot.tipHeight,
|
|
438
583
|
tipHash: snapshot.tipHash,
|
|
439
584
|
openedAtUnixMs: snapshot.openedAtUnixMs,
|
|
@@ -494,17 +639,17 @@ async function main() {
|
|
|
494
639
|
});
|
|
495
640
|
return;
|
|
496
641
|
}
|
|
497
|
-
if (request.method === "PauseBackgroundFollow") {
|
|
498
|
-
await pauseBackgroundFollow();
|
|
499
|
-
writeResponse({
|
|
500
|
-
id: request.id,
|
|
501
|
-
ok: true,
|
|
502
|
-
result: null,
|
|
503
|
-
});
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
642
|
if (request.method === "ResumeBackgroundFollow") {
|
|
507
|
-
|
|
643
|
+
try {
|
|
644
|
+
await withTimeout(resumeBackgroundFollow(), BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS, BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR);
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
if (error instanceof Error
|
|
648
|
+
&& error.message === BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR) {
|
|
649
|
+
await recordBackgroundFollowFailure(error.message).catch(() => undefined);
|
|
650
|
+
}
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
508
653
|
writeResponse({
|
|
509
654
|
id: request.id,
|
|
510
655
|
ok: true,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
|
|
1
|
+
import { type BootstrapPhase, type BootstrapProgress, type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
|
|
2
2
|
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
3
|
+
export declare const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
|
|
3
4
|
interface DaemonRequest {
|
|
4
5
|
id: string;
|
|
5
|
-
method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot" | "
|
|
6
|
+
method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot" | "ResumeBackgroundFollow";
|
|
6
7
|
token?: string;
|
|
7
8
|
}
|
|
8
9
|
interface DaemonResponse {
|
|
@@ -35,6 +36,11 @@ export interface IndexerSnapshotHandle {
|
|
|
35
36
|
lastAppliedAtUnixMs: number | null;
|
|
36
37
|
activeSnapshotCount: number;
|
|
37
38
|
lastError: string | null;
|
|
39
|
+
backgroundFollowActive: boolean;
|
|
40
|
+
bootstrapPhase: BootstrapPhase | null;
|
|
41
|
+
bootstrapProgress: BootstrapProgress | null;
|
|
42
|
+
cogcoinSyncHeight: number | null;
|
|
43
|
+
cogcoinSyncTargetHeight: number | null;
|
|
38
44
|
tipHeight: number | null;
|
|
39
45
|
tipHash: string | null;
|
|
40
46
|
openedAtUnixMs: number;
|
|
@@ -65,7 +71,6 @@ export interface IndexerDaemonClient {
|
|
|
65
71
|
openSnapshot(): Promise<IndexerSnapshotHandle>;
|
|
66
72
|
readSnapshot(token: string): Promise<IndexerSnapshotPayload>;
|
|
67
73
|
closeSnapshot(token: string): Promise<void>;
|
|
68
|
-
pauseBackgroundFollow(): Promise<void>;
|
|
69
74
|
resumeBackgroundFollow(): Promise<void>;
|
|
70
75
|
close(): Promise<void>;
|
|
71
76
|
}
|
|
@@ -84,11 +89,13 @@ export interface CoherentIndexerSnapshotLease {
|
|
|
84
89
|
payload: IndexerSnapshotPayload;
|
|
85
90
|
status: ManagedIndexerDaemonStatus;
|
|
86
91
|
}
|
|
92
|
+
type ManagedIndexerDaemonServiceLifetime = "persistent" | "ephemeral";
|
|
87
93
|
export declare function stopIndexerDaemonServiceWithLockHeld(options: {
|
|
88
94
|
dataDir: string;
|
|
89
95
|
walletRootId?: string;
|
|
90
96
|
shutdownTimeoutMs?: number;
|
|
91
97
|
paths?: ReturnType<typeof resolveManagedServicePaths>;
|
|
98
|
+
processId?: number | null;
|
|
92
99
|
}): Promise<IndexerDaemonStopResult>;
|
|
93
100
|
export declare function probeIndexerDaemon(options: {
|
|
94
101
|
dataDir: string;
|
|
@@ -104,6 +111,10 @@ export declare function attachOrStartIndexerDaemon(options: {
|
|
|
104
111
|
databasePath: string;
|
|
105
112
|
walletRootId?: string;
|
|
106
113
|
startupTimeoutMs?: number;
|
|
114
|
+
shutdownTimeoutMs?: number;
|
|
115
|
+
serviceLifetime?: ManagedIndexerDaemonServiceLifetime;
|
|
116
|
+
ensureBackgroundFollow?: boolean;
|
|
117
|
+
expectedBinaryVersion?: string | null;
|
|
107
118
|
}): Promise<IndexerDaemonClient>;
|
|
108
119
|
export declare function stopIndexerDaemonService(options: {
|
|
109
120
|
dataDir: string;
|