mirror of
https://github.com/mempool/mempool.git
synced 2024-11-19 09:52:14 +01:00
Merge pull request #5277 from mempool/natsoni/acceleration-timeline
Acceleration timeline concept
This commit is contained in:
commit
4cd70941f7
@ -162,6 +162,7 @@ class BitcoinRoutes {
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration,
|
||||
acceleratedBy: tx.acceleratedBy || undefined,
|
||||
acceleratedAt: tx.acceleratedAt || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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) }
|
||||
|
||||
}
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -21,6 +21,7 @@ export interface Transaction {
|
||||
cpfpChecked?: boolean;
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
acceleratedAt?: number;
|
||||
deleteAfter?: number;
|
||||
_unblinded?: any;
|
||||
_deduced?: boolean;
|
||||
|
@ -30,6 +30,7 @@ export interface CpfpInfo {
|
||||
adjustedVsize?: number;
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
acceleratedAt?: number;
|
||||
}
|
||||
|
||||
export interface RbfInfo {
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user