diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts
index a65af3f19..6225a9c1d 100644
--- a/backend/src/api/bitcoin/bitcoin.routes.ts
+++ b/backend/src/api/bitcoin/bitcoin.routes.ts
@@ -165,6 +165,7 @@ class BitcoinRoutes {
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
+ feeDelta: tx.feeDelta || undefined,
});
return;
}
diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts
index e655601e5..5d9dcf8f4 100644
--- a/backend/src/api/mempool-blocks.ts
+++ b/backend/src/api/mempool-blocks.ts
@@ -453,6 +453,7 @@ class MempoolBlocks {
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
+ mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
@@ -460,6 +461,7 @@ class MempoolBlocks {
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
+ mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index 32d306ad2..e57b8221b 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -823,6 +823,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
+ feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
@@ -864,6 +865,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
+ feeDelta: mempoolTx.feeDelta || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateMempoolTxCpfp(mempoolTx, newMempool);
@@ -1138,6 +1140,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
+ feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
@@ -1160,6 +1163,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
+ feeDelta: mempoolTx.feeDelta || undefined,
};
}
}
diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts
index 0ad60f4b9..2dd0f17dd 100644
--- a/backend/src/mempool.interfaces.ts
+++ b/backend/src/mempool.interfaces.ts
@@ -126,6 +126,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
+ feeDelta?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@@ -449,7 +450,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
replacedBy?: string,
- position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
+ position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
cpfp?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@@ -462,6 +463,7 @@ export interface TxTrackingInfo {
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
+ feeDelta?: number,
confirmed?: boolean
}
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html
new file mode 100644
index 000000000..7dfc15f55
--- /dev/null
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html
@@ -0,0 +1,62 @@
+
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss
new file mode 100644
index 000000000..29a157507
--- /dev/null
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss
@@ -0,0 +1,52 @@
+.acceleration-tooltip {
+ position: fixed;
+ z-index: 3;
+ background: color-mix(in srgb, var(--active-bg) 95%, transparent);
+ border-radius: 4px;
+ box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
+ color: var(--tooltip-grey);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 10px 15px;
+ text-align: left;
+ pointer-events: none;
+
+ .badge.badge-accelerated {
+ background-color: var(--tertiary);
+ color: white;
+ }
+
+ .value {
+ text-align: end;
+ }
+
+ .label {
+ padding-right: 30px;
+ }
+
+ .pool-logo {
+ width: 22px;
+ height: 22px;
+ position: relative;
+ top: -1px;
+ margin-right: 3px;
+ }
+
+ .highlight {
+ filter: drop-shadow(0 0 5px #905cf4);
+ animation: pulse 1s infinite;
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.2);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.ts b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.ts
new file mode 100644
index 000000000..b4b3405fc
--- /dev/null
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.ts
@@ -0,0 +1,38 @@
+import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
+
+@Component({
+ selector: 'app-acceleration-timeline-tooltip',
+ templateUrl: './acceleration-timeline-tooltip.component.html',
+ styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
+})
+export class AccelerationTimelineTooltipComponent implements OnChanges {
+ @Input() accelerationInfo: any;
+ @Input() cursorPosition: { x: number, y: number };
+
+ tooltipPosition: any = null;
+
+ @ViewChild('tooltip') tooltipElement: ElementRef;
+
+ constructor() {}
+
+ ngOnChanges(changes): void {
+ if (changes.cursorPosition && changes.cursorPosition.currentValue) {
+ let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
+ let y = changes.cursorPosition.currentValue.y + 20;
+ if (this.tooltipElement) {
+ const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
+ if ((x + elementBounds.width) > (window.innerWidth - 10)) {
+ x = Math.max(0, window.innerWidth - elementBounds.width - 10);
+ }
+ if (y + elementBounds.height > (window.innerHeight - 20)) {
+ y = y - elementBounds.height - 20;
+ }
+ }
+ this.tooltipPosition = { x, y };
+ }
+ }
+
+ hasPoolsData(): boolean {
+ return Object.keys(this.accelerationInfo.poolsData).length > 0;
+ }
+}
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 28076efa5..8eba6cdeb 100644
--- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html
@@ -26,7 +26,7 @@
@@ -58,7 +58,7 @@
-
+
First seen
@@ -80,7 +80,7 @@
} @else {
}
-
+
@if (!tx.status.confirmed) {
@@ -113,7 +113,10 @@
} @else {
}
-
+
@if (tx.status.confirmed) {
@@ -130,4 +133,10 @@
+
+
+
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 93a0cdba1..f351a0114 100644
--- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss
@@ -152,9 +152,16 @@
margin-bottom: -8px;
transform: translateY(-50%);
border-radius: 50%;
- cursor: pointer;
padding: 4px;
background: transparent;
+ transition: background-color 300ms, padding 300ms;
+
+ &.hovering {
+ cursor: pointer;
+ &:hover {
+ padding: 0px;
+ }
+ }
.shape {
position: relative;
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 c8dbed72b..da0eee4a3 100644
--- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts
@@ -1,6 +1,8 @@
-import { Component, Input, OnInit, OnChanges } from '@angular/core';
+import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
+import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
+import { MiningService } from '../../services/mining.service';
@Component({
selector: 'app-acceleration-timeline',
@@ -10,6 +12,7 @@ import { Transaction } from '../../interfaces/electrs.interface';
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() tx: Transaction;
+ @Input() accelerationInfo: Acceleration;
@Input() eta: ETA;
// 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)
@@ -22,13 +25,25 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
useAbsoluteTime: boolean = false;
interval: number;
- constructor() {}
+ tooltipPosition = null;
+ hoverInfo: any = null;
+ poolsData: { [id: number]: SinglePoolStats } = {};
+
+ constructor(
+ private miningService: MiningService,
+ ) {}
ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
+ this.miningService.getPools().subscribe(pools => {
+ for (const pool of pools) {
+ this.poolsData[pool.unique_id] = pool;
+ }
+ });
+
this.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
@@ -52,4 +67,42 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
ngOnDestroy(): void {
clearInterval(this.interval);
}
+
+ onHover(event, status: string): void {
+ if (status === 'seen') {
+ this.hoverInfo = {
+ status,
+ fee: this.tx.fee,
+ weight: this.tx.weight
+ };
+ } else if (status === 'accelerated') {
+ this.hoverInfo = {
+ status,
+ fee: this.accelerationInfo?.effectiveFee || this.tx.fee,
+ weight: this.tx.weight,
+ feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta,
+ pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
+ poolsData: this.poolsData
+ };
+ } else if (status === 'mined') {
+ this.hoverInfo = {
+ status,
+ fee: this.accelerationInfo?.effectiveFee,
+ weight: this.tx.weight,
+ bidBoost: this.accelerationInfo?.bidBoost,
+ minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId,
+ pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
+ poolsData: this.poolsData
+ };
+ }
+ }
+
+ onBlur(event): void {
+ this.hoverInfo = null;
+ }
+
+ @HostListener('pointermove', ['$event'])
+ onPointerMove(event) {
+ this.tooltipPosition = { x: event.clientX, y: event.clientY };
+ }
}
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html
index 9ce22d26c..a6a985fb0 100644
--- a/frontend/src/app/components/transaction/transaction.component.html
+++ b/frontend/src/app/components/transaction/transaction.component.html
@@ -167,7 +167,7 @@
Acceleration Timeline
-
+
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index 924addaa0..107e471fc 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -816,6 +816,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
+ this.tx.feeDelta = cpfpInfo.feeDelta;
this.setIsAccelerated(firstCpfp);
}
diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts
index 942386d8f..2167099c2 100644
--- a/frontend/src/app/interfaces/electrs.interface.ts
+++ b/frontend/src/app/interfaces/electrs.interface.ts
@@ -22,6 +22,7 @@ export interface Transaction {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
+ feeDelta?: number;
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts
index 9a00faadc..077bfa775 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -31,6 +31,7 @@ export interface CpfpInfo {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
+ feeDelta?: number;
}
export interface RbfInfo {
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index 313a43e1f..89bcfafbb 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -68,6 +68,7 @@ import { AddressTransactionsWidgetComponent } from '../components/address-transa
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
import { AccelerationTimelineComponent } from '../components/acceleration-timeline/acceleration-timeline.component';
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
+import { AccelerationTimelineTooltipComponent } from '../components/acceleration-timeline/acceleration-timeline-tooltip.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component';
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component';
@@ -180,6 +181,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
RbfTimelineComponent,
AccelerationTimelineComponent,
RbfTimelineTooltipComponent,
+ AccelerationTimelineTooltipComponent,
PushTransactionComponent,
TestTransactionsComponent,
AssetsNavComponent,
@@ -320,6 +322,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
RbfTimelineComponent,
AccelerationTimelineComponent,
RbfTimelineTooltipComponent,
+ AccelerationTimelineTooltipComponent,
PushTransactionComponent,
TestTransactionsComponent,
AssetsNavComponent,