multi-pool active accelerating details component

This commit is contained in:
Mononaut 2024-05-26 20:38:28 +00:00
parent 46b5b26347
commit 05b022dec8
No known key found for this signature in database
GPG key ID: A3F058E41374C04E
11 changed files with 244 additions and 7 deletions

View file

@ -0,0 +1,35 @@
<table>
<tbody>
<tr>
<td class="td-width" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
<td>
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
</div>
</td>
<td rowspan="2" *ngIf="tx && (tx.acceleratedBy || accelerationInfo) && miningStats" class="text-right" style="width: 100%;">
<div class="chart-container" style="width: 100px; margin-left:auto;">
<div
echarts
*browserOnly
class="chart"
[initOpts]="chartInitOptions"
[options]="chartOptions"
style="height: 72px"
(chartInit)="onChartInit($event)"
></div>
</div>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td *ngIf="acceleratedByPercentage">
{{ acceleratedByPercentage }} <span class="symbol">of hashrate</span>
</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,9 @@
.td-width {
width: 150px;
min-width: 150px;
@media (max-width: 768px) {
width: 175px;
min-width: 175px;
}
}

View file

@ -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;
}
}

View file

@ -347,6 +347,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
if (txPosition.position?.accelerated) { if (txPosition.position?.accelerated) {
this.tx.acceleration = true; this.tx.acceleration = true;
this.tx.acceleratedBy = txPosition.position?.acceleratedBy;
} }
if (txPosition.position?.block === 0) { if (txPosition.position?.block === 0) {
@ -602,6 +603,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
} }
if (cpfpInfo.acceleration) { if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration; this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
} }
this.cpfpInfo = cpfpInfo; this.cpfpInfo = cpfpInfo;

View file

@ -419,7 +419,11 @@
<ng-template #detailsRight> <ng-template #detailsRight>
<ng-container *ngTemplateOutlet="feeRow"></ng-container> <ng-container *ngTemplateOutlet="feeRow"></ng-container>
<ng-container *ngTemplateOutlet="feeRateRow"></ng-container> <ng-container *ngTemplateOutlet="feeRateRow"></ng-container>
<ng-container *ngTemplateOutlet="effectiveRateRow"></ng-container> @if (!isLoadingTx && !tx?.status?.confirmed && ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo)) {
<ng-container *ngTemplateOutlet="acceleratingRow"></ng-container>
} @else {
<ng-container *ngTemplateOutlet="effectiveRateRow"></ng-container>
}
@if (tx?.status?.confirmed) { @if (tx?.status?.confirmed) {
<ng-container *ngTemplateOutlet="minerRow"></ng-container> <ng-container *ngTemplateOutlet="minerRow"></ng-container>
} }
@ -638,6 +642,15 @@
} }
</ng-template> </ng-template>
<ng-template #acceleratingRow>
<tr>
<td rowspan="2" colspan="2" style="padding: 0;">
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats"></app-active-acceleration-box>
</td>
</tr>
<tr></tr>
</ng-template>
<ng-template #minerRow> <ng-template #minerRow>
@if (network === '') { @if (network === '') {
@if (!isLoadingTx) { @if (!isLoadingTx) {

View file

@ -32,6 +32,7 @@ import { isFeatureActive } from '../../bitcoin.utils';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '../../services/services-api.service';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '../../services/enterprise.service';
import { ZONE_SERVICE } from '../../injection-tokens'; import { ZONE_SERVICE } from '../../injection-tokens';
import { MiningService, MiningStats } from '../../services/mining.service';
interface Pool { interface Pool {
id: number; id: number;
@ -98,6 +99,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
isAcceleration: boolean = false; isAcceleration: boolean = false;
filters: Filter[] = []; filters: Filter[] = [];
showCpfpDetails = false; showCpfpDetails = false;
miningStats: MiningStats;
fetchCpfp$ = new Subject<string>(); fetchCpfp$ = new Subject<string>();
fetchRbfHistory$ = new Subject<string>(); fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>(); fetchCachedTx$ = new Subject<string>();
@ -151,6 +153,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private priceService: PriceService, private priceService: PriceService,
private storageService: StorageService, private storageService: StorageService,
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
private miningService: MiningService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@Inject(ZONE_SERVICE) private zoneService: any, @Inject(ZONE_SERVICE) private zoneService: any,
) {} ) {}
@ -696,6 +699,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
if (cpfpInfo.acceleration) { if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration; this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.setIsAccelerated(firstCpfp); this.setIsAccelerated(firstCpfp);
} }
@ -713,6 +717,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.isAcceleration && initialState) { if (this.isAcceleration && initialState) {
this.showAccelerationSummary = false; 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 { setFeatures(): void {
@ -790,6 +800,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); 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() { setupGraph() {
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); 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); this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);

View file

@ -4,6 +4,7 @@ import { Routes, RouterModule } from '@angular/router';
import { TransactionComponent } from './transaction.component'; import { TransactionComponent } from './transaction.component';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
import { GraphsModule } from '../../graphs/graphs.module';
const routes: Routes = [ const routes: Routes = [
{ {
@ -30,6 +31,7 @@ export class TransactionRoutingModule { }
CommonModule, CommonModule,
TransactionRoutingModule, TransactionRoutingModule,
SharedModule, SharedModule,
GraphsModule,
TxBowtieModule, TxBowtieModule,
], ],
declarations: [ declarations: [

View file

@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
import { AddressComponent } from '../components/address/address.component'; import { AddressComponent } from '../components/address/address.component';
import { AddressGraphComponent } from '../components/address-graph/address-graph.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'; import { CommonModule } from '@angular/common';
@NgModule({ @NgModule({
@ -75,6 +76,7 @@ import { CommonModule } from '@angular/common';
HashrateChartPoolsComponent, HashrateChartPoolsComponent,
BlockHealthGraphComponent, BlockHealthGraphComponent,
AddressGraphComponent, AddressGraphComponent,
ActiveAccelerationBox,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -86,6 +88,7 @@ import { CommonModule } from '@angular/common';
], ],
exports: [ exports: [
NgxEchartsModule, NgxEchartsModule,
ActiveAccelerationBox,
] ]
}) })
export class GraphsModule { } export class GraphsModule { }

View file

@ -20,6 +20,7 @@ export interface Transaction {
bestDescendant?: BestDescendant | null; bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean; cpfpChecked?: boolean;
acceleration?: boolean; acceleration?: boolean;
acceleratedBy?: number[];
deleteAfter?: number; deleteAfter?: number;
_unblinded?: any; _unblinded?: any;
_deduced?: boolean; _deduced?: boolean;

View file

@ -29,6 +29,7 @@ export interface CpfpInfo {
sigops?: number; sigops?: number;
adjustedVsize?: number; adjustedVsize?: number;
acceleration?: boolean; acceleration?: boolean;
acceleratedBy?: number[];
} }
export interface RbfInfo { export interface RbfInfo {
@ -132,6 +133,7 @@ export interface ITranslators { [language: string]: string; }
*/ */
export interface SinglePoolStats { export interface SinglePoolStats {
poolId: number; poolId: number;
poolUniqueId: number; // unique global pool id
name: string; name: string;
link: string; link: string;
blockCount: number; blockCount: number;
@ -245,7 +247,8 @@ export interface RbfTransaction extends TransactionStripped {
export interface MempoolPosition { export interface MempoolPosition {
block: number, block: number,
vsize: number, vsize: number,
accelerated?: boolean accelerated?: boolean,
acceleratedBy?: number[],
} }
export interface RewardStats { export interface RewardStats {

View file

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface'; import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface';
import { ApiService } from '../services/api.service'; import { ApiService } from '../services/api.service';
import { StateService } from './state.service'; import { StateService } from './state.service';
@ -25,6 +25,12 @@ export interface MiningStats {
providedIn: 'root' providedIn: 'root'
}) })
export class MiningService { export class MiningService {
cache: {
[interval: string]: {
lastUpdated: number;
data: MiningStats;
}
} = {};
constructor( constructor(
private stateService: StateService, private stateService: StateService,
@ -36,9 +42,20 @@ export class MiningService {
* Generate pool ranking stats * Generate pool ranking stats
*/ */
public getMiningStats(interval: string): Observable<MiningStats> { public getMiningStats(interval: string): Observable<MiningStats> {
return this.apiService.listPools$(interval).pipe( // returned cached data fetched within the last 5 minutes
map(response => this.generateMiningStats(response)) 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,
};
})
);
}
} }
/** /**