@cogcoin/client 1.2.4 → 1.2.5
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 +8 -2
- package/dist/bitcoind/managed-bitcoind-service-config.d.ts +5 -1
- package/dist/bitcoind/managed-bitcoind-service-config.js +27 -18
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +3 -1
- package/dist/bitcoind/managed-bitcoind-service-status.d.ts +9 -2
- package/dist/bitcoind/managed-bitcoind-service-status.js +60 -7
- package/dist/bitcoind/managed-bitcoind-service-types.d.ts +8 -0
- package/dist/cli/command-registry.js +2 -2
- package/dist/cli/commands/service-runtime.js +22 -2
- package/dist/cli/commands/status.js +7 -1
- package/dist/cli/context.js +2 -0
- package/dist/cli/output/rules/cli-surface.js +7 -0
- package/dist/cli/parse.js +9 -0
- package/dist/cli/status-format.d.ts +2 -2
- package/dist/cli/status-format.js +167 -28
- package/dist/cli/types.d.ts +3 -0
- package/dist/passive-status.d.ts +49 -1
- package/dist/passive-status.js +208 -2
- package/dist/wallet/lifecycle/repair-bitcoind.js +5 -1
- package/dist/wallet/mining/competitiveness.js +2 -2
- package/dist/wallet/mining/mempool-index.js +29 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.2.
|
|
3
|
+
`@cogcoin/client@1.2.5` 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
|
|
|
@@ -126,6 +126,10 @@ The published package depends on:
|
|
|
126
126
|
|
|
127
127
|
`@cogcoin/vectors@1.0.1` is kept as a repository development dependency for conformance tests and is not part of the published runtime dependency surface.
|
|
128
128
|
|
|
129
|
+
## Upgrade Notes For `1.2.5`
|
|
130
|
+
|
|
131
|
+
`@cogcoin/client@1.2.5` keeps managed Bitcoin runtime settings in `bitcoin.conf` instead of duplicating RPC, P2P, wallet, and ZMQ settings on the `bitcoind` command line. This prevents duplicate `pubhashblock` and `pubrawtx` ZMQ registrations. Managed startup, repair, `bitcoin status`, and `status --live` also refresh runtime status after successful RPC/ZMQ checks so a stale `starting` status does not remain after Core is ready.
|
|
132
|
+
|
|
129
133
|
## Upgrade Notes For `1.2.4`
|
|
130
134
|
|
|
131
135
|
`@cogcoin/client@1.2.4` treats a live managed Bitcoin Core process returning startup `-28 Loading block index...` as a normal `starting` state. `cogcoin mine` waits instead of repeatedly restarting the process, and `cogcoin repair` prints progress during long checks. If repair restarts a stale managed bitcoind to add raw transaction ZMQ support and Core is still loading, repair exits with a clear note to wait and rerun `cogcoin status` or `cogcoin mine`.
|
|
@@ -173,10 +177,12 @@ The installed `cogcoin` command covers the first-party local wallet and node wor
|
|
|
173
177
|
- COG and reputation commands such as `send`, `cog lock`, `claim`, `reclaim`, `rep give`, and `rep revoke`
|
|
174
178
|
- mining commands such as `mine`, `mine status`, `mine log`, `mine setup`, `mine prompt`, and `mine prompt list`
|
|
175
179
|
|
|
180
|
+
`cogcoin status` is passive by default: it reads local SQLite/runtime status files without prompting for the client password, starting services, or making Bitcoin RPC calls. Use `cogcoin status --live` when you want the full RPC-backed wallet overview and balance report.
|
|
181
|
+
|
|
176
182
|
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.
|
|
177
183
|
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.
|
|
178
184
|
Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
|
|
179
|
-
Ordinary `sync`, `follow`, and wallet-aware read
|
|
185
|
+
Ordinary `sync`, `follow`, and wallet-aware live read flows detach from the managed Bitcoin and indexer services on exit instead of stopping them.
|
|
180
186
|
Use the explicit `bitcoin ...` and `indexer ...` commands when you want direct service inspection or start/stop control.
|
|
181
187
|
For provider-backed local wallets, normal reads, mutations, and mining setup flows load local wallet state on demand whenever the local secret provider is available.
|
|
182
188
|
When no wallet exists yet, `cogcoin init` interactively lets you either create a new wallet or restore an existing one from a 24-word English BIP39 mnemonic, then continues into sync.
|
|
@@ -26,5 +26,9 @@ export declare function writeManagedBitcoindRuntimeConfigFile(filePath: string,
|
|
|
26
26
|
export declare function writeManagedBitcoindRuntimeConfigFileFromStatus(filePath: string, status: Pick<ManagedBitcoindServiceStatus, "chain" | "rpc" | "zmq" | "p2pPort" | "getblockArchiveEndHeight" | "getblockArchiveSha256">, dependencies?: ManagedBitcoindRuntimeConfigFileDeps): Promise<void>;
|
|
27
27
|
export declare function writeBitcoinConfForTesting(filePath: string, options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): Promise<void>;
|
|
28
28
|
export declare function buildManagedServiceArgsForTesting(options: ManagedBitcoindServiceOptions, runtimeConfig: ManagedBitcoindRuntimeConfig): string[];
|
|
29
|
-
export declare function waitForManagedBitcoindCookie(cookieFile: string, timeoutMs: number, sleepImpl: (ms: number) => Promise<void
|
|
29
|
+
export declare function waitForManagedBitcoindCookie(cookieFile: string, timeoutMs: number, sleepImpl: (ms: number) => Promise<void>, options?: {
|
|
30
|
+
now?: () => number;
|
|
31
|
+
progressIntervalMs?: number;
|
|
32
|
+
onProgress?: (elapsedMs: number) => void | Promise<void>;
|
|
33
|
+
}): Promise<void>;
|
|
30
34
|
export type { BitcoindRpcConfig, BitcoindZmqConfig, };
|
|
@@ -147,12 +147,14 @@ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfi
|
|
|
147
147
|
const walletDir = join(options.dataDir ?? "", "wallets");
|
|
148
148
|
await mkdir(dirname(filePath), { recursive: true });
|
|
149
149
|
await mkdir(walletDir, { recursive: true });
|
|
150
|
-
const
|
|
150
|
+
const commonLines = [
|
|
151
151
|
"server=1",
|
|
152
152
|
"prune=0",
|
|
153
|
+
`dbcache=${runtimeConfig.dbcacheMiB}`,
|
|
154
|
+
];
|
|
155
|
+
const networkLines = [
|
|
153
156
|
"dnsseed=1",
|
|
154
157
|
"listen=0",
|
|
155
|
-
`dbcache=${runtimeConfig.dbcacheMiB}`,
|
|
156
158
|
`rpcbind=${LOCAL_HOST}`,
|
|
157
159
|
`rpcallowip=${LOCAL_HOST}`,
|
|
158
160
|
`rpcport=${runtimeConfig.rpc.port}`,
|
|
@@ -161,25 +163,23 @@ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfi
|
|
|
161
163
|
`zmqpubrawtx=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
|
|
162
164
|
`walletdir=${walletDir}`,
|
|
163
165
|
];
|
|
166
|
+
const lines = options.chain === "regtest"
|
|
167
|
+
? [
|
|
168
|
+
...commonLines,
|
|
169
|
+
"",
|
|
170
|
+
"[regtest]",
|
|
171
|
+
...networkLines,
|
|
172
|
+
]
|
|
173
|
+
: [
|
|
174
|
+
...commonLines,
|
|
175
|
+
...networkLines,
|
|
176
|
+
];
|
|
164
177
|
await writeFileAtomic(filePath, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
165
178
|
}
|
|
166
179
|
export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
|
|
167
|
-
const walletDir = join(options.dataDir ?? "", "wallets");
|
|
168
180
|
const args = [
|
|
169
181
|
"-nosettings=1",
|
|
170
182
|
`-datadir=${options.dataDir}`,
|
|
171
|
-
`-rpcbind=${LOCAL_HOST}`,
|
|
172
|
-
`-rpcallowip=${LOCAL_HOST}`,
|
|
173
|
-
`-rpcport=${runtimeConfig.rpc.port}`,
|
|
174
|
-
`-port=${runtimeConfig.p2pPort}`,
|
|
175
|
-
`-zmqpubhashblock=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
|
|
176
|
-
`-zmqpubrawtx=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
|
|
177
|
-
`-walletdir=${walletDir}`,
|
|
178
|
-
"-server=1",
|
|
179
|
-
"-prune=0",
|
|
180
|
-
"-dnsseed=1",
|
|
181
|
-
"-listen=0",
|
|
182
|
-
`-dbcache=${runtimeConfig.dbcacheMiB}`,
|
|
183
183
|
];
|
|
184
184
|
if (options.chain === "regtest") {
|
|
185
185
|
args.push("-chain=regtest");
|
|
@@ -189,14 +189,23 @@ export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
|
|
|
189
189
|
}
|
|
190
190
|
return args;
|
|
191
191
|
}
|
|
192
|
-
export async function waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleepImpl) {
|
|
193
|
-
const
|
|
194
|
-
|
|
192
|
+
export async function waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleepImpl, options = {}) {
|
|
193
|
+
const now = options.now ?? Date.now;
|
|
194
|
+
const progressIntervalMs = options.progressIntervalMs ?? 30_000;
|
|
195
|
+
const startedAt = now();
|
|
196
|
+
const deadline = startedAt + timeoutMs;
|
|
197
|
+
let lastProgressAt = startedAt;
|
|
198
|
+
while (now() < deadline) {
|
|
195
199
|
try {
|
|
196
200
|
await access(cookieFile, constants.R_OK);
|
|
197
201
|
return;
|
|
198
202
|
}
|
|
199
203
|
catch {
|
|
204
|
+
const currentTime = now();
|
|
205
|
+
if (currentTime - lastProgressAt >= progressIntervalMs) {
|
|
206
|
+
await options.onProgress?.(Math.max(0, currentTime - startedAt));
|
|
207
|
+
lastProgressAt = currentTime;
|
|
208
|
+
}
|
|
200
209
|
await sleepImpl(250);
|
|
201
210
|
}
|
|
202
211
|
}
|
|
@@ -218,7 +218,9 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
218
218
|
await writeManagedBitcoindRuntimeConfigFile(paths.bitcoindRuntimeConfigPath, runtimeConfig);
|
|
219
219
|
await writeManagedBitcoindStatus(paths, startingStatus);
|
|
220
220
|
try {
|
|
221
|
-
await waitForManagedBitcoindRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs
|
|
221
|
+
await waitForManagedBitcoindRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs, {
|
|
222
|
+
progress: startOptions.rpcReadyProgress,
|
|
223
|
+
});
|
|
222
224
|
await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint, {
|
|
223
225
|
requireRawTxZmq: true,
|
|
224
226
|
});
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { createRpcClient } from "./node.js";
|
|
2
2
|
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
3
3
|
import { type ManagedBitcoindObservedStatus, type ManagedBitcoindNodeHandle, type ManagedBitcoindServiceStatus, type ManagedCoreWalletReplicaStatus } from "./types.js";
|
|
4
|
-
import type { ManagedBitcoindServiceOwnership, ManagedBitcoindServiceOptions, ManagedBitcoindServiceStopResult } from "./managed-bitcoind-service-types.js";
|
|
4
|
+
import type { ManagedBitcoindRpcReadyProgressReporter, ManagedBitcoindServiceOwnership, ManagedBitcoindServiceOptions, ManagedBitcoindServiceStopResult } from "./managed-bitcoind-service-types.js";
|
|
5
5
|
import { type BitcoindRpcConfig, type BitcoindZmqConfig } from "./managed-bitcoind-service-config.js";
|
|
6
6
|
import type { ManagedBitcoindServiceProbeResult } from "./managed-runtime/types.js";
|
|
7
|
-
|
|
7
|
+
interface WaitForManagedBitcoindRpcReadyOptions {
|
|
8
|
+
progress?: ManagedBitcoindRpcReadyProgressReporter;
|
|
9
|
+
progressIntervalMs?: number;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
sleep?: (ms: number) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function waitForManagedBitcoindRpcReady(rpc: ReturnType<typeof createRpcClient>, cookieFile: string, expectedChain: "main" | "regtest", timeoutMs: number, options?: WaitForManagedBitcoindRpcReadyOptions): Promise<void>;
|
|
8
14
|
export declare function createBitcoindServiceStatus(options: {
|
|
9
15
|
binaryVersion: string;
|
|
10
16
|
serviceInstanceId: string;
|
|
@@ -40,3 +46,4 @@ export declare function createManagedBitcoindNodeHandle(options: {
|
|
|
40
46
|
shutdownTimeoutMs?: number;
|
|
41
47
|
}): Promise<ManagedBitcoindServiceStopResult>;
|
|
42
48
|
}): ManagedBitcoindNodeHandle;
|
|
49
|
+
export {};
|
|
@@ -9,21 +9,70 @@ import { createMissingManagedWalletReplicaStatus, loadManagedWalletReplicaIfPres
|
|
|
9
9
|
import { DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS, isManagedBitcoindProcessAlive, sleep, } from "./managed-bitcoind-service-process.js";
|
|
10
10
|
import { waitForManagedBitcoindCookie, } from "./managed-bitcoind-service-config.js";
|
|
11
11
|
import { isManagedRpcWarmupError } from "./retryable-rpc.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
function describeRpcReadyError(error) {
|
|
13
|
+
if (!(error instanceof Error)) {
|
|
14
|
+
return String(error);
|
|
15
|
+
}
|
|
16
|
+
const warmupMatch = /^bitcoind_rpc_[^_]+_-28_(.+)$/u.exec(error.message);
|
|
17
|
+
if (warmupMatch !== null) {
|
|
18
|
+
return warmupMatch[1].replaceAll("_", " ");
|
|
19
|
+
}
|
|
20
|
+
return error.message;
|
|
21
|
+
}
|
|
22
|
+
export async function waitForManagedBitcoindRpcReady(rpc, cookieFile, expectedChain, timeoutMs, options = {}) {
|
|
23
|
+
const sleepImpl = options.sleep ?? sleep;
|
|
24
|
+
const now = options.now ?? Date.now;
|
|
25
|
+
const progressIntervalMs = options.progressIntervalMs ?? 30_000;
|
|
26
|
+
const startedAt = now();
|
|
27
|
+
await options.progress?.({
|
|
28
|
+
code: "bitcoind-rpc-wait",
|
|
29
|
+
message: "Waiting for Bitcoin Core RPC readiness...",
|
|
30
|
+
elapsedMs: 0,
|
|
31
|
+
lastError: null,
|
|
32
|
+
});
|
|
33
|
+
await waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleepImpl, {
|
|
34
|
+
now,
|
|
35
|
+
progressIntervalMs,
|
|
36
|
+
onProgress: async (elapsedMs) => {
|
|
37
|
+
await options.progress?.({
|
|
38
|
+
code: "bitcoind-rpc-wait-progress",
|
|
39
|
+
message: `Still waiting for Bitcoin Core RPC readiness (${Math.floor(elapsedMs / 1000)}s elapsed). Last RPC state: cookie not ready.`,
|
|
40
|
+
elapsedMs,
|
|
41
|
+
lastError: "cookie not ready",
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const deadline = now() + timeoutMs;
|
|
15
46
|
let lastError = null;
|
|
16
|
-
|
|
47
|
+
let lastProgressAt = startedAt;
|
|
48
|
+
while (now() < deadline) {
|
|
17
49
|
try {
|
|
18
50
|
const info = await rpc.getBlockchainInfo();
|
|
19
51
|
if (info.chain !== expectedChain) {
|
|
20
52
|
throw new Error(`bitcoind_chain_expected_${expectedChain}_got_${info.chain}`);
|
|
21
53
|
}
|
|
54
|
+
await options.progress?.({
|
|
55
|
+
code: "bitcoind-rpc-ready",
|
|
56
|
+
message: "Bitcoin Core RPC is ready.",
|
|
57
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
58
|
+
lastError: null,
|
|
59
|
+
});
|
|
22
60
|
return;
|
|
23
61
|
}
|
|
24
62
|
catch (error) {
|
|
25
63
|
lastError = error;
|
|
26
|
-
|
|
64
|
+
const currentTime = now();
|
|
65
|
+
if (currentTime - lastProgressAt >= progressIntervalMs) {
|
|
66
|
+
const lastErrorText = describeRpcReadyError(lastError);
|
|
67
|
+
await options.progress?.({
|
|
68
|
+
code: "bitcoind-rpc-wait-progress",
|
|
69
|
+
message: `Still waiting for Bitcoin Core RPC readiness (${Math.floor((currentTime - startedAt) / 1000)}s elapsed). Last RPC state: ${lastErrorText}.`,
|
|
70
|
+
elapsedMs: Math.max(0, currentTime - startedAt),
|
|
71
|
+
lastError: lastErrorText,
|
|
72
|
+
});
|
|
73
|
+
lastProgressAt = currentTime;
|
|
74
|
+
}
|
|
75
|
+
await sleepImpl(250);
|
|
27
76
|
}
|
|
28
77
|
}
|
|
29
78
|
throw lastError instanceof Error ? lastError : new Error("bitcoind_rpc_timeout");
|
|
@@ -66,7 +115,9 @@ export async function probeManagedBitcoindStatusCandidate(status, options, runti
|
|
|
66
115
|
}
|
|
67
116
|
const rpc = createRpcClient(status.rpc);
|
|
68
117
|
try {
|
|
69
|
-
await waitForManagedBitcoindRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS
|
|
118
|
+
await waitForManagedBitcoindRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS, {
|
|
119
|
+
progress: options.rpcReadyProgress,
|
|
120
|
+
});
|
|
70
121
|
await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint, {
|
|
71
122
|
requireRawTxZmq: true,
|
|
72
123
|
});
|
|
@@ -98,7 +149,9 @@ export async function refreshManagedBitcoindStatus(status, paths, options) {
|
|
|
98
149
|
const rpc = createRpcClient(status.rpc);
|
|
99
150
|
const targetWalletRootId = options.walletRootId ?? status.walletRootId;
|
|
100
151
|
try {
|
|
101
|
-
await waitForManagedBitcoindRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS
|
|
152
|
+
await waitForManagedBitcoindRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS, {
|
|
153
|
+
progress: options.rpcReadyProgress,
|
|
154
|
+
});
|
|
102
155
|
await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint, {
|
|
103
156
|
requireRawTxZmq: true,
|
|
104
157
|
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { InternalManagedBitcoindOptions } from "./types.js";
|
|
2
|
+
export interface ManagedBitcoindRpcReadyProgressEvent {
|
|
3
|
+
code: "bitcoind-rpc-wait" | "bitcoind-rpc-wait-progress" | "bitcoind-rpc-ready";
|
|
4
|
+
message: string;
|
|
5
|
+
elapsedMs: number;
|
|
6
|
+
lastError: string | null;
|
|
7
|
+
}
|
|
8
|
+
export type ManagedBitcoindRpcReadyProgressReporter = (event: ManagedBitcoindRpcReadyProgressEvent) => void | Promise<void>;
|
|
2
9
|
export interface ManagedWalletReplicaRpc {
|
|
3
10
|
listWallets(): Promise<string[]>;
|
|
4
11
|
loadWallet(walletName: string, loadOnStartup?: boolean): Promise<{
|
|
@@ -23,6 +30,7 @@ export type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions,
|
|
|
23
30
|
getblockArchiveEndHeight?: number | null;
|
|
24
31
|
getblockArchiveSha256?: string | null;
|
|
25
32
|
serviceLifetime?: "persistent" | "ephemeral";
|
|
33
|
+
rpcReadyProgress?: ManagedBitcoindRpcReadyProgressReporter;
|
|
26
34
|
};
|
|
27
35
|
export type ResolvedManagedBitcoindServiceOptions = ManagedBitcoindServiceOptions & {
|
|
28
36
|
dataDir: string;
|
|
@@ -7,8 +7,8 @@ const commandSpecs = [
|
|
|
7
7
|
aliases: [{ tokens: ["status"] }],
|
|
8
8
|
helpEntries: [
|
|
9
9
|
{
|
|
10
|
-
usage: "status",
|
|
11
|
-
description: "Show
|
|
10
|
+
usage: "status [--live]",
|
|
11
|
+
description: "Show passive local status; use --live for RPC-backed wallet balance",
|
|
12
12
|
},
|
|
13
13
|
],
|
|
14
14
|
describeCommand() {
|
|
@@ -44,19 +44,33 @@ async function resolveEffectiveWalletRootId(context) {
|
|
|
44
44
|
source: "default-uninitialized",
|
|
45
45
|
}));
|
|
46
46
|
}
|
|
47
|
+
function createRpcReadyProgressReporter(context) {
|
|
48
|
+
return async (event) => {
|
|
49
|
+
writeLine(context.stdout, event.message);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
47
52
|
async function inspectManagedBitcoindStatus(dataDir, context) {
|
|
48
53
|
const resolution = await resolveEffectiveWalletRootId(context);
|
|
54
|
+
const rpcReadyProgress = createRpcReadyProgressReporter(context);
|
|
49
55
|
const probe = await context.probeManagedBitcoindService({
|
|
50
56
|
dataDir,
|
|
51
57
|
chain: "main",
|
|
52
58
|
startHeight: 0,
|
|
53
59
|
walletRootId: resolution.walletRootId,
|
|
60
|
+
rpcReadyProgress,
|
|
54
61
|
});
|
|
55
62
|
let node = null;
|
|
56
63
|
let nodeError = null;
|
|
64
|
+
let service = probe.status;
|
|
57
65
|
if (probe.compatibility === "compatible" && probe.status !== null) {
|
|
58
66
|
try {
|
|
59
|
-
|
|
67
|
+
service = await context.refreshManagedBitcoindServiceStatus(probe.status, resolveManagedServicePaths(dataDir, resolution.walletRootId), {
|
|
68
|
+
dataDir,
|
|
69
|
+
chain: "main",
|
|
70
|
+
startHeight: 0,
|
|
71
|
+
walletRootId: resolution.walletRootId,
|
|
72
|
+
});
|
|
73
|
+
const rpc = context.createBitcoinRpcClient(service.rpc);
|
|
60
74
|
const [blockchainInfo, networkInfo] = await Promise.all([
|
|
61
75
|
rpc.getBlockchainInfo(),
|
|
62
76
|
rpc.getNetworkInfo(),
|
|
@@ -82,7 +96,7 @@ async function inspectManagedBitcoindStatus(dataDir, context) {
|
|
|
82
96
|
walletRootId: resolution.walletRootId,
|
|
83
97
|
walletRootSource: resolution.source,
|
|
84
98
|
compatibility: probe.compatibility,
|
|
85
|
-
service
|
|
99
|
+
service,
|
|
86
100
|
node,
|
|
87
101
|
nodeError,
|
|
88
102
|
};
|
|
@@ -304,11 +318,13 @@ export async function runServiceRuntimeCommand(parsed, context) {
|
|
|
304
318
|
}
|
|
305
319
|
if (parsed.command === "bitcoin-start") {
|
|
306
320
|
const resolution = await resolveEffectiveWalletRootId(context);
|
|
321
|
+
const rpcReadyProgress = createRpcReadyProgressReporter(context);
|
|
307
322
|
const probe = await context.probeManagedBitcoindService({
|
|
308
323
|
dataDir,
|
|
309
324
|
chain: "main",
|
|
310
325
|
startHeight: 0,
|
|
311
326
|
walletRootId: resolution.walletRootId,
|
|
327
|
+
rpcReadyProgress,
|
|
312
328
|
});
|
|
313
329
|
const genesis = await loadBundledGenesisParameters();
|
|
314
330
|
await context.attachManagedBitcoindService({
|
|
@@ -316,6 +332,7 @@ export async function runServiceRuntimeCommand(parsed, context) {
|
|
|
316
332
|
chain: "main",
|
|
317
333
|
startHeight: resolveCogcoinProcessingStartHeight(genesis),
|
|
318
334
|
walletRootId: resolution.walletRootId,
|
|
335
|
+
rpcReadyProgress: probe.compatibility === "compatible" ? undefined : rpcReadyProgress,
|
|
319
336
|
});
|
|
320
337
|
const bitcoindStatus = probe.compatibility === "compatible" ? "already-running" : "started";
|
|
321
338
|
const payload = {
|
|
@@ -356,17 +373,20 @@ export async function runServiceRuntimeCommand(parsed, context) {
|
|
|
356
373
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
357
374
|
await context.ensureDirectory(dirname(dbPath));
|
|
358
375
|
const genesis = await loadBundledGenesisParameters();
|
|
376
|
+
const rpcReadyProgress = createRpcReadyProgressReporter(context);
|
|
359
377
|
const bitcoindProbe = await context.probeManagedBitcoindService({
|
|
360
378
|
dataDir,
|
|
361
379
|
chain: "main",
|
|
362
380
|
startHeight: 0,
|
|
363
381
|
walletRootId: resolution.walletRootId,
|
|
382
|
+
rpcReadyProgress,
|
|
364
383
|
});
|
|
365
384
|
await context.attachManagedBitcoindService({
|
|
366
385
|
dataDir,
|
|
367
386
|
chain: "main",
|
|
368
387
|
startHeight: resolveCogcoinProcessingStartHeight(genesis),
|
|
369
388
|
walletRootId: resolution.walletRootId,
|
|
389
|
+
rpcReadyProgress: bitcoindProbe.compatibility === "compatible" ? undefined : rpcReadyProgress,
|
|
370
390
|
});
|
|
371
391
|
const indexerProbe = await context.probeIndexerDaemon({
|
|
372
392
|
dataDir,
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { formatBalanceReport, formatWalletOverviewReport } from "../wallet-format.js";
|
|
3
|
+
import { formatStatusReport } from "../status-format.js";
|
|
3
4
|
import { writeLine } from "../io.js";
|
|
4
5
|
import { createTerminalPrompter } from "../prompt.js";
|
|
5
6
|
import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
|
|
6
7
|
export async function runStatusCommand(parsed, context) {
|
|
7
8
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
8
9
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
9
|
-
const packageVersion = await context.readPackageVersion();
|
|
10
10
|
const runtimePaths = context.resolveWalletRuntimePaths();
|
|
11
|
+
const packageVersion = await context.readPackageVersion();
|
|
12
|
+
if (!parsed.statusLive) {
|
|
13
|
+
const status = await context.inspectPassiveClientStatus(dbPath, dataDir, runtimePaths);
|
|
14
|
+
writeLine(context.stdout, formatStatusReport(status, packageVersion));
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
11
17
|
await context.ensureDirectory(dirname(dbPath));
|
|
12
18
|
const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, context.createPrompter?.() ?? createTerminalPrompter(context.stdin, context.stdout));
|
|
13
19
|
const readContext = await context.openWalletReadContext({
|
package/dist/cli/context.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { attachOrStartIndexerDaemon, probeIndexerDaemon, readObservedIndexerDaemonStatus, stopIndexerDaemonService, } from "../bitcoind/indexer-daemon.js";
|
|
4
4
|
import { createRpcClient } from "../bitcoind/node.js";
|
|
5
5
|
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../bitcoind/service.js";
|
|
6
|
+
import { refreshManagedBitcoindStatus } from "../bitcoind/managed-bitcoind-service-status.js";
|
|
6
7
|
import { resolveDefaultBitcoindDataDirForTesting, resolveDefaultClientDatabasePathForTesting, resolveDefaultUpdateCheckStatePathForTesting, } from "../app-paths.js";
|
|
7
8
|
import { openManagedBitcoindClient } from "../bitcoind/index.js";
|
|
8
9
|
import { openManagedIndexerMonitor } from "../bitcoind/indexer-monitor.js";
|
|
@@ -106,6 +107,7 @@ export function createDefaultContext(overrides = {}) {
|
|
|
106
107
|
}),
|
|
107
108
|
attachManagedBitcoindService: overrides.attachManagedBitcoindService ?? attachOrStartManagedBitcoindService,
|
|
108
109
|
probeManagedBitcoindService: overrides.probeManagedBitcoindService ?? probeManagedBitcoindService,
|
|
110
|
+
refreshManagedBitcoindServiceStatus: overrides.refreshManagedBitcoindServiceStatus ?? refreshManagedBitcoindStatus,
|
|
109
111
|
stopManagedBitcoindService: overrides.stopManagedBitcoindService ?? stopManagedBitcoindService,
|
|
110
112
|
createBitcoinRpcClient: overrides.createBitcoinRpcClient ?? createRpcClient,
|
|
111
113
|
attachIndexerDaemon: overrides.attachIndexerDaemon ?? attachOrStartIndexerDaemon,
|
|
@@ -77,6 +77,13 @@ export const cliSurfaceErrorRules = [
|
|
|
77
77
|
next: "Drop `--satvb` for this command, or use it with a wallet mutation command like `cogcoin register` or `cogcoin send`.",
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
if (errorCode === "cli_live_not_supported_for_command") {
|
|
81
|
+
return {
|
|
82
|
+
what: "This command does not support `--live`.",
|
|
83
|
+
why: "`--live` only applies to `cogcoin status`, where it opts into RPC-backed wallet and service checks.",
|
|
84
|
+
next: "Drop `--live`, or run `cogcoin status --live`.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
80
87
|
if (errorCode === "cli_anchor_clear_removed") {
|
|
81
88
|
return {
|
|
82
89
|
what: "`anchor clear` is no longer available.",
|
package/dist/cli/parse.js
CHANGED
|
@@ -37,6 +37,7 @@ export function parseCliArgs(argv) {
|
|
|
37
37
|
let listLimit = null;
|
|
38
38
|
let listAll = false;
|
|
39
39
|
let follow = false;
|
|
40
|
+
let statusLive = false;
|
|
40
41
|
for (let index = 0; index < argv.length; index += 1) {
|
|
41
42
|
const token = argv[index];
|
|
42
43
|
if (token === "--help") {
|
|
@@ -238,6 +239,10 @@ export function parseCliArgs(argv) {
|
|
|
238
239
|
follow = true;
|
|
239
240
|
continue;
|
|
240
241
|
}
|
|
242
|
+
if (token === "--live") {
|
|
243
|
+
statusLive = true;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
241
246
|
if (token === "--yes") {
|
|
242
247
|
assumeYes = true;
|
|
243
248
|
continue;
|
|
@@ -448,6 +453,9 @@ export function parseCliArgs(argv) {
|
|
|
448
453
|
if (follow && command !== "mine-log") {
|
|
449
454
|
throw new Error("cli_follow_not_supported_for_command");
|
|
450
455
|
}
|
|
456
|
+
if (statusLive && command !== "status") {
|
|
457
|
+
throw new Error("cli_live_not_supported_for_command");
|
|
458
|
+
}
|
|
451
459
|
if (command === "mine-log" && follow && (listAll || listLimit !== null)) {
|
|
452
460
|
throw new Error("cli_follow_limit_not_supported");
|
|
453
461
|
}
|
|
@@ -502,5 +510,6 @@ export function parseCliArgs(argv) {
|
|
|
502
510
|
listLimit,
|
|
503
511
|
listAll,
|
|
504
512
|
follow,
|
|
513
|
+
statusLive,
|
|
505
514
|
};
|
|
506
515
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function formatStatusReport(status:
|
|
1
|
+
import type { PassiveClientStatus } from "../passive-status.js";
|
|
2
|
+
export declare function formatStatusReport(status: PassiveClientStatus, version: string): string;
|
|
@@ -1,48 +1,187 @@
|
|
|
1
|
+
function row(ok, text) {
|
|
2
|
+
return { ok, text };
|
|
3
|
+
}
|
|
4
|
+
function formatMarker(ok) {
|
|
5
|
+
return ok ? "✓" : "✗";
|
|
6
|
+
}
|
|
7
|
+
function formatValue(value) {
|
|
8
|
+
return value === null || value === undefined || value === "" ? "none" : String(value);
|
|
9
|
+
}
|
|
10
|
+
function formatYesNo(value) {
|
|
11
|
+
return value ? "yes" : "no";
|
|
12
|
+
}
|
|
1
13
|
function formatBootstrapPercent(current, total) {
|
|
2
14
|
if (total <= 0) {
|
|
3
15
|
return "0.00";
|
|
4
16
|
}
|
|
5
17
|
return ((current / total) * 100).toFixed(2);
|
|
6
18
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
function formatSignedDelta(value) {
|
|
20
|
+
if (value > 0) {
|
|
21
|
+
return `+${value}`;
|
|
22
|
+
}
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
function formatSection(title, rows) {
|
|
26
|
+
return [
|
|
27
|
+
title,
|
|
28
|
+
...rows.map((entry) => `${formatMarker(entry.ok)} ${entry.text}`),
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
function buildPathsRows(status) {
|
|
32
|
+
return [
|
|
33
|
+
row(true, `DB path: ${status.dbPath}`),
|
|
34
|
+
row(true, `Bitcoin datadir: ${status.bitcoinDataDir}`),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
function buildWalletRows(status) {
|
|
38
|
+
const rows = [
|
|
39
|
+
row(status.wallet.walletRootId !== null && status.wallet.error === null, `Wallet root: ${status.wallet.walletRootId ?? "unknown"} (${status.wallet.source})`),
|
|
40
|
+
];
|
|
41
|
+
if (status.wallet.error !== null) {
|
|
42
|
+
rows.push(row(false, `Wallet root error: ${status.wallet.error}`));
|
|
43
|
+
}
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
function buildLocalStoreRows(status) {
|
|
47
|
+
const rows = [
|
|
48
|
+
row(status.storeExists, `Store exists: ${formatYesNo(status.storeExists)}`),
|
|
49
|
+
row(status.storeInitialized, `Store initialized: ${formatYesNo(status.storeInitialized)}`),
|
|
14
50
|
];
|
|
15
51
|
if (status.storeError !== null) {
|
|
16
|
-
|
|
52
|
+
rows.push(row(false, `Store error: ${status.storeError}`));
|
|
17
53
|
}
|
|
18
54
|
if (status.indexedTip === null) {
|
|
19
|
-
|
|
55
|
+
rows.push(row(false, "Indexed tip: none"));
|
|
20
56
|
}
|
|
21
57
|
else {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
58
|
+
rows.push(row(true, `Indexed tip height: ${status.indexedTip.height}`));
|
|
59
|
+
rows.push(row(true, `Indexed tip hash: ${status.indexedTip.blockHashHex}`));
|
|
60
|
+
rows.push(row(status.indexedTip.stateHashHex !== null, `Indexed tip state hash: ${formatValue(status.indexedTip.stateHashHex)}`));
|
|
25
61
|
}
|
|
26
62
|
if (status.latestCheckpoint === null) {
|
|
27
|
-
|
|
63
|
+
rows.push(row(false, "Latest checkpoint: none"));
|
|
28
64
|
}
|
|
29
65
|
else {
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
rows.push(row(true, `Latest checkpoint height: ${status.latestCheckpoint.height}`));
|
|
67
|
+
rows.push(row(true, `Latest checkpoint hash: ${status.latestCheckpoint.blockHashHex}`));
|
|
68
|
+
}
|
|
69
|
+
if (status.indexedTip !== null && status.indexer.appliedTipHeight !== null) {
|
|
70
|
+
const delta = status.indexedTip.height - status.indexer.appliedTipHeight;
|
|
71
|
+
rows.push(row(Math.abs(delta) <= 1, `Store/indexer height delta: ${formatSignedDelta(delta)}`));
|
|
32
72
|
}
|
|
73
|
+
return rows;
|
|
74
|
+
}
|
|
75
|
+
function buildBootstrapRows(status) {
|
|
33
76
|
if (status.bootstrap === null) {
|
|
34
|
-
|
|
77
|
+
return [row(false, "Bootstrap state: none")];
|
|
35
78
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
79
|
+
return [
|
|
80
|
+
row(status.bootstrap.lastError === null, `Bootstrap phase: ${status.bootstrap.phase}`),
|
|
81
|
+
row(status.bootstrap.lastError === null, `Bootstrap download: ${status.bootstrap.downloadedBytes} / ${status.bootstrap.totalBytes} bytes (${formatBootstrapPercent(status.bootstrap.downloadedBytes, status.bootstrap.totalBytes)}%)`),
|
|
82
|
+
row(status.bootstrap.validated, `Bootstrap validated: ${formatYesNo(status.bootstrap.validated)}`),
|
|
83
|
+
row(status.bootstrap.loadTxOutSetComplete, `Bootstrap loaded: ${formatYesNo(status.bootstrap.loadTxOutSetComplete)}`),
|
|
84
|
+
row(status.bootstrap.baseHeight !== null, `Bootstrap base height: ${formatValue(status.bootstrap.baseHeight)}`),
|
|
85
|
+
row(status.bootstrap.tipHashHex !== null, `Bootstrap tip hash: ${formatValue(status.bootstrap.tipHashHex)}`),
|
|
86
|
+
row(status.bootstrap.snapshotHeight !== null, `Bootstrap snapshot height: ${formatValue(status.bootstrap.snapshotHeight)}`),
|
|
87
|
+
row(status.bootstrap.lastError === null, `Bootstrap last error: ${formatValue(status.bootstrap.lastError)}`),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function buildManagedBitcoindRows(status) {
|
|
91
|
+
if (status.managedBitcoind.error !== null) {
|
|
92
|
+
return [
|
|
93
|
+
row(false, "Managed bitcoind: corrupt"),
|
|
94
|
+
row(false, `Managed bitcoind status path: ${formatValue(status.managedBitcoind.statusPath)}`),
|
|
95
|
+
row(false, `Managed bitcoind status error: ${status.managedBitcoind.error}`),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
if (!status.managedBitcoind.present) {
|
|
99
|
+
return [row(false, "Managed bitcoind: unavailable")];
|
|
100
|
+
}
|
|
101
|
+
return [
|
|
102
|
+
row(status.managedBitcoind.state === "ready", `Managed bitcoind: ${formatValue(status.managedBitcoind.state)}`),
|
|
103
|
+
row(status.managedBitcoind.processId !== null, `Managed bitcoind pid: ${formatValue(status.managedBitcoind.processId)}`),
|
|
104
|
+
row(status.managedBitcoind.walletRootId !== null, `Managed bitcoind wallet root: ${formatValue(status.managedBitcoind.walletRootId)}`),
|
|
105
|
+
row(status.managedBitcoind.heartbeatAtUnixMs !== null, `Managed bitcoind heartbeat: ${formatValue(status.managedBitcoind.heartbeatAtUnixMs)}`),
|
|
106
|
+
row(status.managedBitcoind.updatedAtUnixMs !== null, `Managed bitcoind updated: ${formatValue(status.managedBitcoind.updatedAtUnixMs)}`),
|
|
107
|
+
row(status.managedBitcoind.lastError === null, `Managed bitcoind last error: ${formatValue(status.managedBitcoind.lastError)}`),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
function buildIndexerRows(status) {
|
|
111
|
+
if (status.indexer.error !== null) {
|
|
112
|
+
return [
|
|
113
|
+
row(false, "Indexer: corrupt"),
|
|
114
|
+
row(false, `Indexer status path: ${formatValue(status.indexer.statusPath)}`),
|
|
115
|
+
row(false, `Indexer status error: ${status.indexer.error}`),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
if (!status.indexer.present) {
|
|
119
|
+
return [row(false, "Indexer: unavailable")];
|
|
120
|
+
}
|
|
121
|
+
const rows = [
|
|
122
|
+
row(status.indexer.state === "synced", `Indexer: ${formatValue(status.indexer.state)}`),
|
|
123
|
+
row(status.indexer.processId !== null, `Indexer pid: ${formatValue(status.indexer.processId)}`),
|
|
124
|
+
row(status.indexer.walletRootId !== null, `Indexer wallet root: ${formatValue(status.indexer.walletRootId)}`),
|
|
125
|
+
row(status.indexer.coreBestHeight !== null, `Indexer core best height: ${formatValue(status.indexer.coreBestHeight)}`),
|
|
126
|
+
row(status.indexer.appliedTipHeight !== null, `Indexer applied tip height: ${formatValue(status.indexer.appliedTipHeight)}`),
|
|
127
|
+
row(status.indexer.appliedTipHash !== null, `Indexer applied tip hash: ${formatValue(status.indexer.appliedTipHash)}`),
|
|
128
|
+
row(status.indexer.heartbeatAtUnixMs !== null, `Indexer heartbeat: ${formatValue(status.indexer.heartbeatAtUnixMs)}`),
|
|
129
|
+
row(status.indexer.updatedAtUnixMs !== null, `Indexer updated: ${formatValue(status.indexer.updatedAtUnixMs)}`),
|
|
130
|
+
row(status.indexer.lastError === null, `Indexer last error: ${formatValue(status.indexer.lastError)}`),
|
|
131
|
+
];
|
|
132
|
+
if (status.indexer.coreBestHeight !== null && status.indexer.appliedTipHeight !== null) {
|
|
133
|
+
const lag = Math.max(0, status.indexer.coreBestHeight - status.indexer.appliedTipHeight);
|
|
134
|
+
rows.push(row(lag === 0, `Indexer lag: ${lag} blocks`));
|
|
135
|
+
}
|
|
136
|
+
return rows;
|
|
137
|
+
}
|
|
138
|
+
function buildManagedServicesRows(status) {
|
|
139
|
+
return [
|
|
140
|
+
...buildManagedBitcoindRows(status),
|
|
141
|
+
...buildIndexerRows(status),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
function buildMiningRows(status) {
|
|
145
|
+
if (status.mining.error !== null) {
|
|
146
|
+
return [
|
|
147
|
+
row(false, "Mining state: corrupt"),
|
|
148
|
+
row(false, `Mining status path: ${formatValue(status.mining.statusPath)}`),
|
|
149
|
+
row(false, `Mining status error: ${status.mining.error}`),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
if (!status.mining.present) {
|
|
153
|
+
return [row(false, "Mining state: unavailable")];
|
|
154
|
+
}
|
|
155
|
+
const miningHasError = status.mining.lastError !== null;
|
|
156
|
+
const needsBackgroundWorker = status.mining.runMode === "background";
|
|
157
|
+
return [
|
|
158
|
+
row(!miningHasError, `Mining run mode: ${formatValue(status.mining.runMode)}`),
|
|
159
|
+
row(!miningHasError, `Mining state: ${formatValue(status.mining.miningState)}`),
|
|
160
|
+
row(!miningHasError, `Mining phase: ${formatValue(status.mining.currentPhase)}`),
|
|
161
|
+
row(!needsBackgroundWorker || status.mining.backgroundWorkerPid !== null, `Mining background worker pid: ${formatValue(status.mining.backgroundWorkerPid)}`),
|
|
162
|
+
row(!needsBackgroundWorker || status.mining.backgroundWorkerHealth !== null, `Mining background worker health: ${formatValue(status.mining.backgroundWorkerHealth)}`),
|
|
163
|
+
row(status.mining.updatedAtUnixMs !== null, `Mining updated: ${formatValue(status.mining.updatedAtUnixMs)}`),
|
|
164
|
+
row(!miningHasError, `Mining last error: ${formatValue(status.mining.lastError)}`),
|
|
165
|
+
row(status.mining.note === null, `Mining note: ${formatValue(status.mining.note)}`),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
function buildPassiveModeRows() {
|
|
169
|
+
return [
|
|
170
|
+
row(true, "Live node: not checked"),
|
|
171
|
+
row(true, "Password prompt: not required"),
|
|
172
|
+
row(true, "RPC-backed balance: not checked"),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
export function formatStatusReport(status, version) {
|
|
176
|
+
return [
|
|
177
|
+
`⛭ Cogcoin Status v${version} (passive) ⛭`,
|
|
178
|
+
formatSection("Paths", buildPathsRows(status)),
|
|
179
|
+
formatSection("Wallet", buildWalletRows(status)),
|
|
180
|
+
formatSection("Local Store", buildLocalStoreRows(status)),
|
|
181
|
+
formatSection("Bootstrap", buildBootstrapRows(status)),
|
|
182
|
+
formatSection("Managed Services", buildManagedServicesRows(status)),
|
|
183
|
+
formatSection("Mining", buildMiningRows(status)),
|
|
184
|
+
formatSection("Passive Mode", buildPassiveModeRows()),
|
|
185
|
+
"Run cogcoin status --live for RPC-backed balance and full service verification.",
|
|
186
|
+
].join("\n\n");
|
|
48
187
|
}
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createRpcClient } from "../bitcoind/node.js";
|
|
|
4
4
|
import type { ManagedBitcoindProgressEvent } from "../bitcoind/types.js";
|
|
5
5
|
import { attachOrStartIndexerDaemon, probeIndexerDaemon, readObservedIndexerDaemonStatus, stopIndexerDaemonService } from "../bitcoind/indexer-daemon.js";
|
|
6
6
|
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService } from "../bitcoind/service.js";
|
|
7
|
+
import type { refreshManagedBitcoindStatus } from "../bitcoind/managed-bitcoind-service-status.js";
|
|
7
8
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
8
9
|
import type { ClientStoreAdapter } from "../types.js";
|
|
9
10
|
import type { WalletRuntimePaths } from "../wallet/runtime.js";
|
|
@@ -64,6 +65,7 @@ export interface ParsedCliArgs {
|
|
|
64
65
|
listLimit: number | null;
|
|
65
66
|
listAll: boolean;
|
|
66
67
|
follow: boolean;
|
|
68
|
+
statusLive: boolean;
|
|
67
69
|
}
|
|
68
70
|
export interface ManagedClientLike {
|
|
69
71
|
syncToTip(): Promise<{
|
|
@@ -115,6 +117,7 @@ export interface CliRunnerContext {
|
|
|
115
117
|
openManagedIndexerMonitor?: typeof openManagedIndexerMonitor;
|
|
116
118
|
attachManagedBitcoindService?: typeof attachOrStartManagedBitcoindService;
|
|
117
119
|
probeManagedBitcoindService?: typeof probeManagedBitcoindService;
|
|
120
|
+
refreshManagedBitcoindServiceStatus?: typeof refreshManagedBitcoindStatus;
|
|
118
121
|
stopManagedBitcoindService?: typeof stopManagedBitcoindService;
|
|
119
122
|
createBitcoinRpcClient?: typeof createRpcClient;
|
|
120
123
|
attachIndexerDaemon?: typeof attachOrStartIndexerDaemon;
|
package/dist/passive-status.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WalletRuntimePaths } from "./wallet/runtime.js";
|
|
1
2
|
interface PassiveTipStatus {
|
|
2
3
|
height: number;
|
|
3
4
|
blockHashHex: string;
|
|
@@ -22,15 +23,62 @@ interface PassiveBootstrapStatus {
|
|
|
22
23
|
snapshotHeight: number | null;
|
|
23
24
|
updatedAt: number | null;
|
|
24
25
|
}
|
|
26
|
+
export interface PassiveWalletStatus {
|
|
27
|
+
walletRootId: string | null;
|
|
28
|
+
source: "wallet-state" | "none" | "unreadable";
|
|
29
|
+
error: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface PassiveManagedBitcoindStatus {
|
|
32
|
+
statusPath: string | null;
|
|
33
|
+
present: boolean;
|
|
34
|
+
state: string | null;
|
|
35
|
+
processId: number | null;
|
|
36
|
+
walletRootId: string | null;
|
|
37
|
+
heartbeatAtUnixMs: number | null;
|
|
38
|
+
updatedAtUnixMs: number | null;
|
|
39
|
+
lastError: string | null;
|
|
40
|
+
error: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface PassiveIndexerStatus {
|
|
43
|
+
statusPath: string | null;
|
|
44
|
+
present: boolean;
|
|
45
|
+
state: string | null;
|
|
46
|
+
processId: number | null;
|
|
47
|
+
walletRootId: string | null;
|
|
48
|
+
coreBestHeight: number | null;
|
|
49
|
+
appliedTipHeight: number | null;
|
|
50
|
+
appliedTipHash: string | null;
|
|
51
|
+
heartbeatAtUnixMs: number | null;
|
|
52
|
+
updatedAtUnixMs: number | null;
|
|
53
|
+
lastError: string | null;
|
|
54
|
+
error: string | null;
|
|
55
|
+
}
|
|
56
|
+
export interface PassiveMiningStatus {
|
|
57
|
+
statusPath: string | null;
|
|
58
|
+
present: boolean;
|
|
59
|
+
runMode: string | null;
|
|
60
|
+
miningState: string | null;
|
|
61
|
+
currentPhase: string | null;
|
|
62
|
+
backgroundWorkerPid: number | null;
|
|
63
|
+
backgroundWorkerHealth: string | null;
|
|
64
|
+
updatedAtUnixMs: number | null;
|
|
65
|
+
lastError: string | null;
|
|
66
|
+
note: string | null;
|
|
67
|
+
error: string | null;
|
|
68
|
+
}
|
|
25
69
|
export interface PassiveClientStatus {
|
|
26
70
|
dbPath: string;
|
|
27
71
|
bitcoinDataDir: string;
|
|
72
|
+
wallet: PassiveWalletStatus;
|
|
28
73
|
storeInitialized: boolean;
|
|
29
74
|
storeExists: boolean;
|
|
30
75
|
indexedTip: PassiveTipStatus | null;
|
|
31
76
|
latestCheckpoint: PassiveCheckpointStatus | null;
|
|
32
77
|
bootstrap: PassiveBootstrapStatus | null;
|
|
78
|
+
managedBitcoind: PassiveManagedBitcoindStatus;
|
|
79
|
+
indexer: PassiveIndexerStatus;
|
|
80
|
+
mining: PassiveMiningStatus;
|
|
33
81
|
storeError: string | null;
|
|
34
82
|
}
|
|
35
|
-
export declare function inspectPassiveClientStatus(dbPath: string, bitcoinDataDir: string): Promise<PassiveClientStatus>;
|
|
83
|
+
export declare function inspectPassiveClientStatus(dbPath: string, bitcoinDataDir: string, runtimePaths?: WalletRuntimePaths): Promise<PassiveClientStatus>;
|
|
36
84
|
export {};
|
package/dist/passive-status.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { resolveManagedServicePaths } from "./bitcoind/service-paths.js";
|
|
3
4
|
import { openReadonlySqliteDatabase } from "./sqlite/driver.js";
|
|
4
5
|
import { loadLatestCheckpoint } from "./sqlite/checkpoints.js";
|
|
5
6
|
import { loadTipMeta } from "./sqlite/tip-meta.js";
|
|
7
|
+
import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, } from "./wallet/state/storage.js";
|
|
6
8
|
function fileExists(path) {
|
|
7
9
|
return stat(path).then(() => true, () => false);
|
|
8
10
|
}
|
|
@@ -21,6 +23,193 @@ function readBootstrapState(raw) {
|
|
|
21
23
|
updatedAt: parsed.updatedAt ?? null,
|
|
22
24
|
};
|
|
23
25
|
}
|
|
26
|
+
function formatUnknownError(error) {
|
|
27
|
+
return error instanceof Error ? error.message : String(error);
|
|
28
|
+
}
|
|
29
|
+
function isMissingFileError(error) {
|
|
30
|
+
return error instanceof Error
|
|
31
|
+
&& "code" in error
|
|
32
|
+
&& error.code === "ENOENT";
|
|
33
|
+
}
|
|
34
|
+
async function inspectWalletStatus(runtimePaths) {
|
|
35
|
+
if (runtimePaths === undefined) {
|
|
36
|
+
return {
|
|
37
|
+
walletRootId: null,
|
|
38
|
+
source: "none",
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const raw = await loadRawWalletStateEnvelope({
|
|
44
|
+
primaryPath: runtimePaths.walletStatePath,
|
|
45
|
+
backupPath: runtimePaths.walletStateBackupPath,
|
|
46
|
+
});
|
|
47
|
+
if (raw === null) {
|
|
48
|
+
return {
|
|
49
|
+
walletRootId: null,
|
|
50
|
+
source: "none",
|
|
51
|
+
error: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
walletRootId: extractWalletRootIdHintFromWalletStateEnvelope(raw.envelope),
|
|
56
|
+
source: "wallet-state",
|
|
57
|
+
error: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
walletRootId: null,
|
|
63
|
+
source: "unreadable",
|
|
64
|
+
error: formatUnknownError(error),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function readRuntimeStatusFile(statusPath) {
|
|
69
|
+
if (statusPath === null) {
|
|
70
|
+
return {
|
|
71
|
+
status: null,
|
|
72
|
+
present: false,
|
|
73
|
+
error: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return {
|
|
78
|
+
status: JSON.parse(await readFile(statusPath, "utf8")),
|
|
79
|
+
present: true,
|
|
80
|
+
error: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (isMissingFileError(error)) {
|
|
85
|
+
return {
|
|
86
|
+
status: null,
|
|
87
|
+
present: false,
|
|
88
|
+
error: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: null,
|
|
93
|
+
present: true,
|
|
94
|
+
error: formatUnknownError(error),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function emptyManagedBitcoindStatus(statusPath, present, error) {
|
|
99
|
+
return {
|
|
100
|
+
statusPath,
|
|
101
|
+
present,
|
|
102
|
+
state: null,
|
|
103
|
+
processId: null,
|
|
104
|
+
walletRootId: null,
|
|
105
|
+
heartbeatAtUnixMs: null,
|
|
106
|
+
updatedAtUnixMs: null,
|
|
107
|
+
lastError: null,
|
|
108
|
+
error,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function emptyIndexerStatus(statusPath, present, error) {
|
|
112
|
+
return {
|
|
113
|
+
statusPath,
|
|
114
|
+
present,
|
|
115
|
+
state: null,
|
|
116
|
+
processId: null,
|
|
117
|
+
walletRootId: null,
|
|
118
|
+
coreBestHeight: null,
|
|
119
|
+
appliedTipHeight: null,
|
|
120
|
+
appliedTipHash: null,
|
|
121
|
+
heartbeatAtUnixMs: null,
|
|
122
|
+
updatedAtUnixMs: null,
|
|
123
|
+
lastError: null,
|
|
124
|
+
error,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function emptyMiningStatus(statusPath, present, error) {
|
|
128
|
+
return {
|
|
129
|
+
statusPath,
|
|
130
|
+
present,
|
|
131
|
+
runMode: null,
|
|
132
|
+
miningState: null,
|
|
133
|
+
currentPhase: null,
|
|
134
|
+
backgroundWorkerPid: null,
|
|
135
|
+
backgroundWorkerHealth: null,
|
|
136
|
+
updatedAtUnixMs: null,
|
|
137
|
+
lastError: null,
|
|
138
|
+
note: null,
|
|
139
|
+
error,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function resolvePassiveServiceStatusPaths(bitcoinDataDir, runtimePaths, walletRootId) {
|
|
143
|
+
if (walletRootId !== null) {
|
|
144
|
+
const servicePaths = resolveManagedServicePaths(bitcoinDataDir, walletRootId);
|
|
145
|
+
return {
|
|
146
|
+
bitcoindStatusPath: servicePaths.bitcoindStatusPath,
|
|
147
|
+
indexerStatusPath: servicePaths.indexerDaemonStatusPath,
|
|
148
|
+
miningStatusPath: runtimePaths?.miningStatusPath ?? null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
bitcoindStatusPath: runtimePaths?.bitcoindStatusPath ?? null,
|
|
153
|
+
indexerStatusPath: runtimePaths?.indexerStatusPath ?? null,
|
|
154
|
+
miningStatusPath: runtimePaths?.miningStatusPath ?? null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function inspectManagedBitcoindStatus(statusPath) {
|
|
158
|
+
const result = await readRuntimeStatusFile(statusPath);
|
|
159
|
+
if (result.status === null) {
|
|
160
|
+
return emptyManagedBitcoindStatus(statusPath, result.present, result.error);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
statusPath,
|
|
164
|
+
present: true,
|
|
165
|
+
state: result.status.state ?? null,
|
|
166
|
+
processId: result.status.processId ?? null,
|
|
167
|
+
walletRootId: result.status.walletRootId ?? null,
|
|
168
|
+
heartbeatAtUnixMs: result.status.heartbeatAtUnixMs ?? null,
|
|
169
|
+
updatedAtUnixMs: result.status.updatedAtUnixMs ?? null,
|
|
170
|
+
lastError: result.status.lastError ?? null,
|
|
171
|
+
error: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function inspectIndexerStatus(statusPath) {
|
|
175
|
+
const result = await readRuntimeStatusFile(statusPath);
|
|
176
|
+
if (result.status === null) {
|
|
177
|
+
return emptyIndexerStatus(statusPath, result.present, result.error);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
statusPath,
|
|
181
|
+
present: true,
|
|
182
|
+
state: result.status.state ?? null,
|
|
183
|
+
processId: result.status.processId ?? null,
|
|
184
|
+
walletRootId: result.status.walletRootId ?? null,
|
|
185
|
+
coreBestHeight: result.status.coreBestHeight ?? null,
|
|
186
|
+
appliedTipHeight: result.status.appliedTipHeight ?? null,
|
|
187
|
+
appliedTipHash: result.status.appliedTipHash ?? null,
|
|
188
|
+
heartbeatAtUnixMs: result.status.heartbeatAtUnixMs ?? null,
|
|
189
|
+
updatedAtUnixMs: result.status.updatedAtUnixMs ?? null,
|
|
190
|
+
lastError: result.status.lastError ?? null,
|
|
191
|
+
error: null,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function inspectMiningStatus(statusPath) {
|
|
195
|
+
const result = await readRuntimeStatusFile(statusPath);
|
|
196
|
+
if (result.status === null) {
|
|
197
|
+
return emptyMiningStatus(statusPath, result.present, result.error);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
statusPath,
|
|
201
|
+
present: true,
|
|
202
|
+
runMode: result.status.runMode ?? null,
|
|
203
|
+
miningState: result.status.miningState ?? null,
|
|
204
|
+
currentPhase: result.status.currentPhase ?? null,
|
|
205
|
+
backgroundWorkerPid: result.status.backgroundWorkerPid ?? null,
|
|
206
|
+
backgroundWorkerHealth: result.status.backgroundWorkerHealth ?? null,
|
|
207
|
+
updatedAtUnixMs: result.status.updatedAtUnixMs ?? null,
|
|
208
|
+
lastError: result.status.lastError ?? null,
|
|
209
|
+
note: result.status.note ?? null,
|
|
210
|
+
error: null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
24
213
|
async function inspectSqliteStore(dbPath) {
|
|
25
214
|
const database = await openReadonlySqliteDatabase(dbPath);
|
|
26
215
|
try {
|
|
@@ -50,9 +239,14 @@ async function inspectSqliteStore(dbPath) {
|
|
|
50
239
|
await database.close();
|
|
51
240
|
}
|
|
52
241
|
}
|
|
53
|
-
export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
|
|
242
|
+
export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir, runtimePaths) {
|
|
54
243
|
const storeExists = await fileExists(dbPath);
|
|
55
244
|
const bootstrapPath = join(bitcoinDataDir, "bootstrap", "state.json");
|
|
245
|
+
const wallet = await inspectWalletStatus(runtimePaths);
|
|
246
|
+
const statusPaths = resolvePassiveServiceStatusPaths(bitcoinDataDir, runtimePaths, wallet.walletRootId);
|
|
247
|
+
const managedBitcoind = await inspectManagedBitcoindStatus(statusPaths.bitcoindStatusPath);
|
|
248
|
+
const indexer = await inspectIndexerStatus(statusPaths.indexerStatusPath);
|
|
249
|
+
const mining = await inspectMiningStatus(statusPaths.miningStatusPath);
|
|
56
250
|
let bootstrap = null;
|
|
57
251
|
try {
|
|
58
252
|
bootstrap = readBootstrapState(await readFile(bootstrapPath, "utf8"));
|
|
@@ -64,11 +258,15 @@ export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
|
|
|
64
258
|
return {
|
|
65
259
|
dbPath,
|
|
66
260
|
bitcoinDataDir,
|
|
261
|
+
wallet,
|
|
67
262
|
storeInitialized: false,
|
|
68
263
|
storeExists: false,
|
|
69
264
|
indexedTip: null,
|
|
70
265
|
latestCheckpoint: null,
|
|
71
266
|
bootstrap,
|
|
267
|
+
managedBitcoind,
|
|
268
|
+
indexer,
|
|
269
|
+
mining,
|
|
72
270
|
storeError: null,
|
|
73
271
|
};
|
|
74
272
|
}
|
|
@@ -77,11 +275,15 @@ export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
|
|
|
77
275
|
return {
|
|
78
276
|
dbPath,
|
|
79
277
|
bitcoinDataDir,
|
|
278
|
+
wallet,
|
|
80
279
|
storeInitialized: store.storeInitialized,
|
|
81
280
|
storeExists: true,
|
|
82
281
|
indexedTip: store.indexedTip,
|
|
83
282
|
latestCheckpoint: store.latestCheckpoint,
|
|
84
283
|
bootstrap,
|
|
284
|
+
managedBitcoind,
|
|
285
|
+
indexer,
|
|
286
|
+
mining,
|
|
85
287
|
storeError: null,
|
|
86
288
|
};
|
|
87
289
|
}
|
|
@@ -89,12 +291,16 @@ export async function inspectPassiveClientStatus(dbPath, bitcoinDataDir) {
|
|
|
89
291
|
return {
|
|
90
292
|
dbPath,
|
|
91
293
|
bitcoinDataDir,
|
|
294
|
+
wallet,
|
|
92
295
|
storeInitialized: false,
|
|
93
296
|
storeExists: true,
|
|
94
297
|
indexedTip: null,
|
|
95
298
|
latestCheckpoint: null,
|
|
96
299
|
bootstrap,
|
|
97
|
-
|
|
300
|
+
managedBitcoind,
|
|
301
|
+
indexer,
|
|
302
|
+
mining,
|
|
303
|
+
storeError: formatUnknownError(error),
|
|
98
304
|
};
|
|
99
305
|
}
|
|
100
306
|
}
|
|
@@ -18,6 +18,9 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
18
18
|
error: null,
|
|
19
19
|
};
|
|
20
20
|
let bitcoindPostRepairHealth = "unavailable";
|
|
21
|
+
const rpcReadyProgress = async (event) => {
|
|
22
|
+
await reportRepairProgress(options.context, event.code, event.message);
|
|
23
|
+
};
|
|
21
24
|
const bitcoindLock = await acquireFileLock(options.servicePaths.bitcoindLockPath, {
|
|
22
25
|
purpose: "managed-bitcoind-repair",
|
|
23
26
|
walletRootId: state.walletRootId,
|
|
@@ -30,6 +33,7 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
30
33
|
chain: "main",
|
|
31
34
|
startHeight: 0,
|
|
32
35
|
walletRootId: state.walletRootId,
|
|
36
|
+
rpcReadyProgress,
|
|
33
37
|
});
|
|
34
38
|
bitcoindCompatibilityIssue = mapBitcoindCompatibilityToRepairIssue(initialBitcoindProbe.compatibility);
|
|
35
39
|
if (initialBitcoindProbe.compatibility === "starting") {
|
|
@@ -105,12 +109,12 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
105
109
|
await reportRepairProgress(options.context, attachAttempt === 0 ? "bitcoind-start" : "bitcoind-retry-start", bitcoindServiceAction === "none"
|
|
106
110
|
? "Attaching to managed bitcoind..."
|
|
107
111
|
: "Starting managed bitcoind with current ZMQ config...");
|
|
108
|
-
await reportRepairProgress(options.context, "bitcoind-wait-rpc", "Waiting for Bitcoin Core RPC readiness...");
|
|
109
112
|
bitcoindHandle = await options.context.attachService({
|
|
110
113
|
dataDir: options.context.dataDir,
|
|
111
114
|
chain: "main",
|
|
112
115
|
startHeight: 0,
|
|
113
116
|
walletRootId: state.walletRootId,
|
|
117
|
+
rpcReadyProgress,
|
|
114
118
|
});
|
|
115
119
|
await reportRepairProgress(options.context, "bitcoind-normalize-wallet", "Checking managed Bitcoin wallet state...");
|
|
116
120
|
const rpc = options.context.rpcFactory(bitcoindHandle.rpc);
|
|
@@ -673,7 +673,7 @@ export async function runCompetitivenessGate(options) {
|
|
|
673
673
|
});
|
|
674
674
|
options.throwIfStopping?.();
|
|
675
675
|
const txid = visibleTxids[index];
|
|
676
|
-
const context =
|
|
676
|
+
const context = rawTxContexts.get(txid);
|
|
677
677
|
const mempoolEntry = mempoolEntries[txid];
|
|
678
678
|
if (context === undefined || context.payload === null || context.senderScriptHex === null || mempoolEntry === undefined) {
|
|
679
679
|
continue;
|
|
@@ -685,7 +685,7 @@ export async function runCompetitivenessGate(options) {
|
|
|
685
685
|
const overlayDomain = await resolveOverlayAuthorizedMiningDomain({
|
|
686
686
|
readContext: options.readContext,
|
|
687
687
|
txid,
|
|
688
|
-
txContexts:
|
|
688
|
+
txContexts: rawTxContexts,
|
|
689
689
|
domainId: decoded.domainId,
|
|
690
690
|
senderScriptHex: context.senderScriptHex,
|
|
691
691
|
});
|
|
@@ -2,13 +2,14 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { COG_PREFIX } from "../cogop/constants.js";
|
|
5
|
-
import {
|
|
5
|
+
import { writeFileAtomic } from "../fs/atomic.js";
|
|
6
6
|
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
7
7
|
const MINING_MEMPOOL_INDEX_SCHEMA_VERSION = 1;
|
|
8
8
|
const MINING_MEMPOOL_INDEX_RAW_TX_FETCH_CONCURRENCY = 8;
|
|
9
9
|
const MINING_MEMPOOL_INDEX_PROGRESS_REPORT_EVERY = 25;
|
|
10
10
|
const indexStates = new Map();
|
|
11
11
|
const rawTxSubscribers = new Map();
|
|
12
|
+
const RAW_TX_SUBSCRIBER_SAVE_DEBOUNCE_MS = 1_000;
|
|
12
13
|
export function resolveMiningMempoolIndexCachePath(paths) {
|
|
13
14
|
return join(paths.miningRoot, "mempool-index.json");
|
|
14
15
|
}
|
|
@@ -101,6 +102,8 @@ function getOrCreateState(options) {
|
|
|
101
102
|
negativeTxids: new Set(),
|
|
102
103
|
loaded: false,
|
|
103
104
|
savePromise: Promise.resolve(),
|
|
105
|
+
saveScheduled: null,
|
|
106
|
+
saveDirty: false,
|
|
104
107
|
};
|
|
105
108
|
indexStates.set(key, created);
|
|
106
109
|
return created;
|
|
@@ -117,10 +120,30 @@ async function saveState(state) {
|
|
|
117
120
|
};
|
|
118
121
|
state.savePromise = state.savePromise.catch(() => undefined).then(async () => {
|
|
119
122
|
await mkdir(dirname(state.cachePath), { recursive: true });
|
|
120
|
-
await
|
|
123
|
+
await writeFileAtomic(state.cachePath, `${JSON.stringify(payload)}\n`, { mode: 0o600 });
|
|
121
124
|
});
|
|
122
125
|
await state.savePromise;
|
|
123
126
|
}
|
|
127
|
+
function scheduleStateSave(state) {
|
|
128
|
+
state.saveDirty = true;
|
|
129
|
+
if (state.saveScheduled !== null) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
state.saveScheduled = setTimeout(() => {
|
|
133
|
+
state.saveScheduled = null;
|
|
134
|
+
if (!state.saveDirty) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
state.saveDirty = false;
|
|
138
|
+
void saveState(state)
|
|
139
|
+
.catch(() => undefined)
|
|
140
|
+
.finally(() => {
|
|
141
|
+
if (state.saveDirty) {
|
|
142
|
+
scheduleStateSave(state);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}, RAW_TX_SUBSCRIBER_SAVE_DEBOUNCE_MS);
|
|
146
|
+
}
|
|
124
147
|
function pruneStateToVisibleTxids(state, visibleTxids) {
|
|
125
148
|
const visibleSet = new Set(visibleTxids);
|
|
126
149
|
let changed = false;
|
|
@@ -426,8 +449,11 @@ export async function ensureMiningMempoolRawTxSubscriber(options) {
|
|
|
426
449
|
if (parsed === null || isCogPayload(parsed.payload)) {
|
|
427
450
|
continue;
|
|
428
451
|
}
|
|
452
|
+
const previousSize = state.negativeTxids.size;
|
|
429
453
|
state.negativeTxids.add(parsed.txid);
|
|
430
|
-
|
|
454
|
+
if (state.negativeTxids.size !== previousSize) {
|
|
455
|
+
scheduleStateSave(state);
|
|
456
|
+
}
|
|
431
457
|
}
|
|
432
458
|
}
|
|
433
459
|
catch {
|
package/package.json
CHANGED