Merge pull request #2737 from mempool/mononaut/index-cpfp-info
show CPFP info for mined transactions
This commit is contained in:
commit
194e4b4c80
@ -25,7 +25,8 @@
|
|||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
"ADVANCED_TRANSACTION_SELECTION": false
|
"ADVANCED_TRANSACTION_SELECTION": false,
|
||||||
|
"TRANSACTION_INDEXING": false
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
@ -26,7 +26,8 @@
|
|||||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||||
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
||||||
"ADVANCED_TRANSACTION_SELECTION": "__ADVANCED_TRANSACTION_SELECTION__"
|
"ADVANCED_TRANSACTION_SELECTION": "__ADVANCED_TRANSACTION_SELECTION__",
|
||||||
|
"TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__"
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
|
@ -39,6 +39,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
ADVANCED_TRANSACTION_SELECTION: false,
|
ADVANCED_TRANSACTION_SELECTION: false,
|
||||||
|
TRANSACTION_INDEXING: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
||||||
|
@ -10,7 +10,7 @@ export interface AbstractBitcoinApi {
|
|||||||
$getBlockHash(height: number): Promise<string>;
|
$getBlockHash(height: number): Promise<string>;
|
||||||
$getBlockHeader(hash: string): Promise<string>;
|
$getBlockHeader(hash: string): Promise<string>;
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||||
$getRawBlock(hash: string): Promise<string>;
|
$getRawBlock(hash: string): Promise<Buffer>;
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
|
@ -81,7 +81,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<string> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return this.bitcoindClient.getBlock(hash, 0)
|
return this.bitcoindClient.getBlock(hash, 0)
|
||||||
.then((raw: string) => Buffer.from(raw, "hex"));
|
.then((raw: string) => Buffer.from(raw, "hex"));
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,14 @@ import logger from '../../logger';
|
|||||||
import blocks from '../blocks';
|
import blocks from '../blocks';
|
||||||
import bitcoinClient from './bitcoin-client';
|
import bitcoinClient from './bitcoin-client';
|
||||||
import difficultyAdjustment from '../difficulty-adjustment';
|
import difficultyAdjustment from '../difficulty-adjustment';
|
||||||
|
import transactionRepository from '../../repositories/TransactionRepository';
|
||||||
|
|
||||||
class BitcoinRoutes {
|
class BitcoinRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.getCpfpInfo)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
|
||||||
@ -188,29 +189,36 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCpfpInfo(req: Request, res: Response) {
|
private async $getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
res.status(501).send(`Invalid transaction ID.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = mempool.getMempool()[req.params.txId];
|
const tx = mempool.getMempool()[req.params.txId];
|
||||||
if (!tx) {
|
if (tx) {
|
||||||
res.status(404).send(`Transaction doesn't exist in the mempool.`);
|
if (tx?.cpfpChecked) {
|
||||||
|
res.json({
|
||||||
|
ancestors: tx.ancestors,
|
||||||
|
bestDescendant: tx.bestDescendant || null,
|
||||||
|
descendants: tx.descendants || null,
|
||||||
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
|
||||||
|
|
||||||
|
res.json(cpfpInfo);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
const cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||||
|
if (cpfpInfo) {
|
||||||
|
res.json(cpfpInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
res.status(404).send(`Transaction has no CPFP info available.`);
|
||||||
if (tx.cpfpChecked) {
|
|
||||||
res.json({
|
|
||||||
ancestors: tx.ancestors,
|
|
||||||
bestDescendant: tx.bestDescendant || null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
|
|
||||||
|
|
||||||
res.json(cpfpInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackendInfo(req: Request, res: Response) {
|
private getBackendInfo(req: Request, res: Response) {
|
||||||
|
@ -55,9 +55,9 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<string> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig)
|
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
|
||||||
.then((response) => response.data);
|
.then((response) => { return Buffer.from(response.data); });
|
||||||
}
|
}
|
||||||
|
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||||
|
@ -21,10 +21,13 @@ import fiatConversion from './fiat-conversion';
|
|||||||
import poolsParser from './pools-parser';
|
import poolsParser from './pools-parser';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
import cpfpRepository from '../repositories/CpfpRepository';
|
||||||
|
import transactionRepository from '../repositories/TransactionRepository';
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
|
import { Block } from 'bitcoinjs-lib';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -260,7 +263,7 @@ class Blocks {
|
|||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks summaries for the block txs visualization
|
* [INDEXING] Index all blocks summaries for the block txs visualization
|
||||||
*/
|
*/
|
||||||
public async $generateBlocksSummariesDatabase() {
|
public async $generateBlocksSummariesDatabase(): Promise<void> {
|
||||||
if (Common.blocksSummariesIndexingEnabled() === false) {
|
if (Common.blocksSummariesIndexingEnabled() === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -316,6 +319,57 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [INDEXING] Index transaction CPFP data for all blocks
|
||||||
|
*/
|
||||||
|
public async $generateCPFPDatabase(): Promise<void> {
|
||||||
|
if (Common.cpfpIndexingEnabled() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all indexed block hash
|
||||||
|
const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks();
|
||||||
|
|
||||||
|
if (!unindexedBlocks?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
let count = 0;
|
||||||
|
let countThisRun = 0;
|
||||||
|
let timer = new Date().getTime() / 1000;
|
||||||
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
for (const block of unindexedBlocks) {
|
||||||
|
// Logging
|
||||||
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
|
if (elapsedSeconds > 5) {
|
||||||
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
|
const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds);
|
||||||
|
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
|
||||||
|
logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
|
timer = new Date().getTime() / 1000;
|
||||||
|
countThisRun = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
count++;
|
||||||
|
countThisRun++;
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||||
*/
|
*/
|
||||||
@ -359,7 +413,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
++indexedThisRun;
|
++indexedThisRun;
|
||||||
++totalIndexed;
|
++totalIndexed;
|
||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||||
@ -461,9 +515,13 @@ class Blocks {
|
|||||||
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
await HashratesRepository.$deleteLastEntries();
|
await HashratesRepository.$deleteLastEntries();
|
||||||
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
|
await cpfpRepository.$deleteClustersFrom(lastBlock['height'] - 10);
|
||||||
for (let i = 10; i >= 0; --i) {
|
for (let i = 10; i >= 0; --i) {
|
||||||
const newBlock = await this.$indexBlock(lastBlock['height'] - i);
|
const newBlock = await this.$indexBlock(lastBlock['height'] - i);
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
||||||
|
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
||||||
|
await this.$indexCPFP(newBlock.id, lastBlock['height'] - i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||||
@ -489,6 +547,9 @@ class Blocks {
|
|||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||||
}
|
}
|
||||||
|
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
||||||
|
this.$indexCPFP(blockExtended.id, this.currentBlockHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -678,6 +739,62 @@ class Blocks {
|
|||||||
public getCurrentBlockHeight(): number {
|
public getCurrentBlockHeight(): number {
|
||||||
return this.currentBlockHeight;
|
return this.currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
||||||
|
let transactions;
|
||||||
|
if (false/*Common.blocksSummariesIndexingEnabled()*/) {
|
||||||
|
transactions = await this.$getStrippedBlockTransactions(hash);
|
||||||
|
const rawBlock = await bitcoinApi.$getRawBlock(hash);
|
||||||
|
const block = Block.fromBuffer(rawBlock);
|
||||||
|
const txMap = {};
|
||||||
|
for (const tx of block.transactions || []) {
|
||||||
|
txMap[tx.getId()] = tx;
|
||||||
|
}
|
||||||
|
for (const tx of transactions) {
|
||||||
|
if (txMap[tx.txid]?.ins) {
|
||||||
|
tx.vin = txMap[tx.txid].ins.map(vin => {
|
||||||
|
return {
|
||||||
|
txid: vin.hash
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
|
transactions = block.tx.map(tx => {
|
||||||
|
tx.vsize = tx.weight / 4;
|
||||||
|
return tx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cluster: TransactionStripped[] = [];
|
||||||
|
let ancestors: { [txid: string]: boolean } = {};
|
||||||
|
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||||
|
const tx = transactions[i];
|
||||||
|
if (!ancestors[tx.txid]) {
|
||||||
|
let totalFee = 0;
|
||||||
|
let totalVSize = 0;
|
||||||
|
cluster.forEach(tx => {
|
||||||
|
totalFee += tx?.fee || 0;
|
||||||
|
totalVSize += tx.vsize;
|
||||||
|
});
|
||||||
|
const effectiveFeePerVsize = (totalFee * 100_000_000) / totalVSize;
|
||||||
|
if (cluster.length > 1) {
|
||||||
|
await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: (tx.fee || 0) * 100_000_000 }; }), effectiveFeePerVsize);
|
||||||
|
for (const tx of cluster) {
|
||||||
|
await transactionRepository.$setCluster(tx.txid, cluster[0].txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cluster = [];
|
||||||
|
ancestors = {};
|
||||||
|
}
|
||||||
|
cluster.push(tx);
|
||||||
|
tx.vin.forEach(vin => {
|
||||||
|
ancestors[vin.txid] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await blocksRepository.$setCPFPIndexed(hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Blocks();
|
export default new Blocks();
|
||||||
|
@ -187,6 +187,13 @@ export class Common {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static cpfpIndexingEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
Common.indexingEnabled() &&
|
||||||
|
config.MEMPOOL.TRANSACTION_INDEXING === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static setDateMidnight(date: Date): void {
|
static setDateMidnight(date: Date): void {
|
||||||
date.setUTCHours(0);
|
date.setUTCHours(0);
|
||||||
date.setUTCMinutes(0);
|
date.setUTCMinutes(0);
|
||||||
|
@ -4,7 +4,7 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 46;
|
private static currentVersion = 47;
|
||||||
private queryTimeout = 900_000;
|
private queryTimeout = 900_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -373,7 +373,13 @@ class DatabaseMigration {
|
|||||||
if (databaseSchemaVersion < 46) {
|
if (databaseSchemaVersion < 46) {
|
||||||
await this.$executeQuery(`ALTER TABLE blocks MODIFY blockTimestamp timestamp NOT NULL DEFAULT 0`);
|
await this.$executeQuery(`ALTER TABLE blocks MODIFY blockTimestamp timestamp NOT NULL DEFAULT 0`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (databaseSchemaVersion < 47) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks` ADD cpfp_indexed tinyint(1) DEFAULT 0');
|
||||||
|
await this.$executeQuery(this.getCreateCPFPTableQuery(), await this.$checkIfTableExists('cpfp_clusters'));
|
||||||
|
await this.$executeQuery(this.getCreateTransactionsTableQuery(), await this.$checkIfTableExists('transactions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Special case here for the `statistics` table - It appeared that somehow some dbs already had the `added` field indexed
|
* Special case here for the `statistics` table - It appeared that somehow some dbs already had the `added` field indexed
|
||||||
@ -821,6 +827,25 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateCPFPTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS cpfp_clusters (
|
||||||
|
root varchar(65) NOT NULL,
|
||||||
|
height int(10) NOT NULL,
|
||||||
|
txs JSON DEFAULT NULL,
|
||||||
|
fee_rate double unsigned NOT NULL,
|
||||||
|
PRIMARY KEY (root)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateTransactionsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
txid varchar(65) NOT NULL,
|
||||||
|
cluster varchar(65) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (txid),
|
||||||
|
FOREIGN KEY (cluster) REFERENCES cpfp_clusters (root) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
public async $truncateIndexedData(tables: string[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
||||||
|
|
||||||
|
@ -155,6 +155,7 @@ class MempoolBlocks {
|
|||||||
if (newMempool[txid] && mempool[txid]) {
|
if (newMempool[txid] && mempool[txid]) {
|
||||||
newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize;
|
newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize;
|
||||||
newMempool[txid].ancestors = mempool[txid].ancestors;
|
newMempool[txid].ancestors = mempool[txid].ancestors;
|
||||||
|
newMempool[txid].descendants = mempool[txid].descendants;
|
||||||
newMempool[txid].bestDescendant = mempool[txid].bestDescendant;
|
newMempool[txid].bestDescendant = mempool[txid].bestDescendant;
|
||||||
newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked;
|
newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked;
|
||||||
}
|
}
|
||||||
|
@ -108,36 +108,38 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }:
|
|||||||
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||||
blockWeight += nextTx.ancestorWeight;
|
blockWeight += nextTx.ancestorWeight;
|
||||||
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||||
|
const descendants: AuditTransaction[] = [];
|
||||||
// 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];
|
||||||
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
||||||
sortedTxSet.forEach((ancestor, i, arr) => {
|
|
||||||
|
while (sortedTxSet.length) {
|
||||||
|
const ancestor = sortedTxSet.pop();
|
||||||
const mempoolTx = mempool[ancestor.txid];
|
const mempoolTx = mempool[ancestor.txid];
|
||||||
if (ancestor && !ancestor?.used) {
|
if (ancestor && !ancestor?.used) {
|
||||||
ancestor.used = true;
|
ancestor.used = true;
|
||||||
// update original copy of this tx with effective fee rate & relatives data
|
// update original copy of this tx with effective fee rate & relatives data
|
||||||
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||||
mempoolTx.ancestors = (Array.from(ancestor.ancestorMap?.values()) as AuditTransaction[]).map((a) => {
|
mempoolTx.ancestors = sortedTxSet.map((a) => {
|
||||||
|
return {
|
||||||
|
txid: a.txid,
|
||||||
|
fee: a.fee,
|
||||||
|
weight: a.weight,
|
||||||
|
};
|
||||||
|
}).reverse();
|
||||||
|
mempoolTx.descendants = descendants.map((a) => {
|
||||||
return {
|
return {
|
||||||
txid: a.txid,
|
txid: a.txid,
|
||||||
fee: a.fee,
|
fee: a.fee,
|
||||||
weight: a.weight,
|
weight: a.weight,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
descendants.push(ancestor);
|
||||||
mempoolTx.cpfpChecked = true;
|
mempoolTx.cpfpChecked = true;
|
||||||
if (i < arr.length - 1) {
|
|
||||||
mempoolTx.bestDescendant = {
|
|
||||||
txid: arr[arr.length - 1].txid,
|
|
||||||
fee: arr[arr.length - 1].fee,
|
|
||||||
weight: arr[arr.length - 1].weight,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
mempoolTx.bestDescendant = null;
|
|
||||||
}
|
|
||||||
transactions.push(ancestor);
|
transactions.push(ancestor);
|
||||||
blockSize += ancestor.size;
|
blockSize += ancestor.size;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// 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 (sortedTxSet.length) {
|
if (sortedTxSet.length) {
|
||||||
|
@ -30,6 +30,7 @@ interface IConfig {
|
|||||||
POOLS_JSON_URL: string,
|
POOLS_JSON_URL: string,
|
||||||
POOLS_JSON_TREE_URL: string,
|
POOLS_JSON_TREE_URL: string,
|
||||||
ADVANCED_TRANSACTION_SELECTION: boolean;
|
ADVANCED_TRANSACTION_SELECTION: boolean;
|
||||||
|
TRANSACTION_INDEXING: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@ -148,6 +149,7 @@ const defaults: IConfig = {
|
|||||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
'ADVANCED_TRANSACTION_SELECTION': false,
|
'ADVANCED_TRANSACTION_SELECTION': false,
|
||||||
|
'TRANSACTION_INDEXING': false,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
|
@ -77,6 +77,7 @@ class Indexer {
|
|||||||
await mining.$generateNetworkHashrateHistory();
|
await mining.$generateNetworkHashrateHistory();
|
||||||
await mining.$generatePoolHashrateHistory();
|
await mining.$generatePoolHashrateHistory();
|
||||||
await blocks.$generateBlocksSummariesDatabase();
|
await blocks.$generateBlocksSummariesDatabase();
|
||||||
|
await blocks.$generateCPFPDatabase();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.indexerRunning = false;
|
this.indexerRunning = false;
|
||||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
@ -72,6 +72,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
firstSeen?: number;
|
firstSeen?: number;
|
||||||
effectiveFeePerVsize: number;
|
effectiveFeePerVsize: number;
|
||||||
ancestors?: Ancestor[];
|
ancestors?: Ancestor[];
|
||||||
|
descendants?: Ancestor[];
|
||||||
bestDescendant?: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
cpfpChecked?: boolean;
|
cpfpChecked?: boolean;
|
||||||
deleteAfter?: number;
|
deleteAfter?: number;
|
||||||
@ -119,7 +120,9 @@ interface BestDescendant {
|
|||||||
|
|
||||||
export interface CpfpInfo {
|
export interface CpfpInfo {
|
||||||
ancestors: Ancestor[];
|
ancestors: Ancestor[];
|
||||||
bestDescendant: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
|
descendants?: Ancestor[];
|
||||||
|
effectiveFeePerVsize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionStripped {
|
export interface TransactionStripped {
|
||||||
|
@ -662,6 +662,23 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of blocks that have not had CPFP data indexed
|
||||||
|
*/
|
||||||
|
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $setCPFPIndexed(hash: string): Promise<void> {
|
||||||
|
await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the oldest block from a consecutive chain of block from the most recent one
|
* Return the oldest block from a consecutive chain of block from the most recent one
|
||||||
*/
|
*/
|
||||||
|
43
backend/src/repositories/CpfpRepository.ts
Normal file
43
backend/src/repositories/CpfpRepository.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { Ancestor } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
class CpfpRepository {
|
||||||
|
public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const txsJson = JSON.stringify(txs);
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO cpfp_clusters(root, height, txs, fee_rate)
|
||||||
|
VALUE (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
height = ?,
|
||||||
|
txs = ?,
|
||||||
|
fee_rate = ?
|
||||||
|
`,
|
||||||
|
[txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||||
|
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
||||||
|
try {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
DELETE from cpfp_clusters
|
||||||
|
WHERE height >= ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CpfpRepository();
|
77
backend/src/repositories/TransactionRepository.ts
Normal file
77
backend/src/repositories/TransactionRepository.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
interface CpfpSummary {
|
||||||
|
txid: string;
|
||||||
|
cluster: string;
|
||||||
|
root: string;
|
||||||
|
txs: Ancestor[];
|
||||||
|
height: number;
|
||||||
|
fee_rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransactionRepository {
|
||||||
|
public async $setCluster(txid: string, cluster: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO transactions
|
||||||
|
(
|
||||||
|
txid,
|
||||||
|
cluster
|
||||||
|
)
|
||||||
|
VALUE (?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
cluster = ?
|
||||||
|
;`,
|
||||||
|
[txid, cluster, cluster]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
|
||||||
|
try {
|
||||||
|
let query = `
|
||||||
|
SELECT *
|
||||||
|
FROM transactions
|
||||||
|
LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster
|
||||||
|
WHERE transactions.txid = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [txid]);
|
||||||
|
if (rows.length) {
|
||||||
|
rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[];
|
||||||
|
return this.convertCpfp(rows[0]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertCpfp(cpfp: CpfpSummary): CpfpInfo {
|
||||||
|
const descendants: Ancestor[] = [];
|
||||||
|
const ancestors: Ancestor[] = [];
|
||||||
|
let matched = false;
|
||||||
|
for (const tx of cpfp.txs) {
|
||||||
|
if (tx.txid === cpfp.txid) {
|
||||||
|
matched = true;
|
||||||
|
} else if (!matched) {
|
||||||
|
descendants.push(tx);
|
||||||
|
} else {
|
||||||
|
ancestors.push(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
descendants,
|
||||||
|
ancestors,
|
||||||
|
effectiveFeePerVsize: cpfp.fee_rate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TransactionRepository();
|
||||||
|
|
@ -8,10 +8,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
||||||
<app-tx-features [tx]="tx"></app-tx-features>
|
<app-tx-features [tx]="tx"></app-tx-features>
|
||||||
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
|
<span *ngIf="cpfpInfo && (cpfpInfo.bestDescendant || cpfpInfo.descendants.length)" class="badge badge-primary mr-1">
|
||||||
CPFP
|
CPFP
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="cpfpInfo && !cpfpInfo.bestDescendant && cpfpInfo.ancestors.length" class="badge badge-info mr-1">
|
<span *ngIf="cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.descendants.length && cpfpInfo.ancestors.length" class="badge badge-info mr-1">
|
||||||
CPFP
|
CPFP
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,25 +72,31 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
if (!this.tx) {
|
if (!this.tx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
if (cpfpInfo.effectiveFeePerVsize) {
|
||||||
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||||
);
|
} else {
|
||||||
let totalWeight =
|
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
||||||
this.tx.weight +
|
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
);
|
||||||
let totalFees =
|
let totalWeight =
|
||||||
this.tx.fee +
|
this.tx.weight +
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
||||||
|
let totalFees =
|
||||||
|
this.tx.fee +
|
||||||
|
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
||||||
|
|
||||||
if (cpfpInfo.bestDescendant) {
|
if (cpfpInfo?.bestDescendant) {
|
||||||
totalWeight += cpfpInfo.bestDescendant.weight;
|
totalWeight += cpfpInfo?.bestDescendant.weight;
|
||||||
totalFees += cpfpInfo.bestDescendant.fee;
|
totalFees += cpfpInfo?.bestDescendant.fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||||
|
}
|
||||||
|
if (!this.tx.status.confirmed) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
|
||||||
this.stateService.markBlock$.next({
|
|
||||||
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
|
||||||
});
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
this.openGraphService.waitOver('cpfp-data-' + this.txId);
|
this.openGraphService.waitOver('cpfp-data-' + this.txId);
|
||||||
});
|
});
|
||||||
@ -176,8 +182,17 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.getTransactionTime();
|
this.getTransactionTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.tx.status.confirmed) {
|
if (this.tx.status.confirmed) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
blockHeight: tx.status.block_height,
|
||||||
|
});
|
||||||
|
this.openGraphService.waitFor('cpfp-data-' + this.txId);
|
||||||
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
|
} else {
|
||||||
if (tx.cpfpChecked) {
|
if (tx.cpfpChecked) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
txFeePerVSize: tx.effectiveFeePerVsize,
|
||||||
|
});
|
||||||
this.cpfpInfo = {
|
this.cpfpInfo = {
|
||||||
ancestors: tx.ancestors,
|
ancestors: tx.ancestors,
|
||||||
bestDescendant: tx.bestDescendant,
|
bestDescendant: tx.bestDescendant,
|
||||||
|
@ -156,7 +156,20 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template [ngIf]="cpfpInfo.bestDescendant">
|
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
|
||||||
|
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
|
||||||
|
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||||
|
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
||||||
|
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 8 }}</span>
|
||||||
|
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
||||||
|
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||||
|
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||||
<td>
|
<td>
|
||||||
@ -170,7 +183,7 @@
|
|||||||
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="cpfpInfo.ancestors.length">
|
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
|
||||||
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
|
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
|
||||||
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
|
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
|
||||||
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
||||||
@ -468,11 +481,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.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo?.descendants?.length && !cpfpInfo?.bestDescendant && !cpfpInfo?.ancestors?.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="cpfpInfo && (cpfpInfo.bestDescendant || cpfpInfo.ancestors.length)">
|
<tr *ngIf="cpfpInfo && (cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length)">
|
||||||
<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">
|
||||||
|
@ -117,25 +117,31 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (!this.tx) {
|
if (!this.tx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
if (cpfpInfo.effectiveFeePerVsize) {
|
||||||
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||||
);
|
} else {
|
||||||
let totalWeight =
|
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
||||||
this.tx.weight +
|
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
);
|
||||||
let totalFees =
|
let totalWeight =
|
||||||
this.tx.fee +
|
this.tx.weight +
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
||||||
|
let totalFees =
|
||||||
|
this.tx.fee +
|
||||||
|
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
||||||
|
|
||||||
if (cpfpInfo.bestDescendant) {
|
if (cpfpInfo?.bestDescendant) {
|
||||||
totalWeight += cpfpInfo.bestDescendant.weight;
|
totalWeight += cpfpInfo?.bestDescendant.weight;
|
||||||
totalFees += cpfpInfo.bestDescendant.fee;
|
totalFees += cpfpInfo?.bestDescendant.fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||||
|
}
|
||||||
|
if (!this.tx.status.confirmed) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
|
||||||
this.stateService.markBlock$.next({
|
|
||||||
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
|
||||||
});
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -239,6 +245,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
blockHeight: tx.status.block_height,
|
blockHeight: tx.status.block_height,
|
||||||
});
|
});
|
||||||
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
} else {
|
} else {
|
||||||
if (tx.cpfpChecked) {
|
if (tx.cpfpChecked) {
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
|
@ -22,7 +22,9 @@ interface BestDescendant {
|
|||||||
|
|
||||||
export interface CpfpInfo {
|
export interface CpfpInfo {
|
||||||
ancestors: Ancestor[];
|
ancestors: Ancestor[];
|
||||||
bestDescendant: BestDescendant | null;
|
descendants?: Ancestor[];
|
||||||
|
bestDescendant?: BestDescendant | null;
|
||||||
|
effectiveFeePerVsize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DifficultyAdjustment {
|
export interface DifficultyAdjustment {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user