multiblock mempool page

This commit is contained in:
Mononaut 2024-09-19 14:12:38 +00:00
parent 5429d6f264
commit 50eb9b602b
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
13 changed files with 416 additions and 23 deletions

View File

@ -3,7 +3,8 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolDelta, MempoolDeltaTxids
MempoolDelta, MempoolDeltaTxids,
TransactionCompressed
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@ -317,6 +318,7 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
client['track-mempool-blocks'] = undefined;
const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
@ -326,7 +328,31 @@ class WebsocketHandler {
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
} else {
client['track-mempool-block'] = null;
client['track-mempool-block'] = undefined;
}
}
if (parsedMessage && parsedMessage['track-mempool-blocks'] !== undefined) {
if (parsedMessage['track-mempool-blocks'].length > 0) {
client['track-mempool-block'] = undefined;
const indices: number[] = [];
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
const updates: { index: number, sequence: number, blockTransactions: TransactionCompressed[] }[] = [];
for (const i of parsedMessage['track-mempool-blocks']) {
const index = parseInt(i);
if (Number.isInteger(index) && index >= 0) {
indices.push(index);
updates.push({
index: index,
sequence: this.mempoolSequence,
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
}
}
client['track-mempool-blocks'] = indices;
response['projected-block-transactions'] = JSON.stringify(updates);
} else {
client['track-mempool-blocks'] = undefined;
}
}
@ -910,6 +936,19 @@ class WebsocketHandler {
delta: mBlockDeltas[index],
});
}
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
const indices = client['track-mempool-blocks'];
const updates: string[] = [];
for (const index of indices) {
if (mBlockDeltas[index]) {
updates.push(getCachedResponse(`projected-block-transactions-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
}));
}
}
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
}
if (client['track-rbf'] === 'all' && rbfReplacements) {
@ -1306,6 +1345,27 @@ class WebsocketHandler {
});
}
}
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
const indices = client['track-mempool-blocks'];
const updates: string[] = [];
for (const index of indices) {
if (mBlockDeltas && mBlockDeltas[index] && mBlocksWithTransactions[index]?.transactions?.length) {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
updates.push(getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index,
sequence: this.mempoolSequence,
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
}));
} else {
updates.push(getCachedResponse(`projected-block-transactions-delta-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
}));
}
}
}
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
}
if (client['track-mempool-txids']) {

View File

@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { EightMempoolComponent } from './components/eight-mempool/eight-mempool.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
@ -205,6 +206,10 @@ let routes: Routes = [
path: 'view/blocks',
component: EightBlocksComponent,
},
{
path: 'view/mempool-blocks',
component: EightMempoolComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },

View File

@ -278,7 +278,7 @@ export default class BlockScene {
}
private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void {
this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x, maxY: this.y + this.height }));
this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x - this.width, maxX: this.x + this.width + this.width, maxY: this.y + this.height }));
}
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {

View File

@ -17,10 +17,11 @@ export default class TxSprite {
tempAttributes: OptionalAttributes;
minX: number;
maxX: number;
maxY: number;
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX, maxY: number) {
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX: number, maxX: number, maxY: number) {
const offsetTime = params.start;
this.vertexArray = vertexArray;
this.vertexData = Array(VI.length).fill(0);
@ -30,6 +31,7 @@ export default class TxSprite {
};
this.minX = minX;
this.maxX = maxX;
this.maxY = maxY;
this.attributes = {
@ -84,7 +86,7 @@ export default class TxSprite {
minDuration: minimum remaining transition duration when adjust = true
temp: if true, this update is only temporary (can be reversed with 'resume')
*/
update(params: SpriteUpdateParams, minX?: number, maxY?: number): void {
update(params: SpriteUpdateParams, minX?: number, maxX?: number, maxY?: number): void {
const offsetTime = params.start || performance.now();
const v = params.duration > 0 ? (1 / params.duration) : 0;

View File

@ -106,7 +106,7 @@ export default class TxView implements TransactionStripped {
returns minimum transition end time
*/
update(params: ViewUpdateParams, { minX, maxY }: { minX: number, maxY: number }): number {
update(params: ViewUpdateParams, { minX, maxX, maxY }: { minX: number, maxX: number, maxY: number }): number {
if (params.jitter) {
params.delay += (Math.random() * params.jitter);
}
@ -117,6 +117,7 @@ export default class TxView implements TransactionStripped {
toSpriteUpdate(params),
this.vertexArray,
minX,
maxX,
maxY
);
// apply any pending hover event
@ -130,6 +131,7 @@ export default class TxView implements TransactionStripped {
temp: true
},
minX,
maxX,
maxY
);
}
@ -137,6 +139,7 @@ export default class TxView implements TransactionStripped {
this.sprite.update(
toSpriteUpdate(params),
minX,
maxX,
maxY
);
}

View File

@ -40,6 +40,7 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On
@Input() isLoading: boolean;
@Input() resolution: number;
@Input() numBlocks: number;
@Input() padding: number = 0;
@Input() blockWidth: number = 360;
@Input() autofit: boolean = false;
@Input() blockLimit: number;
@ -285,8 +286,8 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On
for (const [index, pendingUpdate] of this.pendingUpdates.entries()) {
if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction);
this.clearUpdateQueue(index);
}
this.clearUpdateQueue(index);
}
}
@ -391,8 +392,8 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
}
for (let i = 0; i < this.scenes.length; i++) {
const blocksPerRow = Math.floor(this.displayWidth / this.blockWidth);
const x = (i % blocksPerRow) * this.blockWidth;
const blocksPerRow = Math.floor((this.displayWidth + this.padding) / (this.blockWidth + this.padding));
const x = (i % blocksPerRow) * (this.blockWidth + this.padding);
const row = Math.floor(i / blocksPerRow);
const y = this.displayHeight - ((row + 1) * this.blockWidth);
if (this.scenes[i]) {
@ -401,7 +402,7 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On
} else {
this.scenes[i] = new BlockScene({ x, y, width: this.blockWidth, height: this.blockWidth, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: 0,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
colorFunction: this.getColorFunction() });
this.start();
}

View File

@ -114,7 +114,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
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 = this.padding * 2;
this.animationOffset = 0;
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);

View File

@ -0,0 +1,26 @@
<!-- <div class="blocks" [class.wrap]="wrapBlocks">
<ng-container *ngFor="let i of blockIndices">
<div class="block-wrapper" [style]="wrapperStyle">
<div class="block-container" [style]="containerStyle"> -->
<app-block-overview-multi
#blockGraph
[isLoading]="false"
[numBlocks]="numBlocks"
[padding]="padding"
[blockWidth]="blockWidth"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'left'"
[flip]="true"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
></app-block-overview-multi>
<!-- <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
<h1 class="height">{{ blockInfo[i].height }}</h1>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
</div>
</div>
</div>
</ng-container>
</div> -->

View File

@ -0,0 +1,69 @@
.blocks {
width: 100%;
height: 100%;
min-width: 100vw;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
&.wrap {
flex-wrap: wrap;
}
.block-wrapper {
flex-grow: 0;
flex-shrink: 0;
position: relative;
--block-width: 1080px;
.info {
position: absolute;
left: 8%;
top: 8%;
right: 8%;
bottom: 8%;
height: 84%;
width: 84%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: calc(var(--block-width) * 0.03);
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
h1 {
font-size: 6em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
h2 {
font-size: 1.8em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.hash {
font-family: monospace;
word-wrap: break-word;
font-size: 1.4em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.mined-by {
position: absolute;
bottom: 0;
margin: auto;
text-align: center;
}
}
}
.block-container {
overflow: hidden;
}
}

View File

@ -0,0 +1,201 @@
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;
}
interface BlockInfo extends BlockExtended {
timeString: string;
}
@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;
tipSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
blockSub: 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: BlockInfo[] = [];
wrapperStyle = {
'--block-width': '1080px',
width: '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']);
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.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) : this.blockWidth;
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',
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.tipSubscription.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;
}
}

View File

@ -33,6 +33,7 @@ export interface WebsocketResponse {
'track-scriptpubkeys'?: string[];
'track-asset'?: string;
'track-mempool-block'?: number;
'track-mempool-blocks'?: number[];
'track-rbf'?: string;
'track-rbf-summary'?: boolean;
'track-accelerations'?: boolean;

View File

@ -29,12 +29,14 @@ export class WebsocketService {
private isTrackingTx = false;
private trackingTxId: string;
private isTrackingMempoolBlock = false;
private isTrackingMempoolBlocks = false;
private isTrackingRbf: 'all' | 'fullRbf' | false = false;
private isTrackingRbfSummary = false;
private isTrackingAddress: string | false = false;
private isTrackingAddresses: string[] | false = false;
private isTrackingAccelerations: boolean = false;
private trackingMempoolBlock: number;
private trackingMempoolBlocks: number[];
private stoppingTrackMempoolBlock: any | null = null;
private latestGitCommit = '';
private onlineCheckTimeout: number;
@ -122,6 +124,9 @@ export class WebsocketService {
if (this.isTrackingMempoolBlock) {
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
}
if (this.isTrackingMempoolBlocks) {
this.startTrackMempoolBlocks(this.trackingMempoolBlocks);
}
if (this.isTrackingRbf) {
this.startTrackRbf(this.isTrackingRbf);
}
@ -218,6 +223,13 @@ export class WebsocketService {
return false;
}
startTrackMempoolBlocks(blocks: number[], force: boolean = false): boolean {
this.websocketSubject.next({ 'track-mempool-blocks': blocks });
this.isTrackingMempoolBlocks = true;
this.trackingMempoolBlocks = blocks;
return true;
}
stopTrackMempoolBlock(): void {
if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
@ -231,6 +243,11 @@ export class WebsocketService {
}, 2000);
}
stopTrackMempoolBlocks(): void {
this.websocketSubject.next({ 'track-mempool-blocks': [] });
this.isTrackingMempoolBlocks = false;
}
startTrackRbf(mode: 'all' | 'fullRbf') {
this.websocketSubject.next({ 'track-rbf': mode });
this.isTrackingRbf = mode;
@ -433,20 +450,25 @@ export class WebsocketService {
}
if (response['projected-block-transactions']) {
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
if (response['projected-block-transactions'].index != null) {
const update = response['projected-block-transactions'];
if (update.blockTransactions) {
this.stateService.mempoolBlockUpdate$.next({
block: this.trackingMempoolBlock,
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
block: update.index,
transactions: update.blockTransactions.map(uncompressTx),
});
} else if (response['projected-block-transactions'].delta) {
if (this.stateService.mempoolSequence && response['projected-block-transactions'].sequence !== this.stateService.mempoolSequence + 1) {
this.stateService.mempoolSequence = 0;
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
} else {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta));
} else if (update.delta) {
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta));
}
} else if (response['projected-block-transactions'].length) {
for (const update of response['projected-block-transactions']) {
if (update.blockTransactions) {
this.stateService.mempoolBlockUpdate$.next({
block: update.index,
transactions: update.blockTransactions.map(uncompressTx),
});
} else if (update.delta) {
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta));
}
}
}

View File

@ -107,6 +107,7 @@ import { OrdDataComponent } from '../components/ord-data/ord-data.component';
import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
import { EightMempoolComponent } from '../components/eight-mempool/eight-mempool.component';
import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
@ -157,6 +158,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BlockchainComponent,
BlockViewComponent,
EightBlocksComponent,
EightMempoolComponent,
MempoolBlockViewComponent,
MempoolBlocksComponent,
BlockchainBlocksComponent,
@ -220,6 +222,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BitcoinsatoshisPipe,
BlockViewComponent,
EightBlocksComponent,
EightMempoolComponent,
MempoolBlockViewComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,