diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 224e31744..57d1a393f 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -143,7 +143,7 @@ class MempoolBlocks { const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0); if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) { onlineStats = true; - feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5); + feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); feeStatsCalculator.processNext(tx); } } @@ -334,7 +334,7 @@ class MempoolBlocks { if (hasBlockStack) { stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0); hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS; - feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5); + feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); } const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees, feeStats }[] = []; diff --git a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts index 8c90036fd..823d271a1 100644 --- a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts +++ b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts @@ -1,5 +1,9 @@ import { OnChanges } from '@angular/core'; import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { TransactionStripped } from '../../interfaces/websocket.interface'; +import { StateService } from '../../services/state.service'; +import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe'; +import { selectPowerOfTen } from '../../bitcoin.utils'; @Component({ selector: 'app-fee-distribution-graph', @@ -7,47 +11,121 @@ import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeeDistributionGraphComponent implements OnInit, OnChanges { - @Input() data: any; + @Input() feeRange: number[]; + @Input() vsize: number; + @Input() transactions: TransactionStripped[]; @Input() height: number | string = 210; @Input() top: number | string = 20; @Input() right: number | string = 22; @Input() left: number | string = 30; + @Input() numSamples: number = 200; + @Input() numLabels: number = 10; + + simple: boolean = false; + data: number[][]; + labelInterval: number = 50; mempoolVsizeFeesOptions: any; mempoolVsizeFeesInitOptions = { renderer: 'svg' }; - constructor() { } + constructor( + private stateService: StateService, + private vbytesPipe: VbytesPipe, + ) { } - ngOnInit() { + ngOnInit(): void { this.mountChart(); } - ngOnChanges() { + ngOnChanges(): void { + this.simple = !!this.feeRange?.length; + this.prepareChart(); this.mountChart(); } - mountChart() { + prepareChart(): void { + if (this.simple) { + this.data = this.feeRange.map((rate, index) => [index * 10, rate]); + this.labelInterval = 1; + return; + } + this.data = []; + if (!this.transactions?.length) { + return; + } + const samples = []; + const txs = this.transactions.map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; }); + const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4; + const sampleInterval = maxBlockVSize / this.numSamples; + let cumVSize = 0; + let sampleIndex = 0; + let nextSample = 0; + let txIndex = 0; + this.labelInterval = this.numSamples / this.numLabels; + while (nextSample <= maxBlockVSize) { + if (txIndex >= txs.length) { + samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0]); + nextSample += sampleInterval; + sampleIndex++; + continue; + } + + while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) { + samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate]); + nextSample += sampleInterval; + sampleIndex++; + } + cumVSize += txs[txIndex].vsize; + txIndex++; + } + this.data = samples.reverse(); + } + + mountChart(): void { this.mempoolVsizeFeesOptions = { grid: { height: '210', right: '20', top: '22', - left: '30', + left: '40', }, xAxis: { type: 'category', boundaryGap: false, + name: '% Weight', + nameLocation: 'middle', + nameGap: 0, + nameTextStyle: { + verticalAlign: 'top', + padding: [30, 0, 0, 0], + }, + axisLabel: { + interval: (index: number): boolean => { return index && (index % this.labelInterval === 0); }, + formatter: (value: number): string => { return Number(value).toFixed(0); }, + }, + axisTick: { + interval: (index:number): boolean => { return (index % this.labelInterval === 0); }, + }, }, yAxis: { type: 'value', + // name: 'Effective Fee Rate s/vb', + // nameLocation: 'middle', splitLine: { lineStyle: { type: 'dotted', color: '#ffffff66', opacity: 0.25, } + }, + axisLabel: { + formatter: (value: number): string => { + const selectedPowerOfTen = selectPowerOfTen(value); + const newVal = Math.round(value / selectedPowerOfTen.divider); + return `${newVal}${selectedPowerOfTen.unit}`; + }, } }, series: [{ @@ -58,14 +136,18 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges { position: 'top', color: '#ffffff', textShadowBlur: 0, - formatter: (label: any) => { - return Math.floor(label.data); + formatter: (label: { data: number[] }): string => { + const value = label.data[1]; + const selectedPowerOfTen = selectPowerOfTen(value); + const newVal = Math.round(value / selectedPowerOfTen.divider); + return `${newVal}${selectedPowerOfTen.unit}`; }, }, + showAllSymbol: false, smooth: true, lineStyle: { color: '#D81B60', - width: 4, + width: 1, }, itemStyle: { color: '#b71c1c', diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.html b/frontend/src/app/components/mempool-block/mempool-block.component.html index 3626e6ff5..7d5b18ccb 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.html +++ b/frontend/src/app/components/mempool-block/mempool-block.component.html @@ -39,11 +39,11 @@ - +
- +
diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index b9bdc55bb..6e0b21196 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -17,6 +17,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { network$: Observable; mempoolBlockIndex: number; mempoolBlock$: Observable; + mempoolBlockTransactions$: Observable; ordinal$: BehaviorSubject = new BehaviorSubject(''); previewTx: TransactionStripped | void; webGlEnabled: boolean; @@ -53,6 +54,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]); this.ordinal$.next(ordinal); this.seoService.setTitle(ordinal); + mempoolBlocks[this.mempoolBlockIndex].isStack = mempoolBlocks[this.mempoolBlockIndex].blockVSize > this.stateService.blockVSize; return mempoolBlocks[this.mempoolBlockIndex]; }) ); @@ -62,6 +64,8 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { }) ); + this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap))); + this.network$ = this.stateService.networkChanged$; } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 31f5f3aab..fb3b37e05 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,11 +1,11 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; -import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; +import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { Transaction } from '../interfaces/electrs.interface'; import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; -import { map, shareReplay } from 'rxjs/operators'; +import { map, scan, shareReplay, tap } from 'rxjs/operators'; import { StorageService } from './storage.service'; interface MarkBlockState { @@ -100,6 +100,7 @@ export class StateService { mempoolBlocks$ = new ReplaySubject(1); mempoolBlockTransactions$ = new Subject(); mempoolBlockDelta$ = new Subject(); + liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; txReplaced$ = new Subject(); txRbfInfo$ = new Subject(); rbfLatest$ = new Subject(); @@ -166,6 +167,30 @@ export class StateService { this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT); + this.liveMempoolBlockTransactions$ = merge( + this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })), + this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })), + ).pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: any): { [txid: string]: TransactionStripped } => { + if (change.transactions) { + const txMap = {} + change.transactions.forEach(tx => { + txMap[tx.txid] = tx; + }) + return txMap; + } else { + change.delta.changed.forEach(tx => { + transactions[tx.txid].rate = tx.rate; + }) + change.delta.removed.forEach(txid => { + delete transactions[txid]; + }); + change.delta.added.forEach(tx => { + transactions[tx.txid] = tx; + }); + return transactions; + } + }, {})); + if (this.env.BASE_MODULE === 'bisq') { this.network = this.env.BASE_MODULE; this.networkChanged$.next(this.env.BASE_MODULE); diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index b38a60898..aea8e8d6e 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -500,7 +500,7 @@ html:lang(ru) .card-title { } .fee-distribution-chart { - height: 250px; + height: 265px; } .fees-wrapper-tooltip-chart {