diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts
index fd04707b5..ac848d4a4 100644
--- a/backend/src/api/bitcoin/bitcoin.routes.ts
+++ b/backend/src/api/bitcoin/bitcoin.routes.ts
@@ -162,6 +162,7 @@ class BitcoinRoutes {
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
+ acceleratedAt: tx.acceleratedAt || undefined,
});
return;
}
diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts
index a144515dc..e655601e5 100644
--- a/backend/src/api/mempool-blocks.ts
+++ b/backend/src/api/mempool-blocks.ts
@@ -452,12 +452,14 @@ class MempoolBlocks {
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
+ mempoolTx.acceleratedAt = acceleration?.added;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
+ mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index 3c8c06ecf..8d1a85994 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -822,6 +822,7 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
+ acceleratedAt: mempoolTx.acceleratedAt || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
@@ -862,6 +863,7 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
+ acceleratedAt: mempoolTx.acceleratedAt || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateCpfp(mempoolTx, newMempool);
@@ -1139,6 +1141,7 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
+ acceleratedAt: mempoolTx.acceleratedAt || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
@@ -1160,6 +1163,7 @@ class WebsocketHandler {
},
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
+ acceleratedAt: mempoolTx.acceleratedAt || undefined,
};
}
}
diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts
index 2d3c7a7c0..34375604e 100644
--- a/backend/src/mempool.interfaces.ts
+++ b/backend/src/mempool.interfaces.ts
@@ -112,6 +112,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
};
acceleration?: boolean;
acceleratedBy?: number[];
+ acceleratedAt?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@@ -434,7 +435,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
replacedBy?: string,
- position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[] },
+ position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
cpfp?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@@ -446,6 +447,7 @@ export interface TxTrackingInfo {
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
accelerated?: boolean,
acceleratedBy?: number[],
+ acceleratedAt?: number,
confirmed?: boolean
}
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html
new file mode 100644
index 000000000..6d3591fb9
--- /dev/null
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+ @if (eta) {
+ ~
+ } @else if (tx.status.block_time) {
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
Sent
+
+
0" kind="since" [time]="transactionTime">
+
+
+
+
+
+
+
+
+
+
Accelerated
+
+
+
+
+
+
+
+
+
Mined
+
+ @if (tx.status.block_time) {
+
+ } @else if (eta) {
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 000000000..d0338ec84
--- /dev/null
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss
@@ -0,0 +1,197 @@
+.acceleration-timeline {
+ position: relative;
+ width: 100%;
+ padding: 1em 0;
+
+ &::after, &::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 2em;
+ z-index: 2;
+ }
+
+ &::before {
+ left: 0;
+ background: linear-gradient(to right, var(--box-bg), var(--box-bg), transparent);
+ }
+
+ &::after {
+ right: 0;
+ background: linear-gradient(to left, var(--box-bg), var(--box-bg), transparent);
+ }
+
+ .timeline-wrapper {
+ position: relative;
+ width: calc(100% - 2em);
+ margin: auto;
+ overflow-x: auto;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ .intervals, .nodes {
+ min-width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-start;
+ text-align: center;
+
+ .node, .node-spacer {
+ width: 6em;
+ min-width: 6em;
+ flex-grow: 1;
+ }
+
+ .interval, .interval-spacer {
+ width: 8em;
+ min-width: 5em;
+ max-width: 8em;
+ height: 32px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-end;
+ }
+
+ .interval {
+ overflow: visible;
+ }
+
+ .interval-time {
+ font-size: 12px;
+ line-height: 16px;
+ white-space: nowrap;
+ }
+ }
+
+ .node, .interval-spacer {
+ position: relative;
+ .seen-to-acc {
+ position: absolute;
+ height: 10px;
+ left: -5px;
+ right: -5px;
+ top: 0;
+ transform: translateY(-50%);
+ background: var(--primary);
+ border-radius: 5px;
+
+ &.loading {
+ animation: standardPulse 1s infinite;
+ }
+
+ &.left {
+ right: 50%;
+ }
+
+ &.right {
+ left: 50%;
+ }
+ }
+
+ .acc-to-confirmed {
+ position: absolute;
+ height: 10px;
+ left: -5px;
+ right: -5px;
+ top: 0;
+ transform: translateY(-50%);
+ background: var(--tertiary);
+ border-radius: 5px;
+
+ &.loading {
+ animation: acceleratePulse 1s infinite;
+ }
+
+ &.left {
+ right: 50%;
+ }
+ &.right {
+ left: 50%;
+ }
+ }
+
+ }
+
+ .nodes {
+ position: relative;
+ margin-top: 1em;
+ .node {
+ .shape-border {
+ display: block;
+ margin: auto;
+ height: calc(1em + 8px);
+ width: calc(1em + 8px);
+ margin-bottom: -8px;
+ transform: translateY(-50%);
+ border-radius: 50%;
+ padding: 2px;
+ background: transparent;
+ transition: background-color 300ms, padding 300ms;
+
+ .shape {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background: white;
+ transition: background-color 300ms, border 300ms;
+ }
+
+ &.sent-selected {
+ .shape {
+ background: var(--primary);
+ }
+ }
+
+ &.accelerated-selected {
+ .shape {
+ background: var(--tertiary);
+ }
+ }
+
+ &.mined-selected {
+ .shape {
+ background: var(--success);
+ }
+ }
+ }
+
+ .status {
+ margin-top: -64px;
+
+ .badge.badge-accelerated {
+ background-color: var(--tertiary);
+ color: white;
+ }
+ }
+
+ .time {
+ margin-top: 33px;
+ font-size: 12px;
+ line-height: 16px;
+ white-space: nowrap;
+ }
+ }
+ }
+}
+
+@keyframes acceleratePulse {
+ 0% { background-color: var(--tertiary) }
+ 50% { background-color: var(--mainnet-alt) }
+ 100% { background-color: var(--tertiary) }
+}
+
+@keyframes standardPulse {
+ 0% { background-color: var(--primary) }
+ 50% { background-color: var(--secondary) }
+ 100% { background-color: var(--primary) }
+
+}
\ 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
new file mode 100644
index 000000000..d40215c1d
--- /dev/null
+++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts
@@ -0,0 +1,33 @@
+import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
+import { ETA } from '../../services/eta.service';
+import { Transaction } from '../../interfaces/electrs.interface';
+
+@Component({
+ selector: 'app-acceleration-timeline',
+ templateUrl: './acceleration-timeline.component.html',
+ styleUrls: ['./acceleration-timeline.component.scss'],
+})
+export class AccelerationTimelineComponent implements OnInit, OnChanges {
+ @Input() transactionTime: number;
+ @Input() tx: Transaction;
+ @Input() eta: ETA;
+
+ acceleratedAt: number;
+ dir: 'rtl' | 'ltr' = 'ltr';
+
+ constructor(
+ @Inject(LOCALE_ID) private locale: string,
+ ) {
+ if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
+ this.dir = 'rtl';
+ }
+ }
+
+ ngOnInit(): void {
+ this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
+ }
+
+ ngOnChanges(changes): void {
+ }
+
+}
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html
index 8967e03b8..0f9a4d9c4 100644
--- a/frontend/src/app/components/transaction/transaction.component.html
+++ b/frontend/src/app/components/transaction/transaction.component.html
@@ -152,9 +152,18 @@
+
+
+
Acceleration Timeline
+
+
+
+
+
+
-
RBF History
+ RBF Timeline
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index 3c316f879..d2cc0789d 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -326,7 +326,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
-
+ this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
this.setIsAccelerated();
}
@@ -777,6 +777,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
+ this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.setIsAccelerated(firstCpfp);
}
diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts
index b799d79a2..0bcc414e9 100644
--- a/frontend/src/app/docs/api-docs/api-docs-data.ts
+++ b/frontend/src/app/docs/api-docs/api-docs-data.ts
@@ -6176,10 +6176,10 @@ export const restApiDocsData = [
type: "endpoint",
category: "transactions",
httpRequestMethod: "GET",
- fragment: "get-transaction-rbf-history",
- title: "GET Transaction RBF History",
+ fragment: "get-transaction-rbf-timeline",
+ title: "GET Transaction RBF Timeline",
description: {
- default: "Returns the RBF tree history of a transaction."
+ default: "Returns the RBF tree timeline of a transaction."
},
urlString: "v1/tx/:txId/rbf",
showConditions: bitcoinNetworks,
diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts
index 726649090..942386d8f 100644
--- a/frontend/src/app/interfaces/electrs.interface.ts
+++ b/frontend/src/app/interfaces/electrs.interface.ts
@@ -21,6 +21,7 @@ export interface Transaction {
cpfpChecked?: boolean;
acceleration?: boolean;
acceleratedBy?: number[];
+ acceleratedAt?: 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 c3c7c9efe..eb5d3ba94 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -30,6 +30,7 @@ export interface CpfpInfo {
adjustedVsize?: number;
acceleration?: boolean;
acceleratedBy?: number[];
+ acceleratedAt?: number;
}
export interface RbfInfo {
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index c060bbbd2..868cb1bd9 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -66,6 +66,7 @@ import { DifficultyMiningComponent } from '../components/difficulty-mining/diffi
import { BalanceWidgetComponent } from '../components/balance-widget/balance-widget.component';
import { AddressTransactionsWidgetComponent } from '../components/address-transactions-widget/address-transactions-widget.component';
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 { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component';
@@ -177,6 +178,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BalanceWidgetComponent,
AddressTransactionsWidgetComponent,
RbfTimelineComponent,
+ AccelerationTimelineComponent,
RbfTimelineTooltipComponent,
PushTransactionComponent,
TestTransactionsComponent,
@@ -316,6 +318,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BalanceWidgetComponent,
AddressTransactionsWidgetComponent,
RbfTimelineComponent,
+ AccelerationTimelineComponent,
RbfTimelineTooltipComponent,
PushTransactionComponent,
TestTransactionsComponent,