mempool/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts

439 lines
14 KiB
TypeScript
Raw Normal View History

2024-04-08 08:00:00 +00:00
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef, ViewChild, ElementRef } from '@angular/core';
2023-08-26 09:52:55 +02:00
import { Subscription, catchError, of, tap } from 'rxjs';
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';
import { ServicesApiServices } from '../../services/services-api.service';
2023-12-15 23:09:24 +07:00
import { AudioService } from '../../services/audio.service';
import { StateService } from '../../services/state.service';
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']
})
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;
2024-04-08 08:00:00 +00:00
@ViewChild('cashappCTA')
cashappCTA: ElementRef;
2023-08-26 09:52:55 +02:00
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;
accelerationUUID: string;
2023-08-30 16:49:54 +09:00
selectFeeRateIndex = 1;
2023-08-30 17:16:39 +09:00
isMobile: boolean = window.innerWidth <= 767.98;
user: any = undefined;
2024-04-08 08:00:00 +00:00
stickyCTA: string = 'non-stick';
2023-08-24 14:17:31 +02:00
2023-08-29 21:20:36 +09:00
maxRateOptions: RateOption[] = [];
2024-03-21 16:44:07 +09:00
// Cashapp payment
paymentType: 'bitcoin' | 'cashapp' = 'bitcoin';
cashAppSubscription: Subscription;
conversionsSubscription: Subscription;
2024-04-08 08:00:00 +00:00
cashappSubmit: any;
2024-03-21 16:44:07 +09:00
payments: any;
showSpinner = false;
square: any;
cashAppPay: any;
hideCashApp = false;
2024-04-10 14:33:33 +09:00
processingPayment = false;
2024-03-21 16:44:07 +09:00
2023-08-24 14:17:31 +02:00
constructor(
public stateService: StateService,
private servicesApiService: ServicesApiServices,
private storageService: StorageService,
2023-12-15 23:09:24 +07:00
private audioService: AudioService,
private cd: ChangeDetectorRef
2024-03-21 16:44:07 +09:00
) {
2024-04-10 14:33:33 +09:00
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) {
this.processingPayment = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
}
if (this.stateService.ref === 'https://cash.app/') {
2024-04-08 08:26:45 +00:00
this.paymentType = 'cashapp';
this.insertSquare();
2024-04-08 08:26:45 +00:00
} else {
this.paymentType = 'bitcoin';
2024-03-21 16:44:07 +09:00
}
}
2023-08-24 14:17:31 +02:00
2023-08-26 09:52:55 +02:00
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
2024-03-21 16:44:07 +09:00
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
2023-08-26 09:52:55 +02:00
}
ngOnInit() {
this.accelerationUUID = window.crypto.randomUUID();
if (this.stateService.ref === 'https://cash.app/') {
this.paymentType = 'cashapp';
} else {
this.paymentType = 'bitcoin';
}
}
2023-08-26 09:52:55 +02:00
ngOnChanges(changes: SimpleChanges): void {
2024-04-08 08:26:45 +00:00
if (changes.scrollEvent && this.paymentType !== 'cashapp' && this.stateService.ref !== 'https://cash.app/') {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
2023-08-26 09:52:55 +02:00
}
}
2024-03-21 16:44:07 +09:00
ngAfterViewInit() {
2024-04-08 08:26:45 +00:00
this.onScroll();
2024-04-05 17:52:07 +09:00
if (this.paymentType === 'cashapp') {
this.showSpinner = true;
}
2024-03-21 16:44:07 +09:00
this.user = this.storageService.getAuth()?.user ?? null;
2024-03-21 16:44:07 +09:00
this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.estimate = undefined;
2023-08-26 09:52:55 +02:00
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
2024-03-21 16:44:07 +09:00
} else {
this.estimate = response.body;
if (!this.estimate) {
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
2024-03-21 16:44:07 +09:00
this.estimateSubscription.unsubscribe();
}
2023-08-30 16:49:54 +09:00
2024-03-21 16:44:07 +09:00
if (this.paymentType === 'cashapp') {
this.estimate.userBalance = 999999999;
this.estimate.enoughBalance = true;
}
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
// Make min extra fee at least 50% of the current tx fee
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
this.maxRateOptions = [1, 2, 4].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.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
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) {
if (this.paymentType === 'cashapp') {
this.setupSquare();
2024-04-08 08:00:00 +00:00
} else {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
2024-04-08 08:26:45 +00:00
setTimeout(() => {
this.onScroll();
}, 100);
2024-03-21 16:44:07 +09:00
}
2023-08-26 09:52:55 +02:00
}
2024-03-21 16:44:07 +09:00
}),
catchError((response) => {
this.estimate = undefined;
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
return of(null);
})
).subscribe();
});
2023-08-26 09:52:55 +02:00
}
/**
* 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) {
this.cd.markForCheck();
2023-08-26 09:52:55 +02:00
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
}
}
2023-08-26 09:52:55 +02:00
/**
* Send acceleration request
*/
accelerate() {
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.servicesApiService.accelerate$(
2023-08-29 21:20:36 +09:00
this.tx.txid,
this.userBid,
this.accelerationUUID
2023-08-26 09:52:55 +02:00
).subscribe({
next: () => {
2023-12-15 23:09:24 +07:00
this.audioService.playSound('ascend-chime-cartoon');
2023-08-26 09:52:55 +02:00
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
},
error: (response) => {
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
}
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;
}
2024-03-21 16:44:07 +09:00
/**
* CashApp payment
*/
setupSquare() {
const init = () => {
this.initSquare();
};
//@ts-ignore
if (!window.Square) {
console.warn('Square.js failed to load properly. Retrying in 1 second.');
setTimeout(init, 1000);
} else {
init();
}
}
async initSquare(): Promise<void> {
try {
//@ts-ignore
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
await this.requestCashAppPayment();
} catch (e) {
console.error(e);
this.error = 'Error loading Square Payments';
return;
}
}
async requestCashAppPayment() {
if (this.cashAppSubscription) {
this.cashAppSubscription.unsubscribe();
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.hideCashApp = false;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
2024-04-10 14:33:33 +09:00
const redirectHostname = document.location.hostname === 'localhost' ? 'http://localhost:4200': 'https://mempool.space';
2024-03-21 16:44:07 +09:00
const maxCostUsd = this.maxCost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: maxCostUsd.toString(),
label: 'Total',
pending: true,
2024-04-10 14:33:33 +09:00
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
2024-04-08 08:00:00 +00:00
},
button: { shape: 'semiround', size: 'small', theme: 'light'}
2024-03-21 16:44:07 +09:00
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
2024-04-10 14:33:33 +09:00
redirectURL: `${redirectHostname}/tx/${this.tx.txid}?acceleration=false`,
2024-03-21 16:44:07 +09:00
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
2024-04-08 08:00:00 +00:00
button: { shape: 'semiround', size: 'small', theme: 'light'}
2024-03-21 16:44:07 +09:00
});
2024-04-08 08:00:00 +00:00
const renderPromise = this.cashAppPay.CashAppPayInstance.render('#cash-app-pay', { button: { theme: 'light', size: 'small', shape: 'semiround' }, manage: false });
2024-03-21 16:44:07 +09:00
this.showSpinner = false;
const that = this;
this.cashAppPay.addEventListener('ontokenization', function (event) {
2024-04-10 14:33:33 +09:00
that.processingPayment = true;
that.scrollToPreviewWithTimeout('successAlert', 'center');
2024-03-21 16:44:07 +09:00
const { tokenResult, error } = event.detail;
if (error) {
this.error = error;
} else if (tokenResult.status === 'OK') {
that.hideCashApp = true;
that.accelerationSubscription = that.servicesApiService.accelerateWithCashApp$(
that.tx.txid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
that.accelerationUUID
2024-03-21 16:44:07 +09:00
).subscribe({
next: () => {
that.audioService.playSound('ascend-chime-cartoon');
that.showSuccess = true;
that.estimateSubscription.unsubscribe();
},
error: (response) => {
if (response.status === 403 && response.error === 'not_available') {
that.error = 'waitlisted';
} else {
that.error = response.error;
}
that.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});
}
});
2024-04-08 08:00:00 +00:00
this.cashappSubmit = await renderPromise;
2024-03-21 16:44:07 +09:00
}
);
}
insertSquare(): void {
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
if (document.location.hostname === 'mempool-staging.fmt.mempool.space' ||
document.location.hostname === 'mempool-staging.va1.mempool.space' ||
document.location.hostname === 'mempool-staging.fra.mempool.space' ||
document.location.hostname === 'mempool-staging.tk7.mempool.space' ||
document.location.hostname === 'mempool.space') {
statsUrl = 'https://web.squarecdn.com/v1/square.js';
}
(function() {
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
// @ts-ignore
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
})();
}
2024-04-08 08:00:00 +00:00
submitCashappPay(): void {
if (this.cashappSubmit) {
this.cashappSubmit?.begin();
}
}
@HostListener('window:scroll', ['$event']) // for window scroll events
onScroll() {
2024-04-08 09:25:42 +00:00
if (this.estimate && !this.cashappCTA?.nativeElement) {
2024-04-08 08:26:45 +00:00
setTimeout(() => {
this.onScroll();
}, 200);
return;
}
2024-04-08 08:00:00 +00:00
if (!this.cashappCTA?.nativeElement || this.paymentType !== 'cashapp' || !this.isMobile) {
return;
}
const cta = this.cashappCTA.nativeElement;
const rect = cta.getBoundingClientRect();
const topOffset = window.innerWidth <= 572 ? 102 : 62;
const bottomOffset = window.innerWidth < 430 ? 50 : 56;
if (rect.top < topOffset) {
this.stickyCTA = 'sticky-top';
} else if (rect.top > window.innerHeight - (bottomOffset + 54)) {
this.stickyCTA = 'sticky-bottom';
} else {
this.stickyCTA = 'non-stick';
}
}
}