Accelerator fee diagram concept
This commit is contained in:
parent
c44276027c
commit
c753a8e92a
@ -0,0 +1,21 @@
|
||||
<div class="fee-graph" *ngIf="tx && estimate">
|
||||
<div class="column">
|
||||
<ng-container *ngFor="let bar of bars">
|
||||
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
|
||||
<div class="fill"></div>
|
||||
<div class="line">
|
||||
<p class="fee-rate">
|
||||
<span class="label">{{ bar.label }}</span>
|
||||
<span class="rate">
|
||||
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="vsize">
|
||||
<span [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></span>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,147 @@
|
||||
.fee-graph {
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
max-height: 100vh;
|
||||
margin-left: 4em;
|
||||
margin-right: 1.5em;
|
||||
padding-bottom: 63px;
|
||||
|
||||
.column {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #181b2d;
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.75;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fee {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: -4.5em;
|
||||
border-top: dashed white 1.5px;
|
||||
|
||||
.fee-rate {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0.2em;
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
|
||||
.label {
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
.rate .symbol {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tx {
|
||||
.fill {
|
||||
background: #105fb0;
|
||||
}
|
||||
.line {
|
||||
.fee-rate {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.fee {
|
||||
opacity: 1;
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
&.target {
|
||||
.fill {
|
||||
background: #3bcc49;
|
||||
}
|
||||
.fee {
|
||||
opacity: 1;
|
||||
z-index: 11;
|
||||
}
|
||||
.line .fee-rate {
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.max {
|
||||
cursor: pointer;
|
||||
.line .fee-rate {
|
||||
.label {
|
||||
opacity: 0;
|
||||
}
|
||||
bottom: 2px;
|
||||
}
|
||||
&.active, &:hover {
|
||||
.fill {
|
||||
background: #653b9c;
|
||||
}
|
||||
.line {
|
||||
.fee-rate .label {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fill {
|
||||
z-index: 10;
|
||||
}
|
||||
.line {
|
||||
z-index: 11;
|
||||
}
|
||||
.fee {
|
||||
opacity: 1;
|
||||
z-index: 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .bar:not(:hover) {
|
||||
&.target, &.max {
|
||||
.fee {
|
||||
opacity: 0;
|
||||
}
|
||||
.line .fee-rate .label {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vsize {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
|
||||
import { tap, switchMap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
|
||||
|
||||
interface GraphBar {
|
||||
rate: number;
|
||||
style: any;
|
||||
class: 'tx' | 'target' | 'max';
|
||||
label: string;
|
||||
active?: boolean;
|
||||
rateIndex?: number;
|
||||
fee?: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-fee-graph',
|
||||
templateUrl: './accelerate-fee-graph.component.html',
|
||||
styleUrls: ['./accelerate-fee-graph.component.scss'],
|
||||
})
|
||||
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() estimate: AccelerationEstimate;
|
||||
@Input() maxRateOptions: RateOption[] = [];
|
||||
@Input() maxRateIndex: number = 0;
|
||||
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
|
||||
|
||||
bars: GraphBar[] = [];
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
if (!this.tx || !this.estimate) {
|
||||
return;
|
||||
}
|
||||
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
|
||||
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
|
||||
const baseHeight = baseRate / maxRate;
|
||||
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
|
||||
return {
|
||||
rate: option.rate,
|
||||
style: this.getStyle(option.rate, maxRate, baseHeight),
|
||||
class: 'max',
|
||||
label: 'max',
|
||||
active: option.index === this.maxRateIndex,
|
||||
rateIndex: option.index,
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'expected',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
class: 'tx',
|
||||
label: 'paid',
|
||||
fee: this.estimate.txSummary.effectiveFee,
|
||||
});
|
||||
this.bars = bars;
|
||||
}
|
||||
|
||||
getStyle(rate, maxRate, base) {
|
||||
const top = (rate / maxRate);
|
||||
return {
|
||||
height: `${(top - base) * 100}%`,
|
||||
bottom: base ? `${base * 100}%` : '0',
|
||||
}
|
||||
}
|
||||
|
||||
onClick(event, bar): void {
|
||||
if (bar.rateIndex != null) {
|
||||
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||
}
|
||||
}
|
@ -20,7 +20,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="estimate">
|
||||
<div class="accelerate-cols">
|
||||
<app-accelerate-fee-graph
|
||||
[tx]="tx"
|
||||
[estimate]="estimate"
|
||||
[maxRateOptions]="maxRateOptions"
|
||||
[maxRateIndex]="selectFeeRateIndex"
|
||||
(setUserBid)="setUserBid($event)"
|
||||
></app-accelerate-fee-graph>
|
||||
|
||||
<ng-container *ngIf="estimate">
|
||||
<div [class]="{estimateDisabled: error}">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
@ -97,10 +106,11 @@
|
||||
<div class="form-group">
|
||||
<div class="fee-card">
|
||||
<div class="d-flex mb-2">
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === 1}" (click)="setUserBid(2, 1)">{{ (estimate.txSummary.effectiveFee + minExtraCost * 2) / estimate.txSummary.effectiveVsize | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></button>
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === 2}" (click)="setUserBid(5, 2)">{{ (estimate.txSummary.effectiveFee + minExtraCost * 5) / estimate.txSummary.effectiveVsize | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></button>
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === 3}" (click)="setUserBid(10, 3)">{{ (estimate.txSummary.effectiveFee + minExtraCost * 10) / estimate.txSummary.effectiveVsize | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></button>
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === 4}" (click)="setUserBid(20, 4)">{{ (estimate.txSummary.effectiveFee + minExtraCost * 20) / estimate.txSummary.effectiveVsize | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></button>
|
||||
<ng-container *ngFor="let option of maxRateOptions">
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
|
||||
{{ option.rate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -228,4 +238,5 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
@ -25,3 +25,10 @@
|
||||
text-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.accelerate-cols {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
margin-top: 1em;
|
||||
}
|
@ -2,6 +2,7 @@ import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges } from '@
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Subscription, catchError, of, tap } from 'rxjs';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
txSummary: TxSummary;
|
||||
@ -20,17 +21,24 @@ export type TxSummary = {
|
||||
ancestorCount: number; // Number of ancestors
|
||||
}
|
||||
|
||||
export interface RateOption {
|
||||
fee: number;
|
||||
rate: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_BID_RATIO = 5;
|
||||
export const MIN_BID_RATIO = 2;
|
||||
export const MAX_BID_RATIO = 20;
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-preview',
|
||||
templateUrl: 'accelerate-preview.component.html',
|
||||
styleUrls: ['accelerate-preview.component.scss']
|
||||
})
|
||||
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() txid: string | undefined;
|
||||
@Input() tx: Transaction | undefined;
|
||||
@Input() scrollEvent: boolean;
|
||||
|
||||
math = Math;
|
||||
@ -47,6 +55,8 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
userBid = 0;
|
||||
selectFeeRateIndex = 2;
|
||||
|
||||
maxRateOptions: RateOption[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private storageService: StorageService
|
||||
@ -65,7 +75,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.estimateSubscription = this.apiService.estimate$(this.txid).pipe(
|
||||
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
|
||||
tap((response) => {
|
||||
if (response.status === 204) {
|
||||
this.estimate = undefined;
|
||||
@ -88,8 +98,15 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
|
||||
// Make min extra fee at least 50% of the current tx fee
|
||||
this.minExtraCost = Math.max(this.estimate.cost, this.estimate.txSummary.effectiveFee / 2);
|
||||
this.minExtraCost = Math.round(this.minExtraCost);
|
||||
this.minExtraCost = Math.round(Math.max(this.estimate.cost, this.estimate.txSummary.effectiveFee / 2));
|
||||
|
||||
this.maxRateOptions = [2, 5, 10, 20].map((multiplier, index) => {
|
||||
return {
|
||||
fee: this.minExtraCost * multiplier,
|
||||
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
|
||||
index,
|
||||
};
|
||||
});
|
||||
|
||||
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
|
||||
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
|
||||
@ -121,10 +138,10 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
/**
|
||||
* User changed his bid
|
||||
*/
|
||||
setUserBid(multiplier: number, index: number) {
|
||||
setUserBid({ fee, index }: { fee: number, index: number}) {
|
||||
if (this.estimate) {
|
||||
this.selectFeeRateIndex = index;
|
||||
this.userBid = Math.max(0, this.minExtraCost * multiplier);
|
||||
this.userBid = Math.max(0, fee);
|
||||
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
}
|
||||
}
|
||||
@ -156,7 +173,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.accelerationSubscription.unsubscribe();
|
||||
}
|
||||
this.accelerationSubscription = this.apiService.accelerate$(
|
||||
this.txid,
|
||||
this.tx.txid,
|
||||
this.userBid
|
||||
).subscribe({
|
||||
next: () => {
|
||||
|
@ -84,7 +84,7 @@
|
||||
<h2>Accelerate</h2>
|
||||
</div>
|
||||
<div class="box">
|
||||
<app-accelerate-preview [txid]="txId" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
|
||||
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -94,6 +94,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
|
||||
import { GlobalFooterComponent } from './components/global-footer/global-footer.component';
|
||||
import { AcceleratePreviewComponent } from '../components/accelerate-preview/accelerate-preview.component';
|
||||
import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component';
|
||||
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
|
||||
|
||||
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
|
||||
@ -192,6 +193,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
TestnetAlertComponent,
|
||||
GlobalFooterComponent,
|
||||
AcceleratePreviewComponent,
|
||||
AccelerateFeeGraphComponent,
|
||||
CalculatorComponent,
|
||||
BitcoinsatoshisPipe,
|
||||
MempoolBlockOverviewComponent,
|
||||
@ -315,6 +317,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
PreviewTitleComponent,
|
||||
GlobalFooterComponent,
|
||||
AcceleratePreviewComponent,
|
||||
AccelerateFeeGraphComponent,
|
||||
MempoolErrorComponent,
|
||||
|
||||
MempoolBlockOverviewComponent,
|
||||
|
Loading…
x
Reference in New Issue
Block a user