From 8fee1955771cd2f5dd5ed13ce970ec2349bd496d Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 21 Mar 2024 16:44:07 +0900 Subject: [PATCH 1/3] [accelerator] prepaid acceleration --- .../accelerate-preview.component.html | 12 +- .../accelerate-preview.component.ts | 245 +++++++++++++----- .../master-page/master-page.component.ts | 2 - .../src/app/services/services-api.service.ts | 8 + 4 files changed, 205 insertions(+), 62 deletions(-) diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html index a848a645b..34881eba5 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html @@ -219,7 +219,7 @@ - + @@ -242,13 +242,21 @@ -
+
+ +
+
+
+ Loading +
+
+
diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts index 3e8dbb6ff..68763fbbe 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts @@ -1,5 +1,4 @@ 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 { StorageService } from '../../services/storage.service'; import { Transaction } from '../../interfaces/electrs.interface'; @@ -40,7 +39,7 @@ export const MAX_BID_RATIO = 4; templateUrl: 'accelerate-preview.component.html', styleUrls: ['accelerate-preview.component.scss'] }) -export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges { +export class AcceleratePreviewComponent implements OnDestroy, OnChanges { @Input() tx: Transaction | undefined; @Input() scrollEvent: boolean; @@ -63,18 +62,37 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges maxRateOptions: RateOption[] = []; + // Cashapp payment + paymentType: 'bitcoin' | 'cashapp' = 'bitcoin'; + cashAppSubscription: Subscription; + conversionsSubscription: Subscription; + payments: any; + showSpinner = false; + square: any; + cashAppPay: any; + hideCashApp = false; + constructor( public stateService: StateService, private servicesApiService: ServicesApiServices, private storageService: StorageService, private audioService: AudioService, private cd: ChangeDetectorRef - ) { } + ) { + if (window.document.referrer === 'cash.app') { + this.paymentType = 'cashapp'; + } else { + this.paymentType = 'bitcoin'; + } + } ngOnDestroy(): void { if (this.estimateSubscription) { this.estimateSubscription.unsubscribe(); } + if (this.cashAppPay) { + this.cashAppPay.destroy(); + } } ngOnChanges(changes: SimpleChanges): void { @@ -83,69 +101,85 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges } } - ngOnInit() { + ngAfterViewInit() { + this.showSpinner = true; + this.user = this.storageService.getAuth()?.user ?? null; - this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe( - 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.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; this.error = `cannot_accelerate_tx`; this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.estimateSubscription.unsubscribe(); - } - - if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { - if (this.isLoggedIn()) { - this.error = `not_enough_balance`; + } else { + this.estimate = response.body; + if (!this.estimate) { + this.error = `cannot_accelerate_tx`; 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; - - // 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'); - } - } - }), - catchError((response) => { - this.estimate = undefined; - this.error = response.error; - this.scrollToPreviewWithTimeout('mempoolError', 'center'); - this.estimateSubscription.unsubscribe(); - return of(null); - }) - ).subscribe(); + }), + catchError((response) => { + this.estimate = undefined; + this.error = response.error; + this.scrollToPreviewWithTimeout('mempoolError', 'center'); + this.estimateSubscription.unsubscribe(); + return of(null); + }) + ).subscribe(); + }); } /** @@ -216,4 +250,99 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges onResize(): void { 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 { + 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://my.website/checkout', + 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'); + } + }); + } + }); + } + ); + } } diff --git a/frontend/src/app/components/master-page/master-page.component.ts b/frontend/src/app/components/master-page/master-page.component.ts index 6f376f923..f3472f204 100644 --- a/frontend/src/app/components/master-page/master-page.component.ts +++ b/frontend/src/app/components/master-page/master-page.component.ts @@ -7,7 +7,6 @@ import { EnterpriseService } from '../../services/enterprise.service'; import { NavigationService } from '../../services/navigation.service'; import { MenuComponent } from '../menu/menu.component'; import { StorageService } from '../../services/storage.service'; -import { ApiService } from '../../services/api.service'; @Component({ selector: 'app-master-page', @@ -45,7 +44,6 @@ export class MasterPageComponent implements OnInit, OnDestroy { private enterpriseService: EnterpriseService, private navigationService: NavigationService, private storageService: StorageService, - private apiService: ApiService, private router: Router, ) { } diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index ad8af5536..e2cef2cc1 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -141,6 +141,10 @@ export class ServicesApiServices { return this.httpClient.post(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid }); } + accelerateWithCashApp$(txInput: string, userBid: number, token: string, cashtag: string, referenceId: string) { + return this.httpClient.post(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, userBid: userBid, token: token, cashtag: cashtag, referenceId: referenceId }); + } + getAccelerations$(): Observable { return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations`); } @@ -160,4 +164,8 @@ export class ServicesApiServices { getAccelerationStats$(): Observable { return this.httpClient.get(`${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`); + } } From 1eb52d8a355e8125964c641df257416bf492c5b1 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 21 Mar 2024 19:08:48 +0900 Subject: [PATCH 2/3] [accelerator] fix redirection link --- .../accelerate-preview/accelerate-preview.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts index 68763fbbe..83eb7c678 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts @@ -304,7 +304,7 @@ export class AcceleratePreviewComponent implements OnDestroy, OnChanges { } }); this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { - redirectURL: 'https://my.website/checkout', + 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'); From bcbd21b922b89b8a9e931ab5e4fb0511ff1e05ae Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 5 Apr 2024 11:16:48 +0900 Subject: [PATCH 3/3] [acclerator] load square for prepaid acceleration --- .../accelerate-preview.component.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts index 83eb7c678..219b68f88 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts @@ -80,6 +80,7 @@ export class AcceleratePreviewComponent implements OnDestroy, OnChanges { private cd: ChangeDetectorRef ) { if (window.document.referrer === 'cash.app') { + this.insertSquare(); this.paymentType = 'cashapp'; } else { this.paymentType = 'bitcoin'; @@ -345,4 +346,17 @@ export class AcceleratePreviewComponent implements OnDestroy, OnChanges { } ); } + + 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); + })(); + } }