@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.2.4` 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.
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/status flows detach from the managed Bitcoin and indexer services on exit instead of stopping them.
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>): 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 lines = [
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 deadline = Date.now() + timeoutMs;
194
- while (Date.now() < deadline) {
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
- export declare function waitForManagedBitcoindRpcReady(rpc: ReturnType<typeof createRpcClient>, cookieFile: string, expectedChain: "main" | "regtest", timeoutMs: number): Promise<void>;
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
- export async function waitForManagedBitcoindRpcReady(rpc, cookieFile, expectedChain, timeoutMs) {
13
- await waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleep);
14
- const deadline = Date.now() + timeoutMs;
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
- while (Date.now() < deadline) {
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
- await sleep(250);
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 wallet-aware local service and chain status",
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
- const rpc = context.createBitcoinRpcClient(probe.status.rpc);
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: probe.status,
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({
@@ -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 { inspectPassiveClientStatus } from "../passive-status.js";
2
- export declare function formatStatusReport(status: Awaited<ReturnType<typeof inspectPassiveClientStatus>>): string;
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
- export function formatStatusReport(status) {
8
- const lines = [
9
- "Cogcoin Client Status",
10
- `DB path: ${status.dbPath}`,
11
- `Bitcoin datadir: ${status.bitcoinDataDir}`,
12
- `Store exists: ${status.storeExists ? "yes" : "no"}`,
13
- `Store initialized: ${status.storeInitialized ? "yes" : "no"}`,
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
- lines.push(`Store error: ${status.storeError}`);
52
+ rows.push(row(false, `Store error: ${status.storeError}`));
17
53
  }
18
54
  if (status.indexedTip === null) {
19
- lines.push("Indexed tip: none");
55
+ rows.push(row(false, "Indexed tip: none"));
20
56
  }
21
57
  else {
22
- lines.push(`Indexed tip height: ${status.indexedTip.height}`);
23
- lines.push(`Indexed tip hash: ${status.indexedTip.blockHashHex}`);
24
- lines.push(`Indexed tip state hash: ${status.indexedTip.stateHashHex ?? "none"}`);
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
- lines.push("Latest checkpoint: none");
63
+ rows.push(row(false, "Latest checkpoint: none"));
28
64
  }
29
65
  else {
30
- lines.push(`Latest checkpoint height: ${status.latestCheckpoint.height}`);
31
- lines.push(`Latest checkpoint hash: ${status.latestCheckpoint.blockHashHex}`);
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
- lines.push("Bootstrap state: none");
77
+ return [row(false, "Bootstrap state: none")];
35
78
  }
36
- else {
37
- lines.push(`Bootstrap phase: ${status.bootstrap.phase}`);
38
- lines.push(`Bootstrap download: ${status.bootstrap.downloadedBytes} / ${status.bootstrap.totalBytes} bytes (${formatBootstrapPercent(status.bootstrap.downloadedBytes, status.bootstrap.totalBytes)}%)`);
39
- lines.push(`Bootstrap validated: ${status.bootstrap.validated ? "yes" : "no"}`);
40
- lines.push(`Bootstrap loaded: ${status.bootstrap.loadTxOutSetComplete ? "yes" : "no"}`);
41
- lines.push(`Bootstrap base height: ${status.bootstrap.baseHeight ?? "none"}`);
42
- lines.push(`Bootstrap tip hash: ${status.bootstrap.tipHashHex ?? "none"}`);
43
- lines.push(`Bootstrap snapshot height: ${status.bootstrap.snapshotHeight ?? "none"}`);
44
- lines.push(`Bootstrap last error: ${status.bootstrap.lastError ?? "none"}`);
45
- }
46
- lines.push("Live node: not checked (passive status)");
47
- return lines.join("\n");
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
  }
@@ -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;
@@ -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 {};
@@ -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
- storeError: error instanceof Error ? error.message : String(error),
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 = cacheState.rawTxContexts.get(txid);
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: cacheState.rawTxContexts,
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 { writeJsonFileAtomic } from "../fs/atomic.js";
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 writeJsonFileAtomic(state.cachePath, payload, { mode: 0o600 });
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
- void saveState(state).catch(() => undefined);
454
+ if (state.negativeTxids.size !== previousSize) {
455
+ scheduleStateSave(state);
456
+ }
431
457
  }
432
458
  }
433
459
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",