From 05b022dec8b514697cc5a4fda994ed0491a2895c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 26 May 2024 20:38:28 +0000 Subject: [PATCH 1/2] multi-pool active accelerating details component --- .../active-acceleration-box.component.html | 35 +++++ .../active-acceleration-box.component.scss | 9 ++ .../active-acceleration-box.component.ts | 128 ++++++++++++++++++ .../components/tracker/tracker.component.ts | 2 + .../transaction/transaction.component.html | 15 +- .../transaction/transaction.component.ts | 24 ++++ .../transaction/transaction.module.ts | 2 + frontend/src/app/graphs/graphs.module.ts | 3 + .../src/app/interfaces/electrs.interface.ts | 1 + .../src/app/interfaces/node-api.interface.ts | 5 +- frontend/src/app/services/mining.service.ts | 27 +++- 11 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html create mode 100644 frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss create mode 100644 frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html new file mode 100644 index 000000000..75e821e9f --- /dev/null +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + +
Accelerated to +
+ @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { + + } @else { + + } +
+
+
+
+
+
Accelerated by + {{ acceleratedByPercentage }} of hashrate +
\ No newline at end of file diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss new file mode 100644 index 000000000..6dba0b06f --- /dev/null +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss @@ -0,0 +1,9 @@ +.td-width { + width: 150px; + min-width: 150px; + + @media (max-width: 768px) { + width: 175px; + min-width: 175px; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts new file mode 100644 index 000000000..b6719f906 --- /dev/null +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts @@ -0,0 +1,128 @@ +import { Component, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Transaction } from '../../../interfaces/electrs.interface'; +import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface'; +import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts'; +import { MiningStats } from '../../../services/mining.service'; + + +@Component({ + selector: 'app-active-acceleration-box', + templateUrl: './active-acceleration-box.component.html', + styleUrls: ['./active-acceleration-box.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActiveAccelerationBox implements OnChanges { + @Input() tx: Transaction; + @Input() accelerationInfo: Acceleration; + @Input() miningStats: MiningStats; + + acceleratedByPercentage: string = ''; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + timespan = ''; + chartInstance: any = undefined; + + constructor() {} + + ngOnChanges(changes: SimpleChanges): void { + if (this.tx && (this.tx.acceleratedBy || this.accelerationInfo) && this.miningStats) { + this.prepareChartOptions(); + } + } + + getChartData() { + const data: object[] = []; + const pools: { [id: number]: SinglePoolStats } = {}; + for (const pool of this.miningStats.pools) { + pools[pool.poolUniqueId] = pool; + } + + const getDataItem = (value, color, tooltip) => ({ + value, + itemStyle: { + color, + borderColor: 'rgba(0,0,0,0)', + borderWidth: 1, + }, + avoidLabelOverlap: false, + label: { + show: false, + }, + labelLine: { + show: false + }, + emphasis: { + disabled: true, + }, + tooltip: { + show: true, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: 'var(--tooltip-grey)', + }, + borderColor: '#000', + formatter: () => { + return tooltip; + } + } + }); + + let totalAcceleratedHashrate = 0; + for (const poolId of (this.accelerationInfo?.pools || this.tx.acceleratedBy || [])) { + const pool = pools[poolId]; + if (!pool) { + continue; + } + totalAcceleratedHashrate += parseFloat(pool.lastEstimatedHashrate); + } + this.acceleratedByPercentage = ((totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%'; + data.push(getDataItem( + totalAcceleratedHashrate, + 'var(--tertiary)', + `${this.acceleratedByPercentage} accelerating`, + ) as PieSeriesOption); + const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate))) * 100).toFixed(1) + '%'; + data.push(getDataItem( + (parseFloat(this.miningStats.lastEstimatedHashrate) - totalAcceleratedHashrate), + 'rgba(127, 127, 127, 0.3)', + `${notAcceleratedByPercentage} not accelerating`, + ) as PieSeriesOption); + + return data; + } + + prepareChartOptions() { + this.chartOptions = { + animation: false, + grid: { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + tooltip: { + show: true, + trigger: 'item', + }, + series: [ + { + type: 'pie', + radius: '100%', + data: this.getChartData(), + } + ] + }; + } + + onChartInit(ec) { + if (this.chartInstance !== undefined) { + return; + } + this.chartInstance = ec; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 291e2dafd..6000e2f10 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -347,6 +347,7 @@ export class TrackerComponent implements OnInit, OnDestroy { if (txPosition.position?.accelerated) { this.tx.acceleration = true; + this.tx.acceleratedBy = txPosition.position?.acceleratedBy; } if (txPosition.position?.block === 0) { @@ -602,6 +603,7 @@ export class TrackerComponent implements OnInit, OnDestroy { } if (cpfpInfo.acceleration) { this.tx.acceleration = cpfpInfo.acceleration; + this.tx.acceleratedBy = cpfpInfo.acceleratedBy; } this.cpfpInfo = cpfpInfo; diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index bc576a128..2add2217a 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -419,7 +419,11 @@ - + @if (!isLoadingTx && !tx?.status?.confirmed && ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo)) { + + } @else { + + } @if (tx?.status?.confirmed) { } @@ -638,6 +642,15 @@ } + + + + + + + + + @if (network === '') { @if (!isLoadingTx) { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index b1b553294..a76720752 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -32,6 +32,7 @@ import { isFeatureActive } from '../../bitcoin.utils'; import { ServicesApiServices } from '../../services/services-api.service'; import { EnterpriseService } from '../../services/enterprise.service'; import { ZONE_SERVICE } from '../../injection-tokens'; +import { MiningService, MiningStats } from '../../services/mining.service'; interface Pool { id: number; @@ -98,6 +99,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { isAcceleration: boolean = false; filters: Filter[] = []; showCpfpDetails = false; + miningStats: MiningStats; fetchCpfp$ = new Subject(); fetchRbfHistory$ = new Subject(); fetchCachedTx$ = new Subject(); @@ -151,6 +153,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { private priceService: PriceService, private storageService: StorageService, private enterpriseService: EnterpriseService, + private miningService: MiningService, private cd: ChangeDetectorRef, @Inject(ZONE_SERVICE) private zoneService: any, ) {} @@ -696,6 +699,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } if (cpfpInfo.acceleration) { this.tx.acceleration = cpfpInfo.acceleration; + this.tx.acceleratedBy = cpfpInfo.acceleratedBy; this.setIsAccelerated(firstCpfp); } @@ -713,6 +717,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { if (this.isAcceleration && initialState) { this.showAccelerationSummary = false; } + if (this.isAcceleration) { + // this immediately returns cached stats if we fetched them recently + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + }); + } } setFeatures(): void { @@ -790,6 +800,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); } + getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number { + if (accelerated) { + let ancestorVsize = tx.weight / 4; + let ancestorFee = tx.fee; + for (const ancestor of tx.ancestors || []) { + ancestorVsize += (ancestor.weight / 4); + ancestorFee += ancestor.fee; + } + return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize)); + } else { + return tx.effectiveFeePerVsize; + } + } + setupGraph() { this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80); diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index d933cc350..a1331a463 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -4,6 +4,7 @@ import { Routes, RouterModule } from '@angular/router'; import { TransactionComponent } from './transaction.component'; import { SharedModule } from '../../shared/shared.module'; import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; +import { GraphsModule } from '../../graphs/graphs.module'; const routes: Routes = [ { @@ -30,6 +31,7 @@ export class TransactionRoutingModule { } CommonModule, TransactionRoutingModule, SharedModule, + GraphsModule, TxBowtieModule, ], declarations: [ diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index 714ff2fd1..de048fd2d 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; import { AddressComponent } from '../components/address/address.component'; import { AddressGraphComponent } from '../components/address-graph/address-graph.component'; +import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component'; import { CommonModule } from '@angular/common'; @NgModule({ @@ -75,6 +76,7 @@ import { CommonModule } from '@angular/common'; HashrateChartPoolsComponent, BlockHealthGraphComponent, AddressGraphComponent, + ActiveAccelerationBox, ], imports: [ CommonModule, @@ -86,6 +88,7 @@ import { CommonModule } from '@angular/common'; ], exports: [ NgxEchartsModule, + ActiveAccelerationBox, ] }) export class GraphsModule { } diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index de2cbeeaf..ab96488fe 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -20,6 +20,7 @@ export interface Transaction { bestDescendant?: BestDescendant | null; cpfpChecked?: boolean; acceleration?: boolean; + acceleratedBy?: number[]; deleteAfter?: number; _unblinded?: any; _deduced?: boolean; diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index ed7f5d9d4..a595d855c 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -29,6 +29,7 @@ export interface CpfpInfo { sigops?: number; adjustedVsize?: number; acceleration?: boolean; + acceleratedBy?: number[]; } export interface RbfInfo { @@ -132,6 +133,7 @@ export interface ITranslators { [language: string]: string; } */ export interface SinglePoolStats { poolId: number; + poolUniqueId: number; // unique global pool id name: string; link: string; blockCount: number; @@ -245,7 +247,8 @@ export interface RbfTransaction extends TransactionStripped { export interface MempoolPosition { block: number, vsize: number, - accelerated?: boolean + accelerated?: boolean, + acceleratedBy?: number[], } export interface RewardStats { diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts index 952d13a78..45d2e4ac8 100644 --- a/frontend/src/app/services/mining.service.ts +++ b/frontend/src/app/services/mining.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface'; import { ApiService } from '../services/api.service'; import { StateService } from './state.service'; @@ -25,6 +25,12 @@ export interface MiningStats { providedIn: 'root' }) export class MiningService { + cache: { + [interval: string]: { + lastUpdated: number; + data: MiningStats; + } + } = {}; constructor( private stateService: StateService, @@ -36,9 +42,20 @@ export class MiningService { * Generate pool ranking stats */ public getMiningStats(interval: string): Observable { - return this.apiService.listPools$(interval).pipe( - map(response => this.generateMiningStats(response)) - ); + // returned cached data fetched within the last 5 minutes + if (this.cache[interval] && this.cache[interval].lastUpdated > (Date.now() - (5 * 60000))) { + return of(this.cache[interval].data); + } else { + return this.apiService.listPools$(interval).pipe( + map(response => this.generateMiningStats(response)), + tap(stats => { + this.cache[interval] = { + lastUpdated: Date.now(), + data: stats, + }; + }) + ); + } } /** From 1498db3b33fa325892018eba8eea034c03f11964 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 26 May 2024 20:40:50 +0000 Subject: [PATCH 2/2] Backend support for multi-pool acceleration details --- backend/src/api/bitcoin/bitcoin.routes.ts | 3 ++- backend/src/api/mempool-blocks.ts | 11 +++++++---- backend/src/api/websocket-handler.ts | 4 ++++ backend/src/mempool.interfaces.ts | 4 +++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index ec63f4ff8..fd04707b5 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -160,7 +160,8 @@ class BitcoinRoutes { effectiveFeePerVsize: tx.effectiveFeePerVsize || null, sigops: tx.sigops, adjustedVsize: tx.adjustedVsize, - acceleration: tx.acceleration + acceleration: tx.acceleration, + acceleratedBy: tx.acceleratedBy || undefined, }); return; } diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index bded93846..670ad1933 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -6,6 +6,7 @@ import config from '../config'; import { Worker } from 'worker_threads'; import path from 'path'; import mempool from './mempool'; +import { Acceleration } from './services/acceleration'; const MAX_UINT32 = Math.pow(2, 32) - 1; @@ -333,7 +334,7 @@ class MempoolBlocks { } } - private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] { + private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations: { [txid: string]: Acceleration }, accelerationPool, saveResults): MempoolBlockWithTransactions[] { for (const txid of Object.keys(candidates?.txs ?? mempool)) { if (txid in mempool) { mempool[txid].cpfpDirty = false; @@ -396,7 +397,7 @@ class MempoolBlocks { } } - const isAccelerated : { [txid: string]: boolean } = {}; + const isAcceleratedBy : { [txid: string]: number[] | false } = {}; const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; // update this thread's mempool with the results @@ -427,17 +428,19 @@ class MempoolBlocks { }; const acceleration = accelerations[txid]; - if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { + if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { if (!mempoolTx.acceleration) { mempoolTx.cpfpDirty = true; } mempoolTx.acceleration = true; + mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; for (const ancestor of mempoolTx.ancestors || []) { if (!mempool[ancestor.txid].acceleration) { mempool[ancestor.txid].cpfpDirty = true; } mempool[ancestor.txid].acceleration = true; - isAccelerated[ancestor.txid] = true; + mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; + isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; } } else { if (mempoolTx.acceleration) { diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index f92e6cdfe..57c5401fd 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -820,6 +820,7 @@ class WebsocketHandler { position: { ...mempoolTx.position, accelerated: mempoolTx.acceleration || undefined, + acceleratedBy: mempoolTx.acceleratedBy || undefined, } }; if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) { @@ -858,6 +859,7 @@ class WebsocketHandler { txInfo.position = { ...mempoolTx.position, accelerated: mempoolTx.acceleration || undefined, + acceleratedBy: mempoolTx.acceleratedBy || undefined, }; if (!mempoolTx.cpfpChecked) { calculateCpfp(mempoolTx, newMempool); @@ -1134,6 +1136,7 @@ class WebsocketHandler { position: { ...mempoolTx.position, accelerated: mempoolTx.acceleration || undefined, + acceleratedBy: mempoolTx.acceleratedBy || undefined, } }); } @@ -1153,6 +1156,7 @@ class WebsocketHandler { ...mempoolTx.position, }, accelerated: mempoolTx.acceleration || undefined, + acceleratedBy: mempoolTx.acceleratedBy || undefined, }; } } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 884ae5c1b..a2951b4d6 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -111,6 +111,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction { vsize: number, }; acceleration?: boolean; + acceleratedBy?: number[]; replacement?: boolean; uid?: number; flags?: number; @@ -432,7 +433,7 @@ export interface OptimizedStatistic { export interface TxTrackingInfo { replacedBy?: string, - position?: { block: number, vsize: number, accelerated?: boolean }, + position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[] }, cpfp?: { ancestors?: Ancestor[], bestDescendant?: Ancestor | null, @@ -443,6 +444,7 @@ export interface TxTrackingInfo { }, utxoSpent?: { [vout: number]: { vin: number, txid: string } }, accelerated?: boolean, + acceleratedBy?: number[], confirmed?: boolean }