Merge pull request #4804 from mempool/nymkappa/prepaid-acceleration

[accelerator] prepaid acceleration
This commit is contained in:
softsimon 2024-04-05 17:32:46 +09:00 committed by GitHub
commit 10a41fb0d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 219 additions and 62 deletions

View File

@ -206,7 +206,7 @@
</ng-container> </ng-container>
<!-- LOGIN CTA --> <!-- LOGIN CTA -->
<ng-container *ngIf="stateService.isMempoolSpaceBuild && !isLoggedIn()"> <ng-container *ngIf="stateService.isMempoolSpaceBuild && !isLoggedIn() && paymentType === 'bitcoin'">
<tr class="group-first group-last" style="border-top: 1px dashed grey"> <tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td> <td class="item"></td>
<td class="amt"></td> <td class="amt"></td>
@ -229,13 +229,21 @@
</div> </div>
</div> </div>
<div class="row mb-3" *ngIf="isLoggedIn()"> <div class="row mb-3" *ngIf="isLoggedIn() && paymentType === 'bitcoin'">
<div class="col"> <div class="col">
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess"> <div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button> <button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button>
</div> </div>
</div> </div>
</div> </div>
<div class="row d-flex justify-content-end mr-1" style="height: 48px" *ngIf="!hideCashApp">
<div id="cash-app-pay" style="max-width: 320px" [style]="showSpinner ? 'opacity: 0' : 'opacity: 1'"></div>
<div *ngIf="showSpinner" class="d-flex align-items-center">
<span class="mr-2">Loading</span>
<div class="spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,5 +1,4 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs'; import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
@ -40,7 +39,7 @@ export const MAX_BID_RATIO = 4;
templateUrl: 'accelerate-preview.component.html', templateUrl: 'accelerate-preview.component.html',
styleUrls: ['accelerate-preview.component.scss'] styleUrls: ['accelerate-preview.component.scss']
}) })
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges { export class AcceleratePreviewComponent implements OnDestroy, OnChanges {
@Input() tx: Transaction | undefined; @Input() tx: Transaction | undefined;
@Input() scrollEvent: boolean; @Input() scrollEvent: boolean;
@ -63,18 +62,38 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxRateOptions: RateOption[] = []; maxRateOptions: RateOption[] = [];
// Cashapp payment
paymentType: 'bitcoin' | 'cashapp' = 'bitcoin';
cashAppSubscription: Subscription;
conversionsSubscription: Subscription;
payments: any;
showSpinner = false;
square: any;
cashAppPay: any;
hideCashApp = false;
constructor( constructor(
public stateService: StateService, public stateService: StateService,
private servicesApiService: ServicesApiServices, private servicesApiService: ServicesApiServices,
private storageService: StorageService, private storageService: StorageService,
private audioService: AudioService, private audioService: AudioService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef
) { } ) {
if (window.document.referrer === 'cash.app') {
this.insertSquare();
this.paymentType = 'cashapp';
} else {
this.paymentType = 'bitcoin';
}
}
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.estimateSubscription) { if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
} }
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@ -83,69 +102,85 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
} }
} }
ngOnInit() { ngAfterViewInit() {
this.showSpinner = true;
this.user = this.storageService.getAuth()?.user ?? null; this.user = this.storageService.getAuth()?.user ?? null;
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe( this.servicesApiService.setupSquare$().subscribe(ids => {
tap((response) => { this.square = {
if (response.status === 204) { appId: ids.squareAppId,
this.estimate = undefined; locationId: ids.squareLocationId
this.error = `cannot_accelerate_tx`; };
this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
this.estimateSubscription.unsubscribe(); tap((response) => {
} else { if (response.status === 204) {
this.estimate = response.body; this.estimate = undefined;
if (!this.estimate) {
this.error = `cannot_accelerate_tx`; this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
} } else {
this.estimate = response.body;
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { if (!this.estimate) {
if (this.isLoggedIn()) { this.error = `cannot_accelerate_tx`;
this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
}
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) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
if (this.paymentType === 'cashapp') {
this.setupSquare();
}
} }
} }
}),
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1; catchError((response) => {
this.estimate = undefined;
// Make min extra fee at least 50% of the current tx fee this.error = response.error;
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee)); this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => { return of(null);
return { })
fee: this.minExtraCost * multiplier, ).subscribe();
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) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
}),
catchError((response) => {
this.estimate = undefined;
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
return of(null);
})
).subscribe();
} }
/** /**
@ -216,4 +251,112 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
onResize(): void { onResize(): void {
this.isMobile = window.innerWidth <= 767.98; this.isMobile = window.innerWidth <= 767.98;
} }
/**
* 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) => {
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,
productUrl: `https://mempool.space/tx/${this.tx.txid}`,
}
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `https://mempool.space/tx/${this.tx.txid}`,
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
});
await this.cashAppPay.attach('#cash-app-pay');
this.showSpinner = false;
const that = this;
this.cashAppPay.addEventListener('ontokenization', function (event) {
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,
that.userBid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId
).subscribe({
next: () => {
that.audioService.playSound('ascend-chime-cartoon');
that.showSuccess = true;
that.scrollToPreviewWithTimeout('successAlert', 'center');
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');
}
});
}
});
}
);
}
insertSquare(): void {
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
if (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);
})();
}
} }

View File

@ -7,7 +7,6 @@ import { EnterpriseService } from '../../services/enterprise.service';
import { NavigationService } from '../../services/navigation.service'; import { NavigationService } from '../../services/navigation.service';
import { MenuComponent } from '../menu/menu.component'; import { MenuComponent } from '../menu/menu.component';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { ApiService } from '../../services/api.service';
@Component({ @Component({
selector: 'app-master-page', selector: 'app-master-page',
@ -45,7 +44,6 @@ export class MasterPageComponent implements OnInit, OnDestroy {
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
private navigationService: NavigationService, private navigationService: NavigationService,
private storageService: StorageService, private storageService: StorageService,
private apiService: ApiService,
private router: Router, private router: Router,
) { } ) { }

View File

@ -132,6 +132,10 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid }); return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid });
} }
accelerateWithCashApp$(txInput: string, userBid: number, token: string, cashtag: string, referenceId: string) {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, userBid: userBid, token: token, cashtag: cashtag, referenceId: referenceId });
}
getAccelerations$(): Observable<Acceleration[]> { getAccelerations$(): Observable<Acceleration[]> {
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`); return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
} }
@ -151,4 +155,8 @@ export class ServicesApiServices {
getAccelerationStats$(): Observable<AccelerationStats> { getAccelerationStats$(): Observable<AccelerationStats> {
return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`); return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`);
} }
setupSquare$(): Observable<{squareAppId: string, squareLocationId: string}> {
return this.httpClient.get<{squareAppId: string, squareLocationId: string}>(`${SERVICES_API_PREFIX}/square/setup`);
}
} }