@cogcoin/client 1.2.3 → 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.
Files changed (36) hide show
  1. package/README.md +12 -2
  2. package/dist/bitcoind/managed-bitcoind-service-config.d.ts +5 -1
  3. package/dist/bitcoind/managed-bitcoind-service-config.js +27 -18
  4. package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +46 -3
  5. package/dist/bitcoind/managed-bitcoind-service-status.d.ts +9 -2
  6. package/dist/bitcoind/managed-bitcoind-service-status.js +65 -9
  7. package/dist/bitcoind/managed-bitcoind-service-types.d.ts +8 -0
  8. package/dist/bitcoind/managed-runtime/bitcoind-policy.js +27 -0
  9. package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +6 -0
  10. package/dist/bitcoind/managed-runtime/types.d.ts +2 -2
  11. package/dist/bitcoind/retryable-rpc.d.ts +1 -0
  12. package/dist/bitcoind/retryable-rpc.js +19 -2
  13. package/dist/cli/command-registry.js +2 -2
  14. package/dist/cli/commands/service-runtime.js +22 -2
  15. package/dist/cli/commands/status.js +7 -1
  16. package/dist/cli/commands/wallet-admin.js +8 -2
  17. package/dist/cli/context.js +2 -0
  18. package/dist/cli/output/rules/cli-surface.js +7 -0
  19. package/dist/cli/output/rules/services.js +7 -0
  20. package/dist/cli/parse.js +9 -0
  21. package/dist/cli/status-format.d.ts +2 -2
  22. package/dist/cli/status-format.js +167 -28
  23. package/dist/cli/types.d.ts +3 -0
  24. package/dist/passive-status.d.ts +49 -1
  25. package/dist/passive-status.js +208 -2
  26. package/dist/wallet/lifecycle/context.js +1 -0
  27. package/dist/wallet/lifecycle/repair-bitcoind.js +44 -1
  28. package/dist/wallet/lifecycle/repair-runtime.d.ts +3 -1
  29. package/dist/wallet/lifecycle/repair-runtime.js +12 -0
  30. package/dist/wallet/lifecycle/repair.js +31 -9
  31. package/dist/wallet/lifecycle/types.d.ts +7 -0
  32. package/dist/wallet/mining/competitiveness.js +2 -2
  33. package/dist/wallet/mining/lifecycle.js +4 -0
  34. package/dist/wallet/mining/mempool-index.js +29 -3
  35. package/dist/wallet/read/managed-bitcoind.js +9 -0
  36. 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` 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,14 @@ 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
+
133
+ ## Upgrade Notes For `1.2.4`
134
+
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`.
136
+
129
137
  ## Upgrade Notes For `1.2.0`
130
138
 
131
139
  `@cogcoin/client@1.2.0` updates the runtime indexer to `@cogcoin/indexer@1.0.2`. Existing wallet state, mining configuration, Bitcoin Core data, and secrets remain compatible and are not reset.
@@ -169,10 +177,12 @@ The installed `cogcoin` command covers the first-party local wallet and node wor
169
177
  - COG and reputation commands such as `send`, `cog lock`, `claim`, `reclaim`, `rep give`, and `rep revoke`
170
178
  - mining commands such as `mine`, `mine status`, `mine log`, `mine setup`, `mine prompt`, and `mine prompt list`
171
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
+
172
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.
173
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.
174
184
  Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
175
- 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.
176
186
  Use the explicit `bitcoin ...` and `indexer ...` commands when you want direct service inspection or start/stop control.
177
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.
178
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
  }
@@ -7,6 +7,7 @@ import { stopIndexerDaemonServiceWithLockHeld } from "./indexer-daemon.js";
7
7
  import { readManagedBitcoindObservedStatus, listManagedBitcoindStatusCandidates } from "./managed-runtime/bitcoind-status.js";
8
8
  import { attachOrStartManagedBitcoindRuntime, probeManagedBitcoindRuntime } from "./managed-runtime/bitcoind-runtime.js";
9
9
  import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
10
+ import { isManagedRpcWarmupError } from "./retryable-rpc.js";
10
11
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
11
12
  import { DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS, } from "./types.js";
12
13
  import { buildManagedServiceArgsForTesting, LOCAL_HOST, resolveManagedBitcoindRuntimeConfig, SUPPORTED_BITCOIND_VERSION, verifyManagedBitcoindVersion, writeManagedBitcoindRuntimeConfigFile, writeManagedBitcoindRuntimeConfigFileFromStatus, writeBitcoinConfForTesting, } from "./managed-bitcoind-service-config.js";
@@ -192,13 +193,52 @@ export async function attachOrStartManagedBitcoindService(options) {
192
193
  child.unref();
193
194
  }
194
195
  const rpc = createRpcClient(rpcConfig);
196
+ const startedAtUnixMs = Date.now();
197
+ const serviceInstanceId = randomBytes(16).toString("hex");
198
+ const startingStatus = createBitcoindServiceStatus({
199
+ binaryVersion,
200
+ serviceInstanceId,
201
+ state: "starting",
202
+ processId: child.pid ?? null,
203
+ walletRootId: startOptions.walletRootId,
204
+ chain: startOptions.chain,
205
+ dataDir: startOptions.dataDir,
206
+ runtimeRoot: paths.walletRuntimeRoot,
207
+ startHeight: startOptions.startHeight,
208
+ rpc: rpcConfig,
209
+ zmq: zmqConfig,
210
+ p2pPort: runtimeConfig.p2pPort,
211
+ getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
212
+ getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
213
+ walletReplica: null,
214
+ startedAtUnixMs,
215
+ heartbeatAtUnixMs: startedAtUnixMs,
216
+ lastError: "Managed bitcoind service is starting.",
217
+ });
218
+ await writeManagedBitcoindRuntimeConfigFile(paths.bitcoindRuntimeConfigPath, runtimeConfig);
219
+ await writeManagedBitcoindStatus(paths, startingStatus);
195
220
  try {
196
- await waitForManagedBitcoindRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
221
+ await waitForManagedBitcoindRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs, {
222
+ progress: startOptions.rpcReadyProgress,
223
+ });
197
224
  await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint, {
198
225
  requireRawTxZmq: true,
199
226
  });
200
227
  }
201
228
  catch (error) {
229
+ const processAlive = child.pid !== undefined && await isManagedBitcoindProcessAlive(child.pid);
230
+ if (startOptions.serviceLifetime !== "ephemeral"
231
+ && processAlive
232
+ && isManagedRpcWarmupError(error)) {
233
+ const nowUnixMs = Date.now();
234
+ await writeManagedBitcoindStatus(paths, {
235
+ ...startingStatus,
236
+ heartbeatAtUnixMs: nowUnixMs,
237
+ updatedAtUnixMs: nowUnixMs,
238
+ lastError: error instanceof Error ? error.message : String(error),
239
+ });
240
+ throw new Error("managed_bitcoind_service_starting");
241
+ }
202
242
  if (child.pid !== undefined) {
203
243
  try {
204
244
  process.kill(child.pid, "SIGTERM");
@@ -215,7 +255,7 @@ export async function attachOrStartManagedBitcoindService(options) {
215
255
  runtimeConfig,
216
256
  status: createBitcoindServiceStatus({
217
257
  binaryVersion,
218
- serviceInstanceId: randomBytes(16).toString("hex"),
258
+ serviceInstanceId,
219
259
  state: "ready",
220
260
  processId: child.pid ?? null,
221
261
  walletRootId: startOptions.walletRootId,
@@ -229,7 +269,7 @@ export async function attachOrStartManagedBitcoindService(options) {
229
269
  getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
230
270
  getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
231
271
  walletReplica,
232
- startedAtUnixMs: nowUnixMs,
272
+ startedAtUnixMs,
233
273
  heartbeatAtUnixMs: nowUnixMs,
234
274
  lastError: walletReplica.message ?? null,
235
275
  }),
@@ -241,6 +281,9 @@ export async function attachOrStartManagedBitcoindService(options) {
241
281
  ({ runtimeConfig, status } = await startManagedProcess(runtimeOptions));
242
282
  }
243
283
  catch (error) {
284
+ if (isManagedRpcWarmupError(error)) {
285
+ throw error;
286
+ }
244
287
  if (runtimeOptions.getblockArchivePath === undefined || runtimeOptions.getblockArchivePath === null) {
245
288
  throw error;
246
289
  }
@@ -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 {};
@@ -8,21 +8,71 @@ import { MANAGED_BITCOIND_SERVICE_API_VERSION, } from "./types.js";
8
8
  import { createMissingManagedWalletReplicaStatus, loadManagedWalletReplicaIfPresent, } from "./managed-bitcoind-service-replica.js";
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
- export async function waitForManagedBitcoindRpcReady(rpc, cookieFile, expectedChain, timeoutMs) {
12
- await waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleep);
13
- const deadline = Date.now() + timeoutMs;
11
+ import { isManagedRpcWarmupError } from "./retryable-rpc.js";
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;
14
46
  let lastError = null;
15
- while (Date.now() < deadline) {
47
+ let lastProgressAt = startedAt;
48
+ while (now() < deadline) {
16
49
  try {
17
50
  const info = await rpc.getBlockchainInfo();
18
51
  if (info.chain !== expectedChain) {
19
52
  throw new Error(`bitcoind_chain_expected_${expectedChain}_got_${info.chain}`);
20
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
+ });
21
60
  return;
22
61
  }
23
62
  catch (error) {
24
63
  lastError = error;
25
- 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);
26
76
  }
27
77
  }
28
78
  throw lastError instanceof Error ? lastError : new Error("bitcoind_rpc_timeout");
@@ -65,7 +115,9 @@ export async function probeManagedBitcoindStatusCandidate(status, options, runti
65
115
  }
66
116
  const rpc = createRpcClient(status.rpc);
67
117
  try {
68
- 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
+ });
69
121
  await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint, {
70
122
  requireRawTxZmq: true,
71
123
  });
@@ -97,7 +149,9 @@ export async function refreshManagedBitcoindStatus(status, paths, options) {
97
149
  const rpc = createRpcClient(status.rpc);
98
150
  const targetWalletRootId = options.walletRootId ?? status.walletRootId;
99
151
  try {
100
- 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
+ });
101
155
  await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint, {
102
156
  requireRawTxZmq: true,
103
157
  });
@@ -117,12 +171,14 @@ export async function refreshManagedBitcoindStatus(status, paths, options) {
117
171
  return nextStatus;
118
172
  }
119
173
  catch (error) {
174
+ const processAlive = await isManagedBitcoindProcessAlive(status.processId);
175
+ const stillStarting = processAlive && isManagedRpcWarmupError(error);
120
176
  const nextStatus = {
121
177
  ...status,
122
178
  walletRootId: targetWalletRootId,
123
179
  runtimeRoot: paths.walletRuntimeRoot,
124
- state: "failed",
125
- processId: await isManagedBitcoindProcessAlive(status.processId) ? status.processId : null,
180
+ state: stillStarting ? "starting" : "failed",
181
+ processId: processAlive ? status.processId : null,
126
182
  heartbeatAtUnixMs: nowUnixMs,
127
183
  updatedAtUnixMs: nowUnixMs,
128
184
  lastError: error instanceof Error ? error.message : String(error),
@@ -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;
@@ -1,4 +1,5 @@
1
1
  import { join } from "node:path";
2
+ import { isManagedRpcWarmupError } from "../retryable-rpc.js";
2
3
  import { resolveManagedServicePaths } from "../service-paths.js";
3
4
  import { MANAGED_BITCOIND_SERVICE_API_VERSION } from "../types.js";
4
5
  function isRuntimeMismatchError(error) {
@@ -64,6 +65,13 @@ export function mapManagedBitcoindRuntimeProbeFailure(error, status) {
64
65
  error: "managed_bitcoind_runtime_mismatch",
65
66
  };
66
67
  }
68
+ if (isManagedRpcWarmupError(error)) {
69
+ return {
70
+ compatibility: "starting",
71
+ status,
72
+ error: error instanceof Error ? error.message : String(error),
73
+ };
74
+ }
67
75
  if (isUnreachableManagedBitcoindError(error)) {
68
76
  return {
69
77
  compatibility: "unreachable",
@@ -84,6 +92,12 @@ export function resolveManagedBitcoindProbeDecision(probe) {
84
92
  error: null,
85
93
  };
86
94
  }
95
+ if (probe.compatibility === "starting") {
96
+ return {
97
+ action: "wait",
98
+ error: probe.error ?? "managed_bitcoind_service_starting",
99
+ };
100
+ }
87
101
  if (probe.compatibility === "unreachable") {
88
102
  return {
89
103
  action: "start",
@@ -121,6 +135,12 @@ function mapManagedBitcoindStartupError(message) {
121
135
  status: null,
122
136
  message: "The live managed bitcoind service runtime does not match this wallet's expected data directory or chain.",
123
137
  };
138
+ case "managed_bitcoind_service_starting":
139
+ return {
140
+ health: "starting",
141
+ status: null,
142
+ message: "Managed bitcoind service is still starting.",
143
+ };
124
144
  case "managed_bitcoind_protocol_error":
125
145
  return {
126
146
  health: "unavailable",
@@ -128,6 +148,13 @@ function mapManagedBitcoindStartupError(message) {
128
148
  message: "The managed bitcoind runtime artifacts are invalid or incomplete.",
129
149
  };
130
150
  default:
151
+ if (/^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)) {
152
+ return {
153
+ health: "starting",
154
+ status: null,
155
+ message,
156
+ };
157
+ }
131
158
  return {
132
159
  health: "unavailable",
133
160
  status: null,
@@ -40,6 +40,9 @@ export async function attachOrStartManagedBitcoindRuntime(options, dependencies)
40
40
  }
41
41
  throw new Error("managed_bitcoind_protocol_error");
42
42
  }
43
+ if (existingDecision.action === "wait") {
44
+ return waitForManagedBitcoindRuntime(options, dependencies);
45
+ }
43
46
  if (existingDecision.action === "reject") {
44
47
  throw new Error(existingDecision.error ?? "managed_bitcoind_protocol_error");
45
48
  }
@@ -56,6 +59,9 @@ export async function attachOrStartManagedBitcoindRuntime(options, dependencies)
56
59
  }
57
60
  throw new Error("managed_bitcoind_protocol_error");
58
61
  }
62
+ if (liveDecision.action === "wait") {
63
+ return waitForManagedBitcoindRuntime(options, dependencies);
64
+ }
59
65
  if (liveDecision.action === "reject") {
60
66
  throw new Error(liveDecision.error ?? "managed_bitcoind_protocol_error");
61
67
  }
@@ -2,7 +2,7 @@ import type { ClientTip } from "../../types.js";
2
2
  import type { ManagedServicePaths } from "../service-paths.js";
3
3
  import type { ManagedBitcoindObservedStatus, ManagedIndexerDaemonObservedStatus, ManagedIndexerTruthSource } from "../types.js";
4
4
  import type { WalletBitcoindStatus, WalletIndexerStatus, WalletNodeStatus, WalletServiceHealth, WalletSnapshotView } from "../../wallet/read/types.js";
5
- export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "rawtx-zmq-missing" | "unreachable" | "protocol-error";
5
+ export type ManagedBitcoindServiceCompatibility = "compatible" | "starting" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "rawtx-zmq-missing" | "unreachable" | "protocol-error";
6
6
  export interface ManagedBitcoindServiceProbeResult {
7
7
  compatibility: ManagedBitcoindServiceCompatibility;
8
8
  status: ManagedBitcoindObservedStatus | null;
@@ -20,7 +20,7 @@ export interface ManagedIndexerDaemonProbeResult<TClient> {
20
20
  error: string | null;
21
21
  }
22
22
  export interface ManagedBitcoindProbeDecision {
23
- action: "attach" | "start" | "reject";
23
+ action: "attach" | "wait" | "start" | "reject";
24
24
  error: string | null;
25
25
  }
26
26
  export interface IndexerDaemonProbeDecision {
@@ -8,4 +8,5 @@ export declare function createManagedRpcRetryState(): ManagedRpcRetryState;
8
8
  export declare function resetManagedRpcRetryState(state: ManagedRpcRetryState): void;
9
9
  export declare function consumeManagedRpcRetryDelayMs(state: ManagedRpcRetryState): number;
10
10
  export declare function isRetryableManagedRpcError(error: unknown): boolean;
11
+ export declare function isManagedRpcWarmupError(error: unknown): boolean;
11
12
  export declare function describeManagedRpcRetryError(error: unknown): string;
@@ -15,15 +15,32 @@ export function consumeManagedRpcRetryDelayMs(state) {
15
15
  return delayMs;
16
16
  }
17
17
  export function isRetryableManagedRpcError(error) {
18
+ if (isManagedRpcWarmupError(error)) {
19
+ return true;
20
+ }
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ if (message.startsWith("The managed Bitcoin RPC request to ")) {
23
+ return message.includes(" failed");
24
+ }
25
+ return false;
26
+ }
27
+ export function isManagedRpcWarmupError(error) {
18
28
  const message = error instanceof Error ? error.message : String(error);
19
- if (message === "bitcoind_rpc_timeout") {
29
+ if (message === "managed_bitcoind_service_starting"
30
+ || message === "bitcoind_rpc_timeout"
31
+ || message === "bitcoind_cookie_timeout") {
20
32
  return true;
21
33
  }
22
34
  if (/^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)) {
23
35
  return true;
24
36
  }
25
37
  if (message.startsWith("The managed Bitcoin RPC request to ")) {
26
- return message.includes(" failed");
38
+ return message.includes(" failed")
39
+ && (message.includes("ECONNREFUSED")
40
+ || message.includes("ECONNRESET")
41
+ || message.includes("socket hang up")
42
+ || message.includes("timeout")
43
+ || message.includes("aborted"));
27
44
  }
28
45
  return message.startsWith("The managed Bitcoin RPC cookie file is unavailable at ")
29
46
  || message.startsWith("The managed Bitcoin RPC cookie file could not be read at ");
@@ -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({