mirror of
https://github.com/mempool/mempool.git
synced 2025-02-25 07:07:36 +01:00
Show unaccelerated ETA in acceleration timeline
This commit is contained in:
parent
a0992f6091
commit
bf51e3e1c9
6 changed files with 357 additions and 79 deletions
|
@ -1,3 +1,151 @@
|
|||
@if (tx.status.confirmed) {
|
||||
<div class="acceleration-timeline box">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<a class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="acc-to-confirmed right"></div>
|
||||
<a class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="acc-to-confirmed"></div>
|
||||
</div>
|
||||
<div class="node mined" [id]="'confirmed'" >
|
||||
<div class="acc-to-confirmed left" ></div>
|
||||
<a class="shape-border mined-selected">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
<div class="time">
|
||||
<app-time kind="since" [time]="tx.status.block_time"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (acceleratedETA) { <!-- Not yet accelerated; to be shown only in acceleration checkout -->
|
||||
<div class="acceleration-timeline">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="now - transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
~<app-time [time]="acceleratedETA / 1000 - now"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<a class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="acc-to-confirmed right"></div>
|
||||
<a class="shape-border waiting">
|
||||
<div class="shape accelerating"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
<div class="time">
|
||||
<span i18n="date.now">Now</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="acc-to-confirmed"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed left"></div>
|
||||
<div class="corner-up"></div>
|
||||
<a class="shape-border waiting">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
~<app-time [time]="eta.time / 1000 - now"></app-time> <span *ngIf="accelerateRatio > 1" style="font-style: italic; color: var(--transparent-fg);"> ({{ accelerateRatio }}x slower)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node-spacer">
|
||||
<div class="connector"><div class="corner-down"></div></div>
|
||||
<div class="seen-to-acc right"></div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (standardETA) { <!-- Accelerated, to be mined -->
|
||||
<div class="acceleration-timeline box">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline">
|
||||
|
@ -13,66 +161,76 @@
|
|||
<div class="interval-time">
|
||||
@if (eta) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time>
|
||||
} @else if (tx.status.block_time) {
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right" [class.loading]="!isAcceleration && !tx.status.confirmed"></div>
|
||||
<a class="shape-border" [class.sent-selected]="!tx.status.confirmed && !isAcceleration">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<a class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-primary" i18n="accelerator.sent-state">Sent</span></div>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc" [class.loading]="!isAcceleration && !tx.status.confirmed"></div>
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left" [class.loading]="!isAcceleration && !tx.status.confirmed"></div>
|
||||
<div class="acc-to-confirmed right" [class.loading]="isAcceleration && !tx.status.confirmed"></div>
|
||||
<a class="shape-border" [class.accelerated-selected]="isAcceleration && !tx.status.confirmed" [class.waiting]="!isAcceleration && !tx.status.confirmed">
|
||||
<div class="shape"></div>
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="acc-to-confirmed right loading"></div>
|
||||
<a class="shape-border accelerated-selected">
|
||||
<div class="shape accelerating"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge" [class]="tx.status.confirmed || isAcceleration ? 'badge-accelerated' : 'badge-waiting'" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
<div class="time">
|
||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
<div class="time sm-margin">
|
||||
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="acc-to-confirmed" [class.loading]="isAcceleration && !tx.status.confirmed"></div>
|
||||
<div class="acc-to-confirmed loading"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'" [class.mined]="tx.status.confirmed">
|
||||
<div class="acc-to-confirmed left" [class.loading]="isAcceleration && !tx.status.confirmed"></div>
|
||||
<a class="shape-border" [class.mined-selected]="tx.status.confirmed" [class.waiting]="!tx.status.confirmed">
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed left loading"></div>
|
||||
<div class="corner-up"></div>
|
||||
<a class="shape-border waiting">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge" [class]="tx.status.confirmed ? 'badge-success' : 'badge-waiting'" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
<div class="time">
|
||||
@if (tx.status.block_time) {
|
||||
<app-time kind="since" [time]="tx.status.block_time"></app-time>
|
||||
} @else if (eta) {
|
||||
<app-time kind="until" [time]="eta?.time"></app-time>
|
||||
}
|
||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #nodeSpacer>
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #intervalSpacer>
|
||||
<div class="interval-spacer"></div>
|
||||
</ng-template>
|
||||
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
~<app-time [time]="standardETA / 1000 - now"></app-time> <span *ngIf="accelerateRatio > 1" style="font-style: italic; color: var(--transparent-fg);"> ({{ accelerateRatio }}x slower)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node-spacer">
|
||||
<div class="connector"><div class="corner-down"></div></div>
|
||||
<div class="seen-to-acc right"></div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -84,10 +84,6 @@
|
|||
background: var(--primary);
|
||||
border-radius: 5px;
|
||||
|
||||
&.loading {
|
||||
animation: standardPulse 1s infinite;
|
||||
}
|
||||
|
||||
&.left {
|
||||
right: 50%;
|
||||
}
|
||||
|
@ -119,6 +115,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
.corner-up {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
left: 48.5%;
|
||||
height: 86px;
|
||||
border-left: solid 10px var(--primary);
|
||||
border-bottom: solid 10px var(--primary);
|
||||
border-bottom-right-radius: 10px;
|
||||
// horrible css:
|
||||
@media (max-width: 1030px) {
|
||||
left: 48%;
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
left: 47%;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
left: 46%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.nodes {
|
||||
|
@ -142,6 +158,9 @@
|
|||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
&.accelerating {
|
||||
animation: acceleratePulse 1s infinite;
|
||||
}
|
||||
transition: background-color 300ms, border 300ms;
|
||||
}
|
||||
|
||||
|
@ -151,12 +170,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.sent-selected {
|
||||
.shape {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.accelerated-selected {
|
||||
.shape {
|
||||
background: var(--tertiary);
|
||||
|
@ -190,6 +203,30 @@
|
|||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
&.sm-margin {
|
||||
@media (max-width: 650px) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connector {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
|
||||
.corner-down {
|
||||
position: absolute;
|
||||
@media (max-width: 650px) {
|
||||
width: 223px;
|
||||
}
|
||||
width: 290px;
|
||||
height: 90px;
|
||||
bottom: 50%;
|
||||
border-left: solid 10px var(--primary);
|
||||
border-bottom: solid 10px var(--primary);
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -201,9 +238,8 @@
|
|||
100% { background-color: var(--tertiary) }
|
||||
}
|
||||
|
||||
@keyframes standardPulse {
|
||||
0% { background-color: var(--primary) }
|
||||
50% { background-color: var(--secondary) }
|
||||
100% { background-color: var(--primary) }
|
||||
|
||||
@keyframes textPulse {
|
||||
0% { color: var(--tertiary) }
|
||||
50% { color: var(--mainnet-alt) }
|
||||
100% { color: var(--tertiary) }
|
||||
}
|
|
@ -11,9 +11,14 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||
@Input() transactionTime: number;
|
||||
@Input() tx: Transaction;
|
||||
@Input() eta: ETA;
|
||||
@Input() isAcceleration: boolean;
|
||||
// A mined transaction has standard ETA and accelerated ETA undefined
|
||||
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
|
||||
@Input() standardETA: number;
|
||||
@Input() acceleratedETA: number;
|
||||
|
||||
acceleratedAt: number;
|
||||
now: number;
|
||||
accelerateRatio: number;
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
@ -22,6 +27,15 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
|
||||
if (changes?.eta?.currentValue) {
|
||||
if (changes?.acceleratedETA?.currentValue) {
|
||||
this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
|
||||
} else if (changes?.standardETA?.currentValue) {
|
||||
this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -152,15 +152,6 @@
|
|||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="transactionTime && isAcceleration">
|
||||
<div class="title float-left">
|
||||
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [isAcceleration]="isAcceleration"></app-acceleration-timeline>
|
||||
<br>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="rbfInfo">
|
||||
<div class="title float-left">
|
||||
<h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2>
|
||||
|
@ -170,6 +161,15 @@
|
|||
<br>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="transactionTime && isAcceleration">
|
||||
<div class="title float-left">
|
||||
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
|
||||
<br>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
|
||||
<div class="title float-left">
|
||||
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
|
|
|
@ -112,6 +112,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself)
|
||||
isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself
|
||||
ETA$: Observable<ETA | null>;
|
||||
standardETA$: Observable<ETA | null>;
|
||||
isCached: boolean = false;
|
||||
now = Date.now();
|
||||
da$: Observable<DifficultyAdjustment>;
|
||||
|
@ -809,6 +810,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.miningStats = stats;
|
||||
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
|
||||
});
|
||||
if (!this.tx.status?.confirmed) {
|
||||
this.standardETA$ = combineLatest([
|
||||
this.stateService.mempoolBlocks$.pipe(startWith(null)),
|
||||
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
|
||||
]).pipe(
|
||||
map(([mempoolBlocks, da]) => {
|
||||
return this.etaService.calculateUnacceleratedETA(
|
||||
this.tx,
|
||||
mempoolBlocks,
|
||||
da,
|
||||
this.cpfpInfo,
|
||||
);
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
this.isAccelerated$.next(this.isAcceleration);
|
||||
}
|
||||
|
|
|
@ -225,4 +225,58 @@ export class EtaService {
|
|||
blocks: Math.ceil(eta / da.adjustedTimeAvg),
|
||||
};
|
||||
}
|
||||
|
||||
calculateUnacceleratedETA(
|
||||
tx: Transaction,
|
||||
mempoolBlocks: MempoolBlock[],
|
||||
da: DifficultyAdjustment,
|
||||
cpfpInfo: CpfpInfo | null,
|
||||
): ETA | null {
|
||||
if (!tx || !mempoolBlocks) {
|
||||
return null;
|
||||
}
|
||||
const now = Date.now();
|
||||
|
||||
// use known projected position, or fall back to feerate-based estimate
|
||||
const mempoolPosition = this.mempoolPositionFromFees(this.getFeeRateFromCpfpInfo(tx, cpfpInfo), mempoolBlocks);
|
||||
if (!mempoolPosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// difficulty adjustment estimate is required to know avg block time on non-Liquid networks
|
||||
if (!da) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blocks = mempoolPosition.block + 1;
|
||||
const wait = da.adjustedTimeAvg * (mempoolPosition.block + 1);
|
||||
return {
|
||||
now,
|
||||
time: wait + now + da.timeOffset,
|
||||
wait,
|
||||
blocks,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
getFeeRateFromCpfpInfo(tx: Transaction, cpfpInfo: CpfpInfo | null): number {
|
||||
if (!cpfpInfo) {
|
||||
return tx.fee / (tx.weight / 4);
|
||||
}
|
||||
|
||||
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
|
||||
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||
relatives.push(cpfpInfo.bestDescendant);
|
||||
}
|
||||
|
||||
if (!!relatives.length) {
|
||||
const totalWeight = tx.weight + relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||
const totalFees = tx.fee + relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||
|
||||
return totalFees / (totalWeight / 4);
|
||||
}
|
||||
|
||||
return tx.fee / (tx.weight / 4);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue