mempool/frontend/src/app/components/eight-mempool/eight-mempool.component.ts
2024-09-23 23:18:59 +00:00

209 lines
7.2 KiB
TypeScript

import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError } from 'rxjs/operators';
import { Subject, Subscription, of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
import { CacheService } from '../../services/cache.service';
import { isMempoolDelta, MempoolBlockDelta } from '../../interfaces/websocket.interface';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
@Component({
selector: 'app-eight-mempool',
templateUrl: './eight-mempool.component.html',
styleUrls: ['./eight-mempool.component.scss'],
animations: [
trigger('infoChange', [
transition(':enter', [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('1000ms 500ms', style({ opacity: 0 }))
])
]),
],
})
export class EightMempoolComponent implements OnInit, OnDestroy {
network = '';
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
webGlEnabled = true;
hoverTx: string | null = null;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
blockSub: Subscription;
mempoolBlockSub: Subscription;
chainDirection: string = 'right';
poolDirection: string = 'left';
lastBlockHeight: number = 0;
lastBlockHeightUpdate: number[] = [];
numBlocks: number = 8;
blockIndices: number[] = [];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 360;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
testing: boolean = true;
testHeight: number = 800000;
testShiftTimeout: number;
showInfo: boolean = true;
blockInfo: { label: string}[] = [
{ label: '' },
{ label: 'mempool' },
{ label: 'blocks' },
];
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
height: '1080px',
maxWidth: '1080px',
padding: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
constructor(
private route: ActivatedRoute,
private router: Router,
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private cacheService: CacheService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']);
this.network = this.stateService.network;
this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
// process update
if (isMempoolDelta(update)) {
// delta
this.updateBlock(update);
} else {
const transactionsStripped = update.transactions;
const inOldBlock = {};
const inNewBlock = {};
const added: TransactionStripped[] = [];
const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = [];
const removed: string[] = [];
for (const tx of transactionsStripped) {
inNewBlock[tx.txid] = true;
}
for (const txid of Object.keys(this.blockGraph?.scenes[update.block]?.txs || {})) {
inOldBlock[txid] = true;
if (!inNewBlock[txid]) {
removed.push(txid);
}
}
for (const tx of transactionsStripped) {
if (!inOldBlock[tx.txid]) {
added.push(tx);
} else {
changed.push({
txid: tx.txid,
rate: tx.rate,
flags: tx.flags,
acc: tx.acc
});
}
}
this.updateBlock({
block: update.block,
removed,
changed,
added
});
}
});
this.mempoolBlockSub = this.stateService.mempoolBlocks$.subscribe((blocks) => {
this.blockInfo[0].label = `+${blocks.length - this.numBlocks}`;
});
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
this.blockIndices = [...Array(this.numBlocks).keys()];
this.lastBlockHeightUpdate = this.blockIndices.map(() => 0);
this.autofit = params.autofit !== 'false';
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 0;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = 0;
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
padding: (this.padding || 0) +'px 0px',
};
this.websocketService.startTrackMempoolBlocks(this.blockIndices);
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
this.blockSub.unsubscribe();
this.mempoolBlockSub.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
updateBlock(delta: MempoolBlockDelta): void {
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeightUpdate[delta.block]);
if (blockMined) {
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
} else {
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], this.poolDirection);
}
this.lastBlockHeightUpdate[delta.block] = this.stateService.latestBlockHeight;
}
}