@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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/dist/bitcoind/client/managed-client.d.ts +2 -0
  3. package/dist/bitcoind/client/managed-client.js +6 -0
  4. package/dist/bitcoind/indexer-daemon/background-follow.d.ts +23 -0
  5. package/dist/bitcoind/indexer-daemon/background-follow.js +132 -0
  6. package/dist/bitcoind/indexer-daemon/client.d.ts +12 -0
  7. package/dist/bitcoind/indexer-daemon/client.js +137 -0
  8. package/dist/bitcoind/indexer-daemon/lifecycle.d.ts +30 -0
  9. package/dist/bitcoind/indexer-daemon/lifecycle.js +153 -0
  10. package/dist/bitcoind/indexer-daemon/process.d.ts +35 -0
  11. package/dist/bitcoind/indexer-daemon/process.js +140 -0
  12. package/dist/bitcoind/indexer-daemon/runtime.d.ts +23 -0
  13. package/dist/bitcoind/indexer-daemon/runtime.js +204 -0
  14. package/dist/bitcoind/indexer-daemon/server.d.ts +12 -0
  15. package/dist/bitcoind/indexer-daemon/server.js +87 -0
  16. package/dist/bitcoind/indexer-daemon/snapshot-leases.d.ts +23 -0
  17. package/dist/bitcoind/indexer-daemon/snapshot-leases.js +139 -0
  18. package/dist/bitcoind/indexer-daemon/status.d.ts +23 -0
  19. package/dist/bitcoind/indexer-daemon/status.js +282 -0
  20. package/dist/bitcoind/indexer-daemon/types.d.ts +141 -0
  21. package/dist/bitcoind/indexer-daemon/types.js +1 -0
  22. package/dist/bitcoind/indexer-daemon-main.js +14 -665
  23. package/dist/bitcoind/indexer-daemon.d.ts +4 -132
  24. package/dist/bitcoind/indexer-daemon.js +2 -417
  25. package/dist/bitcoind/managed-bitcoind-service-config.d.ts +18 -1
  26. package/dist/bitcoind/managed-bitcoind-service-config.js +38 -1
  27. package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +30 -24
  28. package/dist/bitcoind/managed-bitcoind-service-status.js +0 -8
  29. package/dist/bitcoind/rpc.d.ts +2 -1
  30. package/dist/bitcoind/rpc.js +3 -0
  31. package/dist/bitcoind/types.d.ts +1 -0
  32. package/dist/cli/mining-format.js +6 -1
  33. package/dist/cli/wallet-format/balance.js +1 -1
  34. package/dist/client/default-client.d.ts +3 -1
  35. package/dist/client/default-client.js +22 -0
  36. package/dist/types.d.ts +13 -1
  37. package/dist/wallet/fs/atomic.d.ts +11 -2
  38. package/dist/wallet/fs/atomic.js +45 -5
  39. package/dist/wallet/mining/competitiveness.d.ts +6 -0
  40. package/dist/wallet/mining/competitiveness.js +137 -74
  41. package/dist/wallet/mining/cycle.js +32 -4
  42. package/dist/wallet/mining/engine-types.d.ts +10 -0
  43. package/dist/wallet/mining/projection.d.ts +1 -0
  44. package/dist/wallet/mining/projection.js +15 -1
  45. package/dist/wallet/mining/visualizer-sync.js +7 -9
  46. package/dist/wallet/mining/visualizer.js +2 -1
  47. package/dist/wallet/read/context.js +3 -2
  48. package/dist/wallet/read/local-state.d.ts +8 -0
  49. package/dist/wallet/read/local-state.js +32 -6
  50. package/dist/wallet/read/types.d.ts +1 -0
  51. 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 createBitcoindServiceStatus({
211
- binaryVersion,
212
- serviceInstanceId: randomBytes(16).toString("hex"),
213
- state: "ready",
214
- processId: child.pid ?? null,
215
- walletRootId: startOptions.walletRootId,
216
- chain: startOptions.chain,
217
- dataDir: startOptions.dataDir,
218
- runtimeRoot: paths.walletRuntimeRoot,
219
- startHeight: startOptions.startHeight,
220
- rpc: rpcConfig,
221
- zmq: zmqConfig,
222
- p2pPort: runtimeConfig.p2pPort,
223
- getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
224
- getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
225
- walletReplica,
226
- startedAtUnixMs: nowUnixMs,
227
- heartbeatAtUnixMs: nowUnixMs,
228
- lastError: walletReplica.message ?? null,
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);
@@ -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>;
@@ -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
  }
@@ -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.fundingSpendableSats ?? null)}`);
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 declare function writeFileAtomic(filePath: string, data: string | Uint8Array, options?: AtomicWriteOptions): Promise<void>;
6
- export declare function writeJsonFileAtomic(filePath: string, value: unknown, options?: AtomicWriteOptions): Promise<void>;
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>;
@@ -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
- export async function writeFileAtomic(filePath, data, options = {}) {
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
- await rename(tempPath, filePath);
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 options.rpc.getRawMempoolVerbose();
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 = miningGateCache.get(walletRootId);
383
- const cachedTruthMatches = cached !== undefined
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 !== undefined
497
+ const cachedReferencedBlockMatches = cached !== null
388
498
  && cached.referencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay;
389
- if (cached !== undefined && (!cachedTruthMatches || !cachedReferencedBlockMatches)) {
390
- clearMiningGateCache(walletRootId);
499
+ if (cached !== null && (!cachedTruthMatches || !cachedReferencedBlockMatches)) {
500
+ cacheState.decisionReuse = null;
391
501
  }
392
- if (cached !== undefined
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
- const txContexts = cachedTruthMatches && cachedReferencedBlockMatches
406
- ? (cached?.txContexts ?? new Map())
407
- : new Map();
408
- for (const txid of [...txContexts.keys()]) {
409
- if (!visibleTxids.includes(txid)) {
410
- txContexts.delete(txid);
411
- }
412
- }
413
- for (let index = 0; index < visibleTxids.length; index += 1) {
414
- await maybeYieldDuringMempoolScan({
415
- iteration: index,
416
- cooperativeYield: options.cooperativeYield,
417
- cooperativeYieldEvery: options.cooperativeYieldEvery,
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 = txContexts.get(txid);
456
- if (context === undefined || context.payload === null || context.senderScriptHex === null) {
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
- miningGateCache.set(walletRootId, {
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: context.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
- miningGateCache.set(walletRootId, {
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
  }