Merge branch 'master' into nymkappa/fix-possible-crash
This commit is contained in:
		
						commit
						1fbdf97639
					
				@ -14,7 +14,6 @@ class Audit {
 | 
			
		||||
    const matches: string[] = []; // present in both mined block and template
 | 
			
		||||
    const added: string[] = []; // present in mined block, not in template
 | 
			
		||||
    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 isDisplaced = {};
 | 
			
		||||
    let displacedWeight = 0;
 | 
			
		||||
@ -38,8 +37,6 @@ class Audit {
 | 
			
		||||
        // tx is recent, may have reached the miner too late for inclusion
 | 
			
		||||
        if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
 | 
			
		||||
          fresh.push(txid);
 | 
			
		||||
        } else if (this.isPossibleHighSigop(mempool[txid])) {
 | 
			
		||||
          sigop.push(txid);
 | 
			
		||||
        } else {
 | 
			
		||||
          isCensored[txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
@ -140,19 +137,11 @@ class Audit {
 | 
			
		||||
      censored: Object.keys(isCensored),
 | 
			
		||||
      added,
 | 
			
		||||
      fresh,
 | 
			
		||||
      sigop,
 | 
			
		||||
      sigop: [],
 | 
			
		||||
      score,
 | 
			
		||||
      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();
 | 
			
		||||
@ -415,12 +415,38 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) {
 | 
			
		||||
      const witnessScript = vin.witness[vin.witness.length - 2];
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
 | 
			
		||||
      const witnessScript = this.witnessToP2TRScript(vin.witness);
 | 
			
		||||
      if (witnessScript !== null) {
 | 
			
		||||
        vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This function must only be called when we know the witness we are parsing
 | 
			
		||||
   * is a taproot witness.
 | 
			
		||||
   * @param witness An array of hex strings that represents the witness stack of
 | 
			
		||||
   *                the input.
 | 
			
		||||
   * @returns null if the witness is not a script spend, and the hex string of
 | 
			
		||||
   *          the script item if it is a script spend.
 | 
			
		||||
   */
 | 
			
		||||
  private witnessToP2TRScript(witness: string[]): string | null {
 | 
			
		||||
    if (witness.length < 2) return null;
 | 
			
		||||
    // Note: see BIP341 for parsing details of witness stack
 | 
			
		||||
 | 
			
		||||
    // If there are at least two witness elements, and the first byte of the
 | 
			
		||||
    // last element is 0x50, this last element is called annex a and
 | 
			
		||||
    // is removed from the witness stack.
 | 
			
		||||
    const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
 | 
			
		||||
    // If there are at least two witness elements left, script path spending is used.
 | 
			
		||||
    // Call the second-to-last stack element s, the script.
 | 
			
		||||
    // (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
 | 
			
		||||
    if (hasAnnex && witness.length < 3) return null;
 | 
			
		||||
    const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
 | 
			
		||||
    return witness[positionOfScript];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoinApi;
 | 
			
		||||
 | 
			
		||||
@ -211,6 +211,8 @@ class BitcoinRoutes {
 | 
			
		||||
          bestDescendant: tx.bestDescendant || null,
 | 
			
		||||
          descendants: tx.descendants || null,
 | 
			
		||||
          effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
 | 
			
		||||
          sigops: tx.sigops,
 | 
			
		||||
          adjustedVsize: tx.adjustedVsize,
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import config from '../config';
 | 
			
		||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
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 diskCache from './disk-cache';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
@ -76,6 +76,7 @@ class Blocks {
 | 
			
		||||
    blockHeight: number,
 | 
			
		||||
    onlyCoinbase: boolean,
 | 
			
		||||
    quiet: boolean = false,
 | 
			
		||||
    addMempoolData: boolean = false,
 | 
			
		||||
  ): Promise<TransactionExtended[]> {
 | 
			
		||||
    const transactions: TransactionExtended[] = [];
 | 
			
		||||
    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}`);
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
          const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
 | 
			
		||||
          const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
 | 
			
		||||
          transactions.push(tx);
 | 
			
		||||
          transactionsFetched++;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          try {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
              // 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);
 | 
			
		||||
              transactionsFetched++;
 | 
			
		||||
            } else {
 | 
			
		||||
@ -126,11 +127,13 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    transactions.forEach((tx) => {
 | 
			
		||||
      if (!tx.cpfpChecked) {
 | 
			
		||||
        Common.setRelativesAndGetCpfpInfo(tx, mempool); // Child Pay For Parent
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (addMempoolData) {
 | 
			
		||||
      transactions.forEach((tx) => {
 | 
			
		||||
        if (!tx.cpfpChecked) {
 | 
			
		||||
          Common.setRelativesAndGetCpfpInfo(tx as MempoolTransactionExtended, mempool); // Child Pay For Parent
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!quiet) {
 | 
			
		||||
      logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
 | 
			
		||||
@ -596,7 +599,15 @@ class Blocks {
 | 
			
		||||
      const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
 | 
			
		||||
      const block = BitcoinApi.convertBlock(verboseBlock);
 | 
			
		||||
      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);
 | 
			
		||||
      if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
        // fill in missing transaction fee data from verboseBlock
 | 
			
		||||
        for (let i = 0; i < transactions.length; i++) {
 | 
			
		||||
          if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
 | 
			
		||||
            transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
 | 
			
		||||
      const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
 | 
			
		||||
      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 { NodeSocket } from '../repositories/NodesSocketsRepository';
 | 
			
		||||
import { isIP } from 'net';
 | 
			
		||||
@ -57,15 +57,15 @@ export class Common {
 | 
			
		||||
    return arr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended[] } {
 | 
			
		||||
    const matches: { [txid: string]: TransactionExtended[] } = {};
 | 
			
		||||
  static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } {
 | 
			
		||||
    const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
 | 
			
		||||
    added
 | 
			
		||||
      .forEach((addedTx) => {
 | 
			
		||||
        const foundMatches = deleted.filter((deletedTx) => {
 | 
			
		||||
          // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
 | 
			
		||||
          return addedTx.fee > deletedTx.fee
 | 
			
		||||
            // 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
 | 
			
		||||
            && deletedTx.vin.some((deletedVin) =>
 | 
			
		||||
              addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
 | 
			
		||||
@ -77,10 +77,10 @@ export class Common {
 | 
			
		||||
    return matches;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static findMinedRbfTransactions(minedTransactions: TransactionExtended[], spendMap: Map<string, TransactionExtended>): { [txid: string]: { replaced: TransactionExtended[], replacedBy: TransactionExtended }} {
 | 
			
		||||
    const matches: { [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: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
 | 
			
		||||
    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++) {
 | 
			
		||||
        const vin = tx.vin[i];
 | 
			
		||||
        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 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);
 | 
			
		||||
 | 
			
		||||
    tx.ancestors = parents
 | 
			
		||||
      .map((t) => {
 | 
			
		||||
        return {
 | 
			
		||||
          txid: t.txid,
 | 
			
		||||
          weight: t.weight,
 | 
			
		||||
          weight: (t.adjustedVsize * 4),
 | 
			
		||||
          fee: t.fee,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
@ -152,8 +152,8 @@ export class Common {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  private static findAllParents(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): TransactionExtended[] {
 | 
			
		||||
    let parents: TransactionExtended[] = [];
 | 
			
		||||
  private static findAllParents(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): MempoolTransactionExtended[] {
 | 
			
		||||
    let parents: MempoolTransactionExtended[] = [];
 | 
			
		||||
    tx.vin.forEach((parent) => {
 | 
			
		||||
      if (parents.find((p) => p.txid === parent.txid)) {
 | 
			
		||||
        return;
 | 
			
		||||
@ -161,17 +161,17 @@ export class Common {
 | 
			
		||||
 | 
			
		||||
      const parentTx = memPool[parent.txid];
 | 
			
		||||
      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) {
 | 
			
		||||
            parentTx.bestDescendant = {
 | 
			
		||||
              weight: tx.weight + tx.bestDescendant.weight,
 | 
			
		||||
              weight: (tx.adjustedVsize * 4) + tx.bestDescendant.weight,
 | 
			
		||||
              fee: tx.fee + tx.bestDescendant.fee,
 | 
			
		||||
              txid: tx.txid,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        } else if (tx.feePerVsize > parentTx.feePerVsize) {
 | 
			
		||||
        } else if (tx.adjustedFeePerVsize > parentTx.adjustedFeePerVsize) {
 | 
			
		||||
          parentTx.bestDescendant = {
 | 
			
		||||
            weight: tx.weight,
 | 
			
		||||
            weight: (tx.adjustedVsize * 4),
 | 
			
		||||
            fee: tx.fee,
 | 
			
		||||
            txid: tx.txid
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 60;
 | 
			
		||||
  private static currentVersion = 61;
 | 
			
		||||
  private queryTimeout = 3600_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -521,6 +521,18 @@ class DatabaseMigration {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
 | 
			
		||||
      await this.updateToSchemaVersion(60);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 61 && isBitcoin === true) {
 | 
			
		||||
      // Break block templates into their own table
 | 
			
		||||
      if (! await this.$checkIfTableExists('blocks_templates')) {
 | 
			
		||||
        await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
 | 
			
		||||
      }
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
 | 
			
		||||
      await this.updateToSchemaVersion(61);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
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 config from '../config';
 | 
			
		||||
import { Worker } from 'worker_threads';
 | 
			
		||||
@ -36,9 +36,9 @@ class MempoolBlocks {
 | 
			
		||||
    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 memPoolArray: TransactionExtended[] = [];
 | 
			
		||||
    const memPoolArray: MempoolTransactionExtended[] = [];
 | 
			
		||||
    for (const i in latestMempool) {
 | 
			
		||||
      if (latestMempool.hasOwnProperty(i)) {
 | 
			
		||||
        memPoolArray.push(latestMempool[i]);
 | 
			
		||||
@ -52,17 +52,17 @@ class MempoolBlocks {
 | 
			
		||||
      tx.ancestors = [];
 | 
			
		||||
      tx.cpfpChecked = false;
 | 
			
		||||
      if (!tx.effectiveFeePerVsize) {
 | 
			
		||||
        tx.effectiveFeePerVsize = tx.feePerVsize;
 | 
			
		||||
        tx.effectiveFeePerVsize = tx.adjustedFeePerVsize;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // First sort
 | 
			
		||||
    memPoolArray.sort((a, b) => {
 | 
			
		||||
      if (a.feePerVsize === b.feePerVsize) {
 | 
			
		||||
      if (a.adjustedFeePerVsize === b.adjustedFeePerVsize) {
 | 
			
		||||
        // tie-break by lexicographic txid order for stability
 | 
			
		||||
        return a.txid < b.txid ? -1 : 1;
 | 
			
		||||
      } else {
 | 
			
		||||
        return b.feePerVsize - a.feePerVsize;
 | 
			
		||||
        return b.adjustedFeePerVsize - a.adjustedFeePerVsize;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -102,7 +102,7 @@ class MempoolBlocks {
 | 
			
		||||
    return blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: MempoolTransactionExtended[]): MempoolBlockWithTransactions[] {
 | 
			
		||||
    const mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
    let feeStatsCalculator: OnlineFeeStatsCalculator = new OnlineFeeStatsCalculator(config.MEMPOOL.BLOCK_WEIGHT_UNITS);
 | 
			
		||||
    let onlineStats = false;
 | 
			
		||||
@ -112,7 +112,7 @@ class MempoolBlocks {
 | 
			
		||||
    let blockFees = 0;
 | 
			
		||||
    const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
 | 
			
		||||
    let transactionIds: string[] = [];
 | 
			
		||||
    let transactions: TransactionExtended[] = [];
 | 
			
		||||
    let transactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
    transactionsSorted.forEach((tx, index) => {
 | 
			
		||||
      if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
 | 
			
		||||
        || mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
 | 
			
		||||
@ -205,7 +205,7 @@ class MempoolBlocks {
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // reset mempool short ids
 | 
			
		||||
@ -222,9 +222,10 @@ class MempoolBlocks {
 | 
			
		||||
        strippedMempool.set(entry.uid, {
 | 
			
		||||
          uid: entry.uid,
 | 
			
		||||
          fee: entry.fee,
 | 
			
		||||
          weight: entry.weight,
 | 
			
		||||
          feePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
          effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
 | 
			
		||||
          weight: (entry.adjustedVsize * 4),
 | 
			
		||||
          sigops: entry.sigops,
 | 
			
		||||
          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[],
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
@ -268,7 +269,7 @@ class 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) {
 | 
			
		||||
      // need to reset the worker
 | 
			
		||||
      await this.$makeBlockTemplates(newMempool, saveResults);
 | 
			
		||||
@ -287,9 +288,10 @@ class MempoolBlocks {
 | 
			
		||||
      return {
 | 
			
		||||
        uid: entry.uid || 0,
 | 
			
		||||
        fee: entry.fee,
 | 
			
		||||
        weight: entry.weight,
 | 
			
		||||
        feePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
        effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
 | 
			
		||||
        weight: (entry.adjustedVsize * 4),
 | 
			
		||||
        sigops: entry.sigops,
 | 
			
		||||
        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[],
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
@ -341,12 +343,12 @@ class MempoolBlocks {
 | 
			
		||||
    for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
 | 
			
		||||
      const block: string[] = blocks[blockIndex];
 | 
			
		||||
      let txid: string;
 | 
			
		||||
      let mempoolTx: TransactionExtended;
 | 
			
		||||
      let mempoolTx: MempoolTransactionExtended;
 | 
			
		||||
      let totalSize = 0;
 | 
			
		||||
      let totalVsize = 0;
 | 
			
		||||
      let totalWeight = 0;
 | 
			
		||||
      let totalFees = 0;
 | 
			
		||||
      const transactions: TransactionExtended[] = [];
 | 
			
		||||
      const transactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
      for (let txIndex = 0; txIndex < block.length; txIndex++) {
 | 
			
		||||
        txid = block[txIndex];
 | 
			
		||||
        if (txid) {
 | 
			
		||||
@ -397,7 +399,7 @@ class MempoolBlocks {
 | 
			
		||||
              const relative = {
 | 
			
		||||
                txid: txid,
 | 
			
		||||
                fee: mempool[txid].fee,
 | 
			
		||||
                weight: mempool[txid].weight,
 | 
			
		||||
                weight: (mempool[txid].adjustedVsize * 4),
 | 
			
		||||
              };
 | 
			
		||||
              if (matched) {
 | 
			
		||||
                descendants.push(relative);
 | 
			
		||||
@ -426,7 +428,7 @@ class 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) {
 | 
			
		||||
      feeStats = Common.calcEffectiveFeeStatistics(transactions);
 | 
			
		||||
    }
 | 
			
		||||
@ -447,7 +449,7 @@ class MempoolBlocks {
 | 
			
		||||
    this.nextUid = 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setUid(tx: TransactionExtended): number {
 | 
			
		||||
  private setUid(tx: MempoolTransactionExtended): number {
 | 
			
		||||
    const uid = this.nextUid;
 | 
			
		||||
    this.nextUid++;
 | 
			
		||||
    this.uidMap.set(uid, tx.txid);
 | 
			
		||||
@ -455,7 +457,7 @@ class MempoolBlocks {
 | 
			
		||||
    return uid;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getUid(tx: TransactionExtended): number | void {
 | 
			
		||||
  private getUid(tx: MempoolTransactionExtended): number | void {
 | 
			
		||||
    if (tx?.uid != null && this.uidMap.has(tx.uid)) {
 | 
			
		||||
      return tx.uid;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
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 { Common } from './common';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
@ -13,14 +13,14 @@ import rbfCache from './rbf-cache';
 | 
			
		||||
class Mempool {
 | 
			
		||||
  private inSync: boolean = false;
 | 
			
		||||
  private mempoolCacheDelta: number = -1;
 | 
			
		||||
  private mempoolCache: { [txId: string]: TransactionExtended } = {};
 | 
			
		||||
  private spendMap = new Map<string, TransactionExtended>();
 | 
			
		||||
  private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
 | 
			
		||||
  private spendMap = new Map<string, MempoolTransactionExtended>();
 | 
			
		||||
  private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
 | 
			
		||||
                                                    maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => void) | undefined;
 | 
			
		||||
  private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
 | 
			
		||||
    deletedTransactions: MempoolTransactionExtended[]) => void) | undefined;
 | 
			
		||||
  private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
 | 
			
		||||
    deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
 | 
			
		||||
 | 
			
		||||
  private txPerSecondArray: number[] = [];
 | 
			
		||||
  private txPerSecond: number = 0;
 | 
			
		||||
@ -64,26 +64,31 @@ class Mempool {
 | 
			
		||||
    return this.latestTransactions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) {
 | 
			
		||||
  public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
 | 
			
		||||
    this.mempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
 | 
			
		||||
  public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
 | 
			
		||||
    this.$asyncMempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getMempool(): { [txid: string]: TransactionExtended } {
 | 
			
		||||
  public getMempool(): { [txid: string]: MempoolTransactionExtended } {
 | 
			
		||||
    return this.mempoolCache;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getSpendMap(): Map<string, TransactionExtended> {
 | 
			
		||||
  public getSpendMap(): Map<string, MempoolTransactionExtended> {
 | 
			
		||||
    return this.spendMap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
 | 
			
		||||
  public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
 | 
			
		||||
    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) {
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, [], []);
 | 
			
		||||
    }
 | 
			
		||||
@ -133,7 +138,7 @@ class Mempool {
 | 
			
		||||
    const currentMempoolSize = Object.keys(this.mempoolCache).length;
 | 
			
		||||
    this.updateTimerProgress(timer, 'got raw mempool');
 | 
			
		||||
    const diff = transactions.length - currentMempoolSize;
 | 
			
		||||
    const newTransactions: TransactionExtended[] = [];
 | 
			
		||||
    const newTransactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
    this.mempoolCacheDelta = Math.abs(diff);
 | 
			
		||||
 | 
			
		||||
@ -155,7 +160,7 @@ class Mempool {
 | 
			
		||||
    for (const txid of transactions) {
 | 
			
		||||
      if (!this.mempoolCache[txid]) {
 | 
			
		||||
        try {
 | 
			
		||||
          const transaction = await transactionUtils.$getTransactionExtended(txid);
 | 
			
		||||
          const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
 | 
			
		||||
          this.updateTimerProgress(timer, 'fetched new transaction');
 | 
			
		||||
          this.mempoolCache[txid] = transaction;
 | 
			
		||||
          if (this.inSync) {
 | 
			
		||||
@ -205,7 +210,7 @@ class Mempool {
 | 
			
		||||
      }, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const deletedTransactions: TransactionExtended[] = [];
 | 
			
		||||
    const deletedTransactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
    if (this.mempoolProtection !== 1) {
 | 
			
		||||
      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) {
 | 
			
		||||
      if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
 | 
			
		||||
        // 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) {
 | 
			
		||||
      if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
 | 
			
		||||
        // 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 vin of tx.vin) {
 | 
			
		||||
        this.spendMap.set(`${vin.txid}:${vin.vout}`, tx);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import logger from "../logger";
 | 
			
		||||
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
 | 
			
		||||
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { Common } from "./common";
 | 
			
		||||
 | 
			
		||||
@ -23,14 +23,14 @@ class RbfCache {
 | 
			
		||||
  private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
 | 
			
		||||
  private dirtyTrees: Set<string> = new Set();
 | 
			
		||||
  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();
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    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)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -92,7 +92,7 @@ class RbfCache {
 | 
			
		||||
    return this.replaces.get(txId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getTx(txId: string): TransactionExtended | undefined {
 | 
			
		||||
  public getTx(txId: string): MempoolTransactionExtended | undefined {
 | 
			
		||||
    return this.txs.get(txId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -272,7 +272,7 @@ class RbfCache {
 | 
			
		||||
    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 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 { Common } from './common';
 | 
			
		||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import * as bitcoinjs from 'bitcoinjs-lib';
 | 
			
		||||
 | 
			
		||||
class TransactionUtils {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
@ -22,19 +23,27 @@ class TransactionUtils {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param txId 
 | 
			
		||||
   * @param addPrevouts 
 | 
			
		||||
   * @param lazyPrevouts 
 | 
			
		||||
   * @param txId
 | 
			
		||||
   * @param addPrevouts
 | 
			
		||||
   * @param lazyPrevouts
 | 
			
		||||
   * @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;
 | 
			
		||||
    if (forceCore === true) {
 | 
			
		||||
      transaction  = await bitcoinCoreApi.$getRawTransaction(txId, true);
 | 
			
		||||
    } else {
 | 
			
		||||
      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 {
 | 
			
		||||
@ -50,8 +59,32 @@ class TransactionUtils {
 | 
			
		||||
      feePerVsize: feePerVbytes,
 | 
			
		||||
      effectiveFeePerVsize: feePerVbytes,
 | 
			
		||||
    }, transaction);
 | 
			
		||||
    if (!transaction.status.confirmed) {
 | 
			
		||||
      transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
 | 
			
		||||
    if (!transaction?.status?.confirmed && !transactionExtended.firstSeen) {
 | 
			
		||||
      transactionExtended.firstSeen = Math.round((Date.now() / 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 (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
 | 
			
		||||
      transactionExtended.firstSeen = Math.round((Date.now() / 1000));
 | 
			
		||||
    }
 | 
			
		||||
    return transactionExtended;
 | 
			
		||||
  }
 | 
			
		||||
@ -63,6 +96,64 @@ class TransactionUtils {
 | 
			
		||||
    }
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
@ -48,12 +48,14 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
      weight: tx.weight,
 | 
			
		||||
      feePerVsize: tx.feePerVsize,
 | 
			
		||||
      effectiveFeePerVsize: tx.feePerVsize,
 | 
			
		||||
      sigops: tx.sigops,
 | 
			
		||||
      inputs: tx.inputs || [],
 | 
			
		||||
      relativesSet: false,
 | 
			
		||||
      ancestorMap: new Map<number, AuditTransaction>(),
 | 
			
		||||
      children: new Set<AuditTransaction>(),
 | 
			
		||||
      ancestorFee: 0,
 | 
			
		||||
      ancestorWeight: 0,
 | 
			
		||||
      ancestorSigops: 0,
 | 
			
		||||
      score: 0,
 | 
			
		||||
      used: 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)
 | 
			
		||||
  const blocks: number[][] = [];
 | 
			
		||||
  let blockWeight = 4000;
 | 
			
		||||
  let blockSigops = 0;
 | 
			
		||||
  let transactions: AuditTransaction[] = [];
 | 
			
		||||
  const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
 | 
			
		||||
    if (a.score === b.score) {
 | 
			
		||||
@ -118,7 +121,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
 | 
			
		||||
    if (nextTx && !nextTx?.used) {
 | 
			
		||||
      // 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());
 | 
			
		||||
        // 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];
 | 
			
		||||
@ -127,7 +130,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
          cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
 | 
			
		||||
          isCluster = true;
 | 
			
		||||
        }
 | 
			
		||||
        const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
 | 
			
		||||
        const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / (nextTx.ancestorWeight / 4));
 | 
			
		||||
        const used: AuditTransaction[] = [];
 | 
			
		||||
        while (sortedTxSet.length) {
 | 
			
		||||
          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
 | 
			
		||||
        if (used.length) {
 | 
			
		||||
          used.forEach(tx => {
 | 
			
		||||
            updateDescendants(tx, auditPool, modified);
 | 
			
		||||
            updateDescendants(tx, auditPool, modified, effectiveFeeRate);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -237,9 +240,11 @@ function setRelatives(
 | 
			
		||||
  };
 | 
			
		||||
  tx.ancestorFee = tx.fee || 0;
 | 
			
		||||
  tx.ancestorWeight = tx.weight || 0;
 | 
			
		||||
  tx.ancestorSigops = tx.sigops || 0;
 | 
			
		||||
  tx.ancestorMap.forEach((ancestor) => {
 | 
			
		||||
    tx.ancestorFee += ancestor.fee;
 | 
			
		||||
    tx.ancestorWeight += ancestor.weight;
 | 
			
		||||
    tx.ancestorSigops += ancestor.sigops;
 | 
			
		||||
  });
 | 
			
		||||
  tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
 | 
			
		||||
  tx.relativesSet = true;
 | 
			
		||||
@ -251,6 +256,7 @@ function updateDescendants(
 | 
			
		||||
  rootTx: AuditTransaction,
 | 
			
		||||
  mempool: Map<number, AuditTransaction>,
 | 
			
		||||
  modified: PairingHeap<AuditTransaction>,
 | 
			
		||||
  clusterRate: number,
 | 
			
		||||
): void {
 | 
			
		||||
  const descendantSet: Set<AuditTransaction> = new Set();
 | 
			
		||||
  // stack of nodes left to visit
 | 
			
		||||
@ -270,8 +276,10 @@ function updateDescendants(
 | 
			
		||||
      descendantTx.ancestorMap.delete(rootTx.uid);
 | 
			
		||||
      descendantTx.ancestorFee -= rootTx.fee;
 | 
			
		||||
      descendantTx.ancestorWeight -= rootTx.weight;
 | 
			
		||||
      descendantTx.ancestorSigops -= rootTx.sigops;
 | 
			
		||||
      tmpScore = descendantTx.score;
 | 
			
		||||
      descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
 | 
			
		||||
      descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
 | 
			
		||||
 | 
			
		||||
      if (!descendantTx.modifiedNode) {
 | 
			
		||||
        descendantTx.modified = true;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
import {
 | 
			
		||||
  BlockExtended, TransactionExtended, WebsocketResponse,
 | 
			
		||||
  BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
 | 
			
		||||
  OptimizedStatistic, ILoadingIndicators
 | 
			
		||||
} from '../mempool.interfaces';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
@ -122,7 +122,7 @@ class WebsocketHandler {
 | 
			
		||||
                    } else {
 | 
			
		||||
                      // tx.prevout is missing from transactions when in bitcoind mode
 | 
			
		||||
                      try {
 | 
			
		||||
                        const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                        const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                        response['tx'] = fullTx;
 | 
			
		||||
                      } catch (e) {
 | 
			
		||||
                        logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -130,7 +130,7 @@ class WebsocketHandler {
 | 
			
		||||
                    }
 | 
			
		||||
                  } else {
 | 
			
		||||
                    try {
 | 
			
		||||
                      const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                      const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                      response['tx'] = fullTx;
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                      logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -301,8 +301,8 @@ class WebsocketHandler {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
 | 
			
		||||
  async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended },
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
@ -399,7 +399,7 @@ class WebsocketHandler {
 | 
			
		||||
        if (tx) {
 | 
			
		||||
          if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
            try {
 | 
			
		||||
              const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
              const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
              response['tx'] = JSON.stringify(fullTx);
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -419,7 +419,7 @@ class WebsocketHandler {
 | 
			
		||||
          if (someVin) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -433,7 +433,7 @@ class WebsocketHandler {
 | 
			
		||||
          if (someVout) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -657,8 +657,8 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
      if (client['track-tx']) {
 | 
			
		||||
        const trackTxid = client['track-tx'];
 | 
			
		||||
        if (txIds.indexOf(trackTxid) > -1) {
 | 
			
		||||
          response['txConfirmed'] = 'true';
 | 
			
		||||
        if (trackTxid && txIds.indexOf(trackTxid) > -1) {
 | 
			
		||||
          response['txConfirmed'] = JSON.stringify(trackTxid);
 | 
			
		||||
        } else {
 | 
			
		||||
          const mempoolTx = _memPool[trackTxid];
 | 
			
		||||
          if (mempoolTx && mempoolTx.position) {
 | 
			
		||||
 | 
			
		||||
@ -88,28 +88,38 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
 | 
			
		||||
  uid?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolTransactionExtended extends TransactionExtended {
 | 
			
		||||
  sigops: number;
 | 
			
		||||
  adjustedVsize: number;
 | 
			
		||||
  adjustedFeePerVsize: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditTransaction {
 | 
			
		||||
  uid: number;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  effectiveFeePerVsize: number;
 | 
			
		||||
  sigops: number;
 | 
			
		||||
  inputs: number[];
 | 
			
		||||
  relativesSet: boolean;
 | 
			
		||||
  ancestorMap: Map<number, AuditTransaction>;
 | 
			
		||||
  children: Set<AuditTransaction>;
 | 
			
		||||
  ancestorFee: number;
 | 
			
		||||
  ancestorWeight: number;
 | 
			
		||||
  ancestorSigops: number;
 | 
			
		||||
  score: number;
 | 
			
		||||
  used: boolean;
 | 
			
		||||
  modified: boolean;
 | 
			
		||||
  modifiedNode: HeapNode<AuditTransaction>;
 | 
			
		||||
  dependencyRate?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CompactThreadTransaction {
 | 
			
		||||
  uid: number;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  sigops: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  effectiveFeePerVsize?: number;
 | 
			
		||||
  inputs: number[];
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@ class BlocksAuditRepositories {
 | 
			
		||||
        logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
        throw e;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -55,6 +54,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
        transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, sigop_txs as sigopTxs, match_rate as matchRate
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        JOIN blocks ON blocks.hash = blocks_audits.hash
 | 
			
		||||
        JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
 | 
			
		||||
        JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
@ -36,17 +36,16 @@ class BlocksSummariesRepository {
 | 
			
		||||
    try {
 | 
			
		||||
      const transactions = JSON.stringify(params.template?.transactions || []);
 | 
			
		||||
      await DB.query(`
 | 
			
		||||
        INSERT INTO blocks_summaries (height, id, transactions, template)
 | 
			
		||||
        VALUE (?, ?, ?, ?)
 | 
			
		||||
        INSERT INTO blocks_templates (id, template)
 | 
			
		||||
        VALUE (?, ?)
 | 
			
		||||
        ON DUPLICATE KEY UPDATE
 | 
			
		||||
          template = ?
 | 
			
		||||
      `, [params.height, blockId, '[]', transactions, transactions]);
 | 
			
		||||
      `, [blockId, transactions, transactions]);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
 | 
			
		||||
        logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.debug(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
        throw e;
 | 
			
		||||
        logger.warn(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,9 @@
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="grow"></span>
 | 
			
		||||
      <div class="container-buttons">
 | 
			
		||||
        <button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
 | 
			
		||||
          <ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
 | 
			
		||||
          <ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
 | 
			
		||||
          <ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
 | 
			
		||||
        </button>
 | 
			
		||||
        <div *ngIf="(latestBlock$ | async) as latestBlock">
 | 
			
		||||
          <app-confirmations [chainTip]="latestBlock?.height" [height]="bisqTx.blockHeight" [hideUnconfirmed]="true" buttonClass="float-right"></app-confirmations>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@ -70,11 +70,7 @@
 | 
			
		||||
 | 
			
		||||
    <div class="btn-container">
 | 
			
		||||
      <span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
 | 
			
		||||
        <button type="button" class="btn btn-sm btn-success mt-2">
 | 
			
		||||
          <ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>
 | 
			
		||||
          <ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
 | 
			
		||||
          <ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
 | 
			
		||||
        </button>
 | 
			
		||||
        <app-confirmations [chainTip]="latestBlock?.height" [height]="tx.blockHeight" [hideUnconfirmed]="true" buttonClass="mt-2"></app-confirmations>
 | 
			
		||||
         
 | 
			
		||||
      </span>
 | 
			
		||||
      <button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
    <p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <video src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
 | 
			
		||||
  <video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
 | 
			
		||||
    <track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
 | 
			
		||||
    <track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
 | 
			
		||||
    <track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
@ -17,6 +17,7 @@ import { DOCUMENT } from '@angular/common';
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class AboutComponent implements OnInit {
 | 
			
		||||
  @ViewChild('promoVideo') promoVideo: ElementRef;
 | 
			
		||||
  backendInfo$: Observable<IBackendInfo>;
 | 
			
		||||
  sponsors$: Observable<any>;
 | 
			
		||||
  translators$: Observable<ITranslators>;
 | 
			
		||||
@ -91,7 +92,11 @@ export class AboutComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  showSubtitles(language) {
 | 
			
		||||
  showSubtitles(language): boolean {
 | 
			
		||||
    return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  unmutePromoVideo(): void {
 | 
			
		||||
    this.promoVideo.nativeElement.muted = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<ng-container *ngIf="{ val: network$ | async } as network">
 | 
			
		||||
<header>
 | 
			
		||||
  <nav class="navbar navbar-expand-md navbar-dark bg-dark">
 | 
			
		||||
  <a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]">
 | 
			
		||||
  <a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
 | 
			
		||||
  <ng-template [ngIf]="subdomain">
 | 
			
		||||
    <div class="subdomain_container">
 | 
			
		||||
      <img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
 | 
			
		||||
 | 
			
		||||
@ -53,4 +53,8 @@ export class MasterPageComponent implements OnInit {
 | 
			
		||||
  onResize(): void {
 | 
			
		||||
    this.isMobile = window.innerWidth <= 767.98;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  brandClick(e): void {
 | 
			
		||||
    this.stateService.resetScroll$.next(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ export class StartComponent implements OnInit, OnDestroy {
 | 
			
		||||
  markBlockSubscription: Subscription;
 | 
			
		||||
  blockCounterSubscription: Subscription;
 | 
			
		||||
  @ViewChild('blockchainContainer') blockchainContainer: ElementRef;
 | 
			
		||||
  resetScrollSubscription: Subscription; 
 | 
			
		||||
 | 
			
		||||
  isMobile: boolean = false;
 | 
			
		||||
  isiOS: boolean = false;
 | 
			
		||||
@ -106,6 +107,12 @@ export class StartComponent implements OnInit, OnDestroy {
 | 
			
		||||
          }, 60 * 60 * 1000);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    this.resetScrollSubscription = this.stateService.resetScroll$.subscribe(reset => {
 | 
			
		||||
      if (reset) {
 | 
			
		||||
        this.resetScroll();
 | 
			
		||||
        this.stateService.resetScroll$.next(false);
 | 
			
		||||
      } 
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
@ -385,5 +392,6 @@ export class StartComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.chainTipSubscription.unsubscribe();
 | 
			
		||||
    this.markBlockSubscription.unsubscribe();
 | 
			
		||||
    this.blockCounterSubscription.unsubscribe();
 | 
			
		||||
    this.resetScrollSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -196,7 +196,7 @@ export class StatisticsComponent implements OnInit {
 | 
			
		||||
        this.feeLevelDropdownData.push({
 | 
			
		||||
          fee: fee,
 | 
			
		||||
          range,
 | 
			
		||||
          color: _chartColors[i - 1],
 | 
			
		||||
          color: _chartColors[i],
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -18,19 +18,7 @@
 | 
			
		||||
      </span>
 | 
			
		||||
 | 
			
		||||
      <div class="container-buttons">
 | 
			
		||||
        <ng-template [ngIf]="tx?.status?.confirmed">
 | 
			
		||||
          <button *ngIf="latestBlock" type="button" class="btn btn-sm btn-success">
 | 
			
		||||
            <ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
 | 
			
		||||
            <ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
 | 
			
		||||
            <ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
 | 
			
		||||
          </button>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
        <ng-template [ngIf]="tx && !tx?.status?.confirmed && replaced">
 | 
			
		||||
          <button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
        <ng-template [ngIf]="tx && !tx?.status?.confirmed && !replaced">
 | 
			
		||||
          <button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
        <app-confirmations [chainTip]="latestBlock?.height" [height]="tx?.status?.block_height" [replaced]="replaced"></app-confirmations>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </div>
 | 
			
		||||
@ -271,6 +259,10 @@
 | 
			
		||||
                <td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
 | 
			
		||||
              </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>
 | 
			
		||||
                <td i18n="block.weight">Weight</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
 | 
			
		||||
@ -289,6 +281,10 @@
 | 
			
		||||
                <td i18n="transaction.locktime">Locktime</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (tx.locktime | number)"></td>
 | 
			
		||||
              </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>
 | 
			
		||||
                <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>
 | 
			
		||||
@ -477,11 +473,11 @@
 | 
			
		||||
          {{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
          <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>
 | 
			
		||||
        </td>
 | 
			
		||||
      </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>
 | 
			
		||||
          <div class="effective-fee-container">
 | 
			
		||||
@ -490,7 +486,7 @@
 | 
			
		||||
              <app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </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>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
 | 
			
		||||
@ -86,6 +86,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  segwitEnabled: boolean;
 | 
			
		||||
  rbfEnabled: boolean;
 | 
			
		||||
  taprootEnabled: boolean;
 | 
			
		||||
  hasEffectiveFeeRate: boolean;
 | 
			
		||||
 | 
			
		||||
  @ViewChild('graphContainer')
 | 
			
		||||
  graphContainer: ElementRef;
 | 
			
		||||
@ -157,6 +158,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
      .subscribe((cpfpInfo) => {
 | 
			
		||||
        if (!cpfpInfo || !this.tx) {
 | 
			
		||||
          this.cpfpInfo = null;
 | 
			
		||||
          this.hasEffectiveFeeRate = false;
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        // merge ancestors/descendants
 | 
			
		||||
@ -164,16 +166,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
        if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
 | 
			
		||||
          relatives.push(cpfpInfo.bestDescendant);
 | 
			
		||||
        }
 | 
			
		||||
        let totalWeight =
 | 
			
		||||
          this.tx.weight +
 | 
			
		||||
          relatives.reduce((prev, val) => prev + val.weight, 0);
 | 
			
		||||
        let totalFees =
 | 
			
		||||
          this.tx.fee +
 | 
			
		||||
          relatives.reduce((prev, val) => prev + val.fee, 0);
 | 
			
		||||
 | 
			
		||||
        this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
 | 
			
		||||
        const hasRelatives = !!relatives.length;
 | 
			
		||||
        if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
 | 
			
		||||
          let totalWeight =
 | 
			
		||||
            this.tx.weight +
 | 
			
		||||
            relatives.reduce((prev, val) => prev + val.weight, 0);
 | 
			
		||||
          let totalFees =
 | 
			
		||||
            this.tx.fee +
 | 
			
		||||
            relatives.reduce((prev, val) => prev + val.fee, 0);
 | 
			
		||||
          this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.cpfpInfo = cpfpInfo;
 | 
			
		||||
        this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    this.fetchRbfSubscription = this.fetchRbfHistory$
 | 
			
		||||
@ -359,6 +366,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
                ancestors: tx.ancestors,
 | 
			
		||||
                bestDescendant: tx.bestDescendant,
 | 
			
		||||
              };
 | 
			
		||||
              const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
 | 
			
		||||
              this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
 | 
			
		||||
            } else {
 | 
			
		||||
              this.fetchCpfp$.next(this.tx.txid);
 | 
			
		||||
            }
 | 
			
		||||
@ -382,7 +391,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
 | 
			
		||||
      this.latestBlock = block;
 | 
			
		||||
 | 
			
		||||
      if (txConfirmed && this.tx && !this.tx.status.confirmed) {
 | 
			
		||||
      if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
 | 
			
		||||
        this.tx.status = {
 | 
			
		||||
          confirmed: true,
 | 
			
		||||
          block_height: block.height,
 | 
			
		||||
@ -500,6 +509,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.replaced = false;
 | 
			
		||||
    this.transactionTime = -1;
 | 
			
		||||
    this.cpfpInfo = null;
 | 
			
		||||
    this.hasEffectiveFeeRate = false;
 | 
			
		||||
    this.rbfInfo = null;
 | 
			
		||||
    this.rbfReplaces = [];
 | 
			
		||||
    this.showCpfpDetails = false;
 | 
			
		||||
 | 
			
		||||
@ -298,14 +298,7 @@
 | 
			
		||||
 | 
			
		||||
      <div class="float-right">
 | 
			
		||||
        <ng-container *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
 | 
			
		||||
          <button *ngIf="tx.status.confirmed; else unconfirmedButton" type="button" class="btn btn-sm btn-success mt-2">
 | 
			
		||||
            <ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
 | 
			
		||||
            <ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
 | 
			
		||||
            <ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
 | 
			
		||||
          </button>
 | 
			
		||||
          <ng-template #unconfirmedButton>
 | 
			
		||||
            <button type="button" class="btn btn-sm btn-danger mt-2" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <app-confirmations [chainTip]="latestBlock?.height" [height]="tx?.status?.block_height" buttonClass="mt-2"></app-confirmations>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <button *ngIf="address === ''; else viewingAddress" type="button" class="btn btn-sm btn-primary mt-2 ml-2" (click)="switchCurrency()">
 | 
			
		||||
          <ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,9 @@ export interface CpfpInfo {
 | 
			
		||||
  ancestors: Ancestor[];
 | 
			
		||||
  descendants?: Ancestor[];
 | 
			
		||||
  bestDescendant?: BestDescendant | null;
 | 
			
		||||
  effectiveFeePerVsize?: number;
 | 
			
		||||
  sigops?: number;
 | 
			
		||||
  adjustedVsize?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RbfInfo {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ export interface WebsocketResponse {
 | 
			
		||||
  block?: BlockExtended;
 | 
			
		||||
  blocks?: BlockExtended[];
 | 
			
		||||
  conversions?: any;
 | 
			
		||||
  txConfirmed?: boolean;
 | 
			
		||||
  txConfirmed?: string;
 | 
			
		||||
  historicalDate?: string;
 | 
			
		||||
  mempoolInfo?: MempoolInfo;
 | 
			
		||||
  vBytesPerSecond?: number;
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ export class StateService {
 | 
			
		||||
 | 
			
		||||
  networkChanged$ = new ReplaySubject<string>(1);
 | 
			
		||||
  lightningChanged$ = new ReplaySubject<boolean>(1);
 | 
			
		||||
  blocks$: ReplaySubject<[BlockExtended, boolean]>;
 | 
			
		||||
  blocks$: ReplaySubject<[BlockExtended, string]>;
 | 
			
		||||
  transactions$ = new ReplaySubject<TransactionStripped>(6);
 | 
			
		||||
  conversions$ = new ReplaySubject<any>(1);
 | 
			
		||||
  bsqPrice$ = new ReplaySubject<number>(1);
 | 
			
		||||
@ -126,6 +126,7 @@ export class StateService {
 | 
			
		||||
  keyNavigation$ = new Subject<KeyboardEvent>();
 | 
			
		||||
 | 
			
		||||
  blockScrolling$: Subject<boolean> = new Subject<boolean>();
 | 
			
		||||
  resetScroll$: Subject<boolean> = new Subject<boolean>();
 | 
			
		||||
  timeLtr: BehaviorSubject<boolean>;
 | 
			
		||||
  hideFlow: BehaviorSubject<boolean>;
 | 
			
		||||
  hideAudit: BehaviorSubject<boolean>;
 | 
			
		||||
@ -163,7 +164,7 @@ export class StateService {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = new ReplaySubject<[BlockExtended, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
 | 
			
		||||
    this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
 | 
			
		||||
 | 
			
		||||
    if (this.env.BASE_MODULE === 'bisq') {
 | 
			
		||||
      this.network = this.env.BASE_MODULE;
 | 
			
		||||
 | 
			
		||||
@ -241,7 +241,7 @@ export class WebsocketService {
 | 
			
		||||
      blocks.forEach((block: BlockExtended) => {
 | 
			
		||||
        if (block.height > this.stateService.latestBlockHeight) {
 | 
			
		||||
          maxHeight = Math.max(maxHeight, block.height);
 | 
			
		||||
          this.stateService.blocks$.next([block, false]);
 | 
			
		||||
          this.stateService.blocks$.next([block, '']);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      this.stateService.updateChainTip(maxHeight);
 | 
			
		||||
@ -258,7 +258,7 @@ export class WebsocketService {
 | 
			
		||||
    if (response.block) {
 | 
			
		||||
      if (response.block.height > this.stateService.latestBlockHeight) {
 | 
			
		||||
        this.stateService.updateChainTip(response.block.height);
 | 
			
		||||
        this.stateService.blocks$.next([response.block, !!response.txConfirmed]);
 | 
			
		||||
        this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (response.txConfirmed) {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,13 @@
 | 
			
		||||
<ng-template [ngIf]="confirmations">
 | 
			
		||||
  <button type="button" class="btn btn-sm btn-success {{buttonClass}}">
 | 
			
		||||
    <ng-container *ngTemplateOutlet="confirmations == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: confirmations}"></ng-container>
 | 
			
		||||
    <ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
 | 
			
		||||
    <ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
 | 
			
		||||
  </button>
 | 
			
		||||
</ng-template>
 | 
			
		||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
 | 
			
		||||
  <button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
 | 
			
		||||
</ng-template>
 | 
			
		||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced">
 | 
			
		||||
  <button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-confirmations',
 | 
			
		||||
  templateUrl: './confirmations.component.html',
 | 
			
		||||
  styleUrls: ['./confirmations.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ConfirmationsComponent implements OnChanges {
 | 
			
		||||
  @Input() chainTip: number;
 | 
			
		||||
  @Input() height: number;
 | 
			
		||||
  @Input() replaced: boolean = false;
 | 
			
		||||
  @Input() hideUnconfirmed: boolean = false;
 | 
			
		||||
  @Input() buttonClass: string = '';
 | 
			
		||||
 | 
			
		||||
  confirmations: number = 0;
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    if (this.chainTip != null && this.height != null) {
 | 
			
		||||
      this.confirmations = Math.max(1, this.chainTip - this.height + 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.confirmations = 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -85,6 +85,7 @@ import { SatsComponent } from './components/sats/sats.component';
 | 
			
		||||
import { TruncateComponent } from './components/truncate/truncate.component';
 | 
			
		||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
 | 
			
		||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
			
		||||
import { ConfirmationsComponent } from './components/confirmations/confirmations.component';
 | 
			
		||||
import { ToggleComponent } from './components/toggle/toggle.component';
 | 
			
		||||
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
 | 
			
		||||
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
 | 
			
		||||
@ -175,6 +176,7 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
 | 
			
		||||
    TruncateComponent,
 | 
			
		||||
    SearchResultsComponent,
 | 
			
		||||
    TimestampComponent,
 | 
			
		||||
    ConfirmationsComponent,
 | 
			
		||||
    ToggleComponent,
 | 
			
		||||
    GeolocationComponent,
 | 
			
		||||
    TestnetAlertComponent,
 | 
			
		||||
@ -289,6 +291,7 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
 | 
			
		||||
    TruncateComponent,
 | 
			
		||||
    SearchResultsComponent,
 | 
			
		||||
    TimestampComponent,
 | 
			
		||||
    ConfirmationsComponent,
 | 
			
		||||
    ToggleComponent,
 | 
			
		||||
    GeolocationComponent,
 | 
			
		||||
    PreviewTitleComponent,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user