@cogcoin/client 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/bitcoind/client/factory.d.ts +0 -8
- package/dist/bitcoind/client/factory.js +1 -59
- package/dist/bitcoind/client/managed-client.d.ts +1 -3
- package/dist/bitcoind/client/managed-client.js +3 -47
- package/dist/bitcoind/client/sync-engine.js +1 -1
- package/dist/bitcoind/indexer-daemon-main.js +171 -35
- package/dist/bitcoind/indexer-daemon.d.ts +11 -3
- package/dist/bitcoind/indexer-daemon.js +147 -59
- package/dist/bitcoind/indexer-monitor.d.ts +12 -0
- package/dist/bitcoind/indexer-monitor.js +93 -0
- package/dist/bitcoind/progress/controller.js +4 -1
- package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
- package/dist/bitcoind/progress/follow-scene.js +94 -5
- package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
- package/dist/bitcoind/progress/tty-renderer.js +2 -0
- package/dist/bitcoind/testing.d.ts +0 -1
- package/dist/bitcoind/testing.js +0 -1
- package/dist/bitcoind/types.d.ts +5 -2
- package/dist/cli/commands/follow.js +44 -49
- package/dist/cli/commands/mining-admin.js +56 -2
- package/dist/cli/commands/mining-read.js +43 -3
- package/dist/cli/commands/mining-runtime.js +91 -73
- package/dist/cli/commands/service-runtime.js +42 -2
- package/dist/cli/commands/status.js +3 -1
- package/dist/cli/commands/sync.js +50 -90
- package/dist/cli/commands/wallet-admin.js +21 -3
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.d.ts +0 -1
- package/dist/cli/context.js +7 -24
- package/dist/cli/managed-indexer-observer.d.ts +33 -0
- package/dist/cli/managed-indexer-observer.js +163 -0
- package/dist/cli/mining-format.d.ts +3 -1
- package/dist/cli/mining-format.js +35 -0
- package/dist/cli/mining-json.d.ts +11 -1
- package/dist/cli/mining-json.js +9 -0
- package/dist/cli/output.js +24 -0
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +23 -0
- package/dist/cli/read-json.d.ts +13 -1
- package/dist/cli/read-json.js +31 -0
- package/dist/cli/runner.js +4 -2
- package/dist/cli/signals.d.ts +12 -0
- package/dist/cli/signals.js +31 -13
- package/dist/cli/types.d.ts +8 -4
- package/dist/cli/update-service.d.ts +2 -12
- package/dist/cli/update-service.js +2 -68
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +17 -0
- package/dist/semver.d.ts +12 -0
- package/dist/semver.js +68 -0
- package/dist/wallet/lifecycle.js +0 -6
- package/dist/wallet/mining/config.js +54 -3
- package/dist/wallet/mining/control.d.ts +5 -2
- package/dist/wallet/mining/control.js +153 -34
- package/dist/wallet/mining/domain-prompts.d.ts +17 -0
- package/dist/wallet/mining/domain-prompts.js +130 -0
- package/dist/wallet/mining/index.d.ts +2 -1
- package/dist/wallet/mining/index.js +1 -0
- package/dist/wallet/mining/runner.d.ts +58 -2
- package/dist/wallet/mining/runner.js +553 -331
- package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
- package/dist/wallet/mining/sentences.js +7 -4
- package/dist/wallet/mining/types.d.ts +26 -0
- package/dist/wallet/mining/visualizer.d.ts +3 -0
- package/dist/wallet/mining/visualizer.js +106 -12
- package/dist/wallet/read/context.d.ts +1 -0
- package/dist/wallet/read/context.js +19 -10
- package/dist/wallet/reset.js +0 -1
- package/dist/wallet/state/client-password-agent.js +4 -1
- package/dist/wallet/state/client-password.js +15 -8
- package/dist/wallet/tx/anchor.js +0 -1
- package/dist/wallet/tx/bitcoin-transfer.js +0 -1
- package/dist/wallet/tx/cog.js +0 -3
- package/dist/wallet/tx/common.js +1 -1
- package/dist/wallet/tx/domain-admin.js +0 -1
- package/dist/wallet/tx/domain-market.js +0 -3
- package/dist/wallet/tx/field.js +0 -1
- package/dist/wallet/tx/register.js +0 -1
- package/dist/wallet/tx/reputation.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.0
|
|
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
|
|
|
@@ -135,9 +135,10 @@ The installed `cogcoin` command covers the first-party local wallet and node wor
|
|
|
135
135
|
- sync and service commands such as `status`, `sync`, `follow`, `bitcoin start`, `bitcoin stop`, `bitcoin status`, `indexer start`, `indexer stop`, and `indexer status`
|
|
136
136
|
- domain and field commands such as `register`, `anchor`, `show`, `domains`, `fields`, `buy`, `sell`, and `transfer`
|
|
137
137
|
- COG and reputation commands such as `send`, `cog lock`, `claim`, `reclaim`, `rep give`, and `rep revoke`
|
|
138
|
-
- 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`
|
|
139
139
|
|
|
140
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.
|
|
141
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.
|
|
142
143
|
Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
|
|
143
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
|
}
|
|
@@ -317,6 +317,6 @@ export async function syncToTip(dependencies) {
|
|
|
317
317
|
lastError: message,
|
|
318
318
|
message: "Managed sync can be resumed after the last error.",
|
|
319
319
|
});
|
|
320
|
-
throw new Error(message);
|
|
320
|
+
throw new Error(message, { cause: error instanceof Error ? error : undefined });
|
|
321
321
|
}
|
|
322
322
|
}
|
|
@@ -3,15 +3,21 @@ 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";
|
|
8
|
+
import { readPackageVersionFromDisk } from "../package-version.js";
|
|
7
9
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
8
10
|
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
9
11
|
import { createRpcClient } from "./node.js";
|
|
10
12
|
import { normalizeCogcoinProcessingStartHeight } from "./processing-start-height.js";
|
|
13
|
+
import { createBootstrapProgress } from "./progress/formatting.js";
|
|
11
14
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
12
15
|
import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
|
|
13
16
|
const SNAPSHOT_TTL_MS = 30_000;
|
|
14
17
|
const HEARTBEAT_INTERVAL_MS = 1_000;
|
|
18
|
+
const FORCE_RESUME_ERROR_ENV = "COGCOIN_TEST_INDEXER_DAEMON_FORCE_RESUME_ERROR";
|
|
19
|
+
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS = 30_000;
|
|
20
|
+
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR = "indexer_daemon_background_follow_resume_timeout";
|
|
15
21
|
function parseArg(name) {
|
|
16
22
|
const prefix = `--${name}=`;
|
|
17
23
|
const value = process.argv.find((entry) => entry.startsWith(prefix));
|
|
@@ -34,14 +40,20 @@ async function readJsonFile(filePath) {
|
|
|
34
40
|
async function readManagedBitcoindStatus(paths) {
|
|
35
41
|
return readJsonFile(paths.bitcoindStatusPath);
|
|
36
42
|
}
|
|
37
|
-
async function
|
|
43
|
+
async function withTimeout(promise, timeoutMs, errorCode) {
|
|
44
|
+
let timeoutId = null;
|
|
38
45
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
return await Promise.race([
|
|
47
|
+
promise,
|
|
48
|
+
new Promise((_, reject) => {
|
|
49
|
+
timeoutId = setTimeout(() => reject(new Error(errorCode)), timeoutMs);
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
42
52
|
}
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
finally {
|
|
54
|
+
if (timeoutId !== null) {
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
}
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
function createSnapshotKey(appliedTip) {
|
|
@@ -156,7 +168,7 @@ async function main() {
|
|
|
156
168
|
const walletRootId = parseArg("wallet-root-id") || UNINITIALIZED_WALLET_ROOT_ID;
|
|
157
169
|
const paths = resolveManagedServicePaths(dataDir, walletRootId);
|
|
158
170
|
const daemonInstanceId = randomUUID();
|
|
159
|
-
const binaryVersion = await readPackageVersionFromDisk();
|
|
171
|
+
const binaryVersion = await readPackageVersionFromDisk().catch(() => "0.0.0");
|
|
160
172
|
const genesisParameters = await loadBundledGenesisParameters();
|
|
161
173
|
const startedAtUnixMs = Date.now();
|
|
162
174
|
const snapshots = new Map();
|
|
@@ -177,6 +189,12 @@ async function main() {
|
|
|
177
189
|
let backgroundStore = null;
|
|
178
190
|
let backgroundClient = null;
|
|
179
191
|
let backgroundResumePromise = null;
|
|
192
|
+
let backgroundFollowError = null;
|
|
193
|
+
let backgroundFollowActive = false;
|
|
194
|
+
let bootstrapPhase = "paused";
|
|
195
|
+
let bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
|
|
196
|
+
let cogcoinSyncHeight = null;
|
|
197
|
+
let cogcoinSyncTargetHeight = null;
|
|
180
198
|
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
181
199
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
182
200
|
const observeAppliedTip = (appliedTip, now) => {
|
|
@@ -240,12 +258,39 @@ async function main() {
|
|
|
240
258
|
lastAppliedAtUnixMs,
|
|
241
259
|
activeSnapshotCount: snapshots.size,
|
|
242
260
|
lastError,
|
|
261
|
+
backgroundFollowActive,
|
|
262
|
+
bootstrapPhase,
|
|
263
|
+
bootstrapProgress: { ...bootstrapProgress },
|
|
264
|
+
cogcoinSyncHeight,
|
|
265
|
+
cogcoinSyncTargetHeight,
|
|
243
266
|
});
|
|
244
267
|
const writeStatus = async () => {
|
|
245
268
|
const status = buildStatus();
|
|
246
269
|
await writeRuntimeStatusFile(paths.indexerDaemonStatusPath, status);
|
|
247
270
|
return status;
|
|
248
271
|
};
|
|
272
|
+
const recordBackgroundFollowFailure = async (message) => {
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
heartbeatAtUnixMs = now;
|
|
275
|
+
updatedAtUnixMs = now;
|
|
276
|
+
state = "failed";
|
|
277
|
+
lastError = message;
|
|
278
|
+
backgroundFollowError = message;
|
|
279
|
+
backgroundFollowActive = false;
|
|
280
|
+
bootstrapPhase = "error";
|
|
281
|
+
bootstrapProgress = {
|
|
282
|
+
...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
|
|
283
|
+
blocks: coreBestHeight,
|
|
284
|
+
headers: coreBestHeight,
|
|
285
|
+
targetHeight: coreBestHeight,
|
|
286
|
+
message,
|
|
287
|
+
lastError: message,
|
|
288
|
+
updatedAt: now,
|
|
289
|
+
};
|
|
290
|
+
cogcoinSyncHeight = appliedTipHeight;
|
|
291
|
+
cogcoinSyncTargetHeight = coreBestHeight;
|
|
292
|
+
await writeStatus();
|
|
293
|
+
};
|
|
249
294
|
const refreshStatus = async () => {
|
|
250
295
|
const now = Date.now();
|
|
251
296
|
heartbeatAtUnixMs = now;
|
|
@@ -254,23 +299,88 @@ async function main() {
|
|
|
254
299
|
readCoreTipStatus(paths),
|
|
255
300
|
readAppliedTipStatus(databasePath),
|
|
256
301
|
]);
|
|
302
|
+
const backgroundStatus = await backgroundClient?.getNodeStatus().catch(() => null) ?? null;
|
|
303
|
+
if (backgroundStatus?.following === true) {
|
|
304
|
+
backgroundFollowError = null;
|
|
305
|
+
}
|
|
257
306
|
rpcReachable = coreStatus.rpcReachable;
|
|
258
307
|
coreBestHeight = coreStatus.coreBestHeight;
|
|
259
308
|
coreBestHash = coreStatus.coreBestHash;
|
|
260
309
|
observeAppliedTip(indexedStatus.appliedTip, now);
|
|
310
|
+
backgroundFollowActive = backgroundStatus?.following ?? (backgroundClient !== null);
|
|
311
|
+
bootstrapPhase = backgroundStatus?.bootstrapPhase ?? (backgroundFollowActive ? "follow_tip" : "paused");
|
|
312
|
+
bootstrapProgress = backgroundStatus?.bootstrapProgress ?? createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA);
|
|
313
|
+
cogcoinSyncHeight = backgroundStatus?.cogcoinSyncHeight ?? indexedStatus.appliedTip?.height ?? null;
|
|
314
|
+
cogcoinSyncTargetHeight = backgroundStatus?.cogcoinSyncTargetHeight ?? coreStatus.coreBestHeight;
|
|
315
|
+
if (backgroundStatus === null && backgroundFollowError !== null) {
|
|
316
|
+
state = "failed";
|
|
317
|
+
lastError = backgroundFollowError;
|
|
318
|
+
backgroundFollowActive = false;
|
|
319
|
+
bootstrapPhase = "error";
|
|
320
|
+
bootstrapProgress = {
|
|
321
|
+
...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
|
|
322
|
+
blocks: coreStatus.coreBestHeight,
|
|
323
|
+
headers: coreStatus.coreBestHeight,
|
|
324
|
+
targetHeight: coreStatus.coreBestHeight,
|
|
325
|
+
message: backgroundFollowError,
|
|
326
|
+
lastError: backgroundFollowError,
|
|
327
|
+
updatedAt: now,
|
|
328
|
+
};
|
|
329
|
+
cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
|
|
330
|
+
cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
|
|
331
|
+
return writeStatus();
|
|
332
|
+
}
|
|
261
333
|
if (indexedStatus.schemaMismatch) {
|
|
262
334
|
state = "schema-mismatch";
|
|
263
335
|
lastError = indexedStatus.error;
|
|
336
|
+
bootstrapPhase = "error";
|
|
337
|
+
bootstrapProgress = {
|
|
338
|
+
...bootstrapProgress,
|
|
339
|
+
phase: "error",
|
|
340
|
+
message: indexedStatus.error ?? "Indexer schema mismatch.",
|
|
341
|
+
lastError: indexedStatus.error,
|
|
342
|
+
updatedAt: now,
|
|
343
|
+
};
|
|
264
344
|
return writeStatus();
|
|
265
345
|
}
|
|
266
346
|
if (indexedStatus.error !== null) {
|
|
267
347
|
state = "failed";
|
|
268
348
|
lastError = indexedStatus.error;
|
|
349
|
+
bootstrapPhase = "error";
|
|
350
|
+
bootstrapProgress = {
|
|
351
|
+
...bootstrapProgress,
|
|
352
|
+
phase: "error",
|
|
353
|
+
message: indexedStatus.error,
|
|
354
|
+
lastError: indexedStatus.error,
|
|
355
|
+
updatedAt: now,
|
|
356
|
+
};
|
|
269
357
|
return writeStatus();
|
|
270
358
|
}
|
|
271
359
|
const leaseState = deriveLeaseState(coreStatus, indexedStatus.appliedTip);
|
|
272
360
|
state = leaseState.state;
|
|
273
361
|
lastError = leaseState.lastError;
|
|
362
|
+
if (lastError !== null) {
|
|
363
|
+
bootstrapPhase = leaseState.state === "starting" ? "paused" : "error";
|
|
364
|
+
bootstrapProgress = {
|
|
365
|
+
...bootstrapProgress,
|
|
366
|
+
phase: bootstrapPhase,
|
|
367
|
+
message: lastError,
|
|
368
|
+
lastError,
|
|
369
|
+
updatedAt: now,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
else if (backgroundStatus === null) {
|
|
373
|
+
bootstrapPhase = leaseState.state === "synced" ? "follow_tip" : "paused";
|
|
374
|
+
bootstrapProgress = {
|
|
375
|
+
...createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA),
|
|
376
|
+
blocks: coreStatus.coreBestHeight,
|
|
377
|
+
headers: coreStatus.coreBestHeight,
|
|
378
|
+
targetHeight: coreStatus.coreBestHeight,
|
|
379
|
+
updatedAt: now,
|
|
380
|
+
};
|
|
381
|
+
cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
|
|
382
|
+
cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
|
|
383
|
+
}
|
|
274
384
|
return writeStatus();
|
|
275
385
|
};
|
|
276
386
|
const pauseBackgroundFollow = async () => {
|
|
@@ -283,6 +393,12 @@ async function main() {
|
|
|
283
393
|
backgroundStore = null;
|
|
284
394
|
await client?.close().catch(() => undefined);
|
|
285
395
|
await store?.close().catch(() => undefined);
|
|
396
|
+
backgroundFollowError = null;
|
|
397
|
+
backgroundFollowActive = false;
|
|
398
|
+
bootstrapPhase = "paused";
|
|
399
|
+
bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
|
|
400
|
+
cogcoinSyncHeight = appliedTipHeight;
|
|
401
|
+
cogcoinSyncTargetHeight = coreBestHeight;
|
|
286
402
|
};
|
|
287
403
|
const resumeBackgroundFollow = async () => {
|
|
288
404
|
if (backgroundClient !== null) {
|
|
@@ -292,35 +408,50 @@ async function main() {
|
|
|
292
408
|
return backgroundResumePromise;
|
|
293
409
|
}
|
|
294
410
|
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
|
-
});
|
|
411
|
+
let store = null;
|
|
303
412
|
try {
|
|
413
|
+
const forcedResumeError = process.env[FORCE_RESUME_ERROR_ENV]?.trim();
|
|
414
|
+
if (forcedResumeError) {
|
|
415
|
+
throw new Error(forcedResumeError);
|
|
416
|
+
}
|
|
417
|
+
const bitcoindStatus = await readManagedBitcoindStatus(paths);
|
|
418
|
+
store = await openSqliteStore({ filename: databasePath });
|
|
419
|
+
const openedStore = store;
|
|
420
|
+
const chain = bitcoindStatus?.chain ?? "main";
|
|
421
|
+
const startHeight = normalizeCogcoinProcessingStartHeight({
|
|
422
|
+
chain,
|
|
423
|
+
startHeight: bitcoindStatus?.startHeight,
|
|
424
|
+
genesisParameters,
|
|
425
|
+
});
|
|
304
426
|
const client = await openManagedBitcoindClientInternal({
|
|
305
|
-
store,
|
|
427
|
+
store: openedStore,
|
|
306
428
|
dataDir,
|
|
307
429
|
chain,
|
|
308
430
|
startHeight,
|
|
309
431
|
walletRootId,
|
|
310
432
|
progressOutput: "none",
|
|
311
433
|
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
434
|
+
backgroundStore = openedStore;
|
|
435
|
+
backgroundClient = client;
|
|
436
|
+
backgroundFollowError = null;
|
|
437
|
+
backgroundFollowActive = true;
|
|
438
|
+
void client.startFollowingTip().catch(async (error) => {
|
|
439
|
+
if (backgroundClient !== client || backgroundStore !== openedStore) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
backgroundClient = null;
|
|
443
|
+
backgroundStore = null;
|
|
444
|
+
backgroundFollowActive = false;
|
|
318
445
|
await client.close().catch(() => undefined);
|
|
319
|
-
|
|
320
|
-
|
|
446
|
+
await openedStore.close().catch(() => undefined);
|
|
447
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
448
|
+
await recordBackgroundFollowFailure(message).catch(() => undefined);
|
|
449
|
+
});
|
|
321
450
|
}
|
|
322
451
|
catch (error) {
|
|
323
|
-
await store
|
|
452
|
+
await store?.close().catch(() => undefined);
|
|
453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
454
|
+
await recordBackgroundFollowFailure(message).catch(() => undefined);
|
|
324
455
|
throw error;
|
|
325
456
|
}
|
|
326
457
|
})();
|
|
@@ -434,6 +565,11 @@ async function main() {
|
|
|
434
565
|
lastAppliedAtUnixMs: leaseStatus.lastAppliedAtUnixMs,
|
|
435
566
|
activeSnapshotCount: leaseStatus.activeSnapshotCount,
|
|
436
567
|
lastError: leaseStatus.lastError,
|
|
568
|
+
backgroundFollowActive: leaseStatus.backgroundFollowActive ?? false,
|
|
569
|
+
bootstrapPhase: leaseStatus.bootstrapPhase ?? null,
|
|
570
|
+
bootstrapProgress: leaseStatus.bootstrapProgress ?? null,
|
|
571
|
+
cogcoinSyncHeight: leaseStatus.cogcoinSyncHeight ?? null,
|
|
572
|
+
cogcoinSyncTargetHeight: leaseStatus.cogcoinSyncTargetHeight ?? null,
|
|
437
573
|
tipHeight: snapshot.tipHeight,
|
|
438
574
|
tipHash: snapshot.tipHash,
|
|
439
575
|
openedAtUnixMs: snapshot.openedAtUnixMs,
|
|
@@ -494,17 +630,17 @@ async function main() {
|
|
|
494
630
|
});
|
|
495
631
|
return;
|
|
496
632
|
}
|
|
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
633
|
if (request.method === "ResumeBackgroundFollow") {
|
|
507
|
-
|
|
634
|
+
try {
|
|
635
|
+
await withTimeout(resumeBackgroundFollow(), BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS, BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR);
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
if (error instanceof Error
|
|
639
|
+
&& error.message === BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR) {
|
|
640
|
+
await recordBackgroundFollowFailure(error.message).catch(() => undefined);
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
508
644
|
writeResponse({
|
|
509
645
|
id: request.id,
|
|
510
646
|
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
|
}
|
|
@@ -90,6 +95,7 @@ export declare function stopIndexerDaemonServiceWithLockHeld(options: {
|
|
|
90
95
|
walletRootId?: string;
|
|
91
96
|
shutdownTimeoutMs?: number;
|
|
92
97
|
paths?: ReturnType<typeof resolveManagedServicePaths>;
|
|
98
|
+
processId?: number | null;
|
|
93
99
|
}): Promise<IndexerDaemonStopResult>;
|
|
94
100
|
export declare function probeIndexerDaemon(options: {
|
|
95
101
|
dataDir: string;
|
|
@@ -107,6 +113,8 @@ export declare function attachOrStartIndexerDaemon(options: {
|
|
|
107
113
|
startupTimeoutMs?: number;
|
|
108
114
|
shutdownTimeoutMs?: number;
|
|
109
115
|
serviceLifetime?: ManagedIndexerDaemonServiceLifetime;
|
|
116
|
+
ensureBackgroundFollow?: boolean;
|
|
117
|
+
expectedBinaryVersion?: string | null;
|
|
110
118
|
}): Promise<IndexerDaemonClient>;
|
|
111
119
|
export declare function stopIndexerDaemonService(options: {
|
|
112
120
|
dataDir: string;
|