@cogcoin/client 1.1.16 → 1.2.1

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,22 +1,15 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.16` 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.1` 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
 
7
- ## Quick Start
8
-
9
- Install Cogcoin:
7
+ ## Installation
10
8
 
11
9
  ```bash
12
10
  curl -fsSL https://cogcoin.org/install.sh | bash
13
11
  # or on Windows PowerShell:
14
12
  powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://cogcoin.org/install.ps1 | iex"
15
- cogcoin address # Send 0.0015 BTC to address
16
- cogcoin register <domainname> # 6+ character domain for 0.001 BTC
17
- cogcoin anchor <domainname> # You can leave a founding message permanently on Bitcoin!
18
- cogcoin mine setup
19
- cogcoin mine # Use remaining ~0.0005 BTC for mining tx, ~1000 sats per entry (0.00001 BTC)
20
13
  ```
21
14
 
22
15
  ### What The Installer Does
@@ -33,6 +26,16 @@ cogcoin mine # Use remaining ~0.0005 BTC for mining tx, ~1000 sats per entry (0.
33
26
  - If you set `COGCOIN_SKIP_INIT=1`, the installer skips `cogcoin init` and prints the exact manual command to run later.
34
27
  - If macOS Command Line Tools are still installing, the installer either waits and retries automatically or prints the exact resume command.
35
28
 
29
+ ## Quick Start
30
+
31
+ ```bash
32
+ cogcoin address # Send 0.0015 BTC to address
33
+ cogcoin register <domainname> # 6+ character domain for 0.001 BTC
34
+ cogcoin anchor <domainname> # You can leave a founding message permanently on Bitcoin!
35
+ cogcoin mine setup
36
+ cogcoin mine # Use remaining ~0.0005 BTC for mining tx, ~1000 sats per entry (0.00001 BTC)
37
+ ```
38
+
36
39
  ## Preview
37
40
 
38
41
  ```bash
@@ -112,7 +115,7 @@ The published package depends on:
112
115
 
113
116
  - `@cogcoin/bitcoin@30.2.0`
114
117
  - `@cogcoin/genesis@1.0.0`
115
- - `@cogcoin/indexer@1.0.1`
118
+ - `@cogcoin/indexer@1.0.2`
116
119
  - `@cogcoin/scoring@1.0.0`
117
120
  - `@scure/base@^2.0.0`
118
121
  - `@scure/bip32@^2.0.1`
@@ -121,7 +124,21 @@ The published package depends on:
121
124
  - `hash-wasm@^4.12.0`
122
125
  - `zeromq@6.5.0`
123
126
 
124
- `@cogcoin/vectors` is kept as a repository development dependency for conformance tests and is not part of the published runtime dependency surface.
127
+ `@cogcoin/vectors@1.0.1` is kept as a repository development dependency for conformance tests and is not part of the published runtime dependency surface.
128
+
129
+ ## Upgrade Notes For `1.2.0`
130
+
131
+ `@cogcoin/client@1.2.0` updates the runtime indexer to `@cogcoin/indexer@1.0.2`. Existing wallet state, mining configuration, Bitcoin Core data, and secrets remain compatible and are not reset.
132
+
133
+ On the first managed indexer start after upgrading, `cogcoin sync`, `cogcoin mine`, `cogcoin follow`, and related managed-indexer commands automatically clear and replay the local Cogcoin indexer SQLite projection once. This is required so the local index contains the complete explorer history and winner `bip39WordIndices` produced by `@cogcoin/indexer@1.0.2`.
134
+
135
+ Supabase mirror operators should let the local client replay and catch up first, then run:
136
+
137
+ ```bash
138
+ cogcoin-supabase reset --yes --recreate-schema
139
+ ```
140
+
141
+ Old snapshots may deserialize successfully but can lack historical explorer rows and required winner `bip39WordIndices` for blocks indexed before `@cogcoin/indexer@1.0.2`, so they should not be used as the source for a complete Supabase reset.
125
142
 
126
143
  ## API
127
144
 
@@ -2,12 +2,13 @@ import { spawn } from "node:child_process";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { acquireFileLock, FileLockBusyError } from "../../wallet/fs/lock.js";
5
- import { buildManagedIndexerStatusFromSnapshotHandle, validateIndexerSnapshotHandle, validateIndexerSnapshotPayload, } from "../managed-runtime/indexer-policy.js";
5
+ import { buildManagedIndexerStatusFromSnapshotHandle, resolveIndexerDaemonProbeDecision, validateIndexerSnapshotHandle, validateIndexerSnapshotPayload, } from "../managed-runtime/indexer-policy.js";
6
6
  import { attachOrStartManagedIndexerRuntime } from "../managed-runtime/indexer-runtime.js";
7
7
  import { readJsonFileIfPresent } from "../managed-runtime/status.js";
8
8
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
9
9
  import { createIndexerDaemonClient, probeIndexerDaemonAtSocket, } from "./client.js";
10
10
  import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, clearIndexerDaemonRuntimeArtifacts, isIndexerDaemonProcessAlive, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
11
+ import { ensureClientReindexRequirementV12, resolveClientReindexRequirementV12, } from "../../sqlite/reindex-requirement.js";
11
12
  import { openIndexerDaemonStartupLog, recordIndexerDaemonStartupFailure, waitForIndexerDaemonStartup, } from "./startup.js";
12
13
  export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
13
14
  const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
@@ -67,6 +68,93 @@ export async function readObservedIndexerDaemonStatus(options) {
67
68
  const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
68
69
  return readJsonFileIfPresent(paths.indexerDaemonStatusPath);
69
70
  }
71
+ async function ensureV12ReindexRequirementBeforeIndexerStart(options) {
72
+ const readRequirement = async () => {
73
+ try {
74
+ return await resolveClientReindexRequirementV12(options.databasePath);
75
+ }
76
+ catch (error) {
77
+ if (error instanceof Error && error.message === "sqlite_store_schema_version_unsupported") {
78
+ // Let the daemon startup path surface and persist the existing
79
+ // schema-mismatch status instead of preempting it in this guard.
80
+ return "applied";
81
+ }
82
+ throw error;
83
+ }
84
+ };
85
+ const applyRequirement = async () => {
86
+ try {
87
+ await ensureClientReindexRequirementV12(options.databasePath);
88
+ }
89
+ catch (error) {
90
+ if (error instanceof Error && error.message === "sqlite_store_schema_version_unsupported") {
91
+ throw new Error("indexer_daemon_schema_mismatch", { cause: error });
92
+ }
93
+ throw error;
94
+ }
95
+ };
96
+ if (await readRequirement() === "applied") {
97
+ return;
98
+ }
99
+ const deadline = Date.now() + options.startupTimeoutMs;
100
+ while (true) {
101
+ let lock;
102
+ try {
103
+ lock = await acquireFileLock(options.paths.indexerDaemonLockPath, {
104
+ purpose: "indexer-reindex-v1.2.0",
105
+ walletRootId: options.walletRootId,
106
+ dataDir: options.dataDir,
107
+ databasePath: options.databasePath,
108
+ });
109
+ }
110
+ catch (error) {
111
+ if (!(error instanceof FileLockBusyError)) {
112
+ throw error;
113
+ }
114
+ if (Date.now() >= deadline) {
115
+ throw new Error("indexer_daemon_start_timeout");
116
+ }
117
+ await sleep(250);
118
+ if (await readRequirement() === "applied") {
119
+ return;
120
+ }
121
+ continue;
122
+ }
123
+ try {
124
+ const requirement = await readRequirement();
125
+ if (requirement === "applied") {
126
+ return;
127
+ }
128
+ const probe = await probeIndexerDaemonForStart({
129
+ dataDir: options.dataDir,
130
+ walletRootId: options.walletRootId,
131
+ paths: options.paths,
132
+ });
133
+ const decision = resolveIndexerDaemonProbeDecision({
134
+ probe,
135
+ expectedBinaryVersion: options.expectedBinaryVersion,
136
+ });
137
+ await probe.client?.close().catch(() => undefined);
138
+ if (decision.action === "reject") {
139
+ throw new Error(decision.error ?? "indexer_daemon_protocol_error");
140
+ }
141
+ if (decision.action === "replace" || requirement === "reset-required") {
142
+ await stopIndexerDaemonServiceWithLockHeld({
143
+ dataDir: options.dataDir,
144
+ walletRootId: options.walletRootId,
145
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
146
+ paths: options.paths,
147
+ processId: probe.status?.processId ?? null,
148
+ });
149
+ }
150
+ await applyRequirement();
151
+ return;
152
+ }
153
+ finally {
154
+ await lock.release();
155
+ }
156
+ }
157
+ }
70
158
  export async function attachOrStartIndexerDaemon(options) {
71
159
  const requestBackgroundFollow = async (client, observedStatus = null) => {
72
160
  if (options.ensureBackgroundFollow !== true) {
@@ -87,6 +175,15 @@ export async function attachOrStartIndexerDaemon(options) {
87
175
  const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS;
88
176
  const serviceLifetime = options.serviceLifetime ?? "persistent";
89
177
  const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
178
+ await ensureV12ReindexRequirementBeforeIndexerStart({
179
+ dataDir: options.dataDir,
180
+ databasePath: options.databasePath,
181
+ walletRootId,
182
+ paths,
183
+ startupTimeoutMs,
184
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
185
+ expectedBinaryVersion,
186
+ });
90
187
  const startDaemon = async () => {
91
188
  await mkdir(paths.indexerServiceRoot, { recursive: true });
92
189
  const startupLog = await openIndexerDaemonStartupLog(paths);
@@ -158,6 +158,7 @@ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfi
158
158
  `rpcport=${runtimeConfig.rpc.port}`,
159
159
  `port=${runtimeConfig.p2pPort}`,
160
160
  `zmqpubhashblock=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
161
+ `zmqpubrawtx=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
161
162
  `walletdir=${walletDir}`,
162
163
  ];
163
164
  await writeFileAtomic(filePath, `${lines.join("\n")}\n`, { mode: 0o600 });
@@ -172,6 +173,7 @@ export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
172
173
  `-rpcport=${runtimeConfig.rpc.port}`,
173
174
  `-port=${runtimeConfig.p2pPort}`,
174
175
  `-zmqpubhashblock=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
176
+ `-zmqpubrawtx=tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
175
177
  `-walletdir=${walletDir}`,
176
178
  "-server=1",
177
179
  "-prune=0",
@@ -173,6 +173,7 @@ export async function attachOrStartManagedBitcoindService(options) {
173
173
  const zmqConfig = {
174
174
  endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
175
175
  topic: "hashblock",
176
+ rawTxTopic: "rawtx",
176
177
  port: runtimeConfig.zmqPort,
177
178
  pollIntervalMs: startOptions.pollIntervalMs ?? DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS,
178
179
  };
@@ -193,7 +194,9 @@ export async function attachOrStartManagedBitcoindService(options) {
193
194
  const rpc = createRpcClient(rpcConfig);
194
195
  try {
195
196
  await waitForManagedBitcoindRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
196
- await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint);
197
+ await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint, {
198
+ requireRawTxZmq: true,
199
+ });
197
200
  }
198
201
  catch (error) {
199
202
  if (child.pid !== undefined) {
@@ -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;
@@ -3,6 +3,8 @@ 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
- export declare function validateNodeConfigForTesting(rpcClient: BitcoinRpcClient, expectedChain: "main" | "regtest", zmqEndpoint: string): Promise<void>;
6
+ export declare function validateNodeConfigForTesting(rpcClient: BitcoinRpcClient, expectedChain: "main" | "regtest", zmqEndpoint: string, options?: {
7
+ requireRawTxZmq?: boolean;
8
+ }): Promise<void>;
7
9
  export declare function launchManagedBitcoindNode(options: InternalManagedBitcoindOptions): Promise<ManagedBitcoindNodeHandle>;
8
10
  export declare function createRpcClient(config: BitcoindRpcConfig, options?: RpcTransportOptions): BitcoinRpcClient;
@@ -71,6 +71,7 @@ export function buildBitcoindArgsForTesting(options, rpcPort, zmqPort, p2pPort)
71
71
  `-rpcport=${rpcPort}`,
72
72
  `-port=${p2pPort}`,
73
73
  `-zmqpubhashblock=tcp://${LOCAL_HOST}:${zmqPort}`,
74
+ `-zmqpubrawtx=tcp://${LOCAL_HOST}:${zmqPort}`,
74
75
  "-server=1",
75
76
  "-disablewallet=1",
76
77
  "-prune=0",
@@ -120,7 +121,7 @@ async function waitForRpcReady(rpcClient, cookieFile, expectedChain, timeoutMs)
120
121
  }
121
122
  throw lastError instanceof Error ? lastError : new Error("bitcoind_rpc_timeout");
122
123
  }
123
- export async function validateNodeConfigForTesting(rpcClient, expectedChain, zmqEndpoint) {
124
+ export async function validateNodeConfigForTesting(rpcClient, expectedChain, zmqEndpoint, options = {}) {
124
125
  const info = await rpcClient.getBlockchainInfo();
125
126
  if (info.chain !== expectedChain) {
126
127
  throw new Error(`bitcoind_chain_expected_${expectedChain}_got_${info.chain}`);
@@ -133,6 +134,12 @@ export async function validateNodeConfigForTesting(rpcClient, expectedChain, zmq
133
134
  if (!hasHashBlock) {
134
135
  throw new Error("bitcoind_zmq_hashblock_missing");
135
136
  }
137
+ if (options.requireRawTxZmq === true) {
138
+ const hasRawTx = notifications.some((notification) => notification.type === "pubrawtx" && notification.address === zmqEndpoint);
139
+ if (!hasRawTx) {
140
+ throw new Error("bitcoind_zmq_rawtx_missing");
141
+ }
142
+ }
136
143
  }
137
144
  export async function launchManagedBitcoindNode(options) {
138
145
  const resolvedOptions = resolveManagedBitcoindOptions(options);
@@ -156,6 +163,7 @@ export async function launchManagedBitcoindNode(options) {
156
163
  const zmqConfig = {
157
164
  endpoint: zmqEndpoint,
158
165
  topic: "hashblock",
166
+ rawTxTopic: "rawtx",
159
167
  port: zmqPort,
160
168
  pollIntervalMs: options.pollIntervalMs ?? DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS,
161
169
  };
@@ -170,7 +178,9 @@ export async function launchManagedBitcoindNode(options) {
170
178
  child.stderr?.resume();
171
179
  try {
172
180
  await waitForRpcReady(rpcClient, cookieFile, resolvedOptions.chain, startupTimeoutMs);
173
- await validateNodeConfigForTesting(rpcClient, resolvedOptions.chain, zmqEndpoint);
181
+ await validateNodeConfigForTesting(rpcClient, resolvedOptions.chain, zmqEndpoint, {
182
+ requireRawTxZmq: true,
183
+ });
174
184
  }
175
185
  catch (error) {
176
186
  child.kill("SIGTERM");
@@ -186,7 +196,9 @@ export async function launchManagedBitcoindNode(options) {
186
196
  getblockArchiveEndHeight: null,
187
197
  getblockArchiveSha256: null,
188
198
  async validate() {
189
- await validateNodeConfigForTesting(rpcClient, resolvedOptions.chain, zmqEndpoint);
199
+ await validateNodeConfigForTesting(rpcClient, resolvedOptions.chain, zmqEndpoint, {
200
+ requireRawTxZmq: true,
201
+ });
190
202
  },
191
203
  async stop() {
192
204
  if (stopped) {
@@ -75,6 +75,7 @@ export interface BitcoindRpcConfig {
75
75
  export interface BitcoindZmqConfig {
76
76
  endpoint: string;
77
77
  topic: "hashblock";
78
+ rawTxTopic?: "rawtx";
78
79
  port: number;
79
80
  pollIntervalMs: number;
80
81
  }
@@ -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.",
@@ -0,0 +1,8 @@
1
+ export declare const CLIENT_REINDEX_REQUIREMENT_V1_2_0_KEY = "client_reindex_requirement_v1_2_0";
2
+ export type ClientReindexRequirementAction = "already-applied" | "marked-empty" | "marked-current" | "reset-and-marked";
3
+ export type ClientReindexRequirementStatus = "applied" | "mark-current" | "mark-empty" | "reset-required";
4
+ export declare function isClientReindexRequirementV12Applied(databasePath: string): Promise<boolean>;
5
+ export declare function resolveClientReindexRequirementV12(databasePath: string): Promise<ClientReindexRequirementStatus>;
6
+ export declare function ensureClientReindexRequirementV12(databasePath: string): Promise<{
7
+ action: ClientReindexRequirementAction;
8
+ }>;
@@ -0,0 +1,100 @@
1
+ import { encodeText } from "../bytes.js";
2
+ import { openSqliteDatabase } from "./driver.js";
3
+ import { migrateSqliteStore } from "./migrate.js";
4
+ import { clearTipMeta, TIP_META_KEYS } from "./tip-meta.js";
5
+ export const CLIENT_REINDEX_REQUIREMENT_V1_2_0_KEY = "client_reindex_requirement_v1_2_0";
6
+ const textDecoder = new TextDecoder();
7
+ async function hasRows(database, sql, params = []) {
8
+ return (await database.get(sql, params)) !== null;
9
+ }
10
+ async function hasIndexerRows(database) {
11
+ const hasTipMeta = await hasRows(database, `SELECT 1 AS count FROM meta WHERE key = ? LIMIT 1`, [TIP_META_KEYS.tipHeight]);
12
+ if (hasTipMeta) {
13
+ return true;
14
+ }
15
+ if (await hasRows(database, `SELECT 1 AS count FROM checkpoints LIMIT 1`)) {
16
+ return true;
17
+ }
18
+ return await hasRows(database, `SELECT 1 AS count FROM block_records LIMIT 1`);
19
+ }
20
+ async function loadLatestStateBytes(database) {
21
+ const tipStateRow = await database.get(`SELECT value FROM meta WHERE key = ? LIMIT 1`, [TIP_META_KEYS.tipStateBytes]);
22
+ if (tipStateRow?.value !== undefined) {
23
+ return tipStateRow.value;
24
+ }
25
+ const checkpointRow = await database.get(`SELECT state_bytes FROM checkpoints ORDER BY height DESC LIMIT 1`);
26
+ return checkpointRow?.state_bytes ?? null;
27
+ }
28
+ function serializedStateIncludesV12HistoryShape(stateBytes) {
29
+ const serialized = textDecoder.decode(stateBytes);
30
+ return serialized.includes("\"explorerBlocksByHeight\"")
31
+ && serialized.includes("\"explorerTransactionsByHeight\"");
32
+ }
33
+ async function isRequirementApplied(database) {
34
+ const marker = await database.get(`SELECT value FROM meta WHERE key = ?`, [CLIENT_REINDEX_REQUIREMENT_V1_2_0_KEY]);
35
+ return marker !== null;
36
+ }
37
+ async function writeRequirementMarker(database) {
38
+ await database.run(`INSERT INTO meta (key, value) VALUES (?, ?)
39
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [CLIENT_REINDEX_REQUIREMENT_V1_2_0_KEY, encodeText("applied")]);
40
+ }
41
+ export async function isClientReindexRequirementV12Applied(databasePath) {
42
+ const database = await openSqliteDatabase({ filename: databasePath });
43
+ try {
44
+ await migrateSqliteStore(database);
45
+ return await isRequirementApplied(database);
46
+ }
47
+ finally {
48
+ await database.close();
49
+ }
50
+ }
51
+ async function resolveRequirementStatus(database) {
52
+ if (await isRequirementApplied(database)) {
53
+ return "applied";
54
+ }
55
+ if (!await hasIndexerRows(database)) {
56
+ return "mark-empty";
57
+ }
58
+ const stateBytes = await loadLatestStateBytes(database);
59
+ if (stateBytes !== null && serializedStateIncludesV12HistoryShape(stateBytes)) {
60
+ return "mark-current";
61
+ }
62
+ return "reset-required";
63
+ }
64
+ export async function resolveClientReindexRequirementV12(databasePath) {
65
+ const database = await openSqliteDatabase({ filename: databasePath });
66
+ try {
67
+ await migrateSqliteStore(database);
68
+ return await resolveRequirementStatus(database);
69
+ }
70
+ finally {
71
+ await database.close();
72
+ }
73
+ }
74
+ export async function ensureClientReindexRequirementV12(databasePath) {
75
+ const database = await openSqliteDatabase({ filename: databasePath });
76
+ try {
77
+ await migrateSqliteStore(database);
78
+ return await database.transaction(async () => {
79
+ switch (await resolveRequirementStatus(database)) {
80
+ case "applied":
81
+ return { action: "already-applied" };
82
+ case "mark-empty":
83
+ await writeRequirementMarker(database);
84
+ return { action: "marked-empty" };
85
+ case "mark-current":
86
+ await writeRequirementMarker(database);
87
+ return { action: "marked-current" };
88
+ case "reset-required":
89
+ await clearTipMeta(database);
90
+ await database.run(`DELETE FROM checkpoints`);
91
+ await database.run(`DELETE FROM block_records`);
92
+ await writeRequirementMarker(database);
93
+ return { action: "reset-and-marked" };
94
+ }
95
+ });
96
+ }
97
+ finally {
98
+ await database.close();
99
+ }
100
+ }
@@ -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");
47
49
  }
50
+ catch (error) {
51
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
52
+ throw error;
53
+ }
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,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
  }
@@ -169,6 +171,7 @@ export async function clearManagedBitcoindArtifacts(servicePaths) {
169
171
  await rm(servicePaths.bitcoindStatusPath, { force: true }).catch(() => undefined);
170
172
  await rm(servicePaths.bitcoindPidPath, { force: true }).catch(() => undefined);
171
173
  await rm(servicePaths.bitcoindReadyPath, { force: true }).catch(() => undefined);
174
+ await rm(servicePaths.bitcoindRuntimeConfigPath, { force: true }).catch(() => undefined);
172
175
  await rm(servicePaths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
173
176
  }
174
177
  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";
@@ -25,5 +25,10 @@ export declare function runCompetitivenessGate(options: {
25
25
  cooperativeYieldEvery?: number;
26
26
  throwIfStopping?: () => void;
27
27
  onWarmupProgress?: (progress: MiningGateWarmupProgress) => Promise<void> | void;
28
+ mempoolIndex?: {
29
+ rawTxSupported: boolean;
30
+ cachePath: string;
31
+ serviceIdentity: string;
32
+ };
28
33
  }): Promise<CompetitivenessDecision>;
29
34
  export {};