@cogcoin/client 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.2.0` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
3
+ `@cogcoin/client@1.2.2` 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
 
@@ -66,7 +66,9 @@ export async function probeManagedBitcoindStatusCandidate(status, options, runti
66
66
  const rpc = createRpcClient(status.rpc);
67
67
  try {
68
68
  await waitForManagedBitcoindRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS);
69
- await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint);
69
+ await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint, {
70
+ requireRawTxZmq: true,
71
+ });
70
72
  return {
71
73
  compatibility: "compatible",
72
74
  status,
@@ -96,7 +98,9 @@ export async function refreshManagedBitcoindStatus(status, paths, options) {
96
98
  const targetWalletRootId = options.walletRootId ?? status.walletRootId;
97
99
  try {
98
100
  await waitForManagedBitcoindRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS);
99
- await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint);
101
+ await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint, {
102
+ requireRawTxZmq: true,
103
+ });
100
104
  const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, targetWalletRootId, status.dataDir);
101
105
  const nextStatus = {
102
106
  ...status,
@@ -8,6 +8,9 @@ function isRuntimeMismatchError(error) {
8
8
  return error.message.startsWith("bitcoind_chain_expected_")
9
9
  || error.message === "managed_bitcoind_runtime_mismatch";
10
10
  }
11
+ function isMissingRawTxZmqError(error) {
12
+ return error instanceof Error && error.message === "bitcoind_zmq_rawtx_missing";
13
+ }
11
14
  function isUnreachableManagedBitcoindError(error) {
12
15
  if (error instanceof Error) {
13
16
  if ("code" in error) {
@@ -47,6 +50,13 @@ export function mapManagedBitcoindValidationError(error, status) {
47
50
  };
48
51
  }
49
52
  export function mapManagedBitcoindRuntimeProbeFailure(error, status) {
53
+ if (isMissingRawTxZmqError(error)) {
54
+ return {
55
+ compatibility: "rawtx-zmq-missing",
56
+ status,
57
+ error: "bitcoind_zmq_rawtx_missing",
58
+ };
59
+ }
50
60
  if (isRuntimeMismatchError(error)) {
51
61
  return {
52
62
  compatibility: "runtime-mismatch",
@@ -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" | "unreachable" | "protocol-error";
5
+ export type ManagedBitcoindServiceCompatibility = "compatible" | "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;
@@ -108,6 +108,13 @@ export const serviceErrorRules = [
108
108
  next: "Run `cogcoin repair` so the wallet can clear the conflicting runtime and restart a compatible managed bitcoind service.",
109
109
  };
110
110
  }
111
+ if (errorCode === "bitcoind_zmq_rawtx_missing" || errorCode.includes("rawtx_zmq_missing")) {
112
+ return {
113
+ what: "The live managed bitcoind service is missing raw transaction ZMQ.",
114
+ why: "This usually means an older managed bitcoind runtime is still running without the v1.2.0 `zmqpubrawtx` setting.",
115
+ next: "Run `cogcoin repair` so the wallet can stop the stale managed bitcoind service and restart it with the current ZMQ configuration.",
116
+ };
117
+ }
111
118
  if (errorCode.includes("bitcoind_replica_missing")) {
112
119
  return {
113
120
  what: "The managed Core wallet replica is missing.",
@@ -4,7 +4,7 @@ import { persistWalletCoinControlStateIfNeeded } from "../coin-control.js";
4
4
  import { createWalletSecretReference } from "../state/provider.js";
5
5
  import { recreateManagedCoreWalletReplica, verifyManagedCoreWalletReplica } from "./managed-core.js";
6
6
  import { pathExists } from "./context.js";
7
- import { clearManagedBitcoindArtifacts, mapBitcoindCompatibilityToRepairIssue, mapBitcoindRepairHealth, waitForProcessExit, } from "./repair-runtime.js";
7
+ import { clearManagedBitcoindArtifacts, isManagedBitcoindRpcUnavailableError, mapBitcoindCompatibilityToRepairIssue, mapBitcoindRepairHealth, waitForProcessExit, } from "./repair-runtime.js";
8
8
  export async function repairManagedBitcoindStage(options) {
9
9
  let state = options.state;
10
10
  let repairStateNeedsPersist = options.repairStateNeedsPersist;
@@ -33,28 +33,38 @@ export async function repairManagedBitcoindStage(options) {
33
33
  bitcoindCompatibilityIssue = mapBitcoindCompatibilityToRepairIssue(initialBitcoindProbe.compatibility);
34
34
  if (initialBitcoindProbe.compatibility === "service-version-mismatch"
35
35
  || initialBitcoindProbe.compatibility === "wallet-root-mismatch"
36
- || initialBitcoindProbe.compatibility === "runtime-mismatch") {
36
+ || initialBitcoindProbe.compatibility === "runtime-mismatch"
37
+ || initialBitcoindProbe.compatibility === "rawtx-zmq-missing") {
37
38
  const processId = initialBitcoindProbe.status?.processId ?? null;
38
39
  if (processId === null) {
39
- throw new Error("managed_bitcoind_process_id_unavailable");
40
- }
41
- try {
42
- process.kill(processId, "SIGTERM");
40
+ if (initialBitcoindProbe.compatibility !== "rawtx-zmq-missing") {
41
+ throw new Error("managed_bitcoind_process_id_unavailable");
42
+ }
43
+ await clearManagedBitcoindArtifacts(options.servicePaths);
44
+ bitcoindServiceAction = "restarted-missing-rawtx-zmq";
43
45
  }
44
- catch (error) {
45
- if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
46
- throw error;
46
+ else {
47
+ try {
48
+ process.kill(processId, "SIGTERM");
49
+ }
50
+ catch (error) {
51
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
52
+ throw error;
53
+ }
47
54
  }
55
+ await waitForProcessExit(processId, 15_000, "managed_bitcoind_stop_timeout");
56
+ await clearManagedBitcoindArtifacts(options.servicePaths);
57
+ bitcoindServiceAction = initialBitcoindProbe.compatibility === "rawtx-zmq-missing"
58
+ ? "restarted-missing-rawtx-zmq"
59
+ : "stopped-incompatible-service";
48
60
  }
49
- await waitForProcessExit(processId, 15_000, "managed_bitcoind_stop_timeout");
50
- await clearManagedBitcoindArtifacts(options.servicePaths);
51
- bitcoindServiceAction = "stopped-incompatible-service";
52
61
  }
53
62
  else if (initialBitcoindProbe.compatibility === "unreachable") {
54
63
  const hasStaleArtifacts = await Promise.all([
55
64
  options.servicePaths.bitcoindStatusPath,
56
65
  options.servicePaths.bitcoindPidPath,
57
66
  options.servicePaths.bitcoindReadyPath,
67
+ options.servicePaths.bitcoindRuntimeConfigPath,
58
68
  options.servicePaths.bitcoindWalletStatusPath,
59
69
  ].map(pathExists));
60
70
  if (hasStaleArtifacts.some(Boolean)) {
@@ -69,74 +79,105 @@ export async function repairManagedBitcoindStage(options) {
69
79
  finally {
70
80
  await bitcoindLock.release();
71
81
  }
72
- const bitcoindHandle = await options.context.attachService({
73
- dataDir: options.context.dataDir,
74
- chain: "main",
75
- startHeight: 0,
76
- walletRootId: state.walletRootId,
77
- });
78
- try {
79
- const rpc = options.context.rpcFactory(bitcoindHandle.rpc);
80
- const normalizedDescriptorState = await normalizeWalletDescriptorState(state, rpc);
81
- if (normalizedDescriptorState.changed) {
82
- state = normalizedDescriptorState.state;
83
- repairStateNeedsPersist = true;
84
- }
85
- const reconciledCoinControl = await persistWalletCoinControlStateIfNeeded({
86
- state,
87
- access: {
88
- provider: options.context.provider,
89
- secretReference: createWalletSecretReference(state.walletRootId),
90
- },
91
- paths: options.context.paths,
92
- nowUnixMs: options.context.nowUnixMs,
93
- replacePrimary: options.recoveredFromBackup && !repairStateNeedsPersist,
94
- rpc,
95
- });
96
- state = reconciledCoinControl.state;
97
- if (reconciledCoinControl.changed) {
98
- repairStateNeedsPersist = false;
99
- }
100
- let replica = await verifyManagedCoreWalletReplica(state, options.context.dataDir, {
101
- nodeHandle: bitcoindHandle,
102
- attachService: options.context.attachService,
103
- rpcFactory: options.context.rpcFactory,
104
- });
105
- if (replica.proofStatus !== "ready") {
106
- state = await recreateManagedCoreWalletReplica(state, options.context.provider, options.context.paths, options.context.dataDir, options.context.nowUnixMs, {
107
- attachService: options.context.attachService,
108
- rpcFactory: options.context.rpcFactory,
82
+ for (let attachAttempt = 0; attachAttempt < 2; attachAttempt += 1) {
83
+ let bitcoindHandle = null;
84
+ let handleClosed = false;
85
+ try {
86
+ bitcoindHandle = await options.context.attachService({
87
+ dataDir: options.context.dataDir,
88
+ chain: "main",
89
+ startHeight: 0,
90
+ walletRootId: state.walletRootId,
91
+ });
92
+ const rpc = options.context.rpcFactory(bitcoindHandle.rpc);
93
+ const normalizedDescriptorState = await normalizeWalletDescriptorState(state, rpc);
94
+ if (normalizedDescriptorState.changed) {
95
+ state = normalizedDescriptorState.state;
96
+ repairStateNeedsPersist = true;
97
+ }
98
+ const reconciledCoinControl = await persistWalletCoinControlStateIfNeeded({
99
+ state,
100
+ access: {
101
+ provider: options.context.provider,
102
+ secretReference: createWalletSecretReference(state.walletRootId),
103
+ },
104
+ paths: options.context.paths,
105
+ nowUnixMs: options.context.nowUnixMs,
106
+ replacePrimary: options.recoveredFromBackup && !repairStateNeedsPersist,
107
+ rpc,
109
108
  });
110
- recreatedManagedCoreWallet = true;
111
- managedCoreReplicaAction = "recreated";
112
- repairStateNeedsPersist = false;
113
- replica = await verifyManagedCoreWalletReplica(state, options.context.dataDir, {
109
+ state = reconciledCoinControl.state;
110
+ if (reconciledCoinControl.changed) {
111
+ repairStateNeedsPersist = false;
112
+ }
113
+ let replica = await verifyManagedCoreWalletReplica(state, options.context.dataDir, {
114
114
  nodeHandle: bitcoindHandle,
115
115
  attachService: options.context.attachService,
116
116
  rpcFactory: options.context.rpcFactory,
117
117
  });
118
+ if (replica.proofStatus !== "ready") {
119
+ state = await recreateManagedCoreWalletReplica(state, options.context.provider, options.context.paths, options.context.dataDir, options.context.nowUnixMs, {
120
+ attachService: options.context.attachService,
121
+ rpcFactory: options.context.rpcFactory,
122
+ });
123
+ recreatedManagedCoreWallet = true;
124
+ managedCoreReplicaAction = "recreated";
125
+ repairStateNeedsPersist = false;
126
+ replica = await verifyManagedCoreWalletReplica(state, options.context.dataDir, {
127
+ nodeHandle: bitcoindHandle,
128
+ attachService: options.context.attachService,
129
+ rpcFactory: options.context.rpcFactory,
130
+ });
131
+ }
132
+ const finalBitcoindStatus = await bitcoindHandle.refreshServiceStatus?.() ?? null;
133
+ const chainInfo = await rpc.getBlockchainInfo();
134
+ bitcoindPostRepairHealth = mapBitcoindRepairHealth({
135
+ serviceState: finalBitcoindStatus?.state ?? null,
136
+ catchingUp: chainInfo.blocks < chainInfo.headers,
137
+ replica,
138
+ });
139
+ if (bitcoindServiceAction === "none" && initialBitcoindProbe.compatibility === "unreachable") {
140
+ bitcoindServiceAction = "restarted-compatible-service";
141
+ }
142
+ return {
143
+ state,
144
+ repairStateNeedsPersist,
145
+ recreatedManagedCoreWallet,
146
+ bitcoindServiceAction,
147
+ bitcoindCompatibilityIssue,
148
+ managedCoreReplicaAction,
149
+ bitcoindPostRepairHealth,
150
+ };
118
151
  }
119
- const finalBitcoindStatus = await bitcoindHandle.refreshServiceStatus?.() ?? null;
120
- const chainInfo = await rpc.getBlockchainInfo();
121
- bitcoindPostRepairHealth = mapBitcoindRepairHealth({
122
- serviceState: finalBitcoindStatus?.state ?? null,
123
- catchingUp: chainInfo.blocks < chainInfo.headers,
124
- replica,
125
- });
126
- if (bitcoindServiceAction === "none" && initialBitcoindProbe.compatibility === "unreachable") {
127
- bitcoindServiceAction = "restarted-compatible-service";
152
+ catch (error) {
153
+ if (bitcoindHandle !== null) {
154
+ await bitcoindHandle.stop?.().catch(() => undefined);
155
+ handleClosed = true;
156
+ }
157
+ if (attachAttempt === 0 && isManagedBitcoindRpcUnavailableError(error)) {
158
+ const retryLock = await acquireFileLock(options.servicePaths.bitcoindLockPath, {
159
+ purpose: "managed-bitcoind-repair-stale-rpc",
160
+ walletRootId: state.walletRootId,
161
+ dataDir: options.context.dataDir,
162
+ });
163
+ try {
164
+ await clearManagedBitcoindArtifacts(options.servicePaths);
165
+ }
166
+ finally {
167
+ await retryLock.release();
168
+ }
169
+ if (bitcoindServiceAction === "none") {
170
+ bitcoindServiceAction = "restarted-compatible-service";
171
+ }
172
+ continue;
173
+ }
174
+ throw error;
175
+ }
176
+ finally {
177
+ if (!handleClosed) {
178
+ await bitcoindHandle?.stop?.().catch(() => undefined);
179
+ }
128
180
  }
129
181
  }
130
- finally {
131
- await bitcoindHandle.stop?.().catch(() => undefined);
132
- }
133
- return {
134
- state,
135
- repairStateNeedsPersist,
136
- recreatedManagedCoreWallet,
137
- bitcoindServiceAction,
138
- bitcoindCompatibilityIssue,
139
- managedCoreReplicaAction,
140
- bitcoindPostRepairHealth,
141
- };
182
+ throw new Error("managed_bitcoind_repair_retry_exhausted");
142
183
  }
@@ -29,6 +29,7 @@ export declare function verifyIndexerPostRepairHealth(options: {
29
29
  daemonInstanceId: string;
30
30
  }>;
31
31
  export declare function isProcessAlive(pid: number | null): Promise<boolean>;
32
+ export declare function isManagedBitcoindRpcUnavailableError(error: unknown): boolean;
32
33
  export declare function waitForProcessExit(pid: number, timeoutMs?: number, errorCode?: string): Promise<void>;
33
34
  export declare function clearIndexerDaemonArtifacts(servicePaths: ReturnType<typeof resolveManagedServicePaths>): Promise<void>;
34
35
  export declare function clearManagedBitcoindArtifacts(servicePaths: ReturnType<typeof resolveManagedServicePaths>): Promise<void>;
@@ -69,6 +69,8 @@ export function mapBitcoindCompatibilityToRepairIssue(compatibility) {
69
69
  return "wallet-root-mismatch";
70
70
  case "runtime-mismatch":
71
71
  return "runtime-mismatch";
72
+ case "rawtx-zmq-missing":
73
+ return "rawtx-zmq-missing";
72
74
  default:
73
75
  return "none";
74
76
  }
@@ -151,6 +153,22 @@ export async function isProcessAlive(pid) {
151
153
  return true;
152
154
  }
153
155
  }
156
+ export function isManagedBitcoindRpcUnavailableError(error) {
157
+ if (error instanceof Error && "code" in error) {
158
+ const code = error.code;
159
+ if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE") {
160
+ return true;
161
+ }
162
+ }
163
+ if (!(error instanceof Error)) {
164
+ return false;
165
+ }
166
+ return error.message === "bitcoind_cookie_timeout"
167
+ || error.message.includes("cookie file is unavailable")
168
+ || error.message.includes("ECONNREFUSED")
169
+ || error.message.includes("ECONNRESET")
170
+ || error.message.includes("socket hang up");
171
+ }
154
172
  export async function waitForProcessExit(pid, timeoutMs = 15_000, errorCode = "indexer_daemon_stop_timeout") {
155
173
  const deadline = Date.now() + timeoutMs;
156
174
  while (Date.now() < deadline) {
@@ -169,6 +187,7 @@ export async function clearManagedBitcoindArtifacts(servicePaths) {
169
187
  await rm(servicePaths.bitcoindStatusPath, { force: true }).catch(() => undefined);
170
188
  await rm(servicePaths.bitcoindPidPath, { force: true }).catch(() => undefined);
171
189
  await rm(servicePaths.bitcoindReadyPath, { force: true }).catch(() => undefined);
190
+ await rm(servicePaths.bitcoindRuntimeConfigPath, { force: true }).catch(() => undefined);
172
191
  await rm(servicePaths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
173
192
  }
174
193
  export async function stopRecordedManagedProcess(pid, errorCode) {
@@ -36,8 +36,8 @@ export interface WalletRepairResult {
36
36
  recoveredFromBackup: boolean;
37
37
  recreatedManagedCoreWallet: boolean;
38
38
  resetIndexerDatabase: boolean;
39
- bitcoindServiceAction: "none" | "cleared-stale-artifacts" | "stopped-incompatible-service" | "restarted-compatible-service";
40
- bitcoindCompatibilityIssue: "none" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch";
39
+ bitcoindServiceAction: "none" | "cleared-stale-artifacts" | "stopped-incompatible-service" | "restarted-compatible-service" | "restarted-missing-rawtx-zmq";
40
+ bitcoindCompatibilityIssue: "none" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "rawtx-zmq-missing";
41
41
  managedCoreReplicaAction: "none" | "recreated";
42
42
  bitcoindPostRepairHealth: "ready" | "catching-up" | "starting" | "failed" | "unavailable";
43
43
  indexerDaemonAction: "none" | "cleared-stale-artifacts" | "stopped-incompatible-daemon" | "restarted-compatible-daemon";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",