@cogcoin/client 1.2.1 → 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.
|
|
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,
|
|
@@ -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;
|
|
@@ -79,74 +79,105 @@ export async function repairManagedBitcoindStage(options) {
|
|
|
79
79
|
finally {
|
|
80
80
|
await bitcoindLock.release();
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (normalizedDescriptorState.changed) {
|
|
92
|
-
state = normalizedDescriptorState.state;
|
|
93
|
-
repairStateNeedsPersist = true;
|
|
94
|
-
}
|
|
95
|
-
const reconciledCoinControl = await persistWalletCoinControlStateIfNeeded({
|
|
96
|
-
state,
|
|
97
|
-
access: {
|
|
98
|
-
provider: options.context.provider,
|
|
99
|
-
secretReference: createWalletSecretReference(state.walletRootId),
|
|
100
|
-
},
|
|
101
|
-
paths: options.context.paths,
|
|
102
|
-
nowUnixMs: options.context.nowUnixMs,
|
|
103
|
-
replacePrimary: options.recoveredFromBackup && !repairStateNeedsPersist,
|
|
104
|
-
rpc,
|
|
105
|
-
});
|
|
106
|
-
state = reconciledCoinControl.state;
|
|
107
|
-
if (reconciledCoinControl.changed) {
|
|
108
|
-
repairStateNeedsPersist = false;
|
|
109
|
-
}
|
|
110
|
-
let replica = await verifyManagedCoreWalletReplica(state, options.context.dataDir, {
|
|
111
|
-
nodeHandle: bitcoindHandle,
|
|
112
|
-
attachService: options.context.attachService,
|
|
113
|
-
rpcFactory: options.context.rpcFactory,
|
|
114
|
-
});
|
|
115
|
-
if (replica.proofStatus !== "ready") {
|
|
116
|
-
state = await recreateManagedCoreWalletReplica(state, options.context.provider, options.context.paths, options.context.dataDir, options.context.nowUnixMs, {
|
|
117
|
-
attachService: options.context.attachService,
|
|
118
|
-
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,
|
|
119
91
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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,
|
|
108
|
+
});
|
|
109
|
+
state = reconciledCoinControl.state;
|
|
110
|
+
if (reconciledCoinControl.changed) {
|
|
111
|
+
repairStateNeedsPersist = false;
|
|
112
|
+
}
|
|
113
|
+
let replica = await verifyManagedCoreWalletReplica(state, options.context.dataDir, {
|
|
124
114
|
nodeHandle: bitcoindHandle,
|
|
125
115
|
attachService: options.context.attachService,
|
|
126
116
|
rpcFactory: options.context.rpcFactory,
|
|
127
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
|
+
};
|
|
128
151
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
180
|
}
|
|
139
181
|
}
|
|
140
|
-
|
|
141
|
-
await bitcoindHandle.stop?.().catch(() => undefined);
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
state,
|
|
145
|
-
repairStateNeedsPersist,
|
|
146
|
-
recreatedManagedCoreWallet,
|
|
147
|
-
bitcoindServiceAction,
|
|
148
|
-
bitcoindCompatibilityIssue,
|
|
149
|
-
managedCoreReplicaAction,
|
|
150
|
-
bitcoindPostRepairHealth,
|
|
151
|
-
};
|
|
182
|
+
throw new Error("managed_bitcoind_repair_retry_exhausted");
|
|
152
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>;
|
|
@@ -153,6 +153,22 @@ export async function isProcessAlive(pid) {
|
|
|
153
153
|
return true;
|
|
154
154
|
}
|
|
155
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
|
+
}
|
|
156
172
|
export async function waitForProcessExit(pid, timeoutMs = 15_000, errorCode = "indexer_daemon_stop_timeout") {
|
|
157
173
|
const deadline = Date.now() + timeoutMs;
|
|
158
174
|
while (Date.now() < deadline) {
|
package/package.json
CHANGED