@cogcoin/client 1.1.10 → 1.1.12
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 +1 -1
- package/dist/bitcoind/client/managed-client.d.ts +2 -0
- package/dist/bitcoind/client/managed-client.js +6 -0
- package/dist/bitcoind/indexer-daemon/background-follow.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/background-follow.js +132 -0
- package/dist/bitcoind/indexer-daemon/client.d.ts +12 -0
- package/dist/bitcoind/indexer-daemon/client.js +137 -0
- package/dist/bitcoind/indexer-daemon/lifecycle.d.ts +30 -0
- package/dist/bitcoind/indexer-daemon/lifecycle.js +153 -0
- package/dist/bitcoind/indexer-daemon/process.d.ts +35 -0
- package/dist/bitcoind/indexer-daemon/process.js +140 -0
- package/dist/bitcoind/indexer-daemon/runtime.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/runtime.js +204 -0
- package/dist/bitcoind/indexer-daemon/server.d.ts +12 -0
- package/dist/bitcoind/indexer-daemon/server.js +87 -0
- package/dist/bitcoind/indexer-daemon/snapshot-leases.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/snapshot-leases.js +139 -0
- package/dist/bitcoind/indexer-daemon/status.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/status.js +282 -0
- package/dist/bitcoind/indexer-daemon/types.d.ts +141 -0
- package/dist/bitcoind/indexer-daemon/types.js +1 -0
- package/dist/bitcoind/indexer-daemon-main.js +14 -665
- package/dist/bitcoind/indexer-daemon.d.ts +4 -132
- package/dist/bitcoind/indexer-daemon.js +2 -417
- package/dist/bitcoind/managed-bitcoind-service-config.d.ts +18 -1
- package/dist/bitcoind/managed-bitcoind-service-config.js +38 -1
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +30 -24
- package/dist/bitcoind/managed-bitcoind-service-status.js +0 -8
- package/dist/bitcoind/rpc.d.ts +2 -1
- package/dist/bitcoind/rpc.js +3 -0
- package/dist/bitcoind/types.d.ts +1 -0
- package/dist/cli/mining-format.js +6 -1
- package/dist/cli/wallet-format/balance.js +1 -1
- package/dist/client/default-client.d.ts +3 -1
- package/dist/client/default-client.js +22 -0
- package/dist/types.d.ts +13 -1
- package/dist/wallet/fs/atomic.d.ts +11 -2
- package/dist/wallet/fs/atomic.js +45 -5
- package/dist/wallet/mining/competitiveness.d.ts +6 -0
- package/dist/wallet/mining/competitiveness.js +137 -74
- package/dist/wallet/mining/cycle.js +32 -4
- package/dist/wallet/mining/engine-types.d.ts +10 -0
- package/dist/wallet/mining/projection.d.ts +1 -0
- package/dist/wallet/mining/projection.js +15 -1
- package/dist/wallet/mining/visualizer-sync.js +7 -9
- package/dist/wallet/mining/visualizer.js +2 -1
- package/dist/wallet/read/context.js +3 -2
- package/dist/wallet/read/local-state.d.ts +8 -0
- package/dist/wallet/read/local-state.js +32 -6
- package/dist/wallet/read/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ import { attachOrStartManagedBitcoindRuntime, probeManagedBitcoindRuntime } from
|
|
|
9
9
|
import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
|
|
10
10
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
|
|
11
11
|
import { DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS, } from "./types.js";
|
|
12
|
-
import { buildManagedServiceArgsForTesting, LOCAL_HOST, resolveManagedBitcoindRuntimeConfig, SUPPORTED_BITCOIND_VERSION, verifyManagedBitcoindVersion, writeBitcoinConfForTesting, } from "./managed-bitcoind-service-config.js";
|
|
12
|
+
import { buildManagedServiceArgsForTesting, LOCAL_HOST, resolveManagedBitcoindRuntimeConfig, SUPPORTED_BITCOIND_VERSION, verifyManagedBitcoindVersion, writeManagedBitcoindRuntimeConfigFile, writeManagedBitcoindRuntimeConfigFileFromStatus, writeBitcoinConfForTesting, } from "./managed-bitcoind-service-config.js";
|
|
13
13
|
import { DEFAULT_MANAGED_BITCOIND_SHUTDOWN_TIMEOUT_MS, DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS, FileLockBusyError, acquireManagedBitcoindFileLockWithRetry, isManagedBitcoindProcessAlive, waitForManagedBitcoindProcessExit, sleep, } from "./managed-bitcoind-service-process.js";
|
|
14
14
|
import { createManagedWalletReplica, loadManagedWalletReplicaIfPresent, } from "./managed-bitcoind-service-replica.js";
|
|
15
15
|
import { clearManagedBitcoindRuntimeArtifacts, createBitcoindServiceStatus, createManagedBitcoindNodeHandle, probeManagedBitcoindStatusCandidate, refreshManagedBitcoindStatus, waitForManagedBitcoindRpcReady, writeManagedBitcoindStatus, } from "./managed-bitcoind-service-status.js";
|
|
@@ -109,6 +109,7 @@ async function tryAttachExistingManagedBitcoindService(options) {
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
const refreshed = await refreshManagedBitcoindStatus(probe.status, paths, options);
|
|
112
|
+
await writeManagedBitcoindRuntimeConfigFileFromStatus(paths.bitcoindRuntimeConfigPath, refreshed);
|
|
112
113
|
return createManagedBitcoindNodeHandle({
|
|
113
114
|
status: refreshed,
|
|
114
115
|
paths,
|
|
@@ -207,42 +208,47 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
207
208
|
}
|
|
208
209
|
const nowUnixMs = Date.now();
|
|
209
210
|
const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, startOptions.walletRootId, startOptions.dataDir);
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
211
|
+
return {
|
|
212
|
+
runtimeConfig,
|
|
213
|
+
status: createBitcoindServiceStatus({
|
|
214
|
+
binaryVersion,
|
|
215
|
+
serviceInstanceId: randomBytes(16).toString("hex"),
|
|
216
|
+
state: "ready",
|
|
217
|
+
processId: child.pid ?? null,
|
|
218
|
+
walletRootId: startOptions.walletRootId,
|
|
219
|
+
chain: startOptions.chain,
|
|
220
|
+
dataDir: startOptions.dataDir,
|
|
221
|
+
runtimeRoot: paths.walletRuntimeRoot,
|
|
222
|
+
startHeight: startOptions.startHeight,
|
|
223
|
+
rpc: rpcConfig,
|
|
224
|
+
zmq: zmqConfig,
|
|
225
|
+
p2pPort: runtimeConfig.p2pPort,
|
|
226
|
+
getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
|
|
227
|
+
getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
|
|
228
|
+
walletReplica,
|
|
229
|
+
startedAtUnixMs: nowUnixMs,
|
|
230
|
+
heartbeatAtUnixMs: nowUnixMs,
|
|
231
|
+
lastError: walletReplica.message ?? null,
|
|
232
|
+
}),
|
|
233
|
+
};
|
|
230
234
|
};
|
|
235
|
+
let runtimeConfig;
|
|
231
236
|
let status;
|
|
232
237
|
try {
|
|
233
|
-
status = await startManagedProcess(runtimeOptions);
|
|
238
|
+
({ runtimeConfig, status } = await startManagedProcess(runtimeOptions));
|
|
234
239
|
}
|
|
235
240
|
catch (error) {
|
|
236
241
|
if (runtimeOptions.getblockArchivePath === undefined || runtimeOptions.getblockArchivePath === null) {
|
|
237
242
|
throw error;
|
|
238
243
|
}
|
|
239
|
-
status = await startManagedProcess({
|
|
244
|
+
({ runtimeConfig, status } = await startManagedProcess({
|
|
240
245
|
...runtimeOptions,
|
|
241
246
|
getblockArchivePath: null,
|
|
242
247
|
getblockArchiveEndHeight: null,
|
|
243
248
|
getblockArchiveSha256: null,
|
|
244
|
-
});
|
|
249
|
+
}));
|
|
245
250
|
}
|
|
251
|
+
await writeManagedBitcoindRuntimeConfigFile(paths.bitcoindRuntimeConfigPath, runtimeConfig);
|
|
246
252
|
await writeManagedBitcoindStatus(paths, status);
|
|
247
253
|
return createManagedBitcoindNodeHandle({
|
|
248
254
|
status,
|
|
@@ -83,14 +83,6 @@ export async function writeManagedBitcoindStatus(paths, status) {
|
|
|
83
83
|
await writeFileAtomic(paths.bitcoindPidPath, `${status.processId ?? ""}\n`, { mode: 0o600 });
|
|
84
84
|
await writeFileAtomic(paths.bitcoindReadyPath, `${status.updatedAtUnixMs}\n`, { mode: 0o600 });
|
|
85
85
|
await writeRuntimeStatusFile(paths.bitcoindWalletStatusPath, status.walletReplica ?? createMissingManagedWalletReplicaStatus(status.walletRootId, "Managed Core wallet replica is missing."));
|
|
86
|
-
await writeRuntimeStatusFile(paths.bitcoindRuntimeConfigPath, {
|
|
87
|
-
chain: status.chain,
|
|
88
|
-
rpc: status.rpc,
|
|
89
|
-
zmqPort: status.zmq.port,
|
|
90
|
-
p2pPort: status.p2pPort,
|
|
91
|
-
getblockArchiveEndHeight: status.getblockArchiveEndHeight,
|
|
92
|
-
getblockArchiveSha256: status.getblockArchiveSha256,
|
|
93
|
-
});
|
|
94
86
|
}
|
|
95
87
|
export async function clearManagedBitcoindRuntimeArtifacts(paths) {
|
|
96
88
|
await rm(paths.bitcoindStatusPath, { force: true }).catch(() => undefined);
|
package/dist/bitcoind/rpc.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RpcBlock, RpcBlockchainInfo, RpcChainStatesResponse, RpcCreateWalletResult, RpcDecodedPsbt, RpcDescriptorInfo, RpcEstimateSmartFeeResult, RpcFinalizePsbtResult, RpcImportDescriptorRequest, RpcImportDescriptorResult, RpcListUnspentEntry, RpcMempoolEntry, RpcMempoolInfo, RpcRawMempoolVerbose, RpcListDescriptorsResult, RpcLockedUnspent, RpcLoadTxOutSetResult, RpcLoadWalletResult, RpcNetworkInfo, RpcTestMempoolAcceptResult, RpcWalletInfo, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult, RpcTransaction, RpcWalletTransaction, RpcZmqNotification } from "./types.js";
|
|
1
|
+
import type { RpcBlock, RpcBlockchainInfo, RpcChainStatesResponse, RpcCreateWalletResult, RpcDecodedPsbt, RpcDescriptorInfo, RpcEstimateSmartFeeResult, RpcFinalizePsbtResult, RpcImportDescriptorRequest, RpcImportDescriptorResult, RpcListUnspentEntry, RpcMempoolEntry, RpcMempoolInfo, RpcRawMempoolVerbose, RpcRawMempoolEntries, RpcListDescriptorsResult, RpcLockedUnspent, RpcLoadTxOutSetResult, RpcLoadWalletResult, RpcNetworkInfo, RpcTestMempoolAcceptResult, RpcWalletInfo, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult, RpcTransaction, RpcWalletTransaction, RpcZmqNotification } from "./types.js";
|
|
2
2
|
interface RpcRequestPayload {
|
|
3
3
|
readonly body: string;
|
|
4
4
|
readonly headers: Record<string, string>;
|
|
@@ -62,6 +62,7 @@ export declare class BitcoinRpcClient {
|
|
|
62
62
|
sendRawTransaction(hex: string): Promise<string>;
|
|
63
63
|
getRawMempool(): Promise<string[]>;
|
|
64
64
|
getRawMempoolVerbose(): Promise<RpcRawMempoolVerbose>;
|
|
65
|
+
getRawMempoolEntries(): Promise<RpcRawMempoolEntries>;
|
|
65
66
|
getMempoolInfo(): Promise<RpcMempoolInfo>;
|
|
66
67
|
getMempoolEntry(txid: string): Promise<RpcMempoolEntry>;
|
|
67
68
|
estimateSmartFee(confirmTarget: number, mode: "conservative" | "economical"): Promise<RpcEstimateSmartFeeResult>;
|
package/dist/bitcoind/rpc.js
CHANGED
|
@@ -351,6 +351,9 @@ export class BitcoinRpcClient {
|
|
|
351
351
|
getRawMempoolVerbose() {
|
|
352
352
|
return this.call("getrawmempool", [false, true]);
|
|
353
353
|
}
|
|
354
|
+
getRawMempoolEntries() {
|
|
355
|
+
return this.call("getrawmempool", [true]);
|
|
356
|
+
}
|
|
354
357
|
getMempoolInfo() {
|
|
355
358
|
return this.call("getmempoolinfo");
|
|
356
359
|
}
|
package/dist/bitcoind/types.d.ts
CHANGED
|
@@ -313,6 +313,7 @@ export interface RpcRawMempoolVerbose {
|
|
|
313
313
|
txids: string[];
|
|
314
314
|
mempool_sequence: string | number;
|
|
315
315
|
}
|
|
316
|
+
export type RpcRawMempoolEntries = Record<string, RpcMempoolEntry>;
|
|
316
317
|
export interface RpcWalletTransaction {
|
|
317
318
|
txid: string;
|
|
318
319
|
walletconflicts?: string[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveWaitingProviderNote } from "../wallet/mining/projection.js";
|
|
1
2
|
function formatMaybeIso(unixMs) {
|
|
2
3
|
return unixMs === null ? "none" : new Date(unixMs).toISOString();
|
|
3
4
|
}
|
|
@@ -33,7 +34,11 @@ function resolveInsufficientFundsNextStep() {
|
|
|
33
34
|
function resolveMiningRuntimeNote(mining) {
|
|
34
35
|
return mining.runtime.currentPublishDecision === "publish-paused-insufficient-funds"
|
|
35
36
|
? "Insufficient BTC to mine."
|
|
36
|
-
: mining.runtime.note
|
|
37
|
+
: mining.runtime.note !== null
|
|
38
|
+
? mining.runtime.note
|
|
39
|
+
: mining.runtime.currentPhase === "waiting-provider"
|
|
40
|
+
? resolveWaitingProviderNote(mining.runtime.providerState)
|
|
41
|
+
: null;
|
|
37
42
|
}
|
|
38
43
|
export function formatMiningSummaryLine(mining) {
|
|
39
44
|
const provider = mining.provider.configured
|
|
@@ -34,7 +34,7 @@ function renderBalanceArtCard(context) {
|
|
|
34
34
|
return renderBalanceArtLine(line, "Funding address:", ` ${fundingAddress}`);
|
|
35
35
|
}
|
|
36
36
|
if (line.includes("Bitcoin Balance:")) {
|
|
37
|
-
return renderBalanceArtLine(line, "Bitcoin Balance:", ` ${formatBitcoinAmount(context.
|
|
37
|
+
return renderBalanceArtLine(line, "Bitcoin Balance:", ` ${formatBitcoinAmount(context.fundingDisplaySats ?? null)}`);
|
|
38
38
|
}
|
|
39
39
|
if (line.includes("Cogcoin Balance:")) {
|
|
40
40
|
return renderBalanceArtLine(line, "Cogcoin Balance:", ` ${formatCogAmount(spendableCog)}`);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { BitcoinBlock, GenesisParameters, IndexerState } from "@cogcoin/indexer/types";
|
|
2
|
-
import type { ApplyBlockResult, Client, ClientCheckpoint, ClientStoreAdapter, ClientTip } from "../types.js";
|
|
2
|
+
import type { ApplyBlockResult, Client, ClientCheckpoint, ClientMirrorDelta, ClientMirrorSnapshot, ClientStoreAdapter, ClientTip } from "../types.js";
|
|
3
3
|
export declare class DefaultClient implements Client {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(store: ClientStoreAdapter, genesisParameters: GenesisParameters, state: IndexerState, tip: ClientTip | null, snapshotInterval: number, blockRecordRetention: number);
|
|
6
6
|
getTip(): Promise<ClientTip | null>;
|
|
7
7
|
getState(): Promise<IndexerState>;
|
|
8
|
+
readMirrorSnapshot(): Promise<ClientMirrorSnapshot>;
|
|
9
|
+
readMirrorDelta(afterHeight: number): Promise<ClientMirrorDelta>;
|
|
8
10
|
applyBlock(block: BitcoinBlock): Promise<ApplyBlockResult>;
|
|
9
11
|
rewindToHeight(height: number): Promise<ClientTip | null>;
|
|
10
12
|
restoreCheckpoint(checkpoint: ClientCheckpoint): Promise<ClientTip>;
|
|
@@ -26,6 +26,28 @@ export class DefaultClient {
|
|
|
26
26
|
await this.#queue;
|
|
27
27
|
return this.#state;
|
|
28
28
|
}
|
|
29
|
+
async readMirrorSnapshot() {
|
|
30
|
+
return this.#enqueue(async () => {
|
|
31
|
+
this.#assertOpen();
|
|
32
|
+
return {
|
|
33
|
+
tip: this.#tip === null ? null : { ...this.#tip },
|
|
34
|
+
stateBytes: serializeIndexerState(this.#state),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async readMirrorDelta(afterHeight) {
|
|
39
|
+
return this.#enqueue(async () => {
|
|
40
|
+
this.#assertOpen();
|
|
41
|
+
const blockRecords = await this.#store.loadBlockRecordsAfter(afterHeight);
|
|
42
|
+
return {
|
|
43
|
+
tip: this.#tip === null ? null : { ...this.#tip },
|
|
44
|
+
blockRecords: blockRecords.map((record) => ({
|
|
45
|
+
...record,
|
|
46
|
+
recordBytes: new Uint8Array(record.recordBytes),
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
29
51
|
async applyBlock(block) {
|
|
30
52
|
return this.#enqueue(async () => {
|
|
31
53
|
this.#assertOpen();
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,18 @@ export interface StoredBlockRecord {
|
|
|
19
19
|
recordBytes: Uint8Array;
|
|
20
20
|
createdAt: number;
|
|
21
21
|
}
|
|
22
|
+
export interface ClientMirrorSnapshot {
|
|
23
|
+
tip: ClientTip | null;
|
|
24
|
+
stateBytes: Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
export interface ClientMirrorDelta {
|
|
27
|
+
tip: ClientTip | null;
|
|
28
|
+
blockRecords: StoredBlockRecord[];
|
|
29
|
+
}
|
|
30
|
+
export interface ClientMirrorReader {
|
|
31
|
+
readMirrorSnapshot(): Promise<ClientMirrorSnapshot>;
|
|
32
|
+
readMirrorDelta(afterHeight: number): Promise<ClientMirrorDelta>;
|
|
33
|
+
}
|
|
22
34
|
export interface WriteAppliedBlockEntry {
|
|
23
35
|
tip: ClientTip | null;
|
|
24
36
|
stateBytes: Uint8Array | null;
|
|
@@ -48,7 +60,7 @@ export interface ApplyBlockResult {
|
|
|
48
60
|
checkpoint: ClientCheckpoint | null;
|
|
49
61
|
applied: AppliedBlock;
|
|
50
62
|
}
|
|
51
|
-
export interface Client {
|
|
63
|
+
export interface Client extends ClientMirrorReader {
|
|
52
64
|
getTip(): Promise<ClientTip | null>;
|
|
53
65
|
getState(): Promise<IndexerState>;
|
|
54
66
|
applyBlock(block: BitcoinBlock): Promise<ApplyBlockResult>;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { rename, rm } from "node:fs/promises";
|
|
1
2
|
export interface AtomicWriteOptions {
|
|
2
3
|
mode?: number;
|
|
3
4
|
encoding?: BufferEncoding;
|
|
4
5
|
}
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export interface AtomicWriteDependencies {
|
|
7
|
+
platform?: NodeJS.Platform;
|
|
8
|
+
rename?: typeof rename;
|
|
9
|
+
rm?: typeof rm;
|
|
10
|
+
sleep?: (ms: number) => Promise<void>;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
export declare function replaceFileAtomicWithRetryForTesting(tempPath: string, filePath: string, dependencies?: AtomicWriteDependencies): Promise<void>;
|
|
14
|
+
export declare function writeFileAtomic(filePath: string, data: string | Uint8Array, options?: AtomicWriteOptions, dependencies?: AtomicWriteDependencies): Promise<void>;
|
|
15
|
+
export declare function writeJsonFileAtomic(filePath: string, value: unknown, options?: AtomicWriteOptions, dependencies?: AtomicWriteDependencies): Promise<void>;
|
package/dist/wallet/fs/atomic.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, open, rename } from "node:fs/promises";
|
|
2
|
+
import { mkdir, open, rename, rm } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
|
+
const WINDOWS_REPLACE_RETRY_DELAY_MS = 25;
|
|
5
|
+
const WINDOWS_REPLACE_RETRY_TIMEOUT_MS = 1_000;
|
|
4
6
|
async function fsyncDirectory(directoryPath) {
|
|
5
7
|
try {
|
|
6
8
|
const directoryHandle = await open(directoryPath, "r");
|
|
@@ -21,7 +23,39 @@ async function fsyncDirectory(directoryPath) {
|
|
|
21
23
|
throw error;
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
function isRetryableWindowsReplaceError(error) {
|
|
27
|
+
if (!(error instanceof Error) || !("code" in error)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const code = error.code;
|
|
31
|
+
return code === "EPERM" || code === "EACCES" || code === "EBUSY";
|
|
32
|
+
}
|
|
33
|
+
async function sleep(ms) {
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
export async function replaceFileAtomicWithRetryForTesting(tempPath, filePath, dependencies = {}) {
|
|
37
|
+
const renameImpl = dependencies.rename ?? rename;
|
|
38
|
+
if ((dependencies.platform ?? process.platform) !== "win32") {
|
|
39
|
+
await renameImpl(tempPath, filePath);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const nowImpl = dependencies.now ?? Date.now;
|
|
43
|
+
const sleepImpl = dependencies.sleep ?? sleep;
|
|
44
|
+
const deadline = nowImpl() + WINDOWS_REPLACE_RETRY_TIMEOUT_MS;
|
|
45
|
+
while (true) {
|
|
46
|
+
try {
|
|
47
|
+
await renameImpl(tempPath, filePath);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!isRetryableWindowsReplaceError(error) || nowImpl() >= deadline) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
await sleepImpl(WINDOWS_REPLACE_RETRY_DELAY_MS);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function writeFileAtomic(filePath, data, options = {}, dependencies = {}) {
|
|
25
59
|
const directoryPath = dirname(filePath);
|
|
26
60
|
const tempPath = join(directoryPath, `${basename(filePath)}.tmp-${randomUUID()}`);
|
|
27
61
|
await mkdir(directoryPath, { recursive: true });
|
|
@@ -38,9 +72,15 @@ export async function writeFileAtomic(filePath, data, options = {}) {
|
|
|
38
72
|
finally {
|
|
39
73
|
await handle.close();
|
|
40
74
|
}
|
|
41
|
-
|
|
75
|
+
try {
|
|
76
|
+
await replaceFileAtomicWithRetryForTesting(tempPath, filePath, dependencies);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
await (dependencies.rm ?? rm)(tempPath, { force: true }).catch(() => undefined);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
42
82
|
await fsyncDirectory(directoryPath);
|
|
43
83
|
}
|
|
44
|
-
export async function writeJsonFileAtomic(filePath, value, options = {}) {
|
|
45
|
-
await writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, options);
|
|
84
|
+
export async function writeJsonFileAtomic(filePath, value, options = {}, dependencies = {}) {
|
|
85
|
+
await writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, options, dependencies);
|
|
46
86
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { assaySentences } from "@cogcoin/scoring";
|
|
2
2
|
import type { WalletReadContext } from "../read/index.js";
|
|
3
3
|
import type { CompetitivenessDecision, MiningCandidate, MiningCooperativeYield, MiningRpcClient } from "./engine-types.js";
|
|
4
|
+
interface MiningGateWarmupProgress {
|
|
5
|
+
processed: number;
|
|
6
|
+
total: number;
|
|
7
|
+
}
|
|
4
8
|
export declare function clearMiningGateCache(walletRootId: string | null | undefined): void;
|
|
5
9
|
export declare function topologicallyOrderAncestorTxidsForTesting(options: {
|
|
6
10
|
txid: string;
|
|
@@ -20,4 +24,6 @@ export declare function runCompetitivenessGate(options: {
|
|
|
20
24
|
cooperativeYield?: MiningCooperativeYield;
|
|
21
25
|
cooperativeYieldEvery?: number;
|
|
22
26
|
throwIfStopping?: () => void;
|
|
27
|
+
onWarmupProgress?: (progress: MiningGateWarmupProgress) => Promise<void> | void;
|
|
23
28
|
}): Promise<CompetitivenessDecision>;
|
|
29
|
+
export {};
|
|
@@ -6,6 +6,8 @@ import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
|
6
6
|
import { compareLexicographically, numberToSats, resolveBip39WordsFromIndices, rootDomain, tieBreakHash, } from "./engine-utils.js";
|
|
7
7
|
import { getIndexerTruthKey } from "./candidate.js";
|
|
8
8
|
const MINING_MEMPOOL_COOPERATIVE_YIELD_EVERY = 25;
|
|
9
|
+
const MINING_MEMPOOL_RAW_TX_FETCH_CONCURRENCY = 8;
|
|
10
|
+
const MINING_MEMPOOL_PROGRESS_REPORT_EVERY = 25;
|
|
9
11
|
const miningGateCache = new Map();
|
|
10
12
|
function defaultMiningCooperativeYield() {
|
|
11
13
|
return new Promise((resolve) => {
|
|
@@ -19,6 +21,98 @@ async function maybeYieldDuringMempoolScan(options) {
|
|
|
19
21
|
}
|
|
20
22
|
await (options.cooperativeYield ?? defaultMiningCooperativeYield)();
|
|
21
23
|
}
|
|
24
|
+
function getOrCreateMiningGateCacheState(walletRootId) {
|
|
25
|
+
const existing = miningGateCache.get(walletRootId);
|
|
26
|
+
if (existing !== undefined) {
|
|
27
|
+
return existing;
|
|
28
|
+
}
|
|
29
|
+
const created = {
|
|
30
|
+
rawTxContexts: new Map(),
|
|
31
|
+
decisionReuse: null,
|
|
32
|
+
};
|
|
33
|
+
miningGateCache.set(walletRootId, created);
|
|
34
|
+
return created;
|
|
35
|
+
}
|
|
36
|
+
function pruneRawTxContextsToVisibleTxids(options) {
|
|
37
|
+
const visibleSet = new Set(options.visibleTxids);
|
|
38
|
+
for (const txid of [...options.rawTxContexts.keys()]) {
|
|
39
|
+
if (!visibleSet.has(txid)) {
|
|
40
|
+
options.rawTxContexts.delete(txid);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveEffectiveFeeRate(mempoolEntry) {
|
|
45
|
+
return Number([
|
|
46
|
+
mempoolEntry.vsize > 0 ? (numberToSats(mempoolEntry.fees.base) / BigInt(mempoolEntry.vsize)) : 0n,
|
|
47
|
+
(mempoolEntry.ancestorsize ?? 0) > 0
|
|
48
|
+
? (numberToSats(mempoolEntry.fees.ancestor) / BigInt(mempoolEntry.ancestorsize ?? 1))
|
|
49
|
+
: 0n,
|
|
50
|
+
(mempoolEntry.descendantsize ?? 0) > 0
|
|
51
|
+
? (numberToSats(mempoolEntry.fees.descendant) / BigInt(mempoolEntry.descendantsize ?? 1))
|
|
52
|
+
: 0n,
|
|
53
|
+
].reduce((best, candidate) => (candidate > best ? candidate : best), 0n));
|
|
54
|
+
}
|
|
55
|
+
async function warmMissingRawTxContexts(options) {
|
|
56
|
+
const missingTxids = options.visibleTxids.filter((txid) => !options.rawTxContexts.has(txid));
|
|
57
|
+
if (missingTxids.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let completed = 0;
|
|
61
|
+
let nextIndex = 0;
|
|
62
|
+
let lastReportedProcessed = -1;
|
|
63
|
+
let reportPromise = Promise.resolve();
|
|
64
|
+
const reportProgress = async (processed, force = false) => {
|
|
65
|
+
if (options.onWarmupProgress === undefined) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!force && processed !== missingTxids.length && (processed % MINING_MEMPOOL_PROGRESS_REPORT_EVERY) !== 0) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (processed === lastReportedProcessed) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
lastReportedProcessed = processed;
|
|
75
|
+
reportPromise = reportPromise.then(async () => {
|
|
76
|
+
await options.onWarmupProgress?.({
|
|
77
|
+
processed,
|
|
78
|
+
total: missingTxids.length,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
await reportPromise;
|
|
82
|
+
};
|
|
83
|
+
await reportProgress(0, true);
|
|
84
|
+
const workerCount = Math.min(MINING_MEMPOOL_RAW_TX_FETCH_CONCURRENCY, missingTxids.length);
|
|
85
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
86
|
+
while (true) {
|
|
87
|
+
const iteration = nextIndex;
|
|
88
|
+
if (iteration >= missingTxids.length) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
nextIndex += 1;
|
|
92
|
+
await maybeYieldDuringMempoolScan({
|
|
93
|
+
iteration,
|
|
94
|
+
cooperativeYield: options.cooperativeYield,
|
|
95
|
+
cooperativeYieldEvery: options.cooperativeYieldEvery,
|
|
96
|
+
});
|
|
97
|
+
options.throwIfStopping?.();
|
|
98
|
+
const txid = missingTxids[iteration];
|
|
99
|
+
const tx = await options.rpc.getRawTransaction(txid, true).catch(() => null);
|
|
100
|
+
options.throwIfStopping?.();
|
|
101
|
+
if (tx !== null) {
|
|
102
|
+
const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
|
|
103
|
+
options.rawTxContexts.set(txid, {
|
|
104
|
+
txid,
|
|
105
|
+
senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
|
|
106
|
+
rawTransaction: tx,
|
|
107
|
+
payload: payloadHex === undefined ? null : extractOpReturnPayloadFromScriptHex(payloadHex),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
completed += 1;
|
|
111
|
+
await reportProgress(completed, completed === missingTxids.length);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
await Promise.all(workers);
|
|
115
|
+
}
|
|
22
116
|
export function clearMiningGateCache(walletRootId) {
|
|
23
117
|
if (walletRootId === null || walletRootId === undefined) {
|
|
24
118
|
miningGateCache.clear();
|
|
@@ -368,9 +462,25 @@ export async function runCompetitivenessGate(options) {
|
|
|
368
462
|
options.candidate.canonicalBlend.toString(),
|
|
369
463
|
options.candidate.sender.scriptPubKeyHex,
|
|
370
464
|
].join(":");
|
|
465
|
+
const cacheState = getOrCreateMiningGateCacheState(walletRootId);
|
|
466
|
+
const setDecisionReuse = (decision) => {
|
|
467
|
+
cacheState.decisionReuse = {
|
|
468
|
+
indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
|
|
469
|
+
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
470
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
471
|
+
localAssayTupleKey,
|
|
472
|
+
excludedTxidsKey: excludedTxids.join(","),
|
|
473
|
+
mempoolSequence,
|
|
474
|
+
decision,
|
|
475
|
+
};
|
|
476
|
+
};
|
|
371
477
|
let mempoolVerbose;
|
|
478
|
+
let mempoolEntries;
|
|
372
479
|
try {
|
|
373
|
-
mempoolVerbose = await
|
|
480
|
+
[mempoolVerbose, mempoolEntries] = await Promise.all([
|
|
481
|
+
options.rpc.getRawMempoolVerbose(),
|
|
482
|
+
options.rpc.getRawMempoolEntries(),
|
|
483
|
+
]);
|
|
374
484
|
options.throwIfStopping?.();
|
|
375
485
|
}
|
|
376
486
|
catch {
|
|
@@ -379,17 +489,17 @@ export async function runCompetitivenessGate(options) {
|
|
|
379
489
|
});
|
|
380
490
|
}
|
|
381
491
|
const mempoolSequence = String(mempoolVerbose.mempool_sequence);
|
|
382
|
-
const cached =
|
|
383
|
-
const cachedTruthMatches = cached !==
|
|
492
|
+
const cached = cacheState.decisionReuse;
|
|
493
|
+
const cachedTruthMatches = cached !== null
|
|
384
494
|
&& indexerTruthKey !== null
|
|
385
495
|
&& cached.indexerDaemonInstanceId === indexerTruthKey.daemonInstanceId
|
|
386
496
|
&& cached.indexerSnapshotSeq === indexerTruthKey.snapshotSeq;
|
|
387
|
-
const cachedReferencedBlockMatches = cached !==
|
|
497
|
+
const cachedReferencedBlockMatches = cached !== null
|
|
388
498
|
&& cached.referencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay;
|
|
389
|
-
if (cached !==
|
|
390
|
-
|
|
499
|
+
if (cached !== null && (!cachedTruthMatches || !cachedReferencedBlockMatches)) {
|
|
500
|
+
cacheState.decisionReuse = null;
|
|
391
501
|
}
|
|
392
|
-
if (cached !==
|
|
502
|
+
if (cached !== null
|
|
393
503
|
&& cachedTruthMatches
|
|
394
504
|
&& cachedReferencedBlockMatches
|
|
395
505
|
&& cached.localAssayTupleKey === localAssayTupleKey
|
|
@@ -402,47 +512,19 @@ export async function runCompetitivenessGate(options) {
|
|
|
402
512
|
}
|
|
403
513
|
const referencedPrefix = Buffer.from(options.candidate.referencedBlockHashInternal.subarray(0, 4)).toString("hex");
|
|
404
514
|
const visibleTxids = mempoolVerbose.txids.filter((txid) => !excludedTxids.includes(txid));
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
});
|
|
419
|
-
options.throwIfStopping?.();
|
|
420
|
-
const txid = visibleTxids[index];
|
|
421
|
-
if (txContexts.has(txid)) {
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
const [tx, mempoolEntry] = await Promise.all([
|
|
425
|
-
options.rpc.getRawTransaction(txid, true).catch(() => null),
|
|
426
|
-
options.rpc.getMempoolEntry(txid).catch(() => null),
|
|
427
|
-
]);
|
|
428
|
-
options.throwIfStopping?.();
|
|
429
|
-
if (tx === null || mempoolEntry === null) {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
const effectiveFeeRate = Number([
|
|
433
|
-
mempoolEntry.vsize > 0 ? (numberToSats(mempoolEntry.fees.base) / BigInt(mempoolEntry.vsize)) : 0n,
|
|
434
|
-
(mempoolEntry.ancestorsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.ancestor) / BigInt(mempoolEntry.ancestorsize ?? 1)) : 0n,
|
|
435
|
-
(mempoolEntry.descendantsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.descendant) / BigInt(mempoolEntry.descendantsize ?? 1)) : 0n,
|
|
436
|
-
].reduce((best, candidate) => (candidate > best ? candidate : best), 0n));
|
|
437
|
-
const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
|
|
438
|
-
txContexts.set(txid, {
|
|
439
|
-
txid,
|
|
440
|
-
effectiveFeeRate,
|
|
441
|
-
senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
|
|
442
|
-
rawTransaction: tx,
|
|
443
|
-
payload: payloadHex === undefined ? null : extractOpReturnPayloadFromScriptHex(payloadHex),
|
|
444
|
-
});
|
|
445
|
-
}
|
|
515
|
+
pruneRawTxContextsToVisibleTxids({
|
|
516
|
+
rawTxContexts: cacheState.rawTxContexts,
|
|
517
|
+
visibleTxids,
|
|
518
|
+
});
|
|
519
|
+
await warmMissingRawTxContexts({
|
|
520
|
+
rpc: options.rpc,
|
|
521
|
+
rawTxContexts: cacheState.rawTxContexts,
|
|
522
|
+
visibleTxids,
|
|
523
|
+
cooperativeYield: options.cooperativeYield,
|
|
524
|
+
cooperativeYieldEvery: options.cooperativeYieldEvery,
|
|
525
|
+
throwIfStopping: options.throwIfStopping,
|
|
526
|
+
onWarmupProgress: options.onWarmupProgress,
|
|
527
|
+
});
|
|
446
528
|
const entries = new Map();
|
|
447
529
|
for (let index = 0; index < visibleTxids.length; index += 1) {
|
|
448
530
|
await maybeYieldDuringMempoolScan({
|
|
@@ -452,8 +534,9 @@ export async function runCompetitivenessGate(options) {
|
|
|
452
534
|
});
|
|
453
535
|
options.throwIfStopping?.();
|
|
454
536
|
const txid = visibleTxids[index];
|
|
455
|
-
const context =
|
|
456
|
-
|
|
537
|
+
const context = cacheState.rawTxContexts.get(txid);
|
|
538
|
+
const mempoolEntry = mempoolEntries[txid];
|
|
539
|
+
if (context === undefined || context.payload === null || context.senderScriptHex === null || mempoolEntry === undefined) {
|
|
457
540
|
continue;
|
|
458
541
|
}
|
|
459
542
|
const decoded = decodeMinePayload(context.payload);
|
|
@@ -463,7 +546,7 @@ export async function runCompetitivenessGate(options) {
|
|
|
463
546
|
const overlayDomain = await resolveOverlayAuthorizedMiningDomain({
|
|
464
547
|
readContext: options.readContext,
|
|
465
548
|
txid,
|
|
466
|
-
txContexts,
|
|
549
|
+
txContexts: cacheState.rawTxContexts,
|
|
467
550
|
domainId: decoded.domainId,
|
|
468
551
|
senderScriptHex: context.senderScriptHex,
|
|
469
552
|
});
|
|
@@ -475,17 +558,7 @@ export async function runCompetitivenessGate(options) {
|
|
|
475
558
|
mempoolSequenceCacheStatus: "refreshed",
|
|
476
559
|
lastMempoolSequence: mempoolSequence,
|
|
477
560
|
});
|
|
478
|
-
|
|
479
|
-
indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
|
|
480
|
-
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
481
|
-
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
482
|
-
localAssayTupleKey,
|
|
483
|
-
excludedTxidsKey: excludedTxids.join(","),
|
|
484
|
-
mempoolSequence,
|
|
485
|
-
txids: [...visibleTxids],
|
|
486
|
-
txContexts,
|
|
487
|
-
decision,
|
|
488
|
-
});
|
|
561
|
+
setDecisionReuse(decision);
|
|
489
562
|
return decision;
|
|
490
563
|
}
|
|
491
564
|
if (overlayDomain === null || overlayDomain.name === null || !rootDomain(overlayDomain.name)) {
|
|
@@ -499,7 +572,7 @@ export async function runCompetitivenessGate(options) {
|
|
|
499
572
|
}
|
|
500
573
|
entries.set(txid, {
|
|
501
574
|
txid,
|
|
502
|
-
effectiveFeeRate:
|
|
575
|
+
effectiveFeeRate: resolveEffectiveFeeRate(mempoolEntry),
|
|
503
576
|
domainId: decoded.domainId,
|
|
504
577
|
domainName: overlayDomain.name,
|
|
505
578
|
sentence: Buffer.from(decoded.sentenceBytes).toString("utf8"),
|
|
@@ -631,16 +704,6 @@ export async function runCompetitivenessGate(options) {
|
|
|
631
704
|
});
|
|
632
705
|
}
|
|
633
706
|
}
|
|
634
|
-
|
|
635
|
-
indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
|
|
636
|
-
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
637
|
-
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
638
|
-
localAssayTupleKey,
|
|
639
|
-
excludedTxidsKey: excludedTxids.join(","),
|
|
640
|
-
mempoolSequence,
|
|
641
|
-
txids: [...visibleTxids],
|
|
642
|
-
txContexts,
|
|
643
|
-
decision,
|
|
644
|
-
});
|
|
707
|
+
setDecisionReuse(decision);
|
|
645
708
|
return decision;
|
|
646
709
|
}
|