Merge pull request #3818 from mempool/mononaut/calculate-sigops
Count sigops and use adjusted vsizes
This commit is contained in:
commit
11669849ab
@ -14,7 +14,6 @@ class Audit {
|
|||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
|
||||||
const sigop: string[] = []; // missing, but possibly has an adjusted vsize due to high sigop count
|
|
||||||
const isCensored = {}; // missing, without excuse
|
const isCensored = {}; // missing, without excuse
|
||||||
const isDisplaced = {};
|
const isDisplaced = {};
|
||||||
let displacedWeight = 0;
|
let displacedWeight = 0;
|
||||||
@ -38,8 +37,6 @@ class Audit {
|
|||||||
// tx is recent, may have reached the miner too late for inclusion
|
// tx is recent, may have reached the miner too late for inclusion
|
||||||
if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
||||||
fresh.push(txid);
|
fresh.push(txid);
|
||||||
} else if (this.isPossibleHighSigop(mempool[txid])) {
|
|
||||||
sigop.push(txid);
|
|
||||||
} else {
|
} else {
|
||||||
isCensored[txid] = true;
|
isCensored[txid] = true;
|
||||||
}
|
}
|
||||||
@ -140,19 +137,11 @@ class Audit {
|
|||||||
censored: Object.keys(isCensored),
|
censored: Object.keys(isCensored),
|
||||||
added,
|
added,
|
||||||
fresh,
|
fresh,
|
||||||
sigop,
|
sigop: [],
|
||||||
score,
|
score,
|
||||||
similarity,
|
similarity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect transactions with a possibly adjusted vsize due to high sigop count
|
|
||||||
// very rough heuristic based on number of OP_CHECKMULTISIG outputs
|
|
||||||
// will miss cases with other sources of sigops
|
|
||||||
isPossibleHighSigop(tx: TransactionExtended): boolean {
|
|
||||||
const numBareMultisig = tx.vout.reduce((count, vout) => count + (vout.scriptpubkey_asm.includes('OP_CHECKMULTISIG') ? 1 : 0), 0);
|
|
||||||
return (numBareMultisig * 400) > tx.vsize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Audit();
|
export default new Audit();
|
@ -211,6 +211,8 @@ class BitcoinRoutes {
|
|||||||
bestDescendant: tx.bestDescendant || null,
|
bestDescendant: tx.bestDescendant || null,
|
||||||
descendants: tx.descendants || null,
|
descendants: tx.descendants || null,
|
||||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||||
|
sigops: tx.sigops,
|
||||||
|
adjustedVsize: tx.adjustedVsize,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import config from '../config';
|
|||||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary } from '../mempool.interfaces';
|
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import diskCache from './disk-cache';
|
import diskCache from './disk-cache';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
@ -76,6 +76,7 @@ class Blocks {
|
|||||||
blockHeight: number,
|
blockHeight: number,
|
||||||
onlyCoinbase: boolean,
|
onlyCoinbase: boolean,
|
||||||
quiet: boolean = false,
|
quiet: boolean = false,
|
||||||
|
addMempoolData: boolean = false,
|
||||||
): Promise<TransactionExtended[]> {
|
): Promise<TransactionExtended[]> {
|
||||||
const transactions: TransactionExtended[] = [];
|
const transactions: TransactionExtended[] = [];
|
||||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||||
@ -96,14 +97,14 @@ class Blocks {
|
|||||||
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
|
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
|
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
transactionsFetched++;
|
transactionsFetched++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
// Try again with core
|
// Try again with core
|
||||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true);
|
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
transactionsFetched++;
|
transactionsFetched++;
|
||||||
} else {
|
} else {
|
||||||
@ -126,11 +127,13 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transactions.forEach((tx) => {
|
if (addMempoolData) {
|
||||||
if (!tx.cpfpChecked) {
|
transactions.forEach((tx) => {
|
||||||
Common.setRelativesAndGetCpfpInfo(tx, mempool); // Child Pay For Parent
|
if (!tx.cpfpChecked) {
|
||||||
}
|
Common.setRelativesAndGetCpfpInfo(tx as MempoolTransactionExtended, mempool); // Child Pay For Parent
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
|
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
|
||||||
@ -596,7 +599,7 @@ class Blocks {
|
|||||||
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
||||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, false, true);
|
||||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||||
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
@ -57,15 +57,15 @@ export class Common {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended[] } {
|
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } {
|
||||||
const matches: { [txid: string]: TransactionExtended[] } = {};
|
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
||||||
added
|
added
|
||||||
.forEach((addedTx) => {
|
.forEach((addedTx) => {
|
||||||
const foundMatches = deleted.filter((deletedTx) => {
|
const foundMatches = deleted.filter((deletedTx) => {
|
||||||
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
||||||
return addedTx.fee > deletedTx.fee
|
return addedTx.fee > deletedTx.fee
|
||||||
// The new transaction must pay more fee per kB than the replaced tx.
|
// The new transaction must pay more fee per kB than the replaced tx.
|
||||||
&& addedTx.feePerVsize > deletedTx.feePerVsize
|
&& addedTx.adjustedFeePerVsize > deletedTx.adjustedFeePerVsize
|
||||||
// Spends one or more of the same inputs
|
// Spends one or more of the same inputs
|
||||||
&& deletedTx.vin.some((deletedVin) =>
|
&& deletedTx.vin.some((deletedVin) =>
|
||||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||||
@ -77,10 +77,10 @@ export class Common {
|
|||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
static findMinedRbfTransactions(minedTransactions: TransactionExtended[], spendMap: Map<string, TransactionExtended>): { [txid: string]: { replaced: TransactionExtended[], replacedBy: TransactionExtended }} {
|
static findMinedRbfTransactions(minedTransactions: TransactionExtended[], spendMap: Map<string, MempoolTransactionExtended>): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||||
const matches: { [txid: string]: { replaced: TransactionExtended[], replacedBy: TransactionExtended }} = {};
|
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||||
for (const tx of minedTransactions) {
|
for (const tx of minedTransactions) {
|
||||||
const replaced: Set<TransactionExtended> = new Set();
|
const replaced: Set<MempoolTransactionExtended> = new Set();
|
||||||
for (let i = 0; i < tx.vin.length; i++) {
|
for (let i = 0; i < tx.vin.length; i++) {
|
||||||
const vin = tx.vin[i];
|
const vin = tx.vin[i];
|
||||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||||
@ -120,18 +120,18 @@ export class Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static setRelativesAndGetCpfpInfo(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): CpfpInfo {
|
static setRelativesAndGetCpfpInfo(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||||
const parents = this.findAllParents(tx, memPool);
|
const parents = this.findAllParents(tx, memPool);
|
||||||
const lowerFeeParents = parents.filter((parent) => parent.feePerVsize < tx.effectiveFeePerVsize);
|
const lowerFeeParents = parents.filter((parent) => parent.adjustedFeePerVsize < tx.effectiveFeePerVsize);
|
||||||
|
|
||||||
let totalWeight = tx.weight + lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
let totalWeight = (tx.adjustedVsize * 4) + lowerFeeParents.reduce((prev, val) => prev + (val.adjustedVsize * 4), 0);
|
||||||
let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
||||||
|
|
||||||
tx.ancestors = parents
|
tx.ancestors = parents
|
||||||
.map((t) => {
|
.map((t) => {
|
||||||
return {
|
return {
|
||||||
txid: t.txid,
|
txid: t.txid,
|
||||||
weight: t.weight,
|
weight: (t.adjustedVsize * 4),
|
||||||
fee: t.fee,
|
fee: t.fee,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -152,8 +152,8 @@ export class Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static findAllParents(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): TransactionExtended[] {
|
private static findAllParents(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): MempoolTransactionExtended[] {
|
||||||
let parents: TransactionExtended[] = [];
|
let parents: MempoolTransactionExtended[] = [];
|
||||||
tx.vin.forEach((parent) => {
|
tx.vin.forEach((parent) => {
|
||||||
if (parents.find((p) => p.txid === parent.txid)) {
|
if (parents.find((p) => p.txid === parent.txid)) {
|
||||||
return;
|
return;
|
||||||
@ -161,17 +161,17 @@ export class Common {
|
|||||||
|
|
||||||
const parentTx = memPool[parent.txid];
|
const parentTx = memPool[parent.txid];
|
||||||
if (parentTx) {
|
if (parentTx) {
|
||||||
if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.feePerVsize) {
|
if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.adjustedFeePerVsize) {
|
||||||
if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) {
|
if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) {
|
||||||
parentTx.bestDescendant = {
|
parentTx.bestDescendant = {
|
||||||
weight: tx.weight + tx.bestDescendant.weight,
|
weight: (tx.adjustedVsize * 4) + tx.bestDescendant.weight,
|
||||||
fee: tx.fee + tx.bestDescendant.fee,
|
fee: tx.fee + tx.bestDescendant.fee,
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (tx.feePerVsize > parentTx.feePerVsize) {
|
} else if (tx.adjustedFeePerVsize > parentTx.adjustedFeePerVsize) {
|
||||||
parentTx.bestDescendant = {
|
parentTx.bestDescendant = {
|
||||||
weight: tx.weight,
|
weight: (tx.adjustedVsize * 4),
|
||||||
fee: tx.fee,
|
fee: tx.fee,
|
||||||
txid: tx.txid
|
txid: tx.txid
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
|
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
|
||||||
import { Common, OnlineFeeStatsCalculator } from './common';
|
import { Common, OnlineFeeStatsCalculator } from './common';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { Worker } from 'worker_threads';
|
import { Worker } from 'worker_threads';
|
||||||
@ -36,9 +36,9 @@ class MempoolBlocks {
|
|||||||
return this.mempoolBlockDeltas;
|
return this.mempoolBlockDeltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
|
public updateMempoolBlocks(memPool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
|
||||||
const latestMempool = memPool;
|
const latestMempool = memPool;
|
||||||
const memPoolArray: TransactionExtended[] = [];
|
const memPoolArray: MempoolTransactionExtended[] = [];
|
||||||
for (const i in latestMempool) {
|
for (const i in latestMempool) {
|
||||||
if (latestMempool.hasOwnProperty(i)) {
|
if (latestMempool.hasOwnProperty(i)) {
|
||||||
memPoolArray.push(latestMempool[i]);
|
memPoolArray.push(latestMempool[i]);
|
||||||
@ -52,17 +52,17 @@ class MempoolBlocks {
|
|||||||
tx.ancestors = [];
|
tx.ancestors = [];
|
||||||
tx.cpfpChecked = false;
|
tx.cpfpChecked = false;
|
||||||
if (!tx.effectiveFeePerVsize) {
|
if (!tx.effectiveFeePerVsize) {
|
||||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
tx.effectiveFeePerVsize = tx.adjustedFeePerVsize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// First sort
|
// First sort
|
||||||
memPoolArray.sort((a, b) => {
|
memPoolArray.sort((a, b) => {
|
||||||
if (a.feePerVsize === b.feePerVsize) {
|
if (a.adjustedFeePerVsize === b.adjustedFeePerVsize) {
|
||||||
// tie-break by lexicographic txid order for stability
|
// tie-break by lexicographic txid order for stability
|
||||||
return a.txid < b.txid ? -1 : 1;
|
return a.txid < b.txid ? -1 : 1;
|
||||||
} else {
|
} else {
|
||||||
return b.feePerVsize - a.feePerVsize;
|
return b.adjustedFeePerVsize - a.adjustedFeePerVsize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ class MempoolBlocks {
|
|||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
private calculateMempoolBlocks(transactionsSorted: MempoolTransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
let feeStatsCalculator: OnlineFeeStatsCalculator = new OnlineFeeStatsCalculator(config.MEMPOOL.BLOCK_WEIGHT_UNITS);
|
let feeStatsCalculator: OnlineFeeStatsCalculator = new OnlineFeeStatsCalculator(config.MEMPOOL.BLOCK_WEIGHT_UNITS);
|
||||||
let onlineStats = false;
|
let onlineStats = false;
|
||||||
@ -112,7 +112,7 @@ class MempoolBlocks {
|
|||||||
let blockFees = 0;
|
let blockFees = 0;
|
||||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||||
let transactionIds: string[] = [];
|
let transactionIds: string[] = [];
|
||||||
let transactions: TransactionExtended[] = [];
|
let transactions: MempoolTransactionExtended[] = [];
|
||||||
transactionsSorted.forEach((tx, index) => {
|
transactionsSorted.forEach((tx, index) => {
|
||||||
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
||||||
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||||
@ -205,7 +205,7 @@ class MempoolBlocks {
|
|||||||
return mempoolBlockDeltas;
|
return mempoolBlockDeltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
// reset mempool short ids
|
// reset mempool short ids
|
||||||
@ -222,9 +222,10 @@ class MempoolBlocks {
|
|||||||
strippedMempool.set(entry.uid, {
|
strippedMempool.set(entry.uid, {
|
||||||
uid: entry.uid,
|
uid: entry.uid,
|
||||||
fee: entry.fee,
|
fee: entry.fee,
|
||||||
weight: entry.weight,
|
weight: (entry.adjustedVsize * 4),
|
||||||
feePerVsize: entry.fee / (entry.weight / 4),
|
sigops: entry.sigops,
|
||||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||||
|
effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -268,7 +269,7 @@ class MempoolBlocks {
|
|||||||
return this.mempoolBlocks;
|
return this.mempoolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
|
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], saveResults: boolean = false): Promise<void> {
|
||||||
if (!this.txSelectionWorker) {
|
if (!this.txSelectionWorker) {
|
||||||
// need to reset the worker
|
// need to reset the worker
|
||||||
await this.$makeBlockTemplates(newMempool, saveResults);
|
await this.$makeBlockTemplates(newMempool, saveResults);
|
||||||
@ -287,9 +288,10 @@ class MempoolBlocks {
|
|||||||
return {
|
return {
|
||||||
uid: entry.uid || 0,
|
uid: entry.uid || 0,
|
||||||
fee: entry.fee,
|
fee: entry.fee,
|
||||||
weight: entry.weight,
|
weight: (entry.adjustedVsize * 4),
|
||||||
feePerVsize: entry.fee / (entry.weight / 4),
|
sigops: entry.sigops,
|
||||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||||
|
effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -341,12 +343,12 @@ class MempoolBlocks {
|
|||||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||||
const block: string[] = blocks[blockIndex];
|
const block: string[] = blocks[blockIndex];
|
||||||
let txid: string;
|
let txid: string;
|
||||||
let mempoolTx: TransactionExtended;
|
let mempoolTx: MempoolTransactionExtended;
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
let totalVsize = 0;
|
let totalVsize = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
let totalFees = 0;
|
let totalFees = 0;
|
||||||
const transactions: TransactionExtended[] = [];
|
const transactions: MempoolTransactionExtended[] = [];
|
||||||
for (let txIndex = 0; txIndex < block.length; txIndex++) {
|
for (let txIndex = 0; txIndex < block.length; txIndex++) {
|
||||||
txid = block[txIndex];
|
txid = block[txIndex];
|
||||||
if (txid) {
|
if (txid) {
|
||||||
@ -397,7 +399,7 @@ class MempoolBlocks {
|
|||||||
const relative = {
|
const relative = {
|
||||||
txid: txid,
|
txid: txid,
|
||||||
fee: mempool[txid].fee,
|
fee: mempool[txid].fee,
|
||||||
weight: mempool[txid].weight,
|
weight: (mempool[txid].adjustedVsize * 4),
|
||||||
};
|
};
|
||||||
if (matched) {
|
if (matched) {
|
||||||
descendants.push(relative);
|
descendants.push(relative);
|
||||||
@ -426,7 +428,7 @@ class MempoolBlocks {
|
|||||||
return mempoolBlocks;
|
return mempoolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
|
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
|
||||||
if (!feeStats) {
|
if (!feeStats) {
|
||||||
feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
||||||
}
|
}
|
||||||
@ -447,7 +449,7 @@ class MempoolBlocks {
|
|||||||
this.nextUid = 1;
|
this.nextUid = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUid(tx: TransactionExtended): number {
|
private setUid(tx: MempoolTransactionExtended): number {
|
||||||
const uid = this.nextUid;
|
const uid = this.nextUid;
|
||||||
this.nextUid++;
|
this.nextUid++;
|
||||||
this.uidMap.set(uid, tx.txid);
|
this.uidMap.set(uid, tx.txid);
|
||||||
@ -455,7 +457,7 @@ class MempoolBlocks {
|
|||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUid(tx: TransactionExtended): number | void {
|
private getUid(tx: MempoolTransactionExtended): number | void {
|
||||||
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
|
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
|
||||||
return tx.uid;
|
return tx.uid;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
@ -13,14 +13,14 @@ import rbfCache from './rbf-cache';
|
|||||||
class Mempool {
|
class Mempool {
|
||||||
private inSync: boolean = false;
|
private inSync: boolean = false;
|
||||||
private mempoolCacheDelta: number = -1;
|
private mempoolCacheDelta: number = -1;
|
||||||
private mempoolCache: { [txId: string]: TransactionExtended } = {};
|
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||||
private spendMap = new Map<string, TransactionExtended>();
|
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
deletedTransactions: MempoolTransactionExtended[]) => void) | undefined;
|
||||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
|
deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
|
||||||
|
|
||||||
private txPerSecondArray: number[] = [];
|
private txPerSecondArray: number[] = [];
|
||||||
private txPerSecond: number = 0;
|
private txPerSecond: number = 0;
|
||||||
@ -64,26 +64,31 @@ class Mempool {
|
|||||||
return this.latestTransactions;
|
return this.latestTransactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
|
||||||
this.mempoolChangedCallback = fn;
|
this.mempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
|
||||||
this.$asyncMempoolChangedCallback = fn;
|
this.$asyncMempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMempool(): { [txid: string]: TransactionExtended } {
|
public getMempool(): { [txid: string]: MempoolTransactionExtended } {
|
||||||
return this.mempoolCache;
|
return this.mempoolCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSpendMap(): Map<string, TransactionExtended> {
|
public getSpendMap(): Map<string, MempoolTransactionExtended> {
|
||||||
return this.spendMap;
|
return this.spendMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
|
||||||
this.mempoolCache = mempoolData;
|
this.mempoolCache = mempoolData;
|
||||||
|
for (const txid of Object.keys(this.mempoolCache)) {
|
||||||
|
if (this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
|
||||||
|
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (this.mempoolChangedCallback) {
|
if (this.mempoolChangedCallback) {
|
||||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||||
}
|
}
|
||||||
@ -133,7 +138,7 @@ class Mempool {
|
|||||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||||
this.updateTimerProgress(timer, 'got raw mempool');
|
this.updateTimerProgress(timer, 'got raw mempool');
|
||||||
const diff = transactions.length - currentMempoolSize;
|
const diff = transactions.length - currentMempoolSize;
|
||||||
const newTransactions: TransactionExtended[] = [];
|
const newTransactions: MempoolTransactionExtended[] = [];
|
||||||
|
|
||||||
this.mempoolCacheDelta = Math.abs(diff);
|
this.mempoolCacheDelta = Math.abs(diff);
|
||||||
|
|
||||||
@ -155,7 +160,7 @@ class Mempool {
|
|||||||
for (const txid of transactions) {
|
for (const txid of transactions) {
|
||||||
if (!this.mempoolCache[txid]) {
|
if (!this.mempoolCache[txid]) {
|
||||||
try {
|
try {
|
||||||
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
|
||||||
this.updateTimerProgress(timer, 'fetched new transaction');
|
this.updateTimerProgress(timer, 'fetched new transaction');
|
||||||
this.mempoolCache[txid] = transaction;
|
this.mempoolCache[txid] = transaction;
|
||||||
if (this.inSync) {
|
if (this.inSync) {
|
||||||
@ -205,7 +210,7 @@ class Mempool {
|
|||||||
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
|
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedTransactions: TransactionExtended[] = [];
|
const deletedTransactions: MempoolTransactionExtended[] = [];
|
||||||
|
|
||||||
if (this.mempoolProtection !== 1) {
|
if (this.mempoolProtection !== 1) {
|
||||||
this.mempoolProtection = 0;
|
this.mempoolProtection = 0;
|
||||||
@ -273,7 +278,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended[]; }): void {
|
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
for (const rbfTransaction in rbfTransactions) {
|
||||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
||||||
// Store replaced transactions
|
// Store replaced transactions
|
||||||
@ -282,16 +287,16 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: TransactionExtended[], replacedBy: TransactionExtended }}): void {
|
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
for (const rbfTransaction in rbfTransactions) {
|
||||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||||
// Store replaced transactions
|
// Store replaced transactions
|
||||||
rbfCache.add(rbfTransactions[rbfTransaction].replaced, rbfTransactions[rbfTransaction].replacedBy);
|
rbfCache.add(rbfTransactions[rbfTransaction].replaced, transactionUtils.extendMempoolTransaction(rbfTransactions[rbfTransaction].replacedBy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addToSpendMap(transactions: TransactionExtended[]): void {
|
public addToSpendMap(transactions: MempoolTransactionExtended[]): void {
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
for (const vin of tx.vin) {
|
for (const vin of tx.vin) {
|
||||||
this.spendMap.set(`${vin.txid}:${vin.vout}`, tx);
|
this.spendMap.set(`${vin.txid}:${vin.vout}`, tx);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import { Common } from "./common";
|
import { Common } from "./common";
|
||||||
|
|
||||||
@ -23,14 +23,14 @@ class RbfCache {
|
|||||||
private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
|
private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
|
||||||
private dirtyTrees: Set<string> = new Set();
|
private dirtyTrees: Set<string> = new Set();
|
||||||
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
||||||
private txs: Map<string, TransactionExtended> = new Map();
|
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
||||||
private expiring: Map<string, number> = new Map();
|
private expiring: Map<string, number> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void {
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ class RbfCache {
|
|||||||
return this.replaces.get(txId);
|
return this.replaces.get(txId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTx(txId: string): TransactionExtended | undefined {
|
public getTx(txId: string): MempoolTransactionExtended | undefined {
|
||||||
return this.txs.get(txId);
|
return this.txs.get(txId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +272,7 @@ class RbfCache {
|
|||||||
return deflated;
|
return deflated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async importTree(root, txid, deflated, txs: Map<string, TransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
|
async importTree(root, txid, deflated, txs: Map<string, MempoolTransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
|
||||||
const treeInfo = deflated[txid];
|
const treeInfo = deflated[txid];
|
||||||
const replaces: RbfTree[] = [];
|
const replaces: RbfTree[] = [];
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
import { TransactionExtended, MempoolTransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||||
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
|
|
||||||
class TransactionUtils {
|
class TransactionUtils {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -22,19 +23,27 @@ class TransactionUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param txId
|
* @param txId
|
||||||
* @param addPrevouts
|
* @param addPrevouts
|
||||||
* @param lazyPrevouts
|
* @param lazyPrevouts
|
||||||
* @param forceCore - See https://github.com/mempool/mempool/issues/2904
|
* @param forceCore - See https://github.com/mempool/mempool/issues/2904
|
||||||
*/
|
*/
|
||||||
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<TransactionExtended> {
|
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||||
let transaction: IEsploraApi.Transaction;
|
let transaction: IEsploraApi.Transaction;
|
||||||
if (forceCore === true) {
|
if (forceCore === true) {
|
||||||
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
||||||
} else {
|
} else {
|
||||||
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||||
}
|
}
|
||||||
return this.extendTransaction(transaction);
|
if (addMempoolData || !transaction?.status?.confirmed) {
|
||||||
|
return this.extendMempoolTransaction(transaction);
|
||||||
|
} else {
|
||||||
|
return this.extendTransaction(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended> {
|
||||||
|
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
||||||
@ -50,7 +59,31 @@ class TransactionUtils {
|
|||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
effectiveFeePerVsize: feePerVbytes,
|
effectiveFeePerVsize: feePerVbytes,
|
||||||
}, transaction);
|
}, transaction);
|
||||||
if (!transaction.status.confirmed) {
|
if (!transaction?.status?.confirmed) {
|
||||||
|
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
|
||||||
|
}
|
||||||
|
return transactionExtended;
|
||||||
|
}
|
||||||
|
|
||||||
|
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
||||||
|
const vsize = Math.ceil(transaction.weight / 4);
|
||||||
|
const fractionalVsize = (transaction.weight / 4);
|
||||||
|
const sigops = this.countSigops(transaction);
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
|
||||||
|
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||||
|
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
|
||||||
|
(transaction.fee || 0) / fractionalVsize);
|
||||||
|
const adjustedFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1,
|
||||||
|
(transaction.fee || 0) / adjustedVsize);
|
||||||
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||||
|
vsize: Math.round(transaction.weight / 4),
|
||||||
|
adjustedVsize,
|
||||||
|
sigops,
|
||||||
|
feePerVsize: feePerVbytes,
|
||||||
|
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||||
|
effectiveFeePerVsize: adjustedFeePerVsize,
|
||||||
|
});
|
||||||
|
if (!transaction?.status?.confirmed) {
|
||||||
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
|
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
|
||||||
}
|
}
|
||||||
return transactionExtended;
|
return transactionExtended;
|
||||||
@ -63,6 +96,64 @@ class TransactionUtils {
|
|||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
|
||||||
|
let sigops = 0;
|
||||||
|
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
|
||||||
|
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
|
||||||
|
|
||||||
|
// count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
|
||||||
|
if (isRawScript) {
|
||||||
|
// in scriptPubKey or scriptSig, always worth 20
|
||||||
|
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
|
||||||
|
} else {
|
||||||
|
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
|
||||||
|
const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
|
||||||
|
for (const match of matches) {
|
||||||
|
const n = parseInt(match[1]);
|
||||||
|
if (Number.isInteger(n)) {
|
||||||
|
sigops += n;
|
||||||
|
} else {
|
||||||
|
sigops += 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return witness ? sigops : (sigops * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public countSigops(transaction: IEsploraApi.Transaction): number {
|
||||||
|
let sigops = 0;
|
||||||
|
|
||||||
|
for (const input of transaction.vin) {
|
||||||
|
if (input.scriptsig_asm) {
|
||||||
|
sigops += this.countScriptSigops(input.scriptsig_asm, true);
|
||||||
|
}
|
||||||
|
if (input.prevout) {
|
||||||
|
switch (true) {
|
||||||
|
case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'):
|
||||||
|
case input.prevout.scriptpubkey_type === 'v0_p2wpkh':
|
||||||
|
sigops += 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'):
|
||||||
|
case input.prevout.scriptpubkey_type === 'v0_p2wsh':
|
||||||
|
if (input.witness?.length) {
|
||||||
|
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const output of transaction.vout) {
|
||||||
|
if (output.scriptpubkey_asm) {
|
||||||
|
sigops += this.countScriptSigops(output.scriptpubkey_asm, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sigops;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TransactionUtils();
|
export default new TransactionUtils();
|
||||||
|
@ -48,12 +48,14 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
|||||||
weight: tx.weight,
|
weight: tx.weight,
|
||||||
feePerVsize: tx.feePerVsize,
|
feePerVsize: tx.feePerVsize,
|
||||||
effectiveFeePerVsize: tx.feePerVsize,
|
effectiveFeePerVsize: tx.feePerVsize,
|
||||||
|
sigops: tx.sigops,
|
||||||
inputs: tx.inputs || [],
|
inputs: tx.inputs || [],
|
||||||
relativesSet: false,
|
relativesSet: false,
|
||||||
ancestorMap: new Map<number, AuditTransaction>(),
|
ancestorMap: new Map<number, AuditTransaction>(),
|
||||||
children: new Set<AuditTransaction>(),
|
children: new Set<AuditTransaction>(),
|
||||||
ancestorFee: 0,
|
ancestorFee: 0,
|
||||||
ancestorWeight: 0,
|
ancestorWeight: 0,
|
||||||
|
ancestorSigops: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
used: false,
|
used: false,
|
||||||
modified: false,
|
modified: false,
|
||||||
@ -83,6 +85,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
|||||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||||
const blocks: number[][] = [];
|
const blocks: number[][] = [];
|
||||||
let blockWeight = 4000;
|
let blockWeight = 4000;
|
||||||
|
let blockSigops = 0;
|
||||||
let transactions: AuditTransaction[] = [];
|
let transactions: AuditTransaction[] = [];
|
||||||
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
|
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
|
||||||
if (a.score === b.score) {
|
if (a.score === b.score) {
|
||||||
@ -118,7 +121,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
|||||||
|
|
||||||
if (nextTx && !nextTx?.used) {
|
if (nextTx && !nextTx?.used) {
|
||||||
// Check if the package fits into this block
|
// Check if the package fits into this block
|
||||||
if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) {
|
if (blocks.length >= 7 || ((blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) && (blockSigops + nextTx.ancestorSigops <= 80000))) {
|
||||||
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||||
@ -127,7 +130,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
|||||||
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
|
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
|
||||||
isCluster = true;
|
isCluster = true;
|
||||||
}
|
}
|
||||||
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / (nextTx.ancestorWeight / 4));
|
||||||
const used: AuditTransaction[] = [];
|
const used: AuditTransaction[] = [];
|
||||||
while (sortedTxSet.length) {
|
while (sortedTxSet.length) {
|
||||||
const ancestor = sortedTxSet.pop();
|
const ancestor = sortedTxSet.pop();
|
||||||
@ -155,7 +158,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
|||||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||||
if (used.length) {
|
if (used.length) {
|
||||||
used.forEach(tx => {
|
used.forEach(tx => {
|
||||||
updateDescendants(tx, auditPool, modified);
|
updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,9 +240,11 @@ function setRelatives(
|
|||||||
};
|
};
|
||||||
tx.ancestorFee = tx.fee || 0;
|
tx.ancestorFee = tx.fee || 0;
|
||||||
tx.ancestorWeight = tx.weight || 0;
|
tx.ancestorWeight = tx.weight || 0;
|
||||||
|
tx.ancestorSigops = tx.sigops || 0;
|
||||||
tx.ancestorMap.forEach((ancestor) => {
|
tx.ancestorMap.forEach((ancestor) => {
|
||||||
tx.ancestorFee += ancestor.fee;
|
tx.ancestorFee += ancestor.fee;
|
||||||
tx.ancestorWeight += ancestor.weight;
|
tx.ancestorWeight += ancestor.weight;
|
||||||
|
tx.ancestorSigops += ancestor.sigops;
|
||||||
});
|
});
|
||||||
tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
|
tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
|
||||||
tx.relativesSet = true;
|
tx.relativesSet = true;
|
||||||
@ -251,6 +256,7 @@ function updateDescendants(
|
|||||||
rootTx: AuditTransaction,
|
rootTx: AuditTransaction,
|
||||||
mempool: Map<number, AuditTransaction>,
|
mempool: Map<number, AuditTransaction>,
|
||||||
modified: PairingHeap<AuditTransaction>,
|
modified: PairingHeap<AuditTransaction>,
|
||||||
|
clusterRate: number,
|
||||||
): void {
|
): void {
|
||||||
const descendantSet: Set<AuditTransaction> = new Set();
|
const descendantSet: Set<AuditTransaction> = new Set();
|
||||||
// stack of nodes left to visit
|
// stack of nodes left to visit
|
||||||
@ -270,8 +276,10 @@ function updateDescendants(
|
|||||||
descendantTx.ancestorMap.delete(rootTx.uid);
|
descendantTx.ancestorMap.delete(rootTx.uid);
|
||||||
descendantTx.ancestorFee -= rootTx.fee;
|
descendantTx.ancestorFee -= rootTx.fee;
|
||||||
descendantTx.ancestorWeight -= rootTx.weight;
|
descendantTx.ancestorWeight -= rootTx.weight;
|
||||||
|
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||||
tmpScore = descendantTx.score;
|
tmpScore = descendantTx.score;
|
||||||
descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
|
descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
|
||||||
|
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||||
|
|
||||||
if (!descendantTx.modifiedNode) {
|
if (!descendantTx.modifiedNode) {
|
||||||
descendantTx.modified = true;
|
descendantTx.modified = true;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import {
|
import {
|
||||||
BlockExtended, TransactionExtended, WebsocketResponse,
|
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||||
OptimizedStatistic, ILoadingIndicators
|
OptimizedStatistic, ILoadingIndicators
|
||||||
} from '../mempool.interfaces';
|
} from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
@ -122,7 +122,7 @@ class WebsocketHandler {
|
|||||||
} else {
|
} else {
|
||||||
// tx.prevout is missing from transactions when in bitcoind mode
|
// tx.prevout is missing from transactions when in bitcoind mode
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -130,7 +130,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
||||||
@ -301,8 +301,8 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended },
|
||||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
|
||||||
if (!this.wss) {
|
if (!this.wss) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
@ -399,7 +399,7 @@ class WebsocketHandler {
|
|||||||
if (tx) {
|
if (tx) {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = JSON.stringify(fullTx);
|
response['tx'] = JSON.stringify(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -419,7 +419,7 @@ class WebsocketHandler {
|
|||||||
if (someVin) {
|
if (someVin) {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
foundTransactions.push(fullTx);
|
foundTransactions.push(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -433,7 +433,7 @@ class WebsocketHandler {
|
|||||||
if (someVout) {
|
if (someVout) {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
foundTransactions.push(fullTx);
|
foundTransactions.push(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
|
@ -88,28 +88,38 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
uid?: number;
|
uid?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MempoolTransactionExtended extends TransactionExtended {
|
||||||
|
sigops: number;
|
||||||
|
adjustedVsize: number;
|
||||||
|
adjustedFeePerVsize: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuditTransaction {
|
export interface AuditTransaction {
|
||||||
uid: number;
|
uid: number;
|
||||||
fee: number;
|
fee: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
feePerVsize: number;
|
feePerVsize: number;
|
||||||
effectiveFeePerVsize: number;
|
effectiveFeePerVsize: number;
|
||||||
|
sigops: number;
|
||||||
inputs: number[];
|
inputs: number[];
|
||||||
relativesSet: boolean;
|
relativesSet: boolean;
|
||||||
ancestorMap: Map<number, AuditTransaction>;
|
ancestorMap: Map<number, AuditTransaction>;
|
||||||
children: Set<AuditTransaction>;
|
children: Set<AuditTransaction>;
|
||||||
ancestorFee: number;
|
ancestorFee: number;
|
||||||
ancestorWeight: number;
|
ancestorWeight: number;
|
||||||
|
ancestorSigops: number;
|
||||||
score: number;
|
score: number;
|
||||||
used: boolean;
|
used: boolean;
|
||||||
modified: boolean;
|
modified: boolean;
|
||||||
modifiedNode: HeapNode<AuditTransaction>;
|
modifiedNode: HeapNode<AuditTransaction>;
|
||||||
|
dependencyRate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompactThreadTransaction {
|
export interface CompactThreadTransaction {
|
||||||
uid: number;
|
uid: number;
|
||||||
fee: number;
|
fee: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
|
sigops: number;
|
||||||
feePerVsize: number;
|
feePerVsize: number;
|
||||||
effectiveFeePerVsize?: number;
|
effectiveFeePerVsize?: number;
|
||||||
inputs: number[];
|
inputs: number[];
|
||||||
|
@ -271,6 +271,10 @@
|
|||||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||||
<td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
|
<td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="cpfpInfo && cpfpInfo.adjustedVsize && cpfpInfo.adjustedVsize > (tx.weight / 4)">
|
||||||
|
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</td>
|
||||||
|
<td [innerHTML]="'‎' + (cpfpInfo.adjustedVsize | vbytes: 2)"></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.weight">Weight</td>
|
<td i18n="block.weight">Weight</td>
|
||||||
<td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
|
<td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
|
||||||
@ -289,6 +293,10 @@
|
|||||||
<td i18n="transaction.locktime">Locktime</td>
|
<td i18n="transaction.locktime">Locktime</td>
|
||||||
<td [innerHTML]="'‎' + (tx.locktime | number)"></td>
|
<td [innerHTML]="'‎' + (tx.locktime | number)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="cpfpInfo && cpfpInfo.adjustedVsize && cpfpInfo.adjustedVsize > (tx.weight / 4)">
|
||||||
|
<td i18n="transaction.sigops|Transaction Sigops">Sigops</td>
|
||||||
|
<td [innerHTML]="'‎' + (cpfpInfo.sigops | number)"></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.hex">Transaction hex</td>
|
<td i18n="transaction.hex">Transaction hex</td>
|
||||||
<td><a target="_blank" href="{{ network === '' ? '' : '/' + network }}/api/tx/{{ txId }}/hex"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"></fa-icon></a></td>
|
<td><a target="_blank" href="{{ network === '' ? '' : '/' + network }}/api/tx/{{ txId }}/hex"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"></fa-icon></a></td>
|
||||||
@ -477,11 +485,11 @@
|
|||||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
<ng-template [ngIf]="tx?.status?.confirmed">
|
<ng-template [ngIf]="tx?.status?.confirmed">
|
||||||
|
|
||||||
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo?.descendants?.length && !cpfpInfo?.bestDescendant && !cpfpInfo?.ancestors?.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating *ngIf="tx.fee && !hasEffectiveFeeRate" [tx]="tx"></app-tx-fee-rating>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="cpfpInfo && (cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length)">
|
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
|
||||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
@ -490,7 +498,7 @@
|
|||||||
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
<button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -86,6 +86,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
segwitEnabled: boolean;
|
segwitEnabled: boolean;
|
||||||
rbfEnabled: boolean;
|
rbfEnabled: boolean;
|
||||||
taprootEnabled: boolean;
|
taprootEnabled: boolean;
|
||||||
|
hasEffectiveFeeRate: boolean;
|
||||||
|
|
||||||
@ViewChild('graphContainer')
|
@ViewChild('graphContainer')
|
||||||
graphContainer: ElementRef;
|
graphContainer: ElementRef;
|
||||||
@ -157,6 +158,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
.subscribe((cpfpInfo) => {
|
.subscribe((cpfpInfo) => {
|
||||||
if (!cpfpInfo || !this.tx) {
|
if (!cpfpInfo || !this.tx) {
|
||||||
this.cpfpInfo = null;
|
this.cpfpInfo = null;
|
||||||
|
this.hasEffectiveFeeRate = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// merge ancestors/descendants
|
// merge ancestors/descendants
|
||||||
@ -164,16 +166,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||||
relatives.push(cpfpInfo.bestDescendant);
|
relatives.push(cpfpInfo.bestDescendant);
|
||||||
}
|
}
|
||||||
let totalWeight =
|
const hasRelatives = !!relatives.length;
|
||||||
this.tx.weight +
|
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
|
||||||
relatives.reduce((prev, val) => prev + val.weight, 0);
|
let totalWeight =
|
||||||
let totalFees =
|
this.tx.weight +
|
||||||
this.tx.fee +
|
relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||||
relatives.reduce((prev, val) => prev + val.fee, 0);
|
let totalFees =
|
||||||
|
this.tx.fee +
|
||||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||||
|
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||||
|
} else {
|
||||||
|
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||||
|
}
|
||||||
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
|
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fetchRbfSubscription = this.fetchRbfHistory$
|
this.fetchRbfSubscription = this.fetchRbfHistory$
|
||||||
@ -359,6 +366,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
ancestors: tx.ancestors,
|
ancestors: tx.ancestors,
|
||||||
bestDescendant: tx.bestDescendant,
|
bestDescendant: tx.bestDescendant,
|
||||||
};
|
};
|
||||||
|
const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
|
||||||
|
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
|
||||||
} else {
|
} else {
|
||||||
this.fetchCpfp$.next(this.tx.txid);
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
}
|
}
|
||||||
@ -500,6 +509,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.replaced = false;
|
this.replaced = false;
|
||||||
this.transactionTime = -1;
|
this.transactionTime = -1;
|
||||||
this.cpfpInfo = null;
|
this.cpfpInfo = null;
|
||||||
|
this.hasEffectiveFeeRate = false;
|
||||||
this.rbfInfo = null;
|
this.rbfInfo = null;
|
||||||
this.rbfReplaces = [];
|
this.rbfReplaces = [];
|
||||||
this.showCpfpDetails = false;
|
this.showCpfpDetails = false;
|
||||||
|
@ -24,6 +24,9 @@ export interface CpfpInfo {
|
|||||||
ancestors: Ancestor[];
|
ancestors: Ancestor[];
|
||||||
descendants?: Ancestor[];
|
descendants?: Ancestor[];
|
||||||
bestDescendant?: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
|
effectiveFeePerVsize?: number;
|
||||||
|
sigops?: number;
|
||||||
|
adjustedVsize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RbfInfo {
|
export interface RbfInfo {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user