@cogcoin/client 1.1.16 → 1.2.0
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 +28 -11
- package/dist/bitcoind/indexer-daemon/lifecycle.js +98 -1
- package/dist/bitcoind/managed-bitcoind-service-config.js +2 -0
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +4 -1
- package/dist/bitcoind/node.d.ts +3 -1
- package/dist/bitcoind/node.js +15 -3
- package/dist/bitcoind/types.d.ts +1 -0
- package/dist/sqlite/reindex-requirement.d.ts +8 -0
- package/dist/sqlite/reindex-requirement.js +100 -0
- package/dist/wallet/mining/competitiveness.d.ts +5 -0
- package/dist/wallet/mining/competitiveness.js +174 -35
- package/dist/wallet/mining/cycle.d.ts +2 -0
- package/dist/wallet/mining/cycle.js +1 -0
- package/dist/wallet/mining/mempool-index.d.ts +65 -0
- package/dist/wallet/mining/mempool-index.js +451 -0
- package/dist/wallet/mining/runner.js +24 -0
- package/dist/wallet/mining/types.d.ts +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.
|
|
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.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
7
|
-
##
|
|
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.
|
|
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) {
|
package/dist/bitcoind/node.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/bitcoind/node.js
CHANGED
|
@@ -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) {
|
package/dist/bitcoind/types.d.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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 {};
|