@cogcoin/client 1.1.11 → 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,19 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.10` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
3
+ `@cogcoin/client@1.1.13` 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
7
  ## Quick Start
8
8
 
9
- Install the package:
9
+ Install Cogcoin:
10
10
 
11
11
  ```bash
12
- npm install -g @cogcoin/client
13
- cogcoin init
12
+ curl -fsSL https://cogcoin.org/install.sh | bash
13
+ # or on Windows PowerShell:
14
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://cogcoin.org/install.ps1 | iex"
15
+
16
+ # `cogcoin init` starts automatically in an interactive terminal and continues into sync.
14
17
  cogcoin address # Send 0.0015 BTC to address
15
18
  cogcoin register <domainname> # 6+ character domain for 0.001 BTC
16
19
  cogcoin anchor <domainname> # You can leave a founding message permanently on Bitcoin!
@@ -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[];
@@ -23,7 +23,9 @@ async function ensureMiningProviderSetup(options) {
23
23
  paths: options.runtimePaths,
24
24
  });
25
25
  if (!setupReady) {
26
- throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
26
+ throw new Error(options.prompter.isInteractive
27
+ ? "Built-in mining provider is not configured. Run `cogcoin mine setup`."
28
+ : "mine_setup_requires_tty");
27
29
  }
28
30
  }
29
31
  function sleep(ms, signal) {
@@ -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
  }
@@ -339,6 +339,9 @@ export async function runMiningPhaseMachine(options) {
339
339
  score: best.canonicalBlend.toString(),
340
340
  runId: options.backgroundWorkerRunId,
341
341
  }));
342
+ let lastScoringProgressProcessed = -1;
343
+ let lastScoringProgressSavedAtUnixMs = 0;
344
+ let scoringProgressWrite = Promise.resolve();
342
345
  const gate = await runGateImpl({
343
346
  rpc: options.rpc,
344
347
  readContext: options.readContext,
@@ -348,7 +351,32 @@ export async function runMiningPhaseMachine(options) {
348
351
  cooperativeYield: options.cooperativeYieldImpl,
349
352
  cooperativeYieldEvery: options.cooperativeYieldEvery,
350
353
  throwIfStopping: options.throwIfStopping,
354
+ onWarmupProgress: async (progress) => {
355
+ if (progress.total <= 0) {
356
+ return;
357
+ }
358
+ const nowUnixMs = now();
359
+ if (progress.processed === lastScoringProgressProcessed
360
+ || (progress.processed !== 0
361
+ && progress.processed !== progress.total
362
+ && (nowUnixMs - lastScoringProgressSavedAtUnixMs) < 500)) {
363
+ return;
364
+ }
365
+ lastScoringProgressProcessed = progress.processed;
366
+ lastScoringProgressSavedAtUnixMs = nowUnixMs;
367
+ scoringProgressWrite = scoringProgressWrite.then(async () => {
368
+ await options.saveCycleStatus(options.readContext, {
369
+ runMode: options.runMode,
370
+ currentPhase: "scoring",
371
+ currentPublishDecision: null,
372
+ lastError: null,
373
+ note: `Scoring mining candidates for the current tip (mempool ${progress.processed}/${progress.total}).`,
374
+ });
375
+ });
376
+ await scoringProgressWrite;
377
+ },
351
378
  });
379
+ await scoringProgressWrite;
352
380
  throwIfInterrupted();
353
381
  state.gateSnapshot = {
354
382
  higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
@@ -28,6 +28,16 @@ export type MiningRpcClient = WalletMutationRpcClient & {
28
28
  txids: string[];
29
29
  mempool_sequence: string | number;
30
30
  }>;
31
+ getRawMempoolEntries(): Promise<Record<string, {
32
+ vsize: number;
33
+ fees: {
34
+ base: number;
35
+ ancestor: number;
36
+ descendant: number;
37
+ };
38
+ ancestorsize?: number;
39
+ descendantsize?: number;
40
+ }>>;
31
41
  getMempoolEntry(txid: string): Promise<{
32
42
  vsize: number;
33
43
  fees: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",