Timeline of replacements for RBF-d transactions

This commit is contained in:
Mononaut
2022-12-13 17:11:37 -06:00
parent 8db7326a5a
commit 1b843da785
11 changed files with 295 additions and 21 deletions

View File

@@ -0,0 +1,35 @@
<div class="rbf-timeline box">
<div class="timeline">
<div class="intervals">
<ng-container *ngFor="let replacement of replacements; let i = index;">
<div class="interval" *ngIf="i > 0">
<div class="interval-time">
<app-time [time]="replacement.time - replacements[i-1].time" [relative]="false"></app-time>
</div>
</div>
<div class="node-spacer"></div>
</ng-container>
</div>
<div class="nodes">
<ng-container *ngFor="let replacement of replacements; let i = index;">
<div class="interval-spacer" *ngIf="i > 0">
<div class="track"></div>
</div>
<div class="node" [class.selected]="txid === replacement.tx.txid">
<div class="track"></div>
<a class="shape-border" [class.rbf]="replacement.tx.rbf" [routerLink]="['/tx/' | relativeUrl, replacement.tx.txid]" [title]="replacement.tx.txid">
<div class="shape"></div>
</a>
<span class="fee-rate">{{ replacement.tx.fee / (replacement.tx.vsize) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span>
</div>
</ng-container>
</div>
</div>
<!-- <app-rbf-timeline-tooltip
*ngIf=[tooltip]
[line]="hoverLine"
[cursorPosition]="tooltipPosition"
[isConnector]="hoverConnector"
></app-rbf-timeline-tooltip> -->
</div>

View File

@@ -0,0 +1,137 @@
.rbf-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, #24273e, #24273e, transparent);
}
&::after {
right: 0;
background: linear-gradient(to left, #24273e, #24273e, transparent);
}
.timeline {
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: 4em;
min-width: 4em;
flex-grow: 1;
}
.interval, .interval-spacer {
width: 8em;
min-width: 4em;
max-width: 8em;
}
.interval-time {
font-size: 12px;
}
}
.node, .interval-spacer {
position: relative;
.track {
position: absolute;
height: 10px;
left: -5px;
right: -5px;
top: 0;
transform: translateY(-50%);
background: #105fb0;
border-radius: 5px;
}
&:first-child {
.track {
left: 50%;
}
}
&:last-child {
.track {
right: 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: 10%;
cursor: pointer;
padding: 4px;
background: transparent;
transition: background-color 300ms, padding 300ms;
.shape {
width: 100%;
height: 100%;
border-radius: 10%;
background: white;
transition: background-color 300ms;
}
&.rbf, &.rbf .shape {
border-radius: 50%;
}
}
.symbol::ng-deep {
display: block;
margin-top: -0.5em;
}
&.selected {
.shape-border {
background: #9339f4;
}
}
.shape-border:hover {
padding: 0px;
.shape {
background: #1bd8f4;
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
import { Router } from '@angular/router';
import { RbfInfo } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-rbf-timeline',
templateUrl: './rbf-timeline.component.html',
styleUrls: ['./rbf-timeline.component.scss'],
})
export class RbfTimelineComponent implements OnInit, OnChanges {
@Input() replacements: RbfInfo[];
@Input() txid: string;
dir: 'rtl' | 'ltr' = 'ltr';
constructor(
private router: Router,
private stateService: StateService,
private apiService: ApiService,
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.dir = 'rtl';
}
}
ngOnInit(): void {
}
ngOnChanges(): void {
}
}

View File

@@ -197,6 +197,15 @@
<br>
<ng-container *ngIf="rbfInfo?.length">
<div class="title float-left">
<h2 id="rbf" i18n="transaction.replacements|Replacements">Replacements</h2>
</div>
<div class="clearfix"></div>
<app-rbf-timeline [txid]="txId" [replacements]="rbfInfo"></app-rbf-timeline>
<br>
</ng-container>
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
<div class="title float-left">
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>

View File

@@ -19,7 +19,7 @@ import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
import { BlockExtended, CpfpInfo, RbfInfo } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Price, PriceService } from '../../services/price.service';
@@ -53,6 +53,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
rbfReplaces: string[];
rbfInfo: RbfInfo[];
cpfpInfo: CpfpInfo | null;
showCpfpDetails = false;
fetchCpfp$ = new Subject<string>();
@@ -183,10 +184,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
.getRbfHistory$(txId)
),
catchError(() => {
return of([]);
return of(null);
})
).subscribe((replaces) => {
this.rbfReplaces = replaces;
).subscribe((rbfResponse) => {
this.rbfInfo = rbfResponse?.replacements || [];
this.rbfReplaces = rbfResponse?.replaces || null;
});
this.fetchCachedTxSubscription = this.fetchCachedTx$
@@ -460,6 +462,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.replaced = false;
this.transactionTime = -1;
this.cpfpInfo = null;
this.rbfInfo = [];
this.rbfReplaces = [];
this.showCpfpDetails = false;
document.body.scrollTo(0, 0);

View File

@@ -26,6 +26,11 @@ export interface CpfpInfo {
bestDescendant?: BestDescendant | null;
}
export interface RbfInfo {
tx: RbfTransaction,
time: number
}
export interface DifficultyAdjustment {
progressPercent: number;
difficultyChange: number;
@@ -146,6 +151,10 @@ export interface TransactionStripped {
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
}
interface RbfTransaction extends TransactionStripped {
rbf?: boolean;
}
export interface RewardStats {
startBlock: number;
endBlock: number;

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights } from '../interfaces/node-api.interface';
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfInfo } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs';
import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface';
@@ -124,8 +124,8 @@ export class ApiService {
return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
}
getRbfHistory$(txid: string): Observable<string[]> {
return this.httpClient.get<string[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/replaces');
getRbfHistory$(txid: string): Observable<{ replacements: RbfInfo[], replaces: string[] }> {
return this.httpClient.get<{ replacements: RbfInfo[], replaces: string[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/rbf');
}
getRbfCachedTx$(txid: string): Observable<Transaction> {

View File

@@ -61,6 +61,7 @@ import { DifficultyComponent } from '../components/difficulty/difficulty.compone
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component';
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
@@ -138,6 +139,7 @@ import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.
DifficultyComponent,
DifficultyMiningComponent,
DifficultyTooltipComponent,
RbfTimelineComponent,
TxBowtieGraphComponent,
TxBowtieGraphTooltipComponent,
TermsOfServiceComponent,
@@ -242,6 +244,7 @@ import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.
DifficultyComponent,
DifficultyMiningComponent,
DifficultyTooltipComponent,
RbfTimelineComponent,
TxBowtieGraphComponent,
TxBowtieGraphTooltipComponent,
TermsOfServiceComponent,