diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html index cd9d1f2e2..d5f1c0915 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html @@ -1,3 +1,4 @@ +@if (tx.status.confirmed) {
@@ -11,68 +12,225 @@
- @if (eta) { - ~ - } @else if (tx.status.block_time) { - }
- -
-
-
-
- -
-
-
Sent
-
- +
+
+
+ +
+
+
First seen
+
+ +
-
-
-
-
-
-
-
- -
-
-
Accelerated
-
- +
+
-
-
-
-
-
-
- -
-
-
Mined
-
- @if (tx.status.block_time) { +
+
+
+ +
+
+
Accelerated
+
+ +
+
+
+
+
+
+
+ +
+
+
Mined
+
- } @else if (eta) { - - } +
+
+
+
+
+
+} @else if (acceleratedETA) { +
+
+
+
+
+
+
+ +
+
+
+
+
+ ~ +
+
+
+
+
+
+
+ +
+
+
First seen
+
+ +
+
+
+
+
+
+
+
+ +
+
+
Accelerated
+
+ Now +
+
+
+
+
+
+
+
+ +
+
+
Mined
+
+
+
+
+
+
+
+
+
+
+ ~ ({{ accelerateRatio }}x slower) +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- - -
-
- - -
-
- -
\ No newline at end of file +
+} @else if (standardETA) { +
+
+
+
+
+
+
+ +
+
+
+
+
+ @if (eta) { + ~ + } +
+
+
+
+
+
+
+ +
+
+
First seen
+
+ +
+
+
+
+
+
+
+
+ +
+
+
Accelerated
+
+ +
+
+
+
+
+
+
+
+ +
+
+
Mined
+
+
+
+
+
+
+
+
+
+
+ ~ ({{ accelerateRatio }}x slower) +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+} \ No newline at end of file diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss index 73ca2b270..54061f54e 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss @@ -84,10 +84,6 @@ background: var(--primary); border-radius: 5px; - &.loading { - animation: standardPulse 1s infinite; - } - &.left { right: 50%; } @@ -118,6 +114,26 @@ 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%; 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) } } \ No newline at end of file diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts index 6f775e7a8..ba687e093 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts @@ -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)); + } + } + } } - } diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index a0e841303..9528d2e66 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -152,15 +152,6 @@
- -
-

Acceleration Timeline

-
-
- -
-
-

RBF Timeline

@@ -170,6 +161,15 @@
+ +
+

Acceleration Timeline

+
+
+ +
+
+

Flow

diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index d2cc0789d..d1d3fe5d7 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -112,6 +112,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { 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; + standardETA$: Observable; isCached: boolean = false; now = Date.now(); da$: Observable; @@ -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); } diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index cc1436e4c..f632c9adb 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -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); + + } }