2023-08-30 17:16:39 +09:00
|
|
|
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
|
2023-09-03 12:20:30 +03:00
|
|
|
import { Router } from '@angular/router';
|
2023-08-26 09:52:55 +02:00
|
|
|
import { ApiService } from '../../services/api.service';
|
|
|
|
import { Subscription, catchError, of, tap } from 'rxjs';
|
2023-08-26 15:07:05 +02:00
|
|
|
import { StorageService } from '../../services/storage.service';
|
2023-08-29 21:20:36 +09:00
|
|
|
import { Transaction } from '../../interfaces/electrs.interface';
|
2023-08-30 16:49:54 +09:00
|
|
|
import { nextRoundNumber } from '../../shared/common.utils';
|
2023-08-26 09:52:55 +02:00
|
|
|
|
|
|
|
export type AccelerationEstimate = {
|
|
|
|
txSummary: TxSummary;
|
|
|
|
nextBlockFee: number;
|
|
|
|
targetFeeRate: number;
|
|
|
|
userBalance: number;
|
|
|
|
enoughBalance: boolean;
|
|
|
|
cost: number;
|
|
|
|
mempoolBaseFee: number;
|
|
|
|
vsizeFee: number;
|
|
|
|
}
|
|
|
|
export type TxSummary = {
|
|
|
|
txid: string; // txid of the current transaction
|
|
|
|
effectiveVsize: number; // Total vsize of the dependency tree
|
|
|
|
effectiveFee: number; // Total fee of the dependency tree in sats
|
|
|
|
ancestorCount: number; // Number of ancestors
|
|
|
|
}
|
|
|
|
|
2023-08-29 21:20:36 +09:00
|
|
|
export interface RateOption {
|
|
|
|
fee: number;
|
|
|
|
rate: number;
|
|
|
|
index: number;
|
|
|
|
}
|
|
|
|
|
2023-08-31 00:32:54 +09:00
|
|
|
export const MIN_BID_RATIO = 1;
|
|
|
|
export const DEFAULT_BID_RATIO = 2;
|
|
|
|
export const MAX_BID_RATIO = 4;
|
2023-08-29 21:20:36 +09:00
|
|
|
|
2023-08-24 14:17:31 +02:00
|
|
|
@Component({
|
2023-08-26 09:52:55 +02:00
|
|
|
selector: 'app-accelerate-preview',
|
2023-08-24 14:17:31 +02:00
|
|
|
templateUrl: 'accelerate-preview.component.html',
|
|
|
|
styleUrls: ['accelerate-preview.component.scss']
|
|
|
|
})
|
2023-08-26 09:52:55 +02:00
|
|
|
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
|
2023-08-29 21:20:36 +09:00
|
|
|
@Input() tx: Transaction | undefined;
|
2023-08-26 09:52:55 +02:00
|
|
|
@Input() scrollEvent: boolean;
|
|
|
|
|
|
|
|
math = Math;
|
|
|
|
error = '';
|
|
|
|
showSuccess = false;
|
|
|
|
estimateSubscription: Subscription;
|
|
|
|
accelerationSubscription: Subscription;
|
|
|
|
estimate: any;
|
2023-08-30 16:49:54 +09:00
|
|
|
hasAncestors: boolean = false;
|
2023-08-26 09:52:55 +02:00
|
|
|
minExtraCost = 0;
|
|
|
|
minBidAllowed = 0;
|
|
|
|
maxBidAllowed = 0;
|
|
|
|
defaultBid = 0;
|
|
|
|
maxCost = 0;
|
|
|
|
userBid = 0;
|
2023-08-30 16:49:54 +09:00
|
|
|
selectFeeRateIndex = 1;
|
2023-08-30 17:16:39 +09:00
|
|
|
isMobile: boolean = window.innerWidth <= 767.98;
|
2023-08-24 14:17:31 +02:00
|
|
|
|
2023-08-29 21:20:36 +09:00
|
|
|
maxRateOptions: RateOption[] = [];
|
|
|
|
|
2023-08-24 14:17:31 +02:00
|
|
|
constructor(
|
2023-08-26 15:07:05 +02:00
|
|
|
private apiService: ApiService,
|
2023-09-03 12:20:30 +03:00
|
|
|
private storageService: StorageService,
|
|
|
|
private router: Router,
|
2023-08-24 14:17:31 +02:00
|
|
|
) { }
|
|
|
|
|
2023-08-26 09:52:55 +02:00
|
|
|
ngOnDestroy(): void {
|
|
|
|
if (this.estimateSubscription) {
|
|
|
|
this.estimateSubscription.unsubscribe();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ngOnChanges(changes: SimpleChanges): void {
|
|
|
|
if (changes.scrollEvent) {
|
2023-09-03 12:20:30 +03:00
|
|
|
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
2023-08-26 09:52:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-24 14:17:31 +02:00
|
|
|
ngOnInit() {
|
2023-08-29 21:20:36 +09:00
|
|
|
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
|
2023-08-26 09:52:55 +02:00
|
|
|
tap((response) => {
|
|
|
|
if (response.status === 204) {
|
|
|
|
this.estimate = undefined;
|
|
|
|
this.error = `cannot_accelerate_tx`;
|
|
|
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
|
|
|
this.estimateSubscription.unsubscribe();
|
|
|
|
} else {
|
|
|
|
this.estimate = response.body;
|
|
|
|
if (!this.estimate) {
|
|
|
|
this.error = `cannot_accelerate_tx`;
|
|
|
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
|
|
|
this.estimateSubscription.unsubscribe();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.estimate.userBalance <= 0) {
|
2023-08-26 15:07:05 +02:00
|
|
|
if (this.isLoggedIn()) {
|
|
|
|
this.error = `not_enough_balance`;
|
|
|
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
|
|
|
}
|
2023-08-26 09:52:55 +02:00
|
|
|
}
|
2023-08-30 16:49:54 +09:00
|
|
|
|
|
|
|
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
|
2023-08-26 09:52:55 +02:00
|
|
|
|
|
|
|
// Make min extra fee at least 50% of the current tx fee
|
2023-08-30 16:49:54 +09:00
|
|
|
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
|
2023-08-29 21:20:36 +09:00
|
|
|
|
2023-08-30 16:49:54 +09:00
|
|
|
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
|
2023-08-29 21:20:36 +09:00
|
|
|
return {
|
|
|
|
fee: this.minExtraCost * multiplier,
|
|
|
|
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
|
|
|
|
index,
|
|
|
|
};
|
|
|
|
});
|
2023-08-26 09:52:55 +02:00
|
|
|
|
|
|
|
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
|
|
|
|
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
|
2023-08-31 00:32:54 +09:00
|
|
|
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
|
2023-08-26 09:52:55 +02:00
|
|
|
|
|
|
|
this.userBid = this.defaultBid;
|
|
|
|
if (this.userBid < this.minBidAllowed) {
|
|
|
|
this.userBid = this.minBidAllowed;
|
|
|
|
} else if (this.userBid > this.maxBidAllowed) {
|
|
|
|
this.userBid = this.maxBidAllowed;
|
|
|
|
}
|
|
|
|
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
|
|
|
|
|
|
|
if (!this.error) {
|
2023-09-03 12:20:30 +03:00
|
|
|
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
2023-08-26 09:52:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
catchError((response) => {
|
|
|
|
this.estimate = undefined;
|
|
|
|
this.error = response.error;
|
|
|
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
|
|
|
this.estimateSubscription.unsubscribe();
|
|
|
|
return of(null);
|
|
|
|
})
|
|
|
|
).subscribe();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* User changed his bid
|
|
|
|
*/
|
2023-08-29 21:20:36 +09:00
|
|
|
setUserBid({ fee, index }: { fee: number, index: number}) {
|
2023-08-26 09:52:55 +02:00
|
|
|
if (this.estimate) {
|
|
|
|
this.selectFeeRateIndex = index;
|
2023-08-29 21:20:36 +09:00
|
|
|
this.userBid = Math.max(0, fee);
|
2023-08-26 09:52:55 +02:00
|
|
|
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Scroll to element id with or without setTimeout
|
|
|
|
*/
|
|
|
|
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
|
|
|
|
setTimeout(() => {
|
|
|
|
this.scrollToPreview(id, position);
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
scrollToPreview(id: string, position: ScrollLogicalPosition) {
|
|
|
|
const acceleratePreviewAnchor = document.getElementById(id);
|
|
|
|
if (acceleratePreviewAnchor) {
|
|
|
|
acceleratePreviewAnchor.scrollIntoView({
|
2023-08-24 14:17:31 +02:00
|
|
|
behavior: 'smooth',
|
2023-08-26 09:52:55 +02:00
|
|
|
inline: position,
|
|
|
|
block: position,
|
2023-08-24 14:17:31 +02:00
|
|
|
});
|
2023-08-26 09:52:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send acceleration request
|
|
|
|
*/
|
|
|
|
accelerate() {
|
|
|
|
if (this.accelerationSubscription) {
|
|
|
|
this.accelerationSubscription.unsubscribe();
|
|
|
|
}
|
|
|
|
this.accelerationSubscription = this.apiService.accelerate$(
|
2023-08-29 21:20:36 +09:00
|
|
|
this.tx.txid,
|
2023-08-26 09:52:55 +02:00
|
|
|
this.userBid
|
|
|
|
).subscribe({
|
|
|
|
next: () => {
|
|
|
|
this.showSuccess = true;
|
|
|
|
this.scrollToPreviewWithTimeout('successAlert', 'center');
|
|
|
|
this.estimateSubscription.unsubscribe();
|
|
|
|
},
|
|
|
|
error: (response) => {
|
2023-11-19 17:16:05 +09:00
|
|
|
if (response.status === 403 && response.error === 'not_available') {
|
|
|
|
this.error = 'waitlisted';
|
|
|
|
} else {
|
|
|
|
this.error = response.error;
|
|
|
|
}
|
2023-08-26 09:52:55 +02:00
|
|
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
|
|
|
}
|
|
|
|
});
|
2023-08-24 14:17:31 +02:00
|
|
|
}
|
2023-08-26 15:07:05 +02:00
|
|
|
|
|
|
|
isLoggedIn() {
|
|
|
|
const auth = this.storageService.getAuth();
|
|
|
|
return auth !== null;
|
|
|
|
}
|
2023-08-30 17:16:39 +09:00
|
|
|
|
|
|
|
@HostListener('window:resize', ['$event'])
|
|
|
|
onResize(): void {
|
|
|
|
this.isMobile = window.innerWidth <= 767.98;
|
|
|
|
}
|
2023-08-24 14:17:31 +02:00
|
|
|
}
|