Merge pull request #5277 from mempool/natsoni/acceleration-timeline

Acceleration timeline concept
This commit is contained in:
softsimon 2024-07-05 15:49:09 +09:00 committed by GitHub
commit 4cd70941f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 338 additions and 6 deletions

View File

@ -162,6 +162,7 @@ class BitcoinRoutes {
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
});
return;
}

View File

@ -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 {

View File

@ -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,
};
}
}

View File

@ -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
}

View File

@ -0,0 +1,78 @@
<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">
@if (eta) {
~<app-time kind="plain" [time]="eta?.wait / 1000"></app-time>
} @else if (tx.status.block_time) {
<app-time kind="plain" [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]="!tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.sent-selected]="!tx.status.confirmed && !tx.acceleration">
<div class="shape"></div>
</a>
<div class="status"><span class="badge badge-primary">Sent</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]="!tx.acceleration && !tx.status.confirmed"></div>
</div>
<div class="node" [id]="'accelerated'">
<div class="seen-to-acc left" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
<div class="acc-to-confirmed right" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.accelerated-selected]="tx.acceleration && !tx.status.confirmed">
<div class="shape"></div>
</a>
<div class="status" [style]="!tx.acceleration && !tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-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" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
</div>
<div class="node" [id]="'confirmed'" [class.mined]="tx.status.confirmed">
<div class="acc-to-confirmed left" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.mined-selected]="tx.status.confirmed">
<div class="shape"></div>
</a>
<div class="status" [style]="!tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-success">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>
</div>
</div>
</div>
<ng-template #nodeSpacer>
<div class="node-spacer"></div>
</ng-template>
<ng-template #intervalSpacer>
<div class="interval-spacer"></div>
</ng-template>
</div>

View File

@ -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) }
}

View File

@ -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 {
}
}

View File

@ -152,9 +152,18 @@
<br>
<ng-container *ngIf="transactionTime && (tx.acceleration || 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)"></app-acceleration-timeline>
<br>
</ng-container>
<ng-container *ngIf="rbfInfo">
<div class="title float-left">
<h2 id="rbf" i18n="transaction.rbf-history|RBF History">RBF History</h2>
<h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2>
</div>
<div class="clearfix"></div>
<app-rbf-timeline [txid]="txId" [replacements]="rbfInfo"></app-rbf-timeline>

View File

@ -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);
}

View File

@ -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,

View File

@ -21,6 +21,7 @@ export interface Transaction {
cpfpChecked?: boolean;
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;

View File

@ -30,6 +30,7 @@ export interface CpfpInfo {
adjustedVsize?: number;
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
}
export interface RbfInfo {

View File

@ -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,