@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.
Files changed (109) hide show
  1. package/README.md +2 -2
  2. package/dist/bitcoind/indexer-daemon.js +29 -79
  3. package/dist/bitcoind/managed-runtime/bitcoind-runtime.d.ts +20 -0
  4. package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
  5. package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
  6. package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
  7. package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
  8. package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
  9. package/dist/bitcoind/managed-runtime/types.d.ts +40 -0
  10. package/dist/bitcoind/node.d.ts +2 -2
  11. package/dist/bitcoind/node.js +2 -2
  12. package/dist/bitcoind/rpc.d.ts +2 -1
  13. package/dist/bitcoind/rpc.js +53 -3
  14. package/dist/bitcoind/service.js +46 -126
  15. package/dist/cli/command-registry.d.ts +1 -1
  16. package/dist/cli/command-registry.js +2 -64
  17. package/dist/cli/commands/client-admin.js +3 -18
  18. package/dist/cli/commands/mining-runtime.js +4 -60
  19. package/dist/cli/commands/wallet-admin.js +6 -6
  20. package/dist/cli/context.js +1 -3
  21. package/dist/cli/mining-json.d.ts +1 -22
  22. package/dist/cli/mining-json.js +0 -23
  23. package/dist/cli/output.js +16 -2
  24. package/dist/cli/parse.js +0 -2
  25. package/dist/cli/preview-json.d.ts +1 -22
  26. package/dist/cli/preview-json.js +0 -19
  27. package/dist/cli/types.d.ts +1 -3
  28. package/dist/cli/wallet-format.js +1 -1
  29. package/dist/cli/workflow-hints.d.ts +1 -2
  30. package/dist/cli/workflow-hints.js +5 -8
  31. package/dist/wallet/lifecycle/context.js +0 -1
  32. package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
  33. package/dist/wallet/lifecycle/repair-mining.js +5 -39
  34. package/dist/wallet/lifecycle/repair.js +0 -3
  35. package/dist/wallet/lifecycle/setup.js +10 -8
  36. package/dist/wallet/lifecycle/types.d.ts +1 -4
  37. package/dist/wallet/managed-core-wallet.d.ts +2 -0
  38. package/dist/wallet/managed-core-wallet.js +27 -1
  39. package/dist/wallet/mining/candidate.d.ts +1 -0
  40. package/dist/wallet/mining/candidate.js +38 -6
  41. package/dist/wallet/mining/competitiveness.d.ts +1 -0
  42. package/dist/wallet/mining/competitiveness.js +6 -0
  43. package/dist/wallet/mining/cycle.d.ts +2 -0
  44. package/dist/wallet/mining/cycle.js +14 -4
  45. package/dist/wallet/mining/engine-types.d.ts +1 -0
  46. package/dist/wallet/mining/index.d.ts +1 -1
  47. package/dist/wallet/mining/index.js +1 -1
  48. package/dist/wallet/mining/publish.d.ts +3 -0
  49. package/dist/wallet/mining/publish.js +78 -6
  50. package/dist/wallet/mining/runner.d.ts +0 -32
  51. package/dist/wallet/mining/runner.js +59 -104
  52. package/dist/wallet/mining/stop.d.ts +7 -0
  53. package/dist/wallet/mining/stop.js +23 -0
  54. package/dist/wallet/mining/supervisor.d.ts +2 -36
  55. package/dist/wallet/mining/supervisor.js +139 -246
  56. package/dist/wallet/read/context.d.ts +1 -5
  57. package/dist/wallet/read/context.js +20 -204
  58. package/dist/wallet/read/managed-services.d.ts +33 -0
  59. package/dist/wallet/read/managed-services.js +222 -0
  60. package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
  61. package/dist/wallet/state/client-password/bootstrap.js +3 -0
  62. package/dist/wallet/state/client-password/context.d.ts +10 -0
  63. package/dist/wallet/state/client-password/context.js +46 -0
  64. package/dist/wallet/state/client-password/crypto.d.ts +34 -0
  65. package/dist/wallet/state/client-password/crypto.js +117 -0
  66. package/dist/wallet/state/client-password/files.d.ts +10 -0
  67. package/dist/wallet/state/client-password/files.js +109 -0
  68. package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
  69. package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
  70. package/dist/wallet/state/client-password/messages.d.ts +3 -0
  71. package/dist/wallet/state/client-password/messages.js +9 -0
  72. package/dist/wallet/state/client-password/migration.d.ts +4 -0
  73. package/dist/wallet/state/client-password/migration.js +32 -0
  74. package/dist/wallet/state/client-password/prompts.d.ts +12 -0
  75. package/dist/wallet/state/client-password/prompts.js +79 -0
  76. package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
  77. package/dist/wallet/state/client-password/protected-secrets.js +90 -0
  78. package/dist/wallet/state/client-password/readiness.d.ts +4 -0
  79. package/dist/wallet/state/client-password/readiness.js +48 -0
  80. package/dist/wallet/state/client-password/references.d.ts +1 -0
  81. package/dist/wallet/state/client-password/references.js +56 -0
  82. package/dist/wallet/state/client-password/rotation.d.ts +6 -0
  83. package/dist/wallet/state/client-password/rotation.js +98 -0
  84. package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
  85. package/dist/wallet/state/client-password/session-policy.js +28 -0
  86. package/dist/wallet/state/client-password/session.d.ts +19 -0
  87. package/dist/wallet/state/client-password/session.js +170 -0
  88. package/dist/wallet/state/client-password/setup.d.ts +8 -0
  89. package/dist/wallet/state/client-password/setup.js +49 -0
  90. package/dist/wallet/state/client-password/types.d.ts +82 -0
  91. package/dist/wallet/state/client-password/types.js +5 -0
  92. package/dist/wallet/state/client-password.d.ts +7 -38
  93. package/dist/wallet/state/client-password.js +52 -937
  94. package/dist/wallet/tx/anchor.js +123 -216
  95. package/dist/wallet/tx/cog.js +294 -489
  96. package/dist/wallet/tx/common.d.ts +2 -0
  97. package/dist/wallet/tx/common.js +2 -0
  98. package/dist/wallet/tx/domain-admin.js +111 -220
  99. package/dist/wallet/tx/domain-market.js +401 -681
  100. package/dist/wallet/tx/executor.d.ts +176 -0
  101. package/dist/wallet/tx/executor.js +302 -0
  102. package/dist/wallet/tx/field.js +109 -215
  103. package/dist/wallet/tx/register.js +158 -269
  104. package/dist/wallet/tx/reputation.js +120 -227
  105. package/package.json +1 -1
  106. package/dist/wallet/mining/worker-main.d.ts +0 -1
  107. package/dist/wallet/mining/worker-main.js +0 -17
  108. package/dist/wallet/state/client-password-agent.d.ts +0 -1
  109. 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.6` 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.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 start`, `mine stop`, `mine status`, `mine log`, `mine setup`, `mine prompt`, and `mine prompt list`
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, resolveIndexerDaemonProbeDecision, validateIndexerDaemonStatus, validateIndexerSnapshotHandle, validateIndexerSnapshotPayload, } from "./managed-runtime/indexer-policy.js";
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
- const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
351
- const existingDecision = resolveIndexerDaemonProbeDecision({
352
- probe: existingProbe,
351
+ return attachOrStartManagedIndexerRuntime({
352
+ ...options,
353
+ walletRootId,
354
+ startupTimeoutMs,
353
355
  expectedBinaryVersion,
354
- });
355
- if (existingDecision.action === "attach" && existingProbe.client !== null) {
356
- try {
357
- return await requestBackgroundFollow(existingProbe.client, existingProbe.status);
358
- }
359
- catch {
360
- await existingProbe.client.close().catch(() => undefined);
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: options.dataDir,
374
- databasePath: options.databasePath,
375
- });
376
- try {
377
- const liveProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
378
- const liveDecision = resolveIndexerDaemonProbeDecision({
379
- probe: liveProbe,
380
- expectedBinaryVersion,
381
- });
382
- if (liveDecision.action === "attach" && liveProbe.client !== null) {
383
- try {
384
- return await requestBackgroundFollow(liveProbe.client, liveProbe.status);
385
- }
386
- catch {
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
+ }
@@ -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;
@@ -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
  }
@@ -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;
@@ -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 ?? 15_000;
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 abortSignal = AbortSignal.timeout(this.#requestTimeoutMs);
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: abortSignal,
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 {