Show unaccelerated ETA in acceleration timeline

This commit is contained in:
natsoni 2024-07-07 14:41:44 +09:00
parent a0992f6091
commit bf51e3e1c9
No known key found for this signature in database
GPG key ID: C65917583181743B
6 changed files with 357 additions and 79 deletions

View file

@ -1,3 +1,4 @@
@if (tx.status.confirmed) {
<div class="acceleration-timeline box"> <div class="acceleration-timeline box">
<div class="timeline-wrapper"> <div class="timeline-wrapper">
<div class="timeline"> <div class="timeline">
@ -11,68 +12,225 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <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> <app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
}
</div> </div>
</div> </div>
<div class="node-spacer"></div> <div class="node-spacer"></div>
</div> </div>
<div class="nodes">
</div> <div class="node" [id]="'first-seen'">
<div class="nodes"> <div class="seen-to-acc right"></div>
<div class="node" [id]="'first-seen'"> <a class="shape-border">
<div class="seen-to-acc right" [class.loading]="!isAcceleration && !tx.status.confirmed"></div> <div class="shape"></div>
<a class="shape-border" [class.sent-selected]="!tx.status.confirmed && !isAcceleration"> </a>
<div class="shape"></div> <div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
</a> <div class="time">
<div class="status"><span class="badge badge-primary" i18n="accelerator.sent-state">Sent</span></div> <app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
<div class="time"> </div>
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
</div> </div>
</div> <div class="interval-spacer">
<div class="interval-spacer"> <div class="seen-to-acc"></div>
<div class="seen-to-acc" [class.loading]="!isAcceleration && !tx.status.confirmed"></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>
</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">
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
</div> </div>
</div> <div class="node" [id]="'accelerated'">
<div class="interval-spacer"> <div class="seen-to-acc left"></div>
<div class="acc-to-confirmed" [class.loading]="isAcceleration && !tx.status.confirmed"></div> <div class="acc-to-confirmed right"></div>
</div> <a class="shape-border">
<div class="node" [id]="'confirmed'" [class.mined]="tx.status.confirmed"> <div class="shape"></div>
<div class="acc-to-confirmed left" [class.loading]="isAcceleration && !tx.status.confirmed"></div> </a>
<a class="shape-border" [class.mined-selected]="tx.status.confirmed" [class.waiting]="!tx.status.confirmed"> <div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
<div class="shape"></div> <div class="time">
</a> <app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
<div class="status"><span class="badge" [class]="tx.status.confirmed ? 'badge-success' : 'badge-waiting'" i18n="transaction.rbf.mined">Mined</span></div> </div>
<div class="time"> </div>
@if (tx.status.block_time) { <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> <app-time kind="since" [time]="tx.status.block_time"></app-time>
} @else if (eta) { </div>
<app-time kind="until" [time]="eta?.time"></app-time> </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> </div>
</div> </div>
</div>
<ng-template #nodeSpacer> } @else if (standardETA) { <!-- Accelerated, to be mined -->
<div class="node-spacer"></div> <div class="acceleration-timeline box">
</ng-template> <div class="timeline-wrapper">
<div class="timeline">
<ng-template #intervalSpacer> <div class="intervals">
<div class="interval-spacer"></div> <div class="node-spacer"></div>
</ng-template> <div class="interval">
<div class="interval-time">
</div> <app-time [time]="acceleratedAt - transactionTime"></app-time>
</div>
</div>
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (eta) {
~<app-time [time]="eta?.wait / 1000"></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 loading"></div>
<a class="shape-border accelerated-selected">
<div class="shape accelerating"></div>
</a>
<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 loading"></div>
</div>
<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 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]="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>
}

View file

@ -84,10 +84,6 @@
background: var(--primary); background: var(--primary);
border-radius: 5px; border-radius: 5px;
&.loading {
animation: standardPulse 1s infinite;
}
&.left { &.left {
right: 50%; right: 50%;
} }
@ -118,6 +114,26 @@
left: 50%; left: 50%;
} }
} }
.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%;
}
}
} }
@ -142,6 +158,9 @@
height: 100%; height: 100%;
border-radius: 50%; border-radius: 50%;
background: white; background: white;
&.accelerating {
animation: acceleratePulse 1s infinite;
}
transition: background-color 300ms, border 300ms; transition: background-color 300ms, border 300ms;
} }
@ -151,12 +170,6 @@
} }
} }
&.sent-selected {
.shape {
background: var(--primary);
}
}
&.accelerated-selected { &.accelerated-selected {
.shape { .shape {
background: var(--tertiary); background: var(--tertiary);
@ -190,6 +203,30 @@
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
white-space: nowrap; 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) } 100% { background-color: var(--tertiary) }
} }
@keyframes standardPulse { @keyframes textPulse {
0% { background-color: var(--primary) } 0% { color: var(--tertiary) }
50% { background-color: var(--secondary) } 50% { color: var(--mainnet-alt) }
100% { background-color: var(--primary) } 100% { color: var(--tertiary) }
} }

View file

@ -11,9 +11,14 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() eta: ETA; @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; acceleratedAt: number;
now: number;
accelerateRatio: number;
constructor() {} constructor() {}
@ -22,6 +27,15 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
} }
ngOnChanges(changes): void { 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));
}
}
}
} }
} }

View file

@ -152,15 +152,6 @@
<br> <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"> <ng-container *ngIf="rbfInfo">
<div class="title float-left"> <div class="title float-left">
<h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2> <h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2>
@ -170,6 +161,15 @@
<br> <br>
</ng-container> </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"> <ng-container *ngIf="flowEnabled; else flowPlaceholder">
<div class="title float-left"> <div class="title float-left">
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2> <h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>

View file

@ -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) 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 isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself
ETA$: Observable<ETA | null>; ETA$: Observable<ETA | null>;
standardETA$: Observable<ETA | null>;
isCached: boolean = false; isCached: boolean = false;
now = Date.now(); now = Date.now();
da$: Observable<DifficultyAdjustment>; da$: Observable<DifficultyAdjustment>;
@ -809,6 +810,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.miningStats = 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); // 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); this.isAccelerated$.next(this.isAcceleration);
} }

View file

@ -225,4 +225,58 @@ export class EtaService {
blocks: Math.ceil(eta / da.adjustedTimeAvg), 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);
}
} }