diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
index dee770cd8..b4d698bb2 100644
--- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
+++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
@@ -2,6 +2,7 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRe
import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
+import { EtaService } from '../../services/eta.service';
import { Router } from '@angular/router';
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
import { feeLevels } from '../../app.constants';
@@ -89,6 +90,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
constructor(
private router: Router,
public stateService: StateService,
+ private etaService: EtaService,
private themeService: ThemeService,
private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe,
@@ -437,34 +439,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.rightPosition = positionOfBlock + positionInBlock;
}
} else {
- let found = false;
- for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) {
- const block = this.mempoolBlocks[txInBlockIndex];
- for (let i = 0; i < block.feeRange.length - 1 && !found; i++) {
- if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) {
- const feeRangeIndex = i;
- const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
-
- const txFee = this.txFeePerVSize - block.feeRange[i];
- const max = block.feeRange[i + 1] - block.feeRange[i];
- const blockLocation = txFee / max;
-
- const chunkPositionOffset = blockLocation * feeRangeChunkSize;
- const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
-
- const blockedFilledPercentage = (block.blockVSize > this.stateService.blockVSize ? this.stateService.blockVSize : block.blockVSize) / this.stateService.blockVSize;
- const arrowRightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding)
- + ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
-
- this.rightPosition = arrowRightPosition;
- found = true;
- }
- }
- if (this.txFeePerVSize >= block.feeRange[block.feeRange.length - 1]) {
- this.rightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding);
- found = true;
- }
- }
+ const estimatedPosition = this.etaService.mempoolPositionFromFees(this.txFeePerVSize, this.mempoolBlocks);
+ this.rightPosition = estimatedPosition.block * (this.blockWidth + this.blockPadding)
+ + ((estimatedPosition.vsize / this.stateService.blockVSize) * this.blockWidth)
}
this.rightPosition = Math.min(this.maxArrowPosition, this.rightPosition);
}
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html
index a09e16d6b..f70bd3f0e 100644
--- a/frontend/src/app/components/transaction/transaction.component.html
+++ b/frontend/src/app/components/transaction/transaction.component.html
@@ -533,25 +533,28 @@
ETA |
- @if (this.mempoolPosition?.block == null) {
+
+ @if (eta.blocks >= 7) {
+
+ In several hours (or more)
+ @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') {
+ Accelerate
+ }
+
+ } @else if (network === 'liquid' || network === 'liquidtestnet') {
+
+ } @else {
+
+
+ @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') {
+ Accelerate
+ }
+
+ }
+
+
- } @else if (this.mempoolPosition.block >= 7) {
-
- In several hours (or more)
- @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') {
- Accelerate
- }
-
- } @else if (network === 'liquid' || network === 'liquidtestnet') {
-
- } @else {
-
-
- @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') {
- Accelerate
- }
-
- }
+
|
}
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index a066e3e7f..afbc6d62b 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -10,10 +10,11 @@ import {
mergeMap,
tap,
map,
- retry
+ retry,
+ startWith
} from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface';
-import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
+import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs';
import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { WebsocketService } from '../../services/websocket.service';
@@ -22,9 +23,9 @@ import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { StorageService } from '../../services/storage.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
-import { getTransactionFlags } from '../../shared/transaction.utils';
+import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils';
import { Filter, toFilters } from '../../shared/filters.utils';
-import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface';
+import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition, SinglePoolStats } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { PriceService } from '../../services/price.service';
@@ -33,6 +34,7 @@ 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';
+import { ETA, EtaService } from '../../services/eta.service';
interface Pool {
id: number;
@@ -106,6 +108,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
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;
@@ -155,6 +160,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private storageService: StorageService,
private enterpriseService: EnterpriseService,
private miningService: MiningService,
+ private etaService: EtaService,
private cd: ChangeDetectorRef,
@Inject(ZONE_SERVICE) private zoneService: any,
) {}
@@ -281,6 +287,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.rbfInfo = rbfInfo;
}
});
+ this.txChanged$.next(true);
}
});
@@ -365,7 +372,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
})
).subscribe(auditStatus => {
this.auditStatus = auditStatus;
-
this.setIsAccelerated();
});
@@ -375,7 +381,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.mempoolPosition = txPosition.position;
this.accelerationPositions = txPosition.accelerationPositions;
if (this.tx && !this.tx.status.confirmed) {
- const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
+ const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
this.stateService.markBlock$.next({
txid: txPosition.txid,
txFeePerVSize,
@@ -493,6 +499,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, 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;
@@ -519,7 +526,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
this.fetchCpfp$.next(this.tx.txid);
} else {
- const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
+ const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
if (tx.cpfpChecked) {
this.stateService.markBlock$.next({
txid: tx.txid,
@@ -566,6 +573,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
block_hash: block.id,
block_time: block.timestamp,
};
+ this.txChanged$.next(true);
this.stateService.markBlock$.next({ blockHeight: block.height });
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
this.audioService.playSound('wind-chimes-harp-ascend');
@@ -637,6 +645,27 @@ export class TransactionComponent implements OnInit, AfterViewInit, 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,
+ );
+ })
+ )
}
ngAfterViewInit(): void {
@@ -715,6 +744,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.setIsAccelerated(firstCpfp);
}
+ this.txChanged$.next(true);
this.cpfpInfo = cpfpInfo;
if (this.cpfpInfo.adjustedVsize && this.cpfpInfo.sigops != null) {
@@ -734,8 +764,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
// 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);
}
setFeatures(): void {
@@ -780,6 +812,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.firstLoad = false;
this.error = undefined;
this.tx = null;
+ this.txChanged$.next(true);
this.setFeatures();
this.waitingForTransaction = false;
this.isLoadingTx = true;
@@ -802,6 +835,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.accelerationPositions = null;
document.body.scrollTo(0, 0);
this.isAcceleration = false;
+ this.isAccelerated$.next(this.isAcceleration);
this.leaveTransaction();
}
@@ -814,20 +848,6 @@ 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);
@@ -900,7 +920,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.urlFragmentSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.mempoolPositionSubscription.unsubscribe();
- this.mempoolBlocksSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.miningSubscription?.unsubscribe();
this.auditSubscription?.unsubscribe();
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts
index d50d4ba00..c3c7c9efe 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -252,7 +252,7 @@ export interface MempoolPosition {
}
export interface AccelerationPosition extends MempoolPosition {
- pool: string;
+ poolId: number;
offset?: number;
}