multiblock mempool page

This commit is contained in:
Mononaut
2024-09-19 14:12:38 +00:00
parent 5429d6f264
commit 50eb9b602b
13 changed files with 416 additions and 23 deletions

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,