From 8c07e3c31a42b270d81cfe338c7d9f2b99a3beae Mon Sep 17 00:00:00 2001 From: natsoni Date: Fri, 19 Jul 2024 16:12:54 +0200 Subject: [PATCH 01/22] Fix recursion loop in search bar --- frontend/src/app/components/search-form/search-form.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index bcd5afb53..3f48861d5 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -303,7 +303,6 @@ export class SearchFormComponent implements OnInit { (error) => { console.log(error); this.isSearching = false; } ); } else { - this.searchResults.searchButtonClick(); this.isSearching = false; } } From 920f225e6ccf48b2b7cf6aeb71ccb6b412d6b347 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 22:17:47 +0200 Subject: [PATCH 02/22] [accelerator] add support for acceleration with apple pay --- .../accelerate-checkout.component.html | 28 ++-- .../accelerate-checkout.component.scss | 18 +- .../accelerate-checkout.component.ts | 154 +++++++++++++++--- .../src/app/services/services-api.service.ts | 4 + frontend/src/app/shared/common.utils.ts | 44 +++++ frontend/src/resources/apple-pay.svg | 84 ++++++++++ 6 files changed, 301 insertions(+), 31 deletions(-) create mode 100755 frontend/src/resources/apple-pay.svg diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index aa45d7bd5..80bdc1fc5 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -395,12 +395,16 @@ } } - @if (canPayWithCashapp) { -
-

Pay  with

- -
- } +
+

Pay  with

+ @if (canPayWithCashapp) { + + } + @if (canPayWithApplePay) { + @if (canPayWithCashapp) {
} + + } +
} @@ -421,7 +425,7 @@ - } @else if (step === 'cashapp') { + } @else if (step === 'cashapp' || step === 'applepay') {
@@ -437,7 +441,7 @@
- @if (!loadingCashapp) { + @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay) {
@@ -456,8 +460,12 @@
-
- @if (loadingCashapp) { + @if (step === 'applepay') { +
+ } @else if (step === 'cashapp') { +
+ } + @if (loadingCashapp || loadingApplePay) {
Loading payment method...
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss index 4e7be2691..b35308384 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -11,8 +11,7 @@ .paymentMethod { padding: 10px; background-color: var(--secondary); - border-radius: 15px; - border: 2px solid var(--bg); + border-radius: 10px; cursor: pointer; } @@ -202,4 +201,19 @@ .btn-error-wrapper { height: 26px; +} + +.apple-pay-button { + display: inline-block; + -webkit-appearance: -apple-pay-button; + -apple-pay-button-type: plain; /* Use any supported button type. */ +} +.apple-pay-button-black { + -apple-pay-button-style: black; +} +.apple-pay-button-white { + -apple-pay-button-style: white; +} +.apple-pay-button-white-with-line { + -apple-pay-button-style: white-outline; } \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 49b12bbee..d73a81bb2 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { ServicesApiServices } from '../../services/services-api.service'; -import { nextRoundNumber } from '../../shared/common.utils'; +import { md5, nextRoundNumber } from '../../shared/common.utils'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; import { ETA, EtaService } from '../../services/eta.service'; @@ -46,7 +46,7 @@ export const MIN_BID_RATIO = 1; export const DEFAULT_BID_RATIO = 2; export const MAX_BID_RATIO = 4; -type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid' | 'success'; +type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'processing' | 'paid' | 'success'; @Component({ selector: 'app-accelerate-checkout', @@ -60,6 +60,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: ETA; @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; + @Input() applePayEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -109,11 +110,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { // square loadingCashapp = false; + loadingApplePay = false; cashappError = false; cashappSubmit: any; payments: any; cashAppPay: any; - cashAppSubscription: Subscription; + applePay: any; conversionsSubscription: Subscription; conversions: any; @@ -212,6 +214,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingCashapp = true; this.insertSquare(); this.setupSquare(); + } else if (this._step === 'applepay' && this.applePayEnabled) { + this.loadingApplePay = true; + this.insertSquare(); + this.setupSquare(); } else if (this._step === 'paid') { this.timePaid = Date.now(); this.timeoutTimer = setTimeout(() => { @@ -229,8 +235,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } /** - * Scroll to element id with or without setTimeout - */ + * Scroll to element id with or without setTimeout + */ scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { setTimeout(() => { this.scrollToElement(id, position); @@ -421,17 +427,112 @@ export class AccelerateCheckout implements OnInit, OnDestroy { try { //@ts-ignore this.payments = window.Square.payments(this.square.appId, this.square.locationId) - await this.requestCashAppPayment(); + if (this._step === 'cashapp') { + await this.requestCashAppPayment(); + } else if (this._step === 'applepay') { + await this.requestApplePayPayment(); + } } catch (e) { console.debug('Error loading Square Payments', e); this.cashappError = true; return; } } - async requestCashAppPayment() { - if (this.cashAppSubscription) { - this.cashAppSubscription.unsubscribe(); + + /** + * APPLE PAY + */ + async requestApplePayPayment() { + if (this.conversionsSubscription) { + this.conversionsSubscription.unsubscribe(); } + + this.conversionsSubscription = this.stateService.conversions$.subscribe( + async (conversions) => { + this.conversions = conversions; + if (this.applePay) { + this.applePay.destroy(); + } + + const costUSD = this.cost / 100_000_000 * conversions.USD; + const paymentRequest = this.payments.paymentRequest({ + countryCode: 'US', + currencyCode: 'USD', + total: { + amount: costUSD.toFixed(2), + label: 'Total', + }, + }); + + try { + this.applePay = await this.payments.applePay(paymentRequest); + const applePayButton = document.getElementById('apple-pay-button'); + if (!applePayButton) { + console.error(`Unable to find apple pay button id='apple-pay-button'`); + // Try again + setTimeout(this.requestApplePayPayment.bind(this), 500); + return; + } + this.loadingApplePay = false; + applePayButton.addEventListener('click', async event => { + event.preventDefault(); + const tokenResult = await this.applePay.tokenize(); + if (tokenResult?.status === 'OK') { + const card = tokenResult.details?.card; + if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { + console.error(`Cannot retreive payment card details`); + this.accelerateError = 'apple_pay_no_card_details'; + return; + } + const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); + this.servicesApiService.accelerateWithApplePay$( + this.tx.txid, + tokenResult.token, + cardTag, + `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + this.accelerationUUID + ).subscribe({ + next: () => { + this.audioService.playSound('ascend-chime-cartoon'); + if (this.applePay) { + this.applePay.destroy(); + } + setTimeout(() => { + this.moveToStep('paid'); + }, 1000); + }, + error: (response) => { + this.accelerateError = response.error; + if (!(response.status === 403 && response.error === 'not_available')) { + setTimeout(() => { + // Reset everything by reloading the page :D, can be improved + const urlParams = new URLSearchParams(window.location.search); + window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); + }, 3000); + } + } + }); + } else { + let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; + if (tokenResult.errors) { + errorMessage += ` and errors: ${JSON.stringify( + tokenResult.errors, + )}`; + } + throw new Error(errorMessage); + } + }); + } catch (e) { + console.error(e); + } + } + ); + } + + /** + * CASHAPP + */ + async requestCashAppPayment() { if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } @@ -449,7 +550,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { countryCode: 'US', currencyCode: 'USD', total: { - amount: costUSD.toString(), + amount: costUSD.toFixed(2), label: 'Total', pending: true, productUrl: `${redirectHostname}/tracker/${this.tx.txid}`, @@ -467,23 +568,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } this.loadingCashapp = false; - const that = this; - this.cashAppPay.addEventListener('ontokenization', function (event) { + this.cashAppPay.addEventListener('ontokenization', event => { const { tokenResult, error } = event.detail; if (error) { this.accelerateError = error; } else if (tokenResult.status === 'OK') { - that.servicesApiService.accelerateWithCashApp$( - that.tx.txid, + this.servicesApiService.accelerateWithCashApp$( + this.tx.txid, tokenResult.token, tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.referenceId, - that.accelerationUUID + this.accelerationUUID ).subscribe({ next: () => { - that.audioService.playSound('ascend-chime-cartoon'); - if (that.cashAppPay) { - that.cashAppPay.destroy(); + this.audioService.playSound('ascend-chime-cartoon'); + if (this.cashAppPay) { + this.cashAppPay.destroy(); } setTimeout(() => { this.moveToStep('paid'); @@ -494,7 +594,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { - that.accelerateError = response.error; + this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { // Reset everything by reloading the page :D, can be improved @@ -597,6 +697,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return false; } + get canPayWithApplePay() { + if (!this.applePayEnabled || !this.conversions) { + return false; + } + + const paymentMethod = this.estimate?.availablePaymentMethods?.applePay; + if (paymentMethod) { + const costUSD = (this.cost / 100_000_000 * this.conversions.USD); + if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) { + return true; + } + } + + return false; + } + get canPayWithBalance() { if (!this.hasAccessToBalanceMode) { return false; diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index bfd7da81b..c26075198 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -137,6 +137,10 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID }); } + accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); + } + getAccelerations$(): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`); } diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 28e510e14..88ed5d8a9 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -181,4 +181,48 @@ export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): Mempo acc: !!tx[3], })) }; +} + +// https://stackoverflow.com/a/60467595 +export function md5(inputString): string { + var hc="0123456789abcdef"; + function rh(n) {var j,s="";for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;} + function ad(x,y) {var l=(x&0xFFFF)+(y&0xFFFF);var m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);} + function rl(n,c) {return (n<>>(32-c));} + function cm(q,a,b,x,s,t) {return ad(rl(ad(ad(a,q),ad(x,t)),s),b);} + function ff(a,b,c,d,x,s,t) {return cm((b&c)|((~b)&d),a,b,x,s,t);} + function gg(a,b,c,d,x,s,t) {return cm((b&d)|(c&(~d)),a,b,x,s,t);} + function hh(a,b,c,d,x,s,t) {return cm(b^c^d,a,b,x,s,t);} + function ii(a,b,c,d,x,s,t) {return cm(c^(b|(~d)),a,b,x,s,t);} + function sb(x) { + var i;var nblk=((x.length+8)>>6)+1;var blks=new Array(nblk*16);for(i=0;i>2]|=x.charCodeAt(i)<<((i%4)*8); + blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks; + } + var i,x=sb(""+inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd; + for(i=0;i + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 08d3beed724e7758ea4e5bb40e51cf0b79bb5799 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 22:38:49 +0200 Subject: [PATCH 03/22] [accelerator] on mobile, autoscroll after selection cashapp or applepay --- .../accelerate-checkout/accelerate-checkout.component.html | 4 ++-- .../accelerate-checkout/accelerate-checkout.component.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 80bdc1fc5..a8d799837 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -427,7 +427,7 @@
} @else if (step === 'cashapp' || step === 'applepay') { -
+

Confirm your payment

@@ -557,7 +557,7 @@ -Accelerate to ~{{ x | number : '1.0-0' }} sat/vB +Accelerate to ~{{ x | number : '1.0-0' }} sat/vB
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index d73a81bb2..0722160b6 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -214,10 +214,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingCashapp = true; this.insertSquare(); this.setupSquare(); + this.scrollToElementWithTimeout('confirm-title', 'center', 100); } else if (this._step === 'applepay' && this.applePayEnabled) { this.loadingApplePay = true; this.insertSquare(); this.setupSquare(); + this.scrollToElementWithTimeout('confirm-title', 'center', 100); } else if (this._step === 'paid') { this.timePaid = Date.now(); this.timeoutTimer = setTimeout(() => { From 09b09710e47acaa4f8ca64931cb7511511702b3e Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 23:07:55 +0200 Subject: [PATCH 04/22] [accelerator] fix cashapp acceleration on mobile --- .../accelerate-checkout/accelerate-checkout.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 0722160b6..cfcdfb874 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -429,7 +429,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { try { //@ts-ignore this.payments = window.Square.payments(this.square.appId, this.square.locationId) - if (this._step === 'cashapp') { + const urlParams = new URLSearchParams(window.location.search); + if (this._step === 'cashapp' || urlParams.get('cash_request_id')) { await this.requestCashAppPayment(); } else if (this._step === 'applepay') { await this.requestApplePayPayment(); From 3f7a24fb52ef6cf205ffe5187fe1cdd5521dc84f Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 23:08:08 +0200 Subject: [PATCH 05/22] [accelerator] only show apple pay if available --- .../accelerate-checkout/accelerate-checkout.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index cfcdfb874..4552a81f7 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -60,7 +60,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: ETA; @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; - @Input() applePayEnabled: boolean = true; + @Input() applePayEnabled: boolean = false; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -133,6 +133,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { private enterpriseService: EnterpriseService, ) { this.accelerationUUID = window.crypto.randomUUID(); + + // Check if Apple Pay available + // @ts-ignore https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview + if (window.ApplePaySession) { + this.applePayEnabled = true; + } } ngOnInit() { From 8762ccaa09c00d70fe2301f5c2baed6ca5aa774f Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 23:22:11 +0200 Subject: [PATCH 06/22] [accelerator] remove attempt to align fiat payment methods --- .../accelerate-checkout/accelerate-checkout.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index a8d799837..58fd45356 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -395,7 +395,7 @@
} } -
+

Pay  with

@if (canPayWithCashapp) { From a7be59df3e6e71cc1287abe20de3139e2bc468f3 Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 22 Jul 2024 13:36:36 +0200 Subject: [PATCH 07/22] Fix buggy blockchain scroll on resize --- frontend/src/app/components/start/start.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/start/start.component.ts b/frontend/src/app/components/start/start.component.ts index 0a4943bde..78c31cde5 100644 --- a/frontend/src/app/components/start/start.component.ts +++ b/frontend/src/app/components/start/start.component.ts @@ -234,7 +234,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy { this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2); if (firstVisibleBlock != null) { - this.scrollToBlock(firstVisibleBlock, offset); + this.scrollToBlock(firstVisibleBlock, offset + (this.isMobile ? this.blockWidth : 0)); } else { this.updatePages(); } From 4ef4e5b98ace187077da6cf54bc4cf53fc611825 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 22 Jul 2024 15:17:56 +0200 Subject: [PATCH 08/22] [doc] update api key header --- frontend/src/app/docs/api-docs/api-docs-data.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 62f3c54c8..606298702 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -8993,7 +8993,7 @@ export const restApiDocsData = [ fragment: "accelerator-estimate", title: "POST Calculate Estimated Costs", description: { - default: "

Returns estimated costs to accelerate a transaction. Optionally set the api_key header to get customized estimation.

" + default: "

Returns estimated costs to accelerate a transaction. Optionally set the X-Mempool-Authorization header to get customized estimation.

" }, urlString: "/v1/services/accelerator/estimate", showConditions: [""], @@ -9009,7 +9009,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29"], - headers: "api_key: stacksats", + headers: "X-Mempool-Authorization: stacksats", response: `{ "txSummary": { "txid": "ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29", @@ -9240,7 +9240,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "api_key: stacksats", + headers: "X-Mempool-Authorization: stacksats", response: `[ { "type": "Bitcoin", @@ -9288,7 +9288,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "api_key: stacksats", + headers: "X-Mempool-Authorization: stacksats", response: `{ "balance": 99900000, "hold": 101829, @@ -9322,7 +9322,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&userBid=21000000"], - headers: "api_key: stacksats", + headers: "X-Mempool-Authorization: stacksats", response: `HTTP/1.1 200 OK`, }, } @@ -9352,7 +9352,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "api_key: stacksats", + headers: "X-Mempool-Authorization: stacksats", response: `[ { "id": 89, From b9a053387f52e21782890ab0b92c5a11c1cc3265 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 23 Jul 2024 11:44:50 +0200 Subject: [PATCH 09/22] Add txConfirmed subscription variable to fix miner loading forever --- .../src/app/components/transaction/transaction.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index f73a0b225..47b37cafb 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -88,6 +88,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { blocksSubscription: Subscription; miningSubscription: Subscription; auditSubscription: Subscription; + txConfirmedSubscription: Subscription; currencyChangeSubscription: Subscription; fragmentParams: URLSearchParams; rbfTransaction: undefined | Transaction; @@ -625,7 +626,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } ); - this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => { + this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => { if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) { if (this.tx.acceleration) { this.waitingForAccelerationInfo = true; @@ -1070,6 +1071,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.blocksSubscription.unsubscribe(); this.miningSubscription?.unsubscribe(); this.auditSubscription?.unsubscribe(); + this.txConfirmedSubscription?.unsubscribe(); this.currencyChangeSubscription?.unsubscribe(); this.leaveTransaction(); } From b1aa4f50bdc607ba0cb4cbce143e93800c304194 Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 24 Jul 2024 12:25:35 -0500 Subject: [PATCH 10/22] Change X-Mempool-Authorization to X-Mempool-Auth --- frontend/src/app/docs/api-docs/api-docs-data.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 606298702..12bb96166 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -8993,7 +8993,7 @@ export const restApiDocsData = [ fragment: "accelerator-estimate", title: "POST Calculate Estimated Costs", description: { - default: "

Returns estimated costs to accelerate a transaction. Optionally set the X-Mempool-Authorization header to get customized estimation.

" + default: "

Returns estimated costs to accelerate a transaction. Optionally set the X-Mempool-Auth header to get customized estimation.

" }, urlString: "/v1/services/accelerator/estimate", showConditions: [""], @@ -9009,7 +9009,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29"], - headers: "X-Mempool-Authorization: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `{ "txSummary": { "txid": "ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29", @@ -9240,7 +9240,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "X-Mempool-Authorization: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `[ { "type": "Bitcoin", @@ -9288,7 +9288,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "X-Mempool-Authorization: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `{ "balance": 99900000, "hold": 101829, @@ -9322,7 +9322,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&userBid=21000000"], - headers: "X-Mempool-Authorization: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `HTTP/1.1 200 OK`, }, } @@ -9352,7 +9352,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "X-Mempool-Authorization: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `[ { "id": 89, From b49a6c4cacf2ffba5ca882ad1358f5f6aaef4e97 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 23 Jul 2024 14:00:23 +0000 Subject: [PATCH 11/22] [accelerator] on-demand polling support --- backend/src/api/mempool.ts | 4 - backend/src/api/mining/mining-routes.ts | 34 +++++++ backend/src/api/services/acceleration.ts | 90 +++++++++++++++++-- backend/src/api/websocket-handler.ts | 22 ++--- backend/src/index.ts | 2 +- .../repositories/AccelerationRepository.ts | 9 ++ 6 files changed, 134 insertions(+), 27 deletions(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 89377335d..1f55179fb 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -396,10 +396,6 @@ class Mempool { } public $updateAccelerations(newAccelerations: Acceleration[]): string[] { - if (!config.MEMPOOL_SERVICES.ACCELERATIONS) { - return []; - } - try { const changed: string[] = []; diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index bdfc83d43..08ea0d1bc 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -9,6 +9,7 @@ import bitcoinClient from '../bitcoin/bitcoin-client'; import mining from "./mining"; import PricesRepository from '../../repositories/PricesRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository'; +import accelerationApi from '../services/acceleration'; class MiningRoutes { public initRoutes(app: Application) { @@ -41,6 +42,8 @@ class MiningRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations) .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals) + .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations) + .post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration) ; } @@ -445,6 +448,37 @@ class MiningRoutes { res.status(500).send(e instanceof Error ? e.message : e); } } + + private async $getActiveAccelerations(req: Request, res: Response): Promise { + try { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { + res.status(400).send('Acceleration data is not available.'); + return; + } + res.status(200).send(accelerationApi.accelerations || []); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $requestAcceleration(req: Request, res: Response): Promise { + if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) { + res.status(405).send('not available.'); + return; + } + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); + res.setHeader('expires', -1); + try { + accelerationApi.accelerationRequested(req.params.txid); + res.status(200).send('ok'); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } } export default new MiningRoutes(); diff --git a/backend/src/api/services/acceleration.ts b/backend/src/api/services/acceleration.ts index 7debe7119..386c40b8e 100644 --- a/backend/src/api/services/acceleration.ts +++ b/backend/src/api/services/acceleration.ts @@ -1,8 +1,10 @@ import config from '../../config'; import logger from '../../logger'; -import { BlockExtended, PoolTag } from '../../mempool.interfaces'; +import { BlockExtended } from '../../mempool.interfaces'; import axios from 'axios'; +type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; + export interface Acceleration { txid: string, added: number, @@ -35,18 +37,88 @@ export interface AccelerationHistory { }; class AccelerationApi { - public async $fetchAccelerations(): Promise { + private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); + private _accelerations: Acceleration[] | null = null; + private lastPoll = 0; + private forcePoll = false; + private myAccelerations: Record = {}; + + public get accelerations(): Acceleration[] | null { + return this._accelerations; + } + + public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number { + return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0); + } + + public accelerationRequested(txid: string): void { + this.myAccelerations[txid] = { status: 'requested', added: Date.now() }; + } + + public accelerationConfirmed(): void { + this.forcePoll = true; + } + + private async $fetchAccelerations(): Promise { + try { + const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 }); + return response?.data || []; + } catch (e) { + logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e)); + return null; + } + } + + public async $updateAccelerations(): Promise { if (config.MEMPOOL_SERVICES.ACCELERATIONS) { - try { - const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 }); - return response.data as Acceleration[]; - } catch (e) { - logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e)); - return null; + const accelerations = await this.$fetchAccelerations(); + if (accelerations) { + this._accelerations = accelerations; + return this._accelerations; } } else { - return []; + return this.$updateAccelerationsOnDemand(); } + return null; + } + + private async $updateAccelerationsOnDemand(): Promise { + const shouldUpdate = this.forcePoll + || this.countMyAccelerationsWithStatus('requested') > 0 + || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); + + // update accelerations if necessary + if (shouldUpdate) { + const accelerations = await this.$fetchAccelerations(); + this.lastPoll = Date.now(); + this.forcePoll = false; + if (accelerations) { + const latestAccelerations: Record = {}; + // set relevant accelerations to 'accelerating' + for (const acc of accelerations) { + if (this.myAccelerations[acc.txid]) { + latestAccelerations[acc.txid] = acc; + this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc }; + } + } + // txs that are no longer accelerating are either confirmed or canceled, so mark for expiry + for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) { + if (status === 'accelerating' && !latestAccelerations[txid]) { + this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration }; + } + } + } + } + + // clear expired accelerations (confirmed / failed / not accepted) after 10 minutes + for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) { + if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) { + delete this.myAccelerations[txid]; + } + } + + this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]; + return this._accelerations; } public async $fetchAccelerationHistory(page?: number, status?: string): Promise { diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index cf2c56763..32d306ad2 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -538,9 +538,9 @@ class WebsocketHandler { } if (config.MEMPOOL.RUST_GBT) { - await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS); + await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true); } else { - await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS); + await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true); } const mBlocks = mempoolBlocks.getMempoolBlocks(); @@ -949,18 +949,14 @@ class WebsocketHandler { if (config.MEMPOOL.AUDIT && memPool.isInSync()) { let projectedBlocks; const auditMempool = _memPool; - const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); + const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); - if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) { - if (config.MEMPOOL.RUST_GBT) { - const added = memPool.limitGBT ? (candidates?.added || []) : []; - const removed = memPool.limitGBT ? (candidates?.removed || []) : []; - projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id); - } else { - projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id); - } + if (config.MEMPOOL.RUST_GBT) { + const added = memPool.limitGBT ? (candidates?.added || []) : []; + const removed = memPool.limitGBT ? (candidates?.removed || []) : []; + projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id); } else { - projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); + projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id); } if (Common.indexingEnabled()) { @@ -1040,7 +1036,7 @@ class WebsocketHandler { const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions; await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true); } else { - await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS); + await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true); } const mBlocks = mempoolBlocks.getMempoolBlocks(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 2a1afc712..1d83c56a3 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -229,7 +229,7 @@ class Server { const newMempool = await bitcoinApi.$getRawMempool(); const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; - const newAccelerations = await accelerationApi.$fetchAccelerations(); + const newAccelerations = await accelerationApi.$updateAccelerations(); const numHandledBlocks = await blocks.$updateBlocks(); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); if (numHandledBlocks === 0) { diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts index 1c3bdad29..70fa78dc6 100644 --- a/backend/src/repositories/AccelerationRepository.ts +++ b/backend/src/repositories/AccelerationRepository.ts @@ -213,6 +213,15 @@ class AccelerationRepository { this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations); } } + let anyConfirmed = false; + for (const acc of accelerations) { + if (blockTxs[acc.txid]) { + anyConfirmed = true; + } + } + if (anyConfirmed) { + accelerationApi.accelerationConfirmed(); + } const lastSyncedHeight = await this.$getLastSyncedHeight(); // if we've missed any blocks, let the indexer catch up from the last synced height on the next run if (block.height === lastSyncedHeight + 1) { From 7b3cc6372b67747056b7c53bb33bf5cf2b7cc2a2 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 23 Jul 2024 15:17:22 +0000 Subject: [PATCH 12/22] [accelerator] frontend on-demand polling support --- .../accelerate-checkout/accelerate-checkout.component.ts | 9 +++++++-- frontend/src/app/services/api.service.ts | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 2512191b9..185238f72 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -9,6 +9,7 @@ import { Transaction } from '../../interfaces/electrs.interface'; import { MiningStats } from '../../services/mining.service'; import { IAuth, AuthServiceMempool } from '../../services/auth.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { ApiService } from '../../services/api.service'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp'; @@ -123,6 +124,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { constructor( public stateService: StateService, + private apiService: ApiService, private servicesApiService: ServicesApiServices, private etaService: EtaService, private audioService: AudioService, @@ -370,10 +372,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; this.estimateSubscription.unsubscribe(); - this.moveToStep('paid') + this.moveToStep('paid'); }, error: (response) => { this.accelerateError = response.error; @@ -481,6 +484,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { that.accelerationUUID ).subscribe({ next: () => { + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); that.audioService.playSound('ascend-chime-cartoon'); if (that.cashAppPay) { that.cashAppPay.destroy(); @@ -530,9 +534,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } bitcoinPaymentCompleted(): void { + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.estimateSubscription.unsubscribe(); - this.moveToStep('paid') + this.moveToStep('paid'); } isLoggedIn(): boolean { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index d7efa4d02..fa52ec707 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -536,6 +536,10 @@ export class ApiService { ); } + logAccelerationRequest$(txid: string): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, ''); + } + // Cache methods async setBlockAuditLoaded(hash: string) { this.blockAuditLoaded[hash] = true; From af7a962a0b9c26b9e0ea52c5f1ae29ba2f6ba93b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 23 Jul 2024 15:29:52 +0000 Subject: [PATCH 13/22] [accelerator] accelerator_button config --- docker/frontend/entrypoint.sh | 2 ++ .../src/app/components/transaction/transaction.component.ts | 4 ++-- frontend/src/app/services/enterprise.service.ts | 1 + frontend/src/app/services/state.service.ts | 2 ++ production/mempool-frontend-config.mainnet.json | 1 + 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index dc0fa6f7a..20b391087 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -40,6 +40,7 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} __ACCELERATOR__=${ACCELERATOR:=false} +__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true} __SERVICES_API__=${SERVICES_API:=false} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} @@ -70,6 +71,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ export __ACCELERATOR__ +export __ACCELERATOR_BUTTON__ export __SERVICES_API__ export __PUBLIC_ACCELERATIONS__ export __HISTORICAL_PRICE__ diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index f73a0b225..c3b450af0 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -141,7 +141,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { taprootEnabled: boolean; hasEffectiveFeeRate: boolean; accelerateCtaType: 'alert' | 'button' = 'button'; - acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === ''; + acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === ''; eligibleForAcceleration: boolean = false; forceAccelerationSummary = false; hideAccelerationSummary = false; @@ -195,7 +195,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.stateService.networkChanged$.subscribe( (network) => { this.network = network; - this.acceleratorAvailable = this.stateService.env.ACCELERATOR && this.stateService.network === ''; + this.acceleratorAvailable = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === ''; } ); diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts index 4ea890f1f..f9549cc8a 100644 --- a/frontend/src/app/services/enterprise.service.ts +++ b/frontend/src/app/services/enterprise.service.ts @@ -30,6 +30,7 @@ export class EnterpriseService { this.fetchSubdomainInfo(); this.disableSubnetworks(); this.stateService.env.ACCELERATOR = false; + this.stateService.env.ACCELERATOR_BUTTON = false; } else { this.insertMatomo(); } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index ef13ea07d..05f1ac69f 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -71,6 +71,7 @@ export interface Env { SIGNET_BLOCK_AUDIT_START_HEIGHT: number; HISTORICAL_PRICE: boolean; ACCELERATOR: boolean; + ACCELERATOR_BUTTON: boolean; PUBLIC_ACCELERATIONS: boolean; ADDITIONAL_CURRENCIES: boolean; GIT_COMMIT_HASH_MEMPOOL_SPACE?: string; @@ -108,6 +109,7 @@ const defaultEnv: Env = { 'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0, 'HISTORICAL_PRICE': true, 'ACCELERATOR': false, + 'ACCELERATOR_BUTTON': true, 'PUBLIC_ACCELERATIONS': false, 'ADDITIONAL_CURRENCIES': false, 'SERVICES_API': 'https://mempool.space/api/v1/services', diff --git a/production/mempool-frontend-config.mainnet.json b/production/mempool-frontend-config.mainnet.json index 0465cb7d3..84cde82cf 100644 --- a/production/mempool-frontend-config.mainnet.json +++ b/production/mempool-frontend-config.mainnet.json @@ -13,6 +13,7 @@ "ITEMS_PER_PAGE": 25, "LIGHTNING": true, "ACCELERATOR": true, + "ACCELERATOR_BUTTON": true, "PUBLIC_ACCELERATIONS": true, "AUDIT": true } From 544261eafe1213e6facf7d4aa67d9b789741d486 Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 24 Jul 2024 13:31:56 -0500 Subject: [PATCH 14/22] ops: Add /api/v1/accelerations to nginx hot cache --- production/nginx-cache-heater | 1 + production/nginx/location-api-v1-services.conf | 3 +++ 2 files changed, 4 insertions(+) diff --git a/production/nginx-cache-heater b/production/nginx-cache-heater index 4bbe8ee15..24ec8a061 100755 --- a/production/nginx-cache-heater +++ b/production/nginx-cache-heater @@ -9,6 +9,7 @@ heat() heatURLs=( '/api/v1/fees/recommended' + '/api/v1/accelerations' ) while true diff --git a/production/nginx/location-api-v1-services.conf b/production/nginx/location-api-v1-services.conf index 88f510e79..aad13264c 100644 --- a/production/nginx/location-api-v1-services.conf +++ b/production/nginx/location-api-v1-services.conf @@ -2,6 +2,9 @@ # routing # ########### +location /api/v1/accelerations { + try_files /dev/null @mempool-api-v1-services-cache-short; +} location /api/v1/assets { try_files /dev/null @mempool-api-v1-services-cache-short; } From 4d44ee55fcfe97e978daef537737f2a7c27da053 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 24 Jul 2024 22:20:52 +0200 Subject: [PATCH 15/22] [accelerator] add missing getters for applepay --- .../accelerate-checkout.component.html | 2 +- .../accelerate-checkout.component.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 58fd45356..27786bd9b 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -389,7 +389,7 @@
}
- @if (canPayWithCashapp) { + @if (canPayWithCashapp || canPayWithApplePay) {

OR

diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 632a7b127..71c46e2da 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -679,6 +679,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return !!this.estimate?.availablePaymentMethods?.cashapp; } + get couldPayWithApplePay() { + if (!this.applePayEnabled) { + return false; + } + return !!this.estimate?.availablePaymentMethods?.applePay; + } + get couldPayWithBalance() { if (!this.hasAccessToBalanceMode) { return false; @@ -687,7 +694,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get couldPay() { - return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp; + return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay; } get canPayWithBitcoin() { @@ -707,7 +714,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return true; } } - + return false; } @@ -723,7 +730,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return true; } } - + return false; } @@ -736,7 +743,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get canPay() { - return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp; + return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay; } get hasAccessToBalanceMode() { From 96f9f66e7f1433537fb5d43b0bd1fa76e9d65799 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 24 Jul 2024 16:56:30 -0500 Subject: [PATCH 16/22] changing unconfirmed to pending balance/utxo --- .../components/address/address.component.html | 4 ++-- frontend/src/locale/messages.xlf | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 76c64948b..2c39f5b86 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -249,7 +249,7 @@
- Unconfirmed balance + Pending balance @@ -259,7 +259,7 @@ - Unconfirmed UTXOs + Pending UTXOs {{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }} diff --git a/frontend/src/locale/messages.xlf b/frontend/src/locale/messages.xlf index 0f9432320..8dcde6381 100644 --- a/frontend/src/locale/messages.xlf +++ b/frontend/src/locale/messages.xlf @@ -2042,13 +2042,13 @@ address.confirmed-balance - - Unconfirmed balance + + Pending balance src/app/components/address/address.component.html 252 - address.unconfirmed-balance + address.pending-balance Confirmed UTXOs @@ -2058,13 +2058,13 @@ address.confirmed-utxos - - Unconfirmed UTXOs + + Pending UTXOs src/app/components/address/address.component.html 262 - address.unconfirmed-utxos + address.pending-utxos Type @@ -6640,7 +6640,7 @@ src/app/components/transaction/transaction.component.ts - 498 + 499 @@ -6655,7 +6655,7 @@ src/app/components/transaction/transaction.component.ts - 502 + 503 From 570f7841ce8eaf26532335059844ab1ceedd3caf Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 25 Jul 2024 00:10:27 +0200 Subject: [PATCH 17/22] [accelerator] hide fiat payment method section if none available --- .../accelerate-checkout.component.html | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 27786bd9b..82391dc89 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -395,16 +395,18 @@
} } -
-

Pay  with

- @if (canPayWithCashapp) { - - } - @if (canPayWithApplePay) { - @if (canPayWithCashapp) {
} - - } -
+ @if (canPayWithCashapp || canPayWithApplePay) { +
+

Pay  with

+ @if (canPayWithCashapp) { + + } + @if (canPayWithApplePay) { + @if (canPayWithCashapp) {
} + + } +
+ }
}
From 5c814d9c22cac49dcb314d647162f76884199e5d Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 24 Jul 2024 17:21:55 -0500 Subject: [PATCH 18/22] pending balance -> pending --- .../app/components/address/address.component.html | 2 +- frontend/src/locale/messages.xlf | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 2c39f5b86..31dff2fa5 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -249,7 +249,7 @@ - Pending balance + Pending diff --git a/frontend/src/locale/messages.xlf b/frontend/src/locale/messages.xlf index 8dcde6381..5e7e6b8fd 100644 --- a/frontend/src/locale/messages.xlf +++ b/frontend/src/locale/messages.xlf @@ -1520,6 +1520,10 @@ src/app/components/acceleration/accelerations-list/accelerations-list.component.html 53 + + src/app/components/address/address.component.html + 252 + src/app/components/tracker/tracker-bar.component.html 4 @@ -2042,14 +2046,6 @@ address.confirmed-balance - - Pending balance - - src/app/components/address/address.component.html - 252 - - address.pending-balance - Confirmed UTXOs From aee2454a98c49162dce3f24e73cb9c8ad21e9099 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 23 Jul 2024 18:51:30 +0200 Subject: [PATCH 19/22] Add pool name to acceleration list --- .../accelerations-list.component.html | 11 +++++++++++ .../accelerations-list.component.scss | 18 +++++++++++++++++- .../accelerations-list.component.ts | 11 ++++++++++- .../src/app/interfaces/node-api.interface.ts | 1 + 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index df915c286..12386e30c 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -16,6 +16,7 @@ Bid Boost Block + Pool Status Requested @@ -49,6 +50,16 @@ {{ acceleration.blockHeight }} ~ + + @if (acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]) { + + + {{ pools[acceleration.minedByPoolUniqueId].name }} + + } @else { + ~ + } + Pending Completed 🔄 diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss index d4579bcf6..56c92ff02 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss @@ -12,7 +12,7 @@ padding-bottom: 0px; } .container-xl.legacy { - max-width: 1140px; + max-width: 1200px; } .container-xl.widget-container { min-height: 335px; @@ -72,9 +72,25 @@ tr, td, th { .block { width: 15%; + @media (max-width: 900px) { + display: none; + } +} + +.pool { + width: 15%; + @media (max-width: 700px) { display: none; } + + .pool-logo { + width: 22px; + height: 22px; + position: relative; + top: -1px; + margin-right: 2px; + } } .status { diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts index c236032e2..5b2b30d7a 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts @@ -1,11 +1,12 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core'; import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs'; -import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; +import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface'; import { StateService } from '../../../services/state.service'; import { WebsocketService } from '../../../services/websocket.service'; import { ServicesApiServices } from '../../../services/services-api.service'; import { SeoService } from '../../../services/seo.service'; import { ActivatedRoute, Router } from '@angular/router'; +import { MiningService } from '../../../services/mining.service'; @Component({ selector: 'app-accelerations-list', @@ -30,11 +31,13 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { keyNavigationSubscription: Subscription; dir: 'rtl' | 'ltr' = 'ltr'; paramSubscription: Subscription; + pools: { [id: number]: SinglePoolStats } = {}; constructor( private servicesApiService: ServicesApiServices, private websocketService: WebsocketService, public stateService: StateService, + private miningService: MiningService, private cd: ChangeDetectorRef, private seoService: SeoService, private route: ActivatedRoute, @@ -79,6 +82,12 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { ).subscribe(() => { this.pageChange(this.page); }); + + this.miningService.getMiningStats('1m').subscribe(stats => { + for (const pool of stats.pools) { + this.pools[pool.poolUniqueId] = pool; + } + }); } this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index e8fb842ec..9a00faadc 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -408,6 +408,7 @@ export interface Acceleration { bidBoost?: number; boostCost?: number; boostRate?: number; + minedByPoolUniqueId?: number; } export interface AccelerationHistoryParams { From df0f244bd1a60f5b17031f0dea193be85c12aae1 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 23 Jul 2024 12:18:12 +0200 Subject: [PATCH 20/22] Prevent never ending loop of calls to transactionTimes --- .../transaction/transaction.component.ts | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index f73a0b225..24d90b269 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -11,7 +11,9 @@ import { tap, map, retry, - startWith + startWith, + repeat, + take } from 'rxjs/operators'; import { Transaction } from '../../interfaces/electrs.interface'; import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs'; @@ -76,6 +78,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { transactionTime = -1; subscription: Subscription; fetchCpfpSubscription: Subscription; + transactionTimesSubscription: Subscription; fetchRbfSubscription: Subscription; fetchCachedTxSubscription: Subscription; fetchAccelerationSubscription: Subscription; @@ -106,6 +109,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { showCpfpDetails = false; miningStats: MiningStats; fetchCpfp$ = new Subject(); + transactionTimes$ = new Subject(); fetchRbfHistory$ = new Subject(); fetchCachedTx$ = new Subject(); fetchAcceleration$ = new Subject(); @@ -572,7 +576,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { if (tx.firstSeen) { this.transactionTime = tx.firstSeen; } else { - this.getTransactionTime(); + this.transactionTimes$.next(tx.txid); } } else { this.fetchAcceleration$.next(tx.status.block_height); @@ -730,6 +734,25 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { ); }) ) + + this.transactionTimesSubscription = this.transactionTimes$.pipe( + tap(() => { + this.isLoadingFirstSeen = true; + }), + switchMap((txid) => this.apiService.getTransactionTimes$([txid]).pipe( + retry({ count: 2, delay: 2000 }), + // Try again until we either get a valid response, or the transaction is confirmed + repeat({ delay: 2000 }), + filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed), + take(1), + )), + ) + .subscribe((transactionTimes) => { + this.isLoadingFirstSeen = false; + if (transactionTimes?.length && transactionTimes[0]) { + this.transactionTime = transactionTimes[0]; + } + }); } ngAfterViewInit(): void { @@ -763,28 +786,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { return of(false); } - getTransactionTime() { - this.isLoadingFirstSeen = true; - this.apiService - .getTransactionTimes$([this.tx.txid]) - .pipe( - retry({ count: 2, delay: 2000 }), - catchError(() => { - this.isLoadingFirstSeen = false; - return throwError(() => new Error('')); - }) - ) - .subscribe((transactionTimes) => { - if (transactionTimes?.length && transactionTimes[0]) { - this.transactionTime = transactionTimes[0]; - } else { - setTimeout(() => { - this.getTransactionTime(); - }, 2000); - } - }); - } - setCpfpInfo(cpfpInfo: CpfpInfo): void { if (!cpfpInfo || !this.tx) { this.cpfpInfo = null; @@ -1057,6 +1058,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { ngOnDestroy() { this.subscription.unsubscribe(); this.fetchCpfpSubscription.unsubscribe(); + this.transactionTimesSubscription.unsubscribe(); this.fetchRbfSubscription.unsubscribe(); this.fetchCachedTxSubscription.unsubscribe(); this.fetchAccelerationSubscription.unsubscribe(); From ca4b1943a83cda3511c392d3ba79d8e87614b754 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 25 Jul 2024 03:35:17 -0500 Subject: [PATCH 21/22] moving code block --- .../transaction/transaction.component.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 24d90b269..0d3754e0b 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -229,6 +229,25 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.latestBlock = blocks[0]; }); + this.transactionTimesSubscription = this.transactionTimes$.pipe( + tap(() => { + this.isLoadingFirstSeen = true; + }), + switchMap((txid) => this.apiService.getTransactionTimes$([txid]).pipe( + retry({ count: 2, delay: 2000 }), + // Try again until we either get a valid response, or the transaction is confirmed + repeat({ delay: 2000 }), + filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed), + take(1), + )), + ) + .subscribe((transactionTimes) => { + this.isLoadingFirstSeen = false; + if (transactionTimes?.length && transactionTimes[0]) { + this.transactionTime = transactionTimes[0]; + } + }); + this.fetchCpfpSubscription = this.fetchCpfp$ .pipe( switchMap((txId) => @@ -733,26 +752,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.accelerationPositions, ); }) - ) - - this.transactionTimesSubscription = this.transactionTimes$.pipe( - tap(() => { - this.isLoadingFirstSeen = true; - }), - switchMap((txid) => this.apiService.getTransactionTimes$([txid]).pipe( - retry({ count: 2, delay: 2000 }), - // Try again until we either get a valid response, or the transaction is confirmed - repeat({ delay: 2000 }), - filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed), - take(1), - )), - ) - .subscribe((transactionTimes) => { - this.isLoadingFirstSeen = false; - if (transactionTimes?.length && transactionTimes[0]) { - this.transactionTime = transactionTimes[0]; - } - }); + ); } ngAfterViewInit(): void { From b3e59c06e90893b3be4606c4574afda4a80b6fb3 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 25 Jul 2024 04:07:18 -0500 Subject: [PATCH 22/22] add new accelerator button config to sample --- frontend/mempool-frontend-config.sample.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index 38a671edf..f9f2576d6 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -25,6 +25,7 @@ "HISTORICAL_PRICE": true, "ADDITIONAL_CURRENCIES": false, "ACCELERATOR": false, + "ACCELERATOR_BUTTON": true, "PUBLIC_ACCELERATIONS": false, "SERVICES_API": "https://mempool.space/api/v1/services" }