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