@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.
- package/README.md +12 -2
- package/dist/bitcoind/managed-bitcoind-service-config.d.ts +5 -1
- package/dist/bitcoind/managed-bitcoind-service-config.js +27 -18
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +46 -3
- package/dist/bitcoind/managed-bitcoind-service-status.d.ts +9 -2
- package/dist/bitcoind/managed-bitcoind-service-status.js +65 -9
- package/dist/bitcoind/managed-bitcoind-service-types.d.ts +8 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +27 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +6 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +2 -2
- package/dist/bitcoind/retryable-rpc.d.ts +1 -0
- package/dist/bitcoind/retryable-rpc.js +19 -2
- package/dist/cli/command-registry.js +2 -2
- package/dist/cli/commands/service-runtime.js +22 -2
- package/dist/cli/commands/status.js +7 -1
- package/dist/cli/commands/wallet-admin.js +8 -2
- package/dist/cli/context.js +2 -0
- package/dist/cli/output/rules/cli-surface.js +7 -0
- package/dist/cli/output/rules/services.js +7 -0
- package/dist/cli/parse.js +9 -0
- package/dist/cli/status-format.d.ts +2 -2
- package/dist/cli/status-format.js +167 -28
- package/dist/cli/types.d.ts +3 -0
- package/dist/passive-status.d.ts +49 -1
- package/dist/passive-status.js +208 -2
- package/dist/wallet/lifecycle/context.js +1 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +44 -1
- package/dist/wallet/lifecycle/repair-runtime.d.ts +3 -1
- package/dist/wallet/lifecycle/repair-runtime.js +12 -0
- package/dist/wallet/lifecycle/repair.js +31 -9
- package/dist/wallet/lifecycle/types.d.ts +7 -0
- package/dist/wallet/mining/competitiveness.js +2 -2
- package/dist/wallet/mining/lifecycle.js +4 -0
- package/dist/wallet/mining/mempool-index.js +29 -3
- package/dist/wallet/read/managed-bitcoind.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.2.
|
|
3
|
+
`@cogcoin/client@1.2.5` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
@@ -126,6 +126,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
|
|
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
|
|
29
|
+
export declare function waitForManagedBitcoindCookie(cookieFile: string, timeoutMs: number, sleepImpl: (ms: number) => Promise<void>, options?: {
|
|
30
|
+
now?: () => number;
|
|
31
|
+
progressIntervalMs?: number;
|
|
32
|
+
onProgress?: (elapsedMs: number) => void | Promise<void>;
|
|
33
|
+
}): Promise<void>;
|
|
30
34
|
export type { BitcoindRpcConfig, BitcoindZmqConfig, };
|
|
@@ -147,12 +147,14 @@ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfi
|
|
|
147
147
|
const walletDir = join(options.dataDir ?? "", "wallets");
|
|
148
148
|
await mkdir(dirname(filePath), { recursive: true });
|
|
149
149
|
await mkdir(walletDir, { recursive: true });
|
|
150
|
-
const
|
|
150
|
+
const commonLines = [
|
|
151
151
|
"server=1",
|
|
152
152
|
"prune=0",
|
|
153
|
+
`dbcache=${runtimeConfig.dbcacheMiB}`,
|
|
154
|
+
];
|
|
155
|
+
const networkLines = [
|
|
153
156
|
"dnsseed=1",
|
|
154
157
|
"listen=0",
|
|
155
|
-
`dbcache=${runtimeConfig.dbcacheMiB}`,
|
|
156
158
|
`rpcbind=${LOCAL_HOST}`,
|
|
157
159
|
`rpcallowip=${LOCAL_HOST}`,
|
|
158
160
|
`rpcport=${runtimeConfig.rpc.port}`,
|
|
@@ -161,25 +163,23 @@ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfi
|
|
|
161
163
|
`zmqpubrawtx=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
|
|
162
164
|
`walletdir=${walletDir}`,
|
|
163
165
|
];
|
|
166
|
+
const lines = options.chain === "regtest"
|
|
167
|
+
? [
|
|
168
|
+
...commonLines,
|
|
169
|
+
"",
|
|
170
|
+
"[regtest]",
|
|
171
|
+
...networkLines,
|
|
172
|
+
]
|
|
173
|
+
: [
|
|
174
|
+
...commonLines,
|
|
175
|
+
...networkLines,
|
|
176
|
+
];
|
|
164
177
|
await writeFileAtomic(filePath, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
165
178
|
}
|
|
166
179
|
export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
|
|
167
|
-
const walletDir = join(options.dataDir ?? "", "wallets");
|
|
168
180
|
const args = [
|
|
169
181
|
"-nosettings=1",
|
|
170
182
|
`-datadir=${options.dataDir}`,
|
|
171
|
-
`-rpcbind=${LOCAL_HOST}`,
|
|
172
|
-
`-rpcallowip=${LOCAL_HOST}`,
|
|
173
|
-
`-rpcport=${runtimeConfig.rpc.port}`,
|
|
174
|
-
`-port=${runtimeConfig.p2pPort}`,
|
|
175
|
-
`-zmqpubhashblock=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
|
|
176
|
-
`-zmqpubrawtx=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
|
|
177
|
-
`-walletdir=${walletDir}`,
|
|
178
|
-
"-server=1",
|
|
179
|
-
"-prune=0",
|
|
180
|
-
"-dnsseed=1",
|
|
181
|
-
"-listen=0",
|
|
182
|
-
`-dbcache=${runtimeConfig.dbcacheMiB}`,
|
|
183
183
|
];
|
|
184
184
|
if (options.chain === "regtest") {
|
|
185
185
|
args.push("-chain=regtest");
|
|
@@ -189,14 +189,23 @@ export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
|
|
|
189
189
|
}
|
|
190
190
|
return args;
|
|
191
191
|
}
|
|
192
|
-
export async function waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleepImpl) {
|
|
193
|
-
const
|
|
194
|
-
|
|
192
|
+
export async function waitForManagedBitcoindCookie(cookieFile, timeoutMs, sleepImpl, options = {}) {
|
|
193
|
+
const now = options.now ?? Date.now;
|
|
194
|
+
const progressIntervalMs = options.progressIntervalMs ?? 30_000;
|
|
195
|
+
const startedAt = now();
|
|
196
|
+
const deadline = startedAt + timeoutMs;
|
|
197
|
+
let lastProgressAt = startedAt;
|
|
198
|
+
while (now() < deadline) {
|
|
195
199
|
try {
|
|
196
200
|
await access(cookieFile, constants.R_OK);
|
|
197
201
|
return;
|
|
198
202
|
}
|
|
199
203
|
catch {
|
|
204
|
+
const currentTime = now();
|
|
205
|
+
if (currentTime - lastProgressAt >= progressIntervalMs) {
|
|
206
|
+
await options.onProgress?.(Math.max(0, currentTime - startedAt));
|
|
207
|
+
lastProgressAt = currentTime;
|
|
208
|
+
}
|
|
200
209
|
await sleepImpl(250);
|
|
201
210
|
}
|
|
202
211
|
}
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 === "
|
|
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
|
|
10
|
+
usage: "status [--live]",
|
|
11
|
+
description: "Show passive local status; use --live for RPC-backed wallet balance",
|
|
12
12
|
},
|
|
13
13
|
],
|
|
14
14
|
describeCommand() {
|
|
@@ -44,19 +44,33 @@ async function resolveEffectiveWalletRootId(context) {
|
|
|
44
44
|
source: "default-uninitialized",
|
|
45
45
|
}));
|
|
46
46
|
}
|
|
47
|
+
function createRpcReadyProgressReporter(context) {
|
|
48
|
+
return async (event) => {
|
|
49
|
+
writeLine(context.stdout, event.message);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
47
52
|
async function inspectManagedBitcoindStatus(dataDir, context) {
|
|
48
53
|
const resolution = await resolveEffectiveWalletRootId(context);
|
|
54
|
+
const rpcReadyProgress = createRpcReadyProgressReporter(context);
|
|
49
55
|
const probe = await context.probeManagedBitcoindService({
|
|
50
56
|
dataDir,
|
|
51
57
|
chain: "main",
|
|
52
58
|
startHeight: 0,
|
|
53
59
|
walletRootId: resolution.walletRootId,
|
|
60
|
+
rpcReadyProgress,
|
|
54
61
|
});
|
|
55
62
|
let node = null;
|
|
56
63
|
let nodeError = null;
|
|
64
|
+
let service = probe.status;
|
|
57
65
|
if (probe.compatibility === "compatible" && probe.status !== null) {
|
|
58
66
|
try {
|
|
59
|
-
|
|
67
|
+
service = await context.refreshManagedBitcoindServiceStatus(probe.status, resolveManagedServicePaths(dataDir, resolution.walletRootId), {
|
|
68
|
+
dataDir,
|
|
69
|
+
chain: "main",
|
|
70
|
+
startHeight: 0,
|
|
71
|
+
walletRootId: resolution.walletRootId,
|
|
72
|
+
});
|
|
73
|
+
const rpc = context.createBitcoinRpcClient(service.rpc);
|
|
60
74
|
const [blockchainInfo, networkInfo] = await Promise.all([
|
|
61
75
|
rpc.getBlockchainInfo(),
|
|
62
76
|
rpc.getNetworkInfo(),
|
|
@@ -82,7 +96,7 @@ async function inspectManagedBitcoindStatus(dataDir, context) {
|
|
|
82
96
|
walletRootId: resolution.walletRootId,
|
|
83
97
|
walletRootSource: resolution.source,
|
|
84
98
|
compatibility: probe.compatibility,
|
|
85
|
-
service
|
|
99
|
+
service,
|
|
86
100
|
node,
|
|
87
101
|
nodeError,
|
|
88
102
|
};
|
|
@@ -304,11 +318,13 @@ export async function runServiceRuntimeCommand(parsed, context) {
|
|
|
304
318
|
}
|
|
305
319
|
if (parsed.command === "bitcoin-start") {
|
|
306
320
|
const resolution = await resolveEffectiveWalletRootId(context);
|
|
321
|
+
const rpcReadyProgress = createRpcReadyProgressReporter(context);
|
|
307
322
|
const probe = await context.probeManagedBitcoindService({
|
|
308
323
|
dataDir,
|
|
309
324
|
chain: "main",
|
|
310
325
|
startHeight: 0,
|
|
311
326
|
walletRootId: resolution.walletRootId,
|
|
327
|
+
rpcReadyProgress,
|
|
312
328
|
});
|
|
313
329
|
const genesis = await loadBundledGenesisParameters();
|
|
314
330
|
await context.attachManagedBitcoindService({
|
|
@@ -316,6 +332,7 @@ export async function runServiceRuntimeCommand(parsed, context) {
|
|
|
316
332
|
chain: "main",
|
|
317
333
|
startHeight: resolveCogcoinProcessingStartHeight(genesis),
|
|
318
334
|
walletRootId: resolution.walletRootId,
|
|
335
|
+
rpcReadyProgress: probe.compatibility === "compatible" ? undefined : rpcReadyProgress,
|
|
319
336
|
});
|
|
320
337
|
const bitcoindStatus = probe.compatibility === "compatible" ? "already-running" : "started";
|
|
321
338
|
const payload = {
|
|
@@ -356,17 +373,20 @@ export async function runServiceRuntimeCommand(parsed, context) {
|
|
|
356
373
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
357
374
|
await context.ensureDirectory(dirname(dbPath));
|
|
358
375
|
const genesis = await loadBundledGenesisParameters();
|
|
376
|
+
const rpcReadyProgress = createRpcReadyProgressReporter(context);
|
|
359
377
|
const bitcoindProbe = await context.probeManagedBitcoindService({
|
|
360
378
|
dataDir,
|
|
361
379
|
chain: "main",
|
|
362
380
|
startHeight: 0,
|
|
363
381
|
walletRootId: resolution.walletRootId,
|
|
382
|
+
rpcReadyProgress,
|
|
364
383
|
});
|
|
365
384
|
await context.attachManagedBitcoindService({
|
|
366
385
|
dataDir,
|
|
367
386
|
chain: "main",
|
|
368
387
|
startHeight: resolveCogcoinProcessingStartHeight(genesis),
|
|
369
388
|
walletRootId: resolution.walletRootId,
|
|
389
|
+
rpcReadyProgress: bitcoindProbe.compatibility === "compatible" ? undefined : rpcReadyProgress,
|
|
370
390
|
});
|
|
371
391
|
const indexerProbe = await context.probeIndexerDaemon({
|
|
372
392
|
dataDir,
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { formatBalanceReport, formatWalletOverviewReport } from "../wallet-format.js";
|
|
3
|
+
import { formatStatusReport } from "../status-format.js";
|
|
3
4
|
import { writeLine } from "../io.js";
|
|
4
5
|
import { createTerminalPrompter } from "../prompt.js";
|
|
5
6
|
import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
|
|
6
7
|
export async function runStatusCommand(parsed, context) {
|
|
7
8
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
8
9
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
9
|
-
const packageVersion = await context.readPackageVersion();
|
|
10
10
|
const runtimePaths = context.resolveWalletRuntimePaths();
|
|
11
|
+
const packageVersion = await context.readPackageVersion();
|
|
12
|
+
if (!parsed.statusLive) {
|
|
13
|
+
const status = await context.inspectPassiveClientStatus(dbPath, dataDir, runtimePaths);
|
|
14
|
+
writeLine(context.stdout, formatStatusReport(status, packageVersion));
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
11
17
|
await context.ensureDirectory(dirname(dbPath));
|
|
12
18
|
const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, context.createPrompter?.() ?? createTerminalPrompter(context.stdin, context.stdout));
|
|
13
19
|
const readContext = await context.openWalletReadContext({
|