@cogcoin/client 1.1.6 → 1.1.7
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 +2 -2
- package/dist/bitcoind/indexer-daemon.js +29 -79
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.d.ts +20 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +40 -0
- package/dist/bitcoind/node.d.ts +2 -2
- package/dist/bitcoind/node.js +2 -2
- package/dist/bitcoind/rpc.d.ts +2 -1
- package/dist/bitcoind/rpc.js +53 -3
- package/dist/bitcoind/service.js +46 -126
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +2 -64
- package/dist/cli/commands/client-admin.js +3 -18
- package/dist/cli/commands/mining-runtime.js +4 -60
- package/dist/cli/commands/wallet-admin.js +6 -6
- package/dist/cli/context.js +1 -3
- package/dist/cli/mining-json.d.ts +1 -22
- package/dist/cli/mining-json.js +0 -23
- package/dist/cli/output.js +16 -2
- package/dist/cli/parse.js +0 -2
- package/dist/cli/preview-json.d.ts +1 -22
- package/dist/cli/preview-json.js +0 -19
- package/dist/cli/types.d.ts +1 -3
- package/dist/cli/wallet-format.js +1 -1
- package/dist/cli/workflow-hints.d.ts +1 -2
- package/dist/cli/workflow-hints.js +5 -8
- package/dist/wallet/lifecycle/context.js +0 -1
- package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
- package/dist/wallet/lifecycle/repair-mining.js +5 -39
- package/dist/wallet/lifecycle/repair.js +0 -3
- package/dist/wallet/lifecycle/setup.js +10 -8
- package/dist/wallet/lifecycle/types.d.ts +1 -4
- package/dist/wallet/managed-core-wallet.d.ts +2 -0
- package/dist/wallet/managed-core-wallet.js +27 -1
- package/dist/wallet/mining/candidate.d.ts +1 -0
- package/dist/wallet/mining/candidate.js +38 -6
- package/dist/wallet/mining/competitiveness.d.ts +1 -0
- package/dist/wallet/mining/competitiveness.js +6 -0
- package/dist/wallet/mining/cycle.d.ts +2 -0
- package/dist/wallet/mining/cycle.js +14 -4
- package/dist/wallet/mining/engine-types.d.ts +1 -0
- package/dist/wallet/mining/index.d.ts +1 -1
- package/dist/wallet/mining/index.js +1 -1
- package/dist/wallet/mining/publish.d.ts +3 -0
- package/dist/wallet/mining/publish.js +78 -6
- package/dist/wallet/mining/runner.d.ts +0 -32
- package/dist/wallet/mining/runner.js +59 -104
- package/dist/wallet/mining/stop.d.ts +7 -0
- package/dist/wallet/mining/stop.js +23 -0
- package/dist/wallet/mining/supervisor.d.ts +2 -36
- package/dist/wallet/mining/supervisor.js +139 -246
- package/dist/wallet/read/context.d.ts +1 -5
- package/dist/wallet/read/context.js +20 -204
- package/dist/wallet/read/managed-services.d.ts +33 -0
- package/dist/wallet/read/managed-services.js +222 -0
- package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
- package/dist/wallet/state/client-password/bootstrap.js +3 -0
- package/dist/wallet/state/client-password/context.d.ts +10 -0
- package/dist/wallet/state/client-password/context.js +46 -0
- package/dist/wallet/state/client-password/crypto.d.ts +34 -0
- package/dist/wallet/state/client-password/crypto.js +117 -0
- package/dist/wallet/state/client-password/files.d.ts +10 -0
- package/dist/wallet/state/client-password/files.js +109 -0
- package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
- package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
- package/dist/wallet/state/client-password/messages.d.ts +3 -0
- package/dist/wallet/state/client-password/messages.js +9 -0
- package/dist/wallet/state/client-password/migration.d.ts +4 -0
- package/dist/wallet/state/client-password/migration.js +32 -0
- package/dist/wallet/state/client-password/prompts.d.ts +12 -0
- package/dist/wallet/state/client-password/prompts.js +79 -0
- package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
- package/dist/wallet/state/client-password/protected-secrets.js +90 -0
- package/dist/wallet/state/client-password/readiness.d.ts +4 -0
- package/dist/wallet/state/client-password/readiness.js +48 -0
- package/dist/wallet/state/client-password/references.d.ts +1 -0
- package/dist/wallet/state/client-password/references.js +56 -0
- package/dist/wallet/state/client-password/rotation.d.ts +6 -0
- package/dist/wallet/state/client-password/rotation.js +98 -0
- package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
- package/dist/wallet/state/client-password/session-policy.js +28 -0
- package/dist/wallet/state/client-password/session.d.ts +19 -0
- package/dist/wallet/state/client-password/session.js +170 -0
- package/dist/wallet/state/client-password/setup.d.ts +8 -0
- package/dist/wallet/state/client-password/setup.js +49 -0
- package/dist/wallet/state/client-password/types.d.ts +82 -0
- package/dist/wallet/state/client-password/types.js +5 -0
- package/dist/wallet/state/client-password.d.ts +7 -38
- package/dist/wallet/state/client-password.js +52 -937
- package/dist/wallet/tx/anchor.js +123 -216
- package/dist/wallet/tx/cog.js +294 -489
- package/dist/wallet/tx/common.d.ts +2 -0
- package/dist/wallet/tx/common.js +2 -0
- package/dist/wallet/tx/domain-admin.js +111 -220
- package/dist/wallet/tx/domain-market.js +401 -681
- package/dist/wallet/tx/executor.d.ts +176 -0
- package/dist/wallet/tx/executor.js +302 -0
- package/dist/wallet/tx/field.js +109 -215
- package/dist/wallet/tx/register.js +158 -269
- package/dist/wallet/tx/reputation.js +120 -227
- package/package.json +1 -1
- package/dist/wallet/mining/worker-main.d.ts +0 -1
- package/dist/wallet/mining/worker-main.js +0 -17
- package/dist/wallet/state/client-password-agent.d.ts +0 -1
- package/dist/wallet/state/client-password-agent.js +0 -211
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.1.
|
|
3
|
+
`@cogcoin/client@1.1.7` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
@@ -135,7 +135,7 @@ The installed `cogcoin` command covers the first-party local wallet and node wor
|
|
|
135
135
|
- sync and service commands such as `status`, `sync`, `follow`, `bitcoin start`, `bitcoin stop`, `bitcoin status`, `indexer start`, `indexer stop`, and `indexer status`
|
|
136
136
|
- domain and field commands such as `register`, `anchor`, `show`, `domains`, `fields`, `buy`, `sell`, and `transfer`
|
|
137
137
|
- COG and reputation commands such as `send`, `cog lock`, `claim`, `reclaim`, `rep give`, and `rep revoke`
|
|
138
|
-
- mining commands such as `mine`, `mine
|
|
138
|
+
- mining commands such as `mine`, `mine status`, `mine log`, `mine setup`, `mine prompt`, and `mine prompt list`
|
|
139
139
|
|
|
140
140
|
Use `cogcoin mine prompt <domain>` to set or clear a per-domain mining prompt override for one anchored root domain, and `cogcoin mine prompt list` to inspect the current per-domain prompt state alongside the global fallback prompt.
|
|
141
141
|
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.
|
|
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import net from "node:net";
|
|
6
6
|
import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
|
|
7
7
|
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
8
|
-
import { buildManagedIndexerStatusFromSnapshotHandle, mapIndexerDaemonTransportError, mapIndexerDaemonValidationError,
|
|
8
|
+
import { buildManagedIndexerStatusFromSnapshotHandle, mapIndexerDaemonTransportError, mapIndexerDaemonValidationError, validateIndexerDaemonStatus, validateIndexerSnapshotHandle, validateIndexerSnapshotPayload, } from "./managed-runtime/indexer-policy.js";
|
|
9
|
+
import { attachOrStartManagedIndexerRuntime } from "./managed-runtime/indexer-runtime.js";
|
|
9
10
|
import { readJsonFileIfPresent } from "./managed-runtime/status.js";
|
|
10
11
|
import {} from "./types.js";
|
|
11
12
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
@@ -347,86 +348,35 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
347
348
|
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
348
349
|
});
|
|
349
350
|
};
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
return attachOrStartManagedIndexerRuntime({
|
|
352
|
+
...options,
|
|
353
|
+
walletRootId,
|
|
354
|
+
startupTimeoutMs,
|
|
353
355
|
expectedBinaryVersion,
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
if (existingDecision.action === "replace" && existingProbe.client !== null) {
|
|
364
|
-
await existingProbe.client.close().catch(() => undefined);
|
|
365
|
-
}
|
|
366
|
-
if (existingDecision.action === "reject") {
|
|
367
|
-
throw new Error(existingDecision.error ?? "indexer_daemon_protocol_error");
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
const lock = await acquireFileLock(paths.indexerDaemonLockPath, {
|
|
356
|
+
}, {
|
|
357
|
+
getPaths: (runtimeOptions) => resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
|
|
358
|
+
probeDaemon: async (runtimeOptions, runtimePaths) => probeIndexerDaemonAtSocket(runtimePaths.indexerDaemonSocketPath, runtimeOptions.walletRootId),
|
|
359
|
+
requestBackgroundFollow,
|
|
360
|
+
closeClient: async (client) => {
|
|
361
|
+
await client.close();
|
|
362
|
+
},
|
|
363
|
+
acquireStartLock: async (runtimeOptions, runtimePaths) => acquireFileLock(runtimePaths.indexerDaemonLockPath, {
|
|
371
364
|
purpose: "indexer-daemon-start",
|
|
372
|
-
walletRootId,
|
|
373
|
-
dataDir:
|
|
374
|
-
databasePath:
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
await liveProbe.client.close().catch(() => undefined);
|
|
388
|
-
await stopIndexerDaemonServiceWithLockHeld({
|
|
389
|
-
dataDir: options.dataDir,
|
|
390
|
-
walletRootId,
|
|
391
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
392
|
-
paths,
|
|
393
|
-
processId: liveProbe.status?.processId ?? null,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
else if (liveDecision.action === "replace" && liveProbe.client !== null) {
|
|
398
|
-
await liveProbe.client.close().catch(() => undefined);
|
|
399
|
-
await stopIndexerDaemonServiceWithLockHeld({
|
|
400
|
-
dataDir: options.dataDir,
|
|
401
|
-
walletRootId,
|
|
402
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
403
|
-
paths,
|
|
404
|
-
processId: liveProbe.status?.processId ?? null,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
else if (liveDecision.action === "reject") {
|
|
408
|
-
throw new Error(liveDecision.error ?? "indexer_daemon_protocol_error");
|
|
409
|
-
}
|
|
410
|
-
const daemon = await startDaemon();
|
|
411
|
-
try {
|
|
412
|
-
return await requestBackgroundFollow(daemon);
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
await daemon.close().catch(() => undefined);
|
|
416
|
-
throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, { cause: error });
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
finally {
|
|
420
|
-
await lock.release();
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
if (error instanceof FileLockBusyError) {
|
|
425
|
-
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
426
|
-
return attachOrStartIndexerDaemon(options);
|
|
427
|
-
}
|
|
428
|
-
throw error;
|
|
429
|
-
}
|
|
365
|
+
walletRootId: runtimeOptions.walletRootId,
|
|
366
|
+
dataDir: runtimeOptions.dataDir,
|
|
367
|
+
databasePath: runtimeOptions.databasePath,
|
|
368
|
+
}),
|
|
369
|
+
startDaemon: async () => startDaemon(),
|
|
370
|
+
stopWithLockHeld: async (runtimeOptions, runtimePaths, processId) => stopIndexerDaemonServiceWithLockHeld({
|
|
371
|
+
dataDir: runtimeOptions.dataDir,
|
|
372
|
+
walletRootId: runtimeOptions.walletRootId,
|
|
373
|
+
shutdownTimeoutMs: runtimeOptions.shutdownTimeoutMs,
|
|
374
|
+
paths: resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
|
|
375
|
+
processId,
|
|
376
|
+
}),
|
|
377
|
+
isLockBusyError: (error) => error instanceof FileLockBusyError,
|
|
378
|
+
sleep,
|
|
379
|
+
});
|
|
430
380
|
}
|
|
431
381
|
export async function stopIndexerDaemonService(options) {
|
|
432
382
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ManagedBitcoindObservedStatus } from "../types.js";
|
|
2
|
+
import type { ManagedBitcoindRuntimeOptionsLike, ManagedBitcoindRuntimePathsLike, ManagedBitcoindServiceProbeResult, ManagedBitcoindStatusCandidate, ManagedRuntimeLockLike } from "./types.js";
|
|
3
|
+
type ManagedBitcoindRuntimeDependencies<TOptions, THandle> = {
|
|
4
|
+
getPaths(options: TOptions): ManagedBitcoindRuntimePathsLike;
|
|
5
|
+
listStatusCandidates(options: {
|
|
6
|
+
dataDir: string;
|
|
7
|
+
runtimeRoot: string;
|
|
8
|
+
expectedStatusPath: string;
|
|
9
|
+
}): Promise<ManagedBitcoindStatusCandidate[]>;
|
|
10
|
+
isProcessAlive(processId: number | null): Promise<boolean>;
|
|
11
|
+
probeStatusCandidate(status: ManagedBitcoindObservedStatus, options: TOptions, runtimeRoot: string): Promise<ManagedBitcoindServiceProbeResult>;
|
|
12
|
+
attachExisting(options: TOptions): Promise<THandle | null>;
|
|
13
|
+
acquireStartLock(options: TOptions, paths: ManagedBitcoindRuntimePathsLike): Promise<ManagedRuntimeLockLike>;
|
|
14
|
+
startService(options: TOptions, paths: ManagedBitcoindRuntimePathsLike): Promise<THandle>;
|
|
15
|
+
isLockBusyError(error: unknown): boolean;
|
|
16
|
+
sleep(ms: number): Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
export declare function probeManagedBitcoindRuntime<TOptions extends ManagedBitcoindRuntimeOptionsLike>(options: TOptions, dependencies: Pick<ManagedBitcoindRuntimeDependencies<TOptions, never>, "getPaths" | "listStatusCandidates" | "isProcessAlive" | "probeStatusCandidate">): Promise<ManagedBitcoindServiceProbeResult>;
|
|
19
|
+
export declare function attachOrStartManagedBitcoindRuntime<TOptions extends ManagedBitcoindRuntimeOptionsLike, THandle>(options: TOptions, dependencies: ManagedBitcoindRuntimeDependencies<TOptions, THandle>): Promise<THandle>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolveManagedBitcoindProbeDecision } from "./bitcoind-policy.js";
|
|
2
|
+
async function waitForManagedBitcoindRuntime(options, dependencies) {
|
|
3
|
+
const deadline = Date.now() + options.startupTimeoutMs;
|
|
4
|
+
while (Date.now() < deadline) {
|
|
5
|
+
const attached = await dependencies.attachExisting(options).catch(() => null);
|
|
6
|
+
if (attached !== null) {
|
|
7
|
+
return attached;
|
|
8
|
+
}
|
|
9
|
+
await dependencies.sleep(250);
|
|
10
|
+
}
|
|
11
|
+
throw new Error("managed_bitcoind_service_start_timeout");
|
|
12
|
+
}
|
|
13
|
+
export async function probeManagedBitcoindRuntime(options, dependencies) {
|
|
14
|
+
const paths = dependencies.getPaths(options);
|
|
15
|
+
const candidates = await dependencies.listStatusCandidates({
|
|
16
|
+
dataDir: options.dataDir,
|
|
17
|
+
runtimeRoot: paths.runtimeRoot,
|
|
18
|
+
expectedStatusPath: paths.bitcoindStatusPath,
|
|
19
|
+
});
|
|
20
|
+
const expectedCandidate = candidates.find((candidate) => candidate.statusPath === paths.bitcoindStatusPath) ?? null;
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (!await dependencies.isProcessAlive(candidate.status.processId)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
return dependencies.probeStatusCandidate(candidate.status, options, paths.walletRuntimeRoot);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
compatibility: "unreachable",
|
|
29
|
+
status: expectedCandidate?.status ?? candidates[0]?.status ?? null,
|
|
30
|
+
error: null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function attachOrStartManagedBitcoindRuntime(options, dependencies) {
|
|
34
|
+
const existingProbe = await probeManagedBitcoindRuntime(options, dependencies);
|
|
35
|
+
const existingDecision = resolveManagedBitcoindProbeDecision(existingProbe);
|
|
36
|
+
if (existingDecision.action === "attach") {
|
|
37
|
+
const existing = await dependencies.attachExisting(options);
|
|
38
|
+
if (existing !== null) {
|
|
39
|
+
return existing;
|
|
40
|
+
}
|
|
41
|
+
throw new Error("managed_bitcoind_protocol_error");
|
|
42
|
+
}
|
|
43
|
+
if (existingDecision.action === "reject") {
|
|
44
|
+
throw new Error(existingDecision.error ?? "managed_bitcoind_protocol_error");
|
|
45
|
+
}
|
|
46
|
+
const paths = dependencies.getPaths(options);
|
|
47
|
+
try {
|
|
48
|
+
const lock = await dependencies.acquireStartLock(options, paths);
|
|
49
|
+
try {
|
|
50
|
+
const liveProbe = await probeManagedBitcoindRuntime(options, dependencies);
|
|
51
|
+
const liveDecision = resolveManagedBitcoindProbeDecision(liveProbe);
|
|
52
|
+
if (liveDecision.action === "attach") {
|
|
53
|
+
const reattached = await dependencies.attachExisting(options);
|
|
54
|
+
if (reattached !== null) {
|
|
55
|
+
return reattached;
|
|
56
|
+
}
|
|
57
|
+
throw new Error("managed_bitcoind_protocol_error");
|
|
58
|
+
}
|
|
59
|
+
if (liveDecision.action === "reject") {
|
|
60
|
+
throw new Error(liveDecision.error ?? "managed_bitcoind_protocol_error");
|
|
61
|
+
}
|
|
62
|
+
return await dependencies.startService(options, paths);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await lock.release();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (dependencies.isLockBusyError(error)) {
|
|
70
|
+
return waitForManagedBitcoindRuntime(options, dependencies);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ManagedBitcoindObservedStatus } from "../types.js";
|
|
2
|
+
import type { ManagedBitcoindStatusCandidate } from "./types.js";
|
|
3
|
+
export declare function listManagedBitcoindStatusCandidates(options: {
|
|
4
|
+
dataDir: string;
|
|
5
|
+
runtimeRoot: string;
|
|
6
|
+
expectedStatusPath: string;
|
|
7
|
+
}): Promise<ManagedBitcoindStatusCandidate[]>;
|
|
8
|
+
export declare function readManagedBitcoindObservedStatus(options: {
|
|
9
|
+
dataDir: string;
|
|
10
|
+
walletRootId: string;
|
|
11
|
+
}): Promise<ManagedBitcoindObservedStatus | null>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
4
|
+
import { readJsonFileIfPresent } from "./status.js";
|
|
5
|
+
export async function listManagedBitcoindStatusCandidates(options) {
|
|
6
|
+
const candidates = new Map();
|
|
7
|
+
const addCandidate = async (statusPath, allowDataDirMismatch = false) => {
|
|
8
|
+
const status = await readJsonFileIfPresent(statusPath);
|
|
9
|
+
if (status === null) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (!allowDataDirMismatch && status.dataDir !== options.dataDir) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
candidates.set(statusPath, status);
|
|
16
|
+
};
|
|
17
|
+
await addCandidate(options.expectedStatusPath, true);
|
|
18
|
+
try {
|
|
19
|
+
const entries = await readdir(options.runtimeRoot, {
|
|
20
|
+
withFileTypes: true,
|
|
21
|
+
});
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isDirectory()) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const statusPath = join(options.runtimeRoot, entry.name, "bitcoind-status.json");
|
|
27
|
+
if (statusPath === options.expectedStatusPath) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
await addCandidate(statusPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Missing runtime roots are handled by returning no candidates.
|
|
35
|
+
}
|
|
36
|
+
return [...candidates.entries()].map(([statusPath, status]) => ({
|
|
37
|
+
statusPath,
|
|
38
|
+
status,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
export async function readManagedBitcoindObservedStatus(options) {
|
|
42
|
+
const paths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
|
|
43
|
+
return readJsonFileIfPresent(paths.bitcoindStatusPath);
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ManagedIndexerDaemonObservedStatus } from "../types.js";
|
|
2
|
+
import type { ManagedIndexerDaemonProbeResult, ManagedIndexerRuntimeOptionsLike, ManagedIndexerRuntimePathsLike, ManagedRuntimeLockLike } from "./types.js";
|
|
3
|
+
type ManagedIndexerRuntimeDependencies<TOptions, TClient> = {
|
|
4
|
+
getPaths(options: TOptions): ManagedIndexerRuntimePathsLike;
|
|
5
|
+
probeDaemon(options: TOptions, paths: ManagedIndexerRuntimePathsLike): Promise<ManagedIndexerDaemonProbeResult<TClient>>;
|
|
6
|
+
requestBackgroundFollow(client: TClient, observedStatus: ManagedIndexerDaemonObservedStatus | null): Promise<TClient>;
|
|
7
|
+
closeClient(client: TClient): Promise<void>;
|
|
8
|
+
acquireStartLock(options: TOptions, paths: ManagedIndexerRuntimePathsLike): Promise<ManagedRuntimeLockLike>;
|
|
9
|
+
startDaemon(options: TOptions, paths: ManagedIndexerRuntimePathsLike): Promise<TClient>;
|
|
10
|
+
stopWithLockHeld(options: TOptions, paths: ManagedIndexerRuntimePathsLike, processId: number | null): Promise<unknown>;
|
|
11
|
+
isLockBusyError(error: unknown): boolean;
|
|
12
|
+
sleep(ms: number): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export declare function attachOrStartManagedIndexerRuntime<TOptions extends ManagedIndexerRuntimeOptionsLike, TClient>(options: TOptions, dependencies: ManagedIndexerRuntimeDependencies<TOptions, TClient>): Promise<TClient>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resolveIndexerDaemonProbeDecision } from "./indexer-policy.js";
|
|
2
|
+
async function waitForManagedIndexerRuntime(options, dependencies, paths) {
|
|
3
|
+
const deadline = Date.now() + options.startupTimeoutMs;
|
|
4
|
+
while (Date.now() < deadline) {
|
|
5
|
+
const probe = await dependencies.probeDaemon(options, paths);
|
|
6
|
+
if (probe.compatibility === "compatible" && probe.client !== null) {
|
|
7
|
+
await dependencies.closeClient(probe.client).catch(() => undefined);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (probe.compatibility !== "unreachable") {
|
|
11
|
+
throw new Error(probe.error ?? "indexer_daemon_protocol_error");
|
|
12
|
+
}
|
|
13
|
+
await dependencies.sleep(250);
|
|
14
|
+
}
|
|
15
|
+
throw new Error("indexer_daemon_start_timeout");
|
|
16
|
+
}
|
|
17
|
+
export async function attachOrStartManagedIndexerRuntime(options, dependencies) {
|
|
18
|
+
const paths = dependencies.getPaths(options);
|
|
19
|
+
const existingProbe = await dependencies.probeDaemon(options, paths);
|
|
20
|
+
const existingDecision = resolveIndexerDaemonProbeDecision({
|
|
21
|
+
probe: existingProbe,
|
|
22
|
+
expectedBinaryVersion: options.expectedBinaryVersion ?? null,
|
|
23
|
+
});
|
|
24
|
+
if (existingDecision.action === "attach" && existingProbe.client !== null) {
|
|
25
|
+
try {
|
|
26
|
+
return await dependencies.requestBackgroundFollow(existingProbe.client, existingProbe.status);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
await dependencies.closeClient(existingProbe.client).catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (existingDecision.action === "replace" && existingProbe.client !== null) {
|
|
33
|
+
await dependencies.closeClient(existingProbe.client).catch(() => undefined);
|
|
34
|
+
}
|
|
35
|
+
if (existingDecision.action === "reject") {
|
|
36
|
+
throw new Error(existingDecision.error ?? "indexer_daemon_protocol_error");
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const lock = await dependencies.acquireStartLock(options, paths);
|
|
40
|
+
try {
|
|
41
|
+
const liveProbe = await dependencies.probeDaemon(options, paths);
|
|
42
|
+
const liveDecision = resolveIndexerDaemonProbeDecision({
|
|
43
|
+
probe: liveProbe,
|
|
44
|
+
expectedBinaryVersion: options.expectedBinaryVersion ?? null,
|
|
45
|
+
});
|
|
46
|
+
if (liveDecision.action === "attach" && liveProbe.client !== null) {
|
|
47
|
+
try {
|
|
48
|
+
return await dependencies.requestBackgroundFollow(liveProbe.client, liveProbe.status);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
await dependencies.closeClient(liveProbe.client).catch(() => undefined);
|
|
52
|
+
await dependencies.stopWithLockHeld(options, paths, liveProbe.status?.processId ?? null);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (liveDecision.action === "replace" && liveProbe.client !== null) {
|
|
56
|
+
await dependencies.closeClient(liveProbe.client).catch(() => undefined);
|
|
57
|
+
await dependencies.stopWithLockHeld(options, paths, liveProbe.status?.processId ?? null);
|
|
58
|
+
}
|
|
59
|
+
else if (liveDecision.action === "reject") {
|
|
60
|
+
throw new Error(liveDecision.error ?? "indexer_daemon_protocol_error");
|
|
61
|
+
}
|
|
62
|
+
const daemon = await dependencies.startDaemon(options, paths);
|
|
63
|
+
try {
|
|
64
|
+
return await dependencies.requestBackgroundFollow(daemon, null);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await dependencies.closeClient(daemon).catch(() => undefined);
|
|
68
|
+
throw new Error("indexer_daemon_background_follow_recovery_failed", { cause: error });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await lock.release();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (dependencies.isLockBusyError(error)) {
|
|
77
|
+
await waitForManagedIndexerRuntime(options, dependencies, paths);
|
|
78
|
+
return attachOrStartManagedIndexerRuntime(options, dependencies);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { ClientTip } from "../../types.js";
|
|
2
|
+
import type { ManagedServicePaths } from "../service-paths.js";
|
|
2
3
|
import type { ManagedBitcoindObservedStatus, ManagedIndexerDaemonObservedStatus, ManagedIndexerTruthSource } from "../types.js";
|
|
4
|
+
import type { WalletBitcoindStatus, WalletIndexerStatus, WalletNodeStatus, WalletServiceHealth, WalletSnapshotView } from "../../wallet/read/types.js";
|
|
3
5
|
export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
|
|
4
6
|
export interface ManagedBitcoindServiceProbeResult {
|
|
5
7
|
compatibility: ManagedBitcoindServiceCompatibility;
|
|
6
8
|
status: ManagedBitcoindObservedStatus | null;
|
|
7
9
|
error: string | null;
|
|
8
10
|
}
|
|
11
|
+
export interface ManagedBitcoindStatusCandidate {
|
|
12
|
+
status: ManagedBitcoindObservedStatus;
|
|
13
|
+
statusPath: string;
|
|
14
|
+
}
|
|
9
15
|
export type IndexerDaemonCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "schema-mismatch" | "unreachable" | "protocol-error";
|
|
10
16
|
export interface ManagedIndexerDaemonProbeResult<TClient> {
|
|
11
17
|
compatibility: IndexerDaemonCompatibility;
|
|
@@ -21,6 +27,23 @@ export interface IndexerDaemonProbeDecision {
|
|
|
21
27
|
action: "attach" | "replace" | "start" | "reject";
|
|
22
28
|
error: string | null;
|
|
23
29
|
}
|
|
30
|
+
export interface ManagedRuntimeLockLike {
|
|
31
|
+
release(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export type ManagedBitcoindRuntimePathsLike = ManagedServicePaths;
|
|
34
|
+
export type ManagedIndexerRuntimePathsLike = ManagedServicePaths;
|
|
35
|
+
export interface ManagedBitcoindRuntimeOptionsLike {
|
|
36
|
+
dataDir: string;
|
|
37
|
+
walletRootId: string;
|
|
38
|
+
startupTimeoutMs: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ManagedIndexerRuntimeOptionsLike {
|
|
41
|
+
dataDir: string;
|
|
42
|
+
walletRootId: string;
|
|
43
|
+
startupTimeoutMs: number;
|
|
44
|
+
shutdownTimeoutMs?: number;
|
|
45
|
+
expectedBinaryVersion?: string | null;
|
|
46
|
+
}
|
|
24
47
|
export interface ManagedIndexerSnapshotLike {
|
|
25
48
|
tip: ClientTip | null;
|
|
26
49
|
daemonInstanceId?: string | null;
|
|
@@ -35,3 +58,20 @@ export interface ManagedIndexerStatusProjection {
|
|
|
35
58
|
snapshotSeq: string | null;
|
|
36
59
|
openedAtUnixMs: number | null;
|
|
37
60
|
}
|
|
61
|
+
export interface ManagedWalletNodeConnection<TNodeHandle, TRpc> {
|
|
62
|
+
handle: TNodeHandle | null;
|
|
63
|
+
rpc: TRpc | null;
|
|
64
|
+
status: WalletNodeStatus | null;
|
|
65
|
+
observedStatus: ManagedBitcoindObservedStatus | null;
|
|
66
|
+
error: string | null;
|
|
67
|
+
}
|
|
68
|
+
export interface ManagedWalletReadServiceBundle<TNodeHandle, TRpc, TDaemonClient> {
|
|
69
|
+
node: ManagedWalletNodeConnection<TNodeHandle, TRpc>;
|
|
70
|
+
bitcoind: WalletBitcoindStatus;
|
|
71
|
+
nodeHealth: WalletServiceHealth;
|
|
72
|
+
nodeMessage: string | null;
|
|
73
|
+
daemonClient: TDaemonClient | null;
|
|
74
|
+
indexer: WalletIndexerStatus;
|
|
75
|
+
snapshot: WalletSnapshotView | null;
|
|
76
|
+
close(): Promise<void>;
|
|
77
|
+
}
|
package/dist/bitcoind/node.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { resolveDefaultBitcoindDataDirForTesting } from "../app-paths.js";
|
|
2
|
-
import { BitcoinRpcClient } from "./rpc.js";
|
|
2
|
+
import { BitcoinRpcClient, type RpcTransportOptions } from "./rpc.js";
|
|
3
3
|
import type { BitcoindRpcConfig, InternalManagedBitcoindOptions, ManagedBitcoindNodeHandle } from "./types.js";
|
|
4
4
|
export { resolveDefaultBitcoindDataDirForTesting };
|
|
5
5
|
export declare function buildBitcoindArgsForTesting(options: InternalManagedBitcoindOptions, rpcPort: number, zmqPort: number, p2pPort: number): string[];
|
|
6
6
|
export declare function validateNodeConfigForTesting(rpcClient: BitcoinRpcClient, expectedChain: "main" | "regtest", zmqEndpoint: string): Promise<void>;
|
|
7
7
|
export declare function launchManagedBitcoindNode(options: InternalManagedBitcoindOptions): Promise<ManagedBitcoindNodeHandle>;
|
|
8
|
-
export declare function createRpcClient(config: BitcoindRpcConfig): BitcoinRpcClient;
|
|
8
|
+
export declare function createRpcClient(config: BitcoindRpcConfig, options?: RpcTransportOptions): BitcoinRpcClient;
|
package/dist/bitcoind/node.js
CHANGED
|
@@ -217,6 +217,6 @@ export async function launchManagedBitcoindNode(options) {
|
|
|
217
217
|
},
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
|
-
export function createRpcClient(config) {
|
|
221
|
-
return new BitcoinRpcClient(config.url, config.cookieFile);
|
|
220
|
+
export function createRpcClient(config, options = {}) {
|
|
221
|
+
return new BitcoinRpcClient(config.url, config.cookieFile, options);
|
|
222
222
|
}
|
package/dist/bitcoind/rpc.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ interface RpcResponsePayload {
|
|
|
7
7
|
readonly statusCode: number;
|
|
8
8
|
readonly bodyText: string;
|
|
9
9
|
}
|
|
10
|
-
interface RpcTransportOptions {
|
|
10
|
+
export interface RpcTransportOptions {
|
|
11
11
|
fetchImpl?: typeof fetch;
|
|
12
12
|
requestTimeoutMs?: number;
|
|
13
|
+
abortSignal?: AbortSignal;
|
|
13
14
|
requestImpl?: (request: {
|
|
14
15
|
url: URL;
|
|
15
16
|
payload: RpcRequestPayload;
|
package/dist/bitcoind/rpc.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { request as httpRequest } from "node:http";
|
|
2
2
|
import { request as httpsRequest } from "node:https";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
|
+
const DEFAULT_MANAGED_RPC_REQUEST_TIMEOUT_MS = 30_000;
|
|
4
5
|
export class BitcoinRpcClient {
|
|
5
6
|
#url;
|
|
6
7
|
#cookieFile;
|
|
7
8
|
#fetchImpl;
|
|
8
9
|
#requestTimeoutMs;
|
|
10
|
+
#abortSignal;
|
|
9
11
|
#requestImpl;
|
|
10
12
|
constructor(url, cookieFile, options = {}) {
|
|
11
13
|
this.#url = url;
|
|
12
14
|
this.#cookieFile = cookieFile;
|
|
13
15
|
this.#fetchImpl = options.fetchImpl ?? fetch;
|
|
14
|
-
this.#requestTimeoutMs = options.requestTimeoutMs ??
|
|
16
|
+
this.#requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_MANAGED_RPC_REQUEST_TIMEOUT_MS;
|
|
17
|
+
this.#abortSignal = options.abortSignal;
|
|
15
18
|
this.#requestImpl = options.requestImpl ?? this.#sendNodeRequest.bind(this);
|
|
16
19
|
}
|
|
17
20
|
async call(method, params = []) {
|
|
@@ -25,20 +28,67 @@ export class BitcoinRpcClient {
|
|
|
25
28
|
async #callAtUrl(urlString, method, params = []) {
|
|
26
29
|
const payload = await this.#buildRequestPayload(method, params);
|
|
27
30
|
let response;
|
|
28
|
-
const
|
|
31
|
+
const requestSignal = this.#createRequestSignal();
|
|
29
32
|
try {
|
|
30
33
|
response = await this.#fetchImpl(urlString, {
|
|
31
34
|
method: "POST",
|
|
32
35
|
headers: payload.headers,
|
|
33
36
|
body: payload.body,
|
|
34
|
-
signal:
|
|
37
|
+
signal: requestSignal.signal,
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
catch (error) {
|
|
41
|
+
if (this.#abortSignal?.aborted) {
|
|
42
|
+
const reason = this.#abortSignal.reason;
|
|
43
|
+
if (reason instanceof Error) {
|
|
44
|
+
throw reason;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
38
47
|
throw new Error(this.#describeTransportError(urlString, method, error), { cause: error });
|
|
39
48
|
}
|
|
49
|
+
finally {
|
|
50
|
+
requestSignal.dispose();
|
|
51
|
+
}
|
|
40
52
|
return this.#parseResponse(method, response.status, await response.text());
|
|
41
53
|
}
|
|
54
|
+
#createRequestSignal() {
|
|
55
|
+
const timeoutSignal = AbortSignal.timeout(this.#requestTimeoutMs);
|
|
56
|
+
if (this.#abortSignal === undefined) {
|
|
57
|
+
return {
|
|
58
|
+
signal: timeoutSignal,
|
|
59
|
+
dispose() { },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const handleAbort = (source) => {
|
|
64
|
+
controller.abort(source.reason);
|
|
65
|
+
};
|
|
66
|
+
const forwardTimeout = () => {
|
|
67
|
+
handleAbort(timeoutSignal);
|
|
68
|
+
};
|
|
69
|
+
const forwardAbort = () => {
|
|
70
|
+
handleAbort(this.#abortSignal);
|
|
71
|
+
};
|
|
72
|
+
if (timeoutSignal.aborted) {
|
|
73
|
+
handleAbort(timeoutSignal);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
timeoutSignal.addEventListener("abort", forwardTimeout, { once: true });
|
|
77
|
+
}
|
|
78
|
+
if (this.#abortSignal.aborted) {
|
|
79
|
+
handleAbort(this.#abortSignal);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.#abortSignal.addEventListener("abort", forwardAbort, { once: true });
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
dispose: () => {
|
|
87
|
+
timeoutSignal.removeEventListener("abort", forwardTimeout);
|
|
88
|
+
this.#abortSignal?.removeEventListener("abort", forwardAbort);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
42
92
|
async #buildRequestPayload(method, params) {
|
|
43
93
|
let cookie;
|
|
44
94
|
try {
|