Merge pull request #3353 from mempool/mononaut/mempool-block-animations
Improve mempool block animations
This commit is contained in:
		
						commit
						b78fdf5a23
					
				@ -5,9 +5,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
 | 
			
		||||
 | 
			
		||||
class Audit {
 | 
			
		||||
  auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
 | 
			
		||||
   : { censored: string[], added: string[], fresh: string[], score: number } {
 | 
			
		||||
   : { censored: string[], added: string[], fresh: string[], score: number, similarity: number } {
 | 
			
		||||
    if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
 | 
			
		||||
      return { censored: [], added: [], fresh: [], score: 0 };
 | 
			
		||||
      return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const matches: string[] = []; // present in both mined block and template
 | 
			
		||||
@ -16,6 +16,8 @@ class Audit {
 | 
			
		||||
    const isCensored = {}; // missing, without excuse
 | 
			
		||||
    const isDisplaced = {};
 | 
			
		||||
    let displacedWeight = 0;
 | 
			
		||||
    let matchedWeight = 0;
 | 
			
		||||
    let projectedWeight = 0;
 | 
			
		||||
 | 
			
		||||
    const inBlock = {};
 | 
			
		||||
    const inTemplate = {};
 | 
			
		||||
@ -38,11 +40,16 @@ class Audit {
 | 
			
		||||
          isCensored[txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
        displacedWeight += mempool[txid].weight;
 | 
			
		||||
      } else {
 | 
			
		||||
        matchedWeight += mempool[txid].weight;
 | 
			
		||||
      }
 | 
			
		||||
      projectedWeight += mempool[txid].weight;
 | 
			
		||||
      inTemplate[txid] = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    displacedWeight += (4000 - transactions[0].weight);
 | 
			
		||||
    projectedWeight += transactions[0].weight;
 | 
			
		||||
    matchedWeight += transactions[0].weight;
 | 
			
		||||
 | 
			
		||||
    // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
 | 
			
		||||
    // these displaced transactions should occupy the first N weight units of the next projected block
 | 
			
		||||
@ -121,12 +128,14 @@ class Audit {
 | 
			
		||||
    const numCensored = Object.keys(isCensored).length;
 | 
			
		||||
    const numMatches = matches.length - 1; // adjust for coinbase tx
 | 
			
		||||
    const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
 | 
			
		||||
    const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      censored: Object.keys(isCensored),
 | 
			
		||||
      added,
 | 
			
		||||
      fresh,
 | 
			
		||||
      score
 | 
			
		||||
      score,
 | 
			
		||||
      similarity,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
 | 
			
		||||
import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
 | 
			
		||||
import { isIP } from 'net';
 | 
			
		||||
@ -164,6 +164,30 @@ export class Common {
 | 
			
		||||
    return parents;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // calculates the ratio of matched transactions to projected transactions by weight
 | 
			
		||||
  static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number {
 | 
			
		||||
    let matchedWeight = 0;
 | 
			
		||||
    let projectedWeight = 0;
 | 
			
		||||
    const inBlock = {};
 | 
			
		||||
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      inBlock[tx.txid] = tx;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // look for transactions that were expected in the template, but missing from the mined block
 | 
			
		||||
    for (const tx of projectedBlock.transactions) {
 | 
			
		||||
      if (inBlock[tx.txid]) {
 | 
			
		||||
        matchedWeight += tx.vsize * 4;
 | 
			
		||||
      }
 | 
			
		||||
      projectedWeight += tx.vsize * 4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    projectedWeight += transactions[0].weight;
 | 
			
		||||
    matchedWeight += transactions[0].weight;
 | 
			
		||||
 | 
			
		||||
    return projectedWeight ? matchedWeight / projectedWeight : 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getSqlInterval(interval: string | null): string | null {
 | 
			
		||||
    switch (interval) {
 | 
			
		||||
      case '24h': return '1 DAY';
 | 
			
		||||
 | 
			
		||||
@ -432,7 +432,7 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Common.indexingEnabled() && memPool.isInSync()) {
 | 
			
		||||
        const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
 | 
			
		||||
        const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
 | 
			
		||||
        const matchRate = Math.round(score * 100 * 100) / 100;
 | 
			
		||||
 | 
			
		||||
        const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
 | 
			
		||||
@ -464,8 +464,14 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
        if (block.extras) {
 | 
			
		||||
          block.extras.matchRate = matchRate;
 | 
			
		||||
          block.extras.similarity = similarity;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (block.extras) {
 | 
			
		||||
      const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
 | 
			
		||||
      if (mBlocks?.length && mBlocks[0].transactions) {
 | 
			
		||||
        block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const removed: string[] = [];
 | 
			
		||||
 | 
			
		||||
@ -153,6 +153,7 @@ export interface BlockExtension {
 | 
			
		||||
  feeRange: number[]; // fee rate percentiles
 | 
			
		||||
  reward: number;
 | 
			
		||||
  matchRate: number | null;
 | 
			
		||||
  similarity?: number;
 | 
			
		||||
  pool: {
 | 
			
		||||
    id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
  <div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
 | 
			
		||||
    <div class="flashing">
 | 
			
		||||
      <ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
 | 
			
		||||
        <div [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
 | 
			
		||||
        <div @blockEntryTrigger [@.disabled]="!animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
 | 
			
		||||
          <a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
 | 
			
		||||
            class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
 | 
			
		||||
          <div class="block-body">
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
 | 
			
		||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs';
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
 | 
			
		||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
@ -9,11 +9,18 @@ import { specialBlocks } from '../../app.constants';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { Location } from '@angular/common';
 | 
			
		||||
import { DifficultyAdjustment } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { animate, style, transition, trigger } from '@angular/animations';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mempool-blocks',
 | 
			
		||||
  templateUrl: './mempool-blocks.component.html',
 | 
			
		||||
  styleUrls: ['./mempool-blocks.component.scss'],
 | 
			
		||||
  animations: [trigger('blockEntryTrigger', [
 | 
			
		||||
    transition(':enter', [
 | 
			
		||||
      style({ transform: 'translateX(-155px)' }),
 | 
			
		||||
      animate('2s 0s ease', style({ transform: '' })),
 | 
			
		||||
    ]),
 | 
			
		||||
  ])],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
@ -32,12 +39,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  isLoadingWebsocketSubscription: Subscription;
 | 
			
		||||
  blockSubscription: Subscription;
 | 
			
		||||
  networkSubscription: Subscription;
 | 
			
		||||
  chainTipSubscription: Subscription;
 | 
			
		||||
  network = '';
 | 
			
		||||
  now = new Date().getTime();
 | 
			
		||||
  timeOffset = 0;
 | 
			
		||||
  showMiningInfo = false;
 | 
			
		||||
  timeLtrSubscription: Subscription;
 | 
			
		||||
  timeLtr: boolean;
 | 
			
		||||
  animateEntry: boolean = false;
 | 
			
		||||
 | 
			
		||||
  blockWidth = 125;
 | 
			
		||||
  blockPadding = 30;
 | 
			
		||||
@ -53,6 +62,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  resetTransitionTimeout: number;
 | 
			
		||||
 | 
			
		||||
  chainTip: number = -1;
 | 
			
		||||
  blockIndex = 1;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -69,6 +79,8 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.chainTip = this.stateService.latestBlockHeight;
 | 
			
		||||
 | 
			
		||||
    if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
 | 
			
		||||
      this.enabledMiningInfoIfNeeded(this.location.path());
 | 
			
		||||
      this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
 | 
			
		||||
@ -153,11 +165,24 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.blockSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
        if (block?.extras?.matchRate >= 66 && !this.tabHidden) {
 | 
			
		||||
        if (this.chainTip === -1) {
 | 
			
		||||
          this.animateEntry = block.height === this.stateService.latestBlockHeight;
 | 
			
		||||
        } else {
 | 
			
		||||
          this.animateEntry = block.height > this.chainTip;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.chainTip = this.stateService.latestBlockHeight;
 | 
			
		||||
        if ((block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
 | 
			
		||||
          this.blockIndex++;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => {
 | 
			
		||||
      if (this.chainTip === -1) {
 | 
			
		||||
        this.chainTip = height;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.networkSubscription = this.stateService.networkChanged$
 | 
			
		||||
      .subscribe((network) => this.network = network);
 | 
			
		||||
 | 
			
		||||
@ -193,11 +218,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.blockSubscription.unsubscribe();
 | 
			
		||||
    this.networkSubscription.unsubscribe();
 | 
			
		||||
    this.timeLtrSubscription.unsubscribe();
 | 
			
		||||
    this.chainTipSubscription.unsubscribe();
 | 
			
		||||
    clearTimeout(this.resetTransitionTimeout);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByFn(index: number, block: MempoolBlock) {
 | 
			
		||||
    return block.index;
 | 
			
		||||
    return (block.isStack) ? 'stack' : block.index;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
 | 
			
		||||
@ -214,6 +240,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
      lastBlock.medianFee = this.median(lastBlock.feeRange);
 | 
			
		||||
      lastBlock.totalFees += block.totalFees;
 | 
			
		||||
    }
 | 
			
		||||
    if (blocks.length) {
 | 
			
		||||
      blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
 | 
			
		||||
    }
 | 
			
		||||
    return blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -332,4 +361,4 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
    return emptyBlocks;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -118,6 +118,7 @@ export interface BlockExtension {
 | 
			
		||||
  reward?: number;
 | 
			
		||||
  coinbaseRaw?: string;
 | 
			
		||||
  matchRate?: number;
 | 
			
		||||
  similarity?: number;
 | 
			
		||||
  pool?: {
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ export interface MempoolBlock {
 | 
			
		||||
  totalFees: number;
 | 
			
		||||
  feeRange: number[];
 | 
			
		||||
  index: number;
 | 
			
		||||
  isStack?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlockWithTransactions extends MempoolBlock {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user