Merge branch 'master' into fix-liquid-frontend-crash
This commit is contained in:
		
						commit
						e478fb2279
					
				
							
								
								
									
										6
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -7,6 +7,12 @@ mempool-config.json
 | 
			
		||||
pools.json
 | 
			
		||||
icons.json
 | 
			
		||||
 | 
			
		||||
# docker
 | 
			
		||||
Dockerfile
 | 
			
		||||
GeoIP
 | 
			
		||||
start.sh
 | 
			
		||||
wait-for-it.sh
 | 
			
		||||
 | 
			
		||||
# compiled output
 | 
			
		||||
/dist
 | 
			
		||||
/tmp
 | 
			
		||||
 | 
			
		||||
@ -646,7 +646,7 @@ class BisqMarketsApi {
 | 
			
		||||
        case 'year':
 | 
			
		||||
            return strtotime('midnight first day of january', ts);
 | 
			
		||||
        default:
 | 
			
		||||
            throw new Error('Unsupported interval: ' + interval);
 | 
			
		||||
            throw new Error('Unsupported interval');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces';
 | 
			
		||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import diskCache from './disk-cache';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
@ -451,7 +451,9 @@ class Blocks {
 | 
			
		||||
        if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
          const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
 | 
			
		||||
          const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
 | 
			
		||||
          if (cpfpSummary) {
 | 
			
		||||
            await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
 | 
			
		||||
        }
 | 
			
		||||
@ -995,11 +997,11 @@ class Blocks {
 | 
			
		||||
    return state;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateTimerProgress(state, msg) {
 | 
			
		||||
  private updateTimerProgress(state, msg): void {
 | 
			
		||||
    state.progress = msg;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private clearTimer(state) {
 | 
			
		||||
  private clearTimer(state): void {
 | 
			
		||||
    if (state.timer) {
 | 
			
		||||
      clearTimeout(state.timer);
 | 
			
		||||
    }
 | 
			
		||||
@ -1088,13 +1090,19 @@ class Blocks {
 | 
			
		||||
      summary = {
 | 
			
		||||
        id: hash,
 | 
			
		||||
        transactions: cpfpSummary.transactions.map(tx => {
 | 
			
		||||
          let flags: number = 0;
 | 
			
		||||
          try {
 | 
			
		||||
            flags = tx.flags || Common.getTransactionFlags(tx);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            txid: tx.txid,
 | 
			
		||||
            fee: tx.fee || 0,
 | 
			
		||||
            vsize: tx.vsize,
 | 
			
		||||
            value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
 | 
			
		||||
            rate: tx.effectiveFeePerVsize,
 | 
			
		||||
            flags: tx.flags || Common.getTransactionFlags(tx),
 | 
			
		||||
            flags: flags,
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
      };
 | 
			
		||||
@ -1284,7 +1292,7 @@ class Blocks {
 | 
			
		||||
    return blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditSummary(hash: string): Promise<any> {
 | 
			
		||||
  public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
      return BlocksAuditsRepository.$getBlockAudit(hash);
 | 
			
		||||
    } else {
 | 
			
		||||
@ -1304,7 +1312,7 @@ class Blocks {
 | 
			
		||||
    return this.currentBlockHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
 | 
			
		||||
  public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
 | 
			
		||||
    let transactions = txs;
 | 
			
		||||
    if (!transactions) {
 | 
			
		||||
      if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
@ -1319,6 +1327,7 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transactions?.length != null) {
 | 
			
		||||
      const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
 | 
			
		||||
 | 
			
		||||
      await this.$saveCpfp(hash, height, summary);
 | 
			
		||||
@ -1327,6 +1336,10 @@ class Blocks {
 | 
			
		||||
      await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
 | 
			
		||||
 | 
			
		||||
      return summary;
 | 
			
		||||
    } else {
 | 
			
		||||
      logger.err(`Cannot index CPFP for block ${height} - missing transaction data`);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository';
 | 
			
		||||
import { isIP } from 'net';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
import { isPoint } from '../utils/secp256k1';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
export class Common {
 | 
			
		||||
  static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
 | 
			
		||||
    '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
 | 
			
		||||
@ -261,6 +262,9 @@ export class Common {
 | 
			
		||||
        case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
 | 
			
		||||
        case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
 | 
			
		||||
        case 'v1_p2tr': {
 | 
			
		||||
          if (!vin.witness?.length) {
 | 
			
		||||
            throw new Error('Taproot input missing witness data');
 | 
			
		||||
          }
 | 
			
		||||
          flags |= TransactionFlags.p2tr;
 | 
			
		||||
          // in taproot, if the last witness item begins with 0x50, it's an annex
 | 
			
		||||
          const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
 | 
			
		||||
@ -301,7 +305,7 @@ export class Common {
 | 
			
		||||
        case 'p2pk': {
 | 
			
		||||
          flags |= TransactionFlags.p2pk;
 | 
			
		||||
          // detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
 | 
			
		||||
          hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2));
 | 
			
		||||
          hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey?.slice(2, -2));
 | 
			
		||||
        } break;
 | 
			
		||||
        case 'multisig': {
 | 
			
		||||
          flags |= TransactionFlags.p2ms;
 | 
			
		||||
@ -348,7 +352,12 @@ export class Common {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static classifyTransaction(tx: TransactionExtended): TransactionClassified {
 | 
			
		||||
    const flags = Common.getTransactionFlags(tx);
 | 
			
		||||
    let flags = 0;
 | 
			
		||||
    try {
 | 
			
		||||
      flags = Common.getTransactionFlags(tx);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
    tx.flags = flags;
 | 
			
		||||
    return {
 | 
			
		||||
      ...Common.stripTransaction(tx),
 | 
			
		||||
 | 
			
		||||
@ -142,7 +142,7 @@ class Mining {
 | 
			
		||||
  public async $getPoolStat(slug: string): Promise<object> {
 | 
			
		||||
    const pool = await PoolsRepository.$getPool(slug);
 | 
			
		||||
    if (!pool) {
 | 
			
		||||
      throw new Error('This mining pool does not exist ' + escape(slug));
 | 
			
		||||
      throw new Error('This mining pool does not exist');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const blockCount: number = await BlocksRepository.$blockCount(pool.id);
 | 
			
		||||
 | 
			
		||||
@ -59,7 +59,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAudit(hash: string): Promise<any> {
 | 
			
		||||
  public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
 | 
			
		||||
@ -75,8 +75,8 @@ class BlocksAuditRepositories {
 | 
			
		||||
        expected_weight as expectedWeight
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
        WHERE blocks_audits.hash = ?
 | 
			
		||||
      `, [hash]);
 | 
			
		||||
      
 | 
			
		||||
      if (rows.length) {
 | 
			
		||||
        rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
 | 
			
		||||
@ -101,8 +101,8 @@ class BlocksAuditRepositories {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
        WHERE blocks_audits.hash = ?
 | 
			
		||||
      `, [hash]);
 | 
			
		||||
      return rows[0];
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import logger from '../logger';
 | 
			
		||||
import { Common } from '../api/common';
 | 
			
		||||
import PoolsRepository from './PoolsRepository';
 | 
			
		||||
import HashratesRepository from './HashratesRepository';
 | 
			
		||||
import { escape } from 'mysql2';
 | 
			
		||||
import { RowDataPacket, escape } from 'mysql2';
 | 
			
		||||
import BlocksSummariesRepository from './BlocksSummariesRepository';
 | 
			
		||||
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
 | 
			
		||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
 | 
			
		||||
@ -478,7 +478,7 @@ class BlocksRepository {
 | 
			
		||||
  public async $getBlocksByPool(slug: string, startHeight?: number): Promise<BlockExtended[]> {
 | 
			
		||||
    const pool = await PoolsRepository.$getPool(slug);
 | 
			
		||||
    if (!pool) {
 | 
			
		||||
      throw new Error('This mining pool does not exist ' + escape(slug));
 | 
			
		||||
      throw new Error('This mining pool does not exist');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
@ -802,10 +802,10 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get a list of blocks that have been indexed
 | 
			
		||||
   */
 | 
			
		||||
  public async $getIndexedBlocks(): Promise<any[]> {
 | 
			
		||||
  public async $getIndexedBlocks(): Promise<{ height: number, hash: string }[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`);
 | 
			
		||||
      return rows;
 | 
			
		||||
      const [rows] = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`) as RowDataPacket[][];
 | 
			
		||||
      return rows as { height: number, hash: string }[];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
@ -815,7 +815,7 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get a list of blocks that have not had CPFP data indexed
 | 
			
		||||
   */
 | 
			
		||||
   public async $getCPFPUnindexedBlocks(): Promise<any[]> {
 | 
			
		||||
   public async $getCPFPUnindexedBlocks(): Promise<number[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockchainInfo = await bitcoinClient.getBlockchainInfo();
 | 
			
		||||
      const currentBlockHeight = blockchainInfo.blocks;
 | 
			
		||||
@ -825,13 +825,13 @@ class BlocksRepository {
 | 
			
		||||
      }
 | 
			
		||||
      const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
 | 
			
		||||
 | 
			
		||||
      const [rows]: any[] = await DB.query(`
 | 
			
		||||
      const [rows] = await DB.query(`
 | 
			
		||||
        SELECT height
 | 
			
		||||
        FROM compact_cpfp_clusters
 | 
			
		||||
        WHERE height <= ? AND height >= ?
 | 
			
		||||
        GROUP BY height
 | 
			
		||||
        ORDER BY height DESC;
 | 
			
		||||
      `, [currentBlockHeight, minHeight]);
 | 
			
		||||
      `, [currentBlockHeight, minHeight]) as RowDataPacket[][];
 | 
			
		||||
 | 
			
		||||
      const indexedHeights = {};
 | 
			
		||||
      rows.forEach((row) => { indexedHeights[row.height] = true; });
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { BlockSummary, TransactionClassified } from '../mempool.interfaces';
 | 
			
		||||
@ -69,7 +70,7 @@ class BlocksSummariesRepository {
 | 
			
		||||
 | 
			
		||||
  public async $getIndexedSummariesId(): Promise<string[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
 | 
			
		||||
      const [rows] = await DB.query(`SELECT id from blocks_summaries`) as RowDataPacket[][];
 | 
			
		||||
      return rows.map(row => row.id);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
 | 
			
		||||
@ -139,7 +139,7 @@ class HashratesRepository {
 | 
			
		||||
  public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> {
 | 
			
		||||
    const pool = await PoolsRepository.$getPool(slug);
 | 
			
		||||
    if (!pool) {
 | 
			
		||||
      throw new Error('This mining pool does not exist ' + escape(slug));
 | 
			
		||||
      throw new Error('This mining pool does not exist');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find hashrate boundaries
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,9 @@ const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
 | 
			
		||||
 * @returns {boolean} true if the point is on the SECP256K1 curve
 | 
			
		||||
 */
 | 
			
		||||
export function isPoint(pointHex: string): boolean {
 | 
			
		||||
  if (!pointHex?.length) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  if (
 | 
			
		||||
    !(
 | 
			
		||||
      // is uncompressed
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/jamesblacklock.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/jamesblacklock.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 20, 2023.
 | 
			
		||||
 | 
			
		||||
Signed: jamesblacklock
 | 
			
		||||
@ -55,7 +55,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
 | 
			
		||||
 | 
			
		||||
# ESPLORA
 | 
			
		||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
 | 
			
		||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
 | 
			
		||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=""}
 | 
			
		||||
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
 | 
			
		||||
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
 | 
			
		||||
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -6,6 +6,13 @@
 | 
			
		||||
/out-tsc
 | 
			
		||||
server.run.js
 | 
			
		||||
 | 
			
		||||
# docker
 | 
			
		||||
Dockerfile
 | 
			
		||||
entrypoint.sh
 | 
			
		||||
nginx-mempool.conf
 | 
			
		||||
nginx.conf
 | 
			
		||||
wait-for
 | 
			
		||||
 | 
			
		||||
# Only exists if Bazel was run
 | 
			
		||||
/bazel-out
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -45,28 +45,30 @@ export class AcceleratorDashboardComponent implements OnInit {
 | 
			
		||||
    this.pendingAccelerations$ = interval(30000).pipe(
 | 
			
		||||
      startWith(true),
 | 
			
		||||
      switchMap(() => {
 | 
			
		||||
        return this.apiService.getAccelerations$();
 | 
			
		||||
      }),
 | 
			
		||||
      catchError((e) => {
 | 
			
		||||
        return this.apiService.getAccelerations$().pipe(
 | 
			
		||||
          catchError(() => {
 | 
			
		||||
            return of([]);
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
      }),
 | 
			
		||||
      share(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.accelerations$ = this.stateService.chainTip$.pipe(
 | 
			
		||||
      distinctUntilChanged(),
 | 
			
		||||
      switchMap((chainTip) => {
 | 
			
		||||
        return this.apiService.getAccelerationHistory$({ timeframe: '1m' });
 | 
			
		||||
      }),
 | 
			
		||||
      catchError((e) => {
 | 
			
		||||
      switchMap(() => {
 | 
			
		||||
        return this.apiService.getAccelerationHistory$({ timeframe: '1m' }).pipe(
 | 
			
		||||
          catchError(() => {
 | 
			
		||||
            return of([]);
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
      }),
 | 
			
		||||
      share(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.minedAccelerations$ = this.accelerations$.pipe(
 | 
			
		||||
      map(accelerations => {
 | 
			
		||||
        return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status))
 | 
			
		||||
        return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -540,7 +540,7 @@
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </div>
 | 
			
		||||
          <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>
 | 
			
		||||
          <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>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user