From 05724b9d586f2c4dfd25caceebb688b425475b6c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 30 May 2024 21:27:10 +0000 Subject: [PATCH] Integrate multi-pool ETA into pizza tracker --- .../components/tracker/tracker.component.html | 27 +++--- .../components/tracker/tracker.component.ts | 85 ++++++++++++++++--- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 1d1399a07..571c02f96 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -54,7 +54,7 @@ }
- @if (tx && !tx.status?.confirmed && mempoolPosition?.block != null) { + @if (tx && !tx.status?.confirmed) {
First seen
@@ -68,16 +68,21 @@
ETA
- - @if (mempoolPosition?.block >= 7) { - In several hours (or more) - } @else { - - } - @if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { - Accelerate - } - + + + @if (eta.blocks >= 7) { + In several hours (or more) + } @else { + + } + @if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { + Accelerate + } + + + + +
} @else if (tx && tx.status?.confirmed) { diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 7de851c6e..62ecc9bf0 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -9,10 +9,11 @@ import { delay, mergeMap, tap, - map + map, + startWith } from 'rxjs/operators'; import { Transaction } from '../../interfaces/electrs.interface'; -import { of, merge, Subscription, Observable, Subject, throwError, combineLatest } from 'rxjs'; +import { of, merge, Subscription, Observable, Subject, throwError, combineLatest, BehaviorSubject } from 'rxjs'; import { StateService } from '../../services/state.service'; import { CacheService } from '../../services/cache.service'; import { WebsocketService } from '../../services/websocket.service'; @@ -21,12 +22,15 @@ import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; import { Filter } from '../../shared/filters.utils'; -import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration } from '../../interfaces/node-api.interface'; +import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface'; import { PriceService } from '../../services/price.service'; import { ServicesApiServices } from '../../services/services-api.service'; import { EnterpriseService } from '../../services/enterprise.service'; import { ZONE_SERVICE } from '../../injection-tokens'; import { TrackerStage } from './tracker-bar.component'; +import { MiningService, MiningStats } from '../../services/mining.service'; +import { ETA, EtaService } from '../../services/eta.service'; +import { getUnacceleratedFeeRate } from '../../shared/transaction.utils'; interface Pool { id: number; @@ -57,6 +61,7 @@ export class TrackerComponent implements OnInit, OnDestroy { txId: string; txInBlockIndex: number; mempoolPosition: MempoolPosition; + accelerationPositions: AccelerationPosition[]; isLoadingTx = true; error: any = undefined; loadingCachedTx = false; @@ -89,11 +94,15 @@ export class TrackerComponent implements OnInit, OnDestroy { isAcceleration: boolean = false; filters: Filter[] = []; showCpfpDetails = false; + miningStats: MiningStats; fetchCpfp$ = new Subject(); fetchRbfHistory$ = new Subject(); fetchCachedTx$ = new Subject(); fetchAcceleration$ = new Subject(); fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>(); + txChanged$ = new BehaviorSubject(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself) + isAccelerated$ = new BehaviorSubject(false); // refactor this to make isAccelerated an observable itself + ETA$: Observable; isCached: boolean = false; now = Date.now(); da$: Observable; @@ -122,6 +131,7 @@ export class TrackerComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private electrsApiService: ElectrsApiService, public stateService: StateService, + private etaService: EtaService, private cacheService: CacheService, private websocketService: WebsocketService, private audioService: AudioService, @@ -130,6 +140,7 @@ export class TrackerComponent implements OnInit, OnDestroy { private seoService: SeoService, private priceService: PriceService, private enterpriseService: EnterpriseService, + private miningService: MiningService, private cd: ChangeDetectorRef, private zone: NgZone, @Inject(ZONE_SERVICE) private zoneService: any, @@ -273,6 +284,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.transactionTime = tx.firstSeen || 0; this.fetchRbfHistory$.next(this.tx.txid); + this.txChanged$.next(true); } }); @@ -354,10 +366,14 @@ export class TrackerComponent implements OnInit, OnDestroy { this.now = Date.now(); if (txPosition && txPosition.txid === this.txId && txPosition.position) { this.mempoolPosition = txPosition.position; + this.accelerationPositions = txPosition.accelerationPositions; if (this.tx && !this.tx.status.confirmed) { + const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated); this.stateService.markBlock$.next({ txid: txPosition.txid, - mempoolPosition: this.mempoolPosition + txFeePerVSize, + mempoolPosition: this.mempoolPosition, + accelerationPositions: this.accelerationPositions, }); this.txInBlockIndex = this.mempoolPosition.block; @@ -372,13 +388,8 @@ export class TrackerComponent implements OnInit, OnDestroy { if (this.replaced) { this.trackerStage = 'replaced'; - } else if (txPosition.position?.block === 0) { - this.trackerStage = 'next'; - } else if (txPosition.position?.block < 3){ - this.trackerStage = 'soon'; - } else { - this.trackerStage = 'pending'; } + if (txPosition.position?.block > 0 && this.tx.weight < 4000) { this.accelerationEligible = true; if (this.acceleratorAvailable && this.paymentType === 'cashapp') { @@ -388,6 +399,7 @@ export class TrackerComponent implements OnInit, OnDestroy { } } else { this.mempoolPosition = null; + this.accelerationPositions = null; } }); @@ -453,6 +465,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.adjustedVsize = Math.max(this.tx.weight / 4, this.sigops * 5); } this.tx.feePerVsize = tx.fee / (tx.weight / 4); + this.txChanged$.next(true); this.isLoadingTx = false; this.error = undefined; this.loadingCachedTx = false; @@ -479,11 +492,13 @@ export class TrackerComponent implements OnInit, OnDestroy { }); this.fetchCpfp$.next(this.tx.txid); } else { + const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated); if (tx.cpfpChecked) { this.stateService.markBlock$.next({ txid: tx.txid, - txFeePerVSize: tx.effectiveFeePerVsize, + txFeePerVSize, mempoolPosition: this.mempoolPosition, + accelerationPositions: this.accelerationPositions, }); this.setCpfpInfo({ ancestors: tx.ancestors, @@ -522,6 +537,7 @@ export class TrackerComponent implements OnInit, OnDestroy { block_hash: block.id, block_time: block.timestamp, }; + this.txChanged$.next(true); this.trackerStage = 'confirmed'; this.stateService.markBlock$.next({ blockHeight: block.height }); if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) { @@ -580,6 +596,38 @@ export class TrackerComponent implements OnInit, OnDestroy { this.txInBlockIndex = 7; } }); + + this.ETA$ = combineLatest([ + this.stateService.mempoolTxPosition$.pipe(startWith(null)), + this.stateService.mempoolBlocks$.pipe(startWith(null)), + this.stateService.difficultyAdjustment$.pipe(startWith(null)), + this.isAccelerated$, + this.txChanged$, + ]).pipe( + map(([position, mempoolBlocks, da, isAccelerated]) => { + return this.etaService.calculateETA( + this.network, + this.tx, + mempoolBlocks, + position, + da, + this.miningStats, + isAccelerated, + this.accelerationPositions, + ); + }), + tap(eta => { + if (this.replaced) { + this.trackerStage = 'replaced' + } else if (eta?.blocks === 0) { + this.trackerStage = 'next'; + } else if (eta?.blocks < 3){ + this.trackerStage = 'soon'; + } else { + this.trackerStage = 'pending'; + } + }) + ) } handleLoadElectrsTransactionError(error: any): Observable { @@ -610,6 +658,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.hasEffectiveFeeRate = false; return; } + const firstCpfp = this.cpfpInfo == null; // merge ancestors/descendants const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])]; if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) { @@ -625,12 +674,14 @@ export class TrackerComponent implements OnInit, OnDestroy { relatives.reduce((prev, val) => prev + val.fee, 0); this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4); } else { - this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; + this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize || this.tx.effectiveFeePerVsize || this.tx.feePerVsize || (this.tx.fee / (this.tx.weight / 4)); } if (cpfpInfo.acceleration) { this.tx.acceleration = cpfpInfo.acceleration; this.tx.acceleratedBy = cpfpInfo.acceleratedBy; + this.setIsAccelerated(firstCpfp); } + this.txChanged$.next(true); this.cpfpInfo = cpfpInfo; if (this.cpfpInfo.adjustedVsize && this.cpfpInfo.sigops != null) { @@ -666,6 +717,14 @@ export class TrackerComponent implements OnInit, OnDestroy { setIsAccelerated(initialState: boolean = false) { this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))); + if (this.isAcceleration) { + // this immediately returns cached stats if we fetched them recently + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable + }); + } + this.isAccelerated$.next(this.isAcceleration); } dismissAccelAlert(): void { @@ -685,6 +744,7 @@ export class TrackerComponent implements OnInit, OnDestroy { resetTransaction() { this.error = undefined; this.tx = null; + this.txChanged$.next(true); this.waitingForTransaction = false; this.isLoadingTx = true; this.rbfTransaction = undefined; @@ -704,6 +764,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.mempoolPosition = null; this.pool = null; this.auditStatus = null; + this.accelerationPositions = null; this.accelerationEligible = false; this.trackerStage = 'waiting'; document.body.scrollTo(0, 0);