diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 4fd1d2013..8e996953d 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -146,8 +146,9 @@ let routes: Routes = [ data: { preload: true }, }, { - path: 'tracker/:id', - component: TrackerComponent, + path: 'tracker', + data: { networkSpecific: true }, + loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), }, { path: 'wallet', 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 8f82fe69c..a0f84e226 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -1,77 +1,412 @@ -
+
+ +
+
+
+ Transaction has now been submitted to mining pools for acceleration. +
+
+
@if (error) { -
- -
- } - - @else if (step === 'cta') { - -
-
-

Accelerate your Bitcoin transaction?

+
+
+
+ } + @else if (step === 'quote') { +
+ + + -
-
-
-
- - + +
+
+
You are currently on the waitlist
+
+ + @if (showDetails) { +
Your transaction
+
+
+ + Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s) + + + + + + + + + + + + + + + + + + +
Virtual size
+ Size in vbytes of this transaction (including unconfirmed ancestors) +
In-band fees + {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} sats +
+ Fees already paid by this transaction (including unconfirmed ancestors) +
+
+
+
+ } +
How much faster?
+
+
+ + Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + This will reduce your expected waiting time until the first confirmation to + +
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+
+
+ +
Summary
+
+
+ + + + + @if (isLoggedIn()) { + + + + + + + + + + + } + @else { + + + + + + + + + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Next block market rate + {{ estimate.targetFeeRate | number : '1.0-0' }} + sat/vB
+ Estimated extra fee required + + {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} + + sats + +
Target rate + {{ maxRateOptions[selectFeeRateIndex].rate | number : '1.0-0' }} + sat/vB
+ Extra fee required + + {{ maxRateOptions[selectFeeRateIndex].fee | number }} + + sats + +
Mempool Accelerator™ fees
+ Accelerator Service Fee + + +{{ estimate.mempoolBaseFee | number }} + + sats + +
+ Transaction Size Surcharge + + +{{ estimate.vsizeFee | number }} + + sats + +
+ Estimated acceleration cost ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB + + + {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} + + + sats + +
+ @if (isLoggedIn()) { + Maximum acceleration cost + } @else { + Acceleration cost + } + + + {{ cost | number }} + + + sats + + + +
Available balance + {{ estimate.userBalance | number }} + + sats + + + +
+
+ +
+
+
+
+
+ +
+
+
+
-
-
-
- -
+ + +
+
+
+ } + @else if (step === 'summary') { + + + @if (!noCTA) { +
+
+

Accelerate your Bitcoin transaction?

+
+
+ } + +
+
You are currently on the waitlist for Mempool Accelerator™
+
+ + +
+
+
+
+ + + +
+
+
+ Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + +
+ +
+ +
+ +
+
+
+
-
-
-
+ } @else { +
+
+

Payment to mempool.space for acceleration of txid {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}

+
+
+ @if (canPayWithBitcoin) { +
+ @if (invoice) { +

Pay {{ ((invoice.btcDue * 100_000_000) || cost) | number }} sats

+ + } @else { +

Loading invoice...

+
+
+
+ } +
+ @if (canPayWithCashapp) { +
+

OR

+
+ } + } + @if (canPayWithCashapp) { +
+

Pay with

+ +
+ } +
+
+ } + + +
+
+
+
+
+
- - } - - @else if (step === 'checkout') { +
+ +
+
+
+ +
+
+ } @else if (step === 'cashapp') {
-
-

Confirm your payment

+
+

Confirm your payment

- Payment to mempool.space for acceleration of txid {{ txid.substr(0, 10) }}..{{ txid.substr(-10) }} + Payment to mempool.space for acceleration of txid {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}
@@ -109,16 +444,14 @@
- Changed your mind? - +
} - @else if (step === 'processing') {
-

Confirm your payment

+

Confirming your payment

@@ -135,5 +468,38 @@
} - + @else if (step === 'paid') { +
+
+

Accelerating your transaction

+
+
+ +
+
+
+ Confirming your acceleration with our mining pool partners... +
+
+
+
+ }
+ + + + + + + @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { + + } @else { + + } + \ No newline at end of file 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 315bdbbd2..5cfd153bd 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -7,3 +7,204 @@ .estimating { color: var(--green) } + +.paymentMethod { + padding: 10px; + background-color: var(--secondary); + border-radius: 15px; + border: 2px solid var(--bg); + cursor: pointer; +} + +.default-slot:not(:only-child) { + display: none; +} + +.pie { + display: flex; + align-items: center; + max-width: 330px; +} + +.fee-card { + padding: 15px; + background-color: var(--bg); + + .feerate { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .rate { + font-size: 0.9em; + .symbol { + color: white; + } + } + } +} + +.btn-border { + border: solid 1px black; + background-color: #0c4a87; +} + +.feerate.active { + background-color: var(--primary) !important; + opacity: 1; + border: 1px solid #007fff !important; +} +.feerate:focus { + box-shadow: none !important; +} + +.grayOut { + opacity: 0.5; +} + +.disabled { + opacity: 0.5; + pointer-events: none; +} + +.table-toggle { + width: 100%; + margin-top: 0.5em; +} + +.tab { + &:first-child { + margin-right: 1px; + } + border: solid 1px black; + border-bottom: none; + background-color: #323655; + border-top-left-radius: 10px !important; + border-top-right-radius: 10px !important; +} +.tab.active { + background-color: #5d659d !important; + opacity: 1; +} +.tab:focus { + box-shadow: none !important; +} + +.table-accelerator { + tr { + td { + padding-top: 0; + padding-bottom: 0; + vertical-align: baseline; + } + + &.group-first { + td { + padding-top: 0.75rem; + } + } + &.group-last, &:last-child { + td { + padding-bottom: 0.75rem; + } + } + &.dashed-top { + border-top: 1px dashed grey; + } + &.dashed-bottom { + border-bottom: 1px dashed grey + } + } + td { + &:first-child { + width: 100vw; + } + &.info { + color: #6c757d; + white-space: initial; + } + &.amt { + text-align: right; + padding-right: 0.2em; + } + &.units { + padding-left: 0.2em; + white-space: nowrap; + display: flex; + justify-content: space-between; + align-items: center; + } + } +} + +.accelerate-cols { + display: flex; + flex-direction: row; + align-items: stretch; + margin-top: 1em; +} + +.payment-area { + background: var(--bg); +} + +.col.pie { + flex-grow: 0; + padding: 0 1em; + position: relative; + top: -15px; +} + +.item { + white-space: initial; +} + +.table-background { + background-color: var(--bg); +} + +.checkout-text { + color: rgb(186, 186, 186); + font-size: 14px; +} + +.btn-accelerate { + background-color: var(--tertiary); +} + +.btn-small-height { + line-height: 1; +} + +.summary-row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0 2em; + flex-wrap: wrap; + + @media (max-width: 640px) { + flex-direction: column; + } +} + +@keyframes box-shake { + 0% { transform: rotate(0deg); } + 10% { transform: rotate(-8deg); } + 20% { transform: rotate(8deg); } + 30% { transform: rotate(-8deg); } + 40% { transform: rotate(8deg); } + 50% { transform: rotate(-8deg); } + 60% { transform: rotate(8deg); } + 70% { transform: rotate(-8deg); } + 80% { transform: rotate(8deg); } + 90% { transform: rotate(-8deg); } + 100% { transform: rotate(0deg); } +} + +.error-shake { + box-shadow: 0 0 10px 2px var(--danger); + animation: box-shake 1.5s ease-in-out; +} \ 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 16400a70a..8c0d35dd9 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,11 +1,47 @@ -import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges } from '@angular/core'; -import { Subscription, tap, of, catchError, Observable } from 'rxjs'; +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 { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; -import { AccelerationEstimate } from '../accelerate-preview/accelerate-preview.component'; -import { EtaService } from '../../services/eta.service'; +import { ETA, EtaService } from '../../services/eta.service'; +import { Transaction } from '../../interfaces/electrs.interface'; +import { MiningStats } from '../../services/mining.service'; +import { StorageService } from '../../services/storage.service'; + +export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp'; + +export type AccelerationEstimate = { + hasAccess: boolean; + txSummary: TxSummary; + nextBlockFee: number; + targetFeeRate: number; + userBalance: number; + enoughBalance: boolean; + cost: number; + mempoolBaseFee: number; + vsizeFee: number; + pools: number[]; + availablePaymentMethods: PaymentMethod[]; +} +export type TxSummary = { + txid: string; // txid of the current transaction + effectiveVsize: number; // Total vsize of the dependency tree + effectiveFee: number; // Total fee of the dependency tree in sats + ancestorCount: number; // Number of ancestors +} + +export interface RateOption { + fee: number; + rate: number; + index: number; +} + +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'; @Component({ selector: 'app-accelerate-checkout', @@ -13,23 +49,50 @@ import { EtaService } from '../../services/eta.service'; styleUrls: ['./accelerate-checkout.component.scss'] }) export class AccelerateCheckout implements OnInit, OnDestroy { - @Input() eta: number | null = null; - @Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad'; + @Input() tx: Transaction; + @Input() miningStats: MiningStats; + @Input() eta: ETA; @Input() scrollEvent: boolean; - @Output() close = new EventEmitter(); + @Input() cashappEnabled: boolean = true; + @Input() advancedEnabled: boolean = false; + @Input() forceMobile: boolean = false; + @Input() showDetails: boolean = false; + @Input() noCTA: boolean = false; + @Output() hasDetails = new EventEmitter(); + @Output() changeMode = new EventEmitter(); calculating = true; - choosenOption: 'wait' | 'accelerate' = 'wait'; + armed = false; + misfire = false; error = ''; + math = Math; + isMobile: boolean = window.innerWidth <= 767.98; + + private _step: CheckoutStep = 'summary'; + simpleMode: boolean = true; + paymentMethod: 'cashapp' | 'btcpay'; + + user: any = undefined; // accelerator stuff square: { appId: string, locationId: string}; accelerationUUID: string; + accelerationSubscription: Subscription; + difficultySubscription: Subscription; estimateSubscription: Subscription; estimate: AccelerationEstimate; maxBidBoost: number; // sats cost: number; // sats etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>; + showSuccess = false; + hasAncestors: boolean = false; + minExtraCost = 0; + minBidAllowed = 0; + maxBidAllowed = 0; + defaultBid = 0; + userBid = 0; + selectFeeRateIndex = 1; + maxRateOptions: RateOption[] = []; // square loadingCashapp = false; @@ -38,11 +101,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy { cashAppPay: any; cashAppSubscription: Subscription; conversionsSubscription: Subscription; - step: 'cta' | 'checkout' | 'processing' = 'cta'; + + // btcpay + loadingBtcpayInvoice = false; + invoice = undefined; constructor( + public stateService: StateService, private servicesApiService: ServicesApiServices, - private stateService: StateService, + private storageService: StorageService, private etaService: EtaService, private audioService: AudioService, private cd: ChangeDetectorRef @@ -51,11 +118,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } ngOnInit() { + this.user = this.storageService.getAuth()?.user ?? null; const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('cash_request_id')) { // Redirected from cashapp + this.moveToStep('processing'); this.insertSquare(); this.setupSquare(); - this.step = 'processing'; + } else { + this.moveToStep('summary'); } this.servicesApiService.setupSquare$().subscribe(ids => { @@ -63,9 +133,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { appId: ids.squareAppId, locationId: ids.squareLocationId }; - if (this.step === 'cta') { - this.fetchEstimate(); - } }); } @@ -76,20 +143,38 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } ngOnChanges(changes: SimpleChanges): void { - if (changes.scrollEvent) { - this.scrollToPreview('acceleratePreviewAnchor', 'start'); + if (changes.scrollEvent && this.scrollEvent) { + this.scrollToElement('acceleratePreviewAnchor', 'start'); } } + moveToStep(step: CheckoutStep) { + this._step = step; + this.misfire = false; + if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { + this.fetchEstimate(); + } + if (this._step === 'checkout' && this.canPayWithBitcoin) { + this.loadingBtcpayInvoice = true; + this.invoice = null; + this.requestBTCPayInvoice(); + } else if (this._step === 'cashapp' && this.cashappEnabled) { + this.loadingCashapp = true; + this.insertSquare(); + this.setupSquare(); + } + this.hasDetails.emit(this._step === 'quote'); + } + /** * Scroll to element id with or without setTimeout */ - scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) { + scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { setTimeout(() => { - this.scrollToPreview(id, position); - }, 1000); + this.scrollToElement(id, position); + }, timeout); } - scrollToPreview(id: string, position: ScrollLogicalPosition) { + scrollToElement(id: string, position: ScrollLogicalPosition) { const acceleratePreviewAnchor = document.getElementById(id); if (acceleratePreviewAnchor) { this.cd.markForCheck(); @@ -109,9 +194,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.estimateSubscription.unsubscribe(); } this.calculating = true; - this.estimateSubscription = this.servicesApiService.estimate$(this.txid).pipe( + this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe( tap((response) => { - this.calculating = false; if (response.status === 204) { this.error = `cannot_accelerate_tx`; } else { @@ -120,22 +204,113 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.error = `cannot_accelerate_tx`; return; } + if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { + if (this.isLoggedIn()) { + this.error = `not_enough_balance`; + } + } + this.hasAncestors = this.estimate.txSummary.ancestorCount > 1; + this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate, this.miningStats); + // Make min extra fee at least 50% of the current tx fee - const minExtraBoost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee)); - const DEFAULT_BID_RATIO = 1.5; - this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO; - this.cost = this.maxBidBoost + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; - this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate); + 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.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; + + if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { + this.loadingBtcpayInvoice = true; + this.requestBTCPayInvoice(); + } + + this.calculating = false; + this.cd.markForCheck(); } }), catchError((response) => { + this.estimate = undefined; this.error = `cannot_accelerate_tx`; + this.estimateSubscription.unsubscribe(); return of(null); }) ).subscribe(); } + /** + * User changed his bid + */ + setUserBid({ fee, index }: { fee: number, index: number}): void { + if (this.estimate) { + this.selectFeeRateIndex = index; + this.userBid = Math.max(0, fee); + this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; + } + } + + /** + * Advanced mode acceleration button clicked + */ + accelerate(): void { + if (this.canPay && !this.calculating) { + if ((!this.armed && this.step === 'summary')) { + this.misfire = true; + } else { + if (this.isLoggedIn()) { + this.accelerateWithMempoolAccount(); + } else { + this.armed = true; + this.moveToStep('checkout'); + } + } + } + } + + /** + * Account-based acceleration request + */ + accelerateWithMempoolAccount(): void { + if (this.accelerationSubscription) { + this.accelerationSubscription.unsubscribe(); + } + this.accelerationSubscription = this.servicesApiService.accelerate$( + this.tx.txid, + this.userBid, + this.accelerationUUID + ).subscribe({ + next: () => { + this.audioService.playSound('ascend-chime-cartoon'); + this.showSuccess = true; + this.estimateSubscription.unsubscribe(); + this.moveToStep('paid') + }, + error: (response) => { + if (response.status === 403 && response.error === 'not_available') { + this.error = 'waitlisted'; + } else { + this.error = response.error; + } + } + }); + } + /** * Square */ @@ -205,17 +380,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy { amount: costUSD.toString(), label: 'Total', pending: true, - productUrl: `${redirectHostname}/tracker/${this.txid}`, + productUrl: `${redirectHostname}/tracker/${this.tx.txid}`, }, button: { shape: 'semiround', size: 'small', theme: 'light'} }); this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { - redirectURL: `${redirectHostname}/tracker/${this.txid}`, - referenceId: `accelerator-${this.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`, + referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, button: { shape: 'semiround', size: 'small', theme: 'light'} }); - if (this.step === 'checkout') { + if (this.step === 'cashapp') { await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' }) } this.loadingCashapp = false; @@ -227,7 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.error = error; } else if (tokenResult.status === 'OK') { that.servicesApiService.accelerateWithCashApp$( - that.txid, + that.tx.txid, tokenResult.token, tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.referenceId, @@ -239,7 +414,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { that.cashAppPay.destroy(); } setTimeout(() => { - that.closeModal(); + this.moveToStep('paid'); if (window.history.replaceState) { const urlParams = new URLSearchParams(window.location.search); window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, '')); @@ -266,18 +441,56 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } /** - * UI events + * BTCPay */ - enableCheckoutPage() { - this.step = 'checkout'; - this.loadingCashapp = true; - this.insertSquare(); - this.setupSquare(); + async requestBTCPayInvoice() { + this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( + switchMap(response => { + return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); + }), + catchError(error => { + console.log(error); + return of(null); + }) + ).subscribe((invoice) => { + this.invoice = invoice; + this.cd.markForCheck(); + }); } - selectedOptionChanged(event) { - this.choosenOption = event.target.id; + + bitcoinPaymentCompleted(): void { + this.audioService.playSound('ascend-chime-cartoon'); + this.estimateSubscription.unsubscribe(); + this.moveToStep('paid') } - closeModal(): void { - this.close.emit(); + + isLoggedIn(): boolean { + const auth = this.storageService.getAuth(); + return auth !== null; + } + + get step() { + return this._step; + } + + get canPayWithBitcoin() { + return this.estimate?.availablePaymentMethods?.includes('bitcoin'); + } + + get canPayWithCashapp() { + return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp') && this.cost < 400000 && this.stateService.referrer === 'https://cash.app/'; + } + + get canPayWithBalance() { + return this.isLoggedIn() && this.estimate?.availablePaymentMethods?.includes('balance') && this.estimate?.hasAccess; + } + + get canPay() { + return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp; + } + + @HostListener('window:resize', ['$event']) + onResize(): void { + this.isMobile = window.innerWidth <= 767.98; } } diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html similarity index 100% rename from frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.html rename to frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss similarity index 100% rename from frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss rename to frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts similarity index 93% rename from frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts rename to frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts index ebfa019a1..d85d2ee46 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts @@ -5,7 +5,7 @@ import { Router } from '@angular/router'; import { ReplaySubject, merge, Subscription, of } from 'rxjs'; import { tap, switchMap } from 'rxjs/operators'; import { ApiService } from '../../services/api.service'; -import { AccelerationEstimate, RateOption } from './accelerate-preview.component'; +import { AccelerationEstimate, RateOption } from './accelerate-checkout.component'; interface GraphBar { rate: number; @@ -25,6 +25,7 @@ interface GraphBar { export class AccelerateFeeGraphComponent implements OnInit, OnChanges { @Input() tx: Transaction; @Input() estimate: AccelerationEstimate; + @Input() showEstimate = false; @Input() maxRateOptions: RateOption[] = []; @Input() maxRateIndex: number = 0; @Output() setUserBid = new EventEmitter<{ fee: number, index: number }>(); @@ -52,7 +53,7 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges { rate: option.rate, style: this.getStyle(option.rate, maxRate, baseHeight), class: 'max', - label: $localize`maximum`, + label: this.showEstimate ? $localize`maximum` : $localize`accelerated`, active: option.index === this.maxRateIndex, rateIndex: option.index, fee: option.fee, diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html deleted file mode 100644 index 92dc8d0f8..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html +++ /dev/null @@ -1,239 +0,0 @@ - -
-
-
- Transaction has now been submitted to mining pools for acceleration. -
-
-
- - -
-
- -
-
- -
- - - - - -
- -
-
You are currently on the waitlist
-
- - -
Your transaction
-
-
- - Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s) - - - - - - - - - - - - - - - - - - -
Virtual size
- Size in vbytes of this transaction (including unconfirmed ancestors) -
In-band fees - {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} sats -
- Fees already paid by this transaction (including unconfirmed ancestors) -
-
-
-
-
-
How much faster?
-
-
- - Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. - This will reduce your expected waiting time until the first confirmation to - -
-
- -
-
- -
-
-
-
-
- - - -
-
-
-
-
- -
Summary
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Next block market rate - {{ estimate.targetFeeRate | number : '1.0-0' }} - sat/vB
- Estimated extra fee required - - {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} - - sats - -
Mempool Accelerator™ fees
- Accelerator Service Fee - - +{{ estimate.mempoolBaseFee | number }} - - sats - -
- Transaction Size Surcharge - - +{{ estimate.vsizeFee | number }} - - sats - -
- Estimated acceleration cost ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB - - - {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} - - - sats - -
- Maximum acceleration cost - - - {{ maxCost | number }} - - - sats - - - -
Available balance - {{ estimate.userBalance | number }} - - sats - - - -
-
- @if (isLoggedIn()) { - @if (user && estimate.hasAccess) { - - } - } @else if (stateService.isMempoolSpaceBuild) { - Sign In - } @else { - Accelerate on mempool.space - } -
-
-
-
-
-
-
- - -
-
-
\ No newline at end of file diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss deleted file mode 100644 index 7194a4782..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss +++ /dev/null @@ -1,132 +0,0 @@ -.fee-card { - padding: 15px; - background-color: var(--bg); - - .feerate { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - .rate { - font-size: 0.9em; - .symbol { - color: white; - } - } - } -} - -.btn-border { - border: solid 1px black; - background-color: #0c4a87; -} - -.feerate.active { - background-color: var(--primary) !important; - opacity: 1; - border: 1px solid #007fff !important; -} -.feerate:focus { - box-shadow: none !important; -} - -.estimateDisabled { - opacity: 0.5; - pointer-events: none; -} - -.table-toggle { - width: 100%; - margin-top: 0.5em; -} - -.tab { - &:first-child { - margin-right: 1px; - } - border: solid 1px black; - border-bottom: none; - background-color: #323655; - border-top-left-radius: 10px !important; - border-top-right-radius: 10px !important; -} -.tab.active { - background-color: #5d659d !important; - opacity: 1; -} -.tab:focus { - box-shadow: none !important; -} - -.table-accelerator { - tr { - td { - padding-top: 0; - padding-bottom: 0; - vertical-align: baseline; - } - - &.group-first { - td { - padding-top: 0.75rem; - } - } - &.group-last, &:last-child { - td { - padding-bottom: 0.75rem; - } - } - &.dashed-top { - border-top: 1px dashed grey; - } - &.dashed-bottom { - border-bottom: 1px dashed grey - } - } - td { - &:first-child { - width: 100vw; - } - &.info { - color: #6c757d; - white-space: initial; - } - &.amt { - text-align: right; - padding-right: 0.2em; - } - &.units { - padding-left: 0.2em; - white-space: nowrap; - display: flex; - justify-content: space-between; - align-items: center; - } - } -} - -.accelerate-cols { - display: flex; - flex-direction: row; - align-items: stretch; - margin-top: 1em; -} - -.col.pie { - flex-grow: 0; - padding: 0 1em; -} - -.item { - white-space: initial; -} - -.table-background { - background-color: var(--bg); -} - -.col.pie { - position: relative; - top: -15px; -} \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts deleted file mode 100644 index 8ec675041..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core'; -import { Observable, Subscription, catchError, of, tap } from 'rxjs'; -import { StorageService } from '../../services/storage.service'; -import { Transaction } from '../../interfaces/electrs.interface'; -import { nextRoundNumber } from '../../shared/common.utils'; -import { ServicesApiServices } from '../../services/services-api.service'; -import { AudioService } from '../../services/audio.service'; -import { StateService } from '../../services/state.service'; -import { MiningStats } from '../../services/mining.service'; -import { EtaService } from '../../services/eta.service'; - -export type AccelerationEstimate = { - txSummary: TxSummary; - nextBlockFee: number; - targetFeeRate: number; - userBalance: number; - enoughBalance: boolean; - cost: number; - mempoolBaseFee: number; - vsizeFee: number; - pools: number[] -} -export type TxSummary = { - txid: string; // txid of the current transaction - effectiveVsize: number; // Total vsize of the dependency tree - effectiveFee: number; // Total fee of the dependency tree in sats - ancestorCount: number; // Number of ancestors -} - -export interface RateOption { - fee: number; - rate: number; - index: number; -} - -export const MIN_BID_RATIO = 1; -export const DEFAULT_BID_RATIO = 2; -export const MAX_BID_RATIO = 4; - -@Component({ - selector: 'app-accelerate-preview', - templateUrl: 'accelerate-preview.component.html', - styleUrls: ['accelerate-preview.component.scss'] -}) -export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges { - @Input() tx: Transaction; - @Input() miningStats: MiningStats; - @Input() scrollEvent: boolean; - @Input() showDetails: boolean; - - math = Math; - error = ''; - showSuccess = false; - estimateSubscription: Subscription; - accelerationSubscription: Subscription; - difficultySubscription: Subscription; - estimate: any; - etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>; - hasAncestors: boolean = false; - minExtraCost = 0; - minBidAllowed = 0; - maxBidAllowed = 0; - defaultBid = 0; - maxCost = 0; - userBid = 0; - accelerationUUID: string; - selectFeeRateIndex = 1; - isMobile: boolean = window.innerWidth <= 767.98; - user: any = undefined; - - maxRateOptions: RateOption[] = []; - - constructor( - public stateService: StateService, - private servicesApiService: ServicesApiServices, - private storageService: StorageService, - private etaService: EtaService, - private audioService: AudioService, - private cd: ChangeDetectorRef - ) { - } - - ngOnDestroy(): void { - if (this.estimateSubscription) { - this.estimateSubscription.unsubscribe(); - } - } - - ngOnInit(): void { - this.accelerationUUID = window.crypto.randomUUID(); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.scrollEvent) { - this.scrollToPreview('acceleratePreviewAnchor', 'start'); - } - } - - ngAfterViewInit(): void { - 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.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`; - this.scrollToPreviewWithTimeout('mempoolError', 'center'); - } - } - - this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate, this.miningStats); - - 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'); - - setTimeout(() => { - this.onScroll(); - }, 100); - } - } - }), - catchError((response) => { - this.estimate = undefined; - this.error = response.error; - this.scrollToPreviewWithTimeout('mempoolError', 'center'); - this.estimateSubscription.unsubscribe(); - return of(null); - }) - ).subscribe(); - } - - /** - * User changed his bid - */ - setUserBid({ fee, index }: { fee: number, index: number}): void { - if (this.estimate) { - this.selectFeeRateIndex = index; - this.userBid = Math.max(0, fee); - this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; - } - } - - /** - * Scroll to element id with or without setTimeout - */ - scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition): void { - setTimeout(() => { - this.scrollToPreview(id, position); - }, 100); - } - scrollToPreview(id: string, position: ScrollLogicalPosition): void { - const acceleratePreviewAnchor = document.getElementById(id); - if (acceleratePreviewAnchor) { - this.cd.markForCheck(); - acceleratePreviewAnchor.scrollIntoView({ - behavior: 'smooth', - inline: position, - block: position, - }); - } - } - - /** - * Send acceleration request - */ - accelerate(): void { - if (this.accelerationSubscription) { - this.accelerationSubscription.unsubscribe(); - } - this.accelerationSubscription = this.servicesApiService.accelerate$( - this.tx.txid, - this.userBid, - this.accelerationUUID - ).subscribe({ - next: () => { - this.audioService.playSound('ascend-chime-cartoon'); - this.showSuccess = true; - this.scrollToPreviewWithTimeout('successAlert', 'center'); - this.estimateSubscription.unsubscribe(); - }, - error: (response) => { - if (response.status === 403 && response.error === 'not_available') { - this.error = 'waitlisted'; - } else { - this.error = response.error; - } - this.scrollToPreviewWithTimeout('mempoolError', 'center'); - } - }); - } - - isLoggedIn(): boolean { - const auth = this.storageService.getAuth(); - return auth !== null; - } - - @HostListener('window:resize', ['$event']) - onResize(): void { - this.isMobile = window.innerWidth <= 767.98; - } - - - @HostListener('window:scroll', ['$event']) // for window scroll events - onScroll(): void { - if (this.estimate) { - setTimeout(() => { - this.onScroll(); - }, 200); - return; - } - } -} diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html new file mode 100644 index 000000000..5fd4f6701 --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -0,0 +1,99 @@ +
+ + @if (!minimal) { + + Payment successful. You can close this page. + + + + A transaction has been detected in the mempool fully paying for this invoice. Waiting for on-chain confirmation. + + } + +
+ +
+ +
+
+ + + +
+
+ +
+ + + +
+ + + +
+ +
+ +
+ +
+
+ @if (!minimal) { +

{{ loadedInvoice.btcDue | number: '1.0-8' }} BTC

+ } + +
+ + + +
+ + + +
+ +
+ +
+ +
+
+ + @if (!minimal) { +

{{ loadedInvoice.btcDue * 100_000_000 | number: '1.0-0' }} sats

+ } + +
+ + + +
+ + + +
+
+
+ +
+ +
+
+ @if (!minimal) { +

{{ loadedInvoice.btcDue | number: '1.0-8' }} BTC

+ } + +
+ + @if (!minimal) { +

Waiting for transaction...

+
+ } +
+
\ No newline at end of file diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss new file mode 100644 index 000000000..b88a2ef74 --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss @@ -0,0 +1,150 @@ +.form-panel { + background-color: #292b45; + padding: 20px; +} + + +.sponsor-page { + text-align: center; +} + +.qr-wrapper { + background-color: #FFF; + padding: 10px; + display: inline-block; + padding-bottom: 5px; + margin: 20px auto 0px; +} + +.info-group { + max-width: 400px; +} + +.card { + width: 240px; + height: 220px; + background-color: var(--bg); + border: 2px solid var(--bg); + cursor: pointer; + position: relative; + transition: 100ms all; + margin: 30px 30px 20px 30px; + @media(min-width: 476px) { + margin: 30px 100px 20px 100px; + } + @media(min-width: 851px) { + margin: 60px 20px 40px 20px; + } + + .card-title { + font-weight: bold; + span { + font-weight: 100; + } + } + + &.bigger { + height: 220px; + width: 240px; + margin-top: 40px; + } + + &:hover { + background-color: #5058926b; + border: 2px solid #505892; + transform: scale(1.1) translateY(-10px); + margin-top: 70px; + + .card-header { + background-color: #505892; + } + } +} + +.donation-form { + max-width: 280px; + margin: auto; + button { + width: 100%; + } +} + +.card-header { + background-color: #171929; +} + +.flex-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; +} + +.middle-card { + width: 280px; + height: 260px; + margin-top: 40px; + &:hover { + margin-top: 50px; + } +} + +.shiny-border { + background-color: #5058926b; + border: 2px solid #505892; + transform: scale(1.1) translateY(-10px); + margin-top: 70px; + box-shadow: 0px 0px 100px #9858ff52; + .card-header { + background-color: #505892; + } + + &.middle-card { + margin-top: 50px; + } +} + +.input-group { + margin: 20px auto; +} + +.donation-confirmed { + h2 { + margin-top: 50px; + span { + display: block; + &:last-child { + color: #9858ff; + font-weight: bold; + font-size: 2rem; + } + } + } + + .order-details { + margin-top: 50px; + span { + color: #d81b60; + margin-left: 10px; + } + } +} + +.card-body { + align-items: center; + display: flex; + justify-content: center; + flex-direction: column; + height: 100%; +} + +.wrapper { + text-align: center; + width: 100%; +} + +.input-dark { + background-color: var(--bg); + border-color: var(--active-bg); + color: white; +} diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts new file mode 100644 index 000000000..cb7e78ebd --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -0,0 +1,110 @@ +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription, of, timer } from 'rxjs'; +import { retry, switchMap, tap } from 'rxjs/operators'; +import { ServicesApiServices } from '../../services/services-api.service'; + +@Component({ + selector: 'app-bitcoin-invoice', + templateUrl: './bitcoin-invoice.component.html', + styleUrls: ['./bitcoin-invoice.component.scss'] +}) +export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy { + @Input() invoice; + @Input() invoiceId: string; + @Input() redirect = true; + @Input() minimal = false; + @Output() completed = new EventEmitter(); + + paymentForm: FormGroup; + requestSubscription: Subscription | undefined; + paymentStatusSubscription: Subscription | undefined; + loadedInvoice: any; + paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed + paramMapSubscription: Subscription | undefined; + invoiceSubscription: Subscription | undefined; + invoiceTimeout; // Wait for angular to load all the things before making a request + + constructor( + private formBuilder: FormBuilder, + private apiService: ServicesApiServices, + private sanitizer: DomSanitizer, + private activatedRoute: ActivatedRoute + ) { } + + ngOnDestroy() { + if (this.requestSubscription) { + this.requestSubscription.unsubscribe(); + } + if (this.paramMapSubscription) { + this.paramMapSubscription.unsubscribe(); + } + if (this.invoiceSubscription) { + this.invoiceSubscription.unsubscribe(); + } + if (this.paymentStatusSubscription) { + this.paymentStatusSubscription.unsubscribe(); + } + } + + ngOnInit(): void { + this.paymentForm = this.formBuilder.group({ + 'method': 'lightning' + }); + + /** + * If the invoice is passed in the url, fetch it and display btcpay payment + * Otherwise get a new invoice + */ + this.paramMapSubscription = this.activatedRoute.paramMap + .pipe( + tap((paramMap) => { + this.fetchInvoice(paramMap.get('invoiceId') ?? this.invoiceId); + }) + ).subscribe(); + } + + ngOnChanges(changes: SimpleChanges): void { + if ((changes.invoice || changes.invoiceId) && this.invoiceId) { + this.fetchInvoice(this.invoiceId); + } + } + + fetchInvoice(invoiceId: string): void { + if (invoiceId) { + if (this.paymentStatusSubscription) { + this.paymentStatusSubscription.unsubscribe(); + } + this.paymentStatusSubscription = ((this.invoice && this.invoice.id === invoiceId) ? of(this.invoice) : this.apiService.retreiveInvoice$(invoiceId)).pipe( + tap((invoice: any) => { + this.loadedInvoice = invoice; + if (this.loadedInvoice.btcDue > 0) { + this.paymentStatus = 2; + } else { + this.paymentStatus = 4; + } + }), + switchMap(() => this.apiService.getPaymentStatus$(this.loadedInvoice.id) + .pipe( + retry({ delay: () => timer(2000)}) + ) + ), + ).subscribe({ + next: ((result) => { + this.paymentStatus = 3; + this.completed.emit(); + }), + }); + } + } + + get availableMethods(): string[] { + return Object.keys(this.loadedInvoice?.addresses || {}).filter(k => k === 'BTC_LightningLike'); + } + + bypassSecurityTrustUrl(text: string): SafeUrl { + return this.sanitizer.bypassSecurityTrustUrl(text); + } +} diff --git a/frontend/src/app/components/qrcode/qrcode.component.scss b/frontend/src/app/components/qrcode/qrcode.component.scss index d4de43026..9c2aafe23 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.scss +++ b/frontend/src/app/components/qrcode/qrcode.component.scss @@ -1,9 +1,10 @@ img { position: absolute; - top: 67px; - left: 67px; - width: 65px; - height: 65px; + top: 50%; + left: 50%; + width: 42px; + height: 42px; + transform: translate(-50%, -50%); } .holder { diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index dad7522c6..f377895c0 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -37,7 +37,7 @@ export class QrcodeComponent implements AfterViewInit { return; } const opts: QRCode.QRCodeRenderersOptions = { - errorCorrectionLevel: 'L', + errorCorrectionLevel: 'M', margin: 0, color: { dark: '#000', diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 571c02f96..a0f242d46 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -75,9 +75,9 @@ } @else { } - @if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { + @@ -115,8 +115,15 @@
- @if (showAccelerationSummary && !accelerationFlowCompleted) { - + @if (isLoading) { +
+
+
+   + } @else if (showAccelerationSummary) { + + + } @else { @if (tx?.acceleration && !tx.status?.confirmed) {
diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index fe42ef0dd..349e8d43b 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -63,8 +63,9 @@ export class TrackerComponent implements OnInit, OnDestroy { mempoolPosition: MempoolPosition; accelerationPositions: AccelerationPosition[]; isLoadingTx = true; - error: any = undefined; loadingCachedTx = false; + loadingPosition = true; + error: any = undefined; waitingForTransaction = false; latestBlock: BlockExtended; transactionTime = -1; @@ -107,7 +108,6 @@ export class TrackerComponent implements OnInit, OnDestroy { now = Date.now(); da$: Observable; isMobile: boolean; - paymentType: 'bitcoin' | 'cashapp' = 'bitcoin'; trackerStage: TrackerStage = 'waiting'; @@ -149,18 +149,12 @@ export class TrackerComponent implements OnInit, OnDestroy { ngOnInit() { this.onResize(); - window['setStage'] = ((stage: TrackerStage) => { - this.zone.run(() => { - this.trackerStage = stage; - this.cd.markForCheck(); - }); - }).bind(this); - this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; - if (this.acceleratorAvailable && this.stateService.referrer === 'https://cash.app/') { - this.paymentType = 'cashapp'; - } + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + }); + const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('cash_request_id')) { this.showAccelerationSummary = true; @@ -365,6 +359,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { this.now = Date.now(); if (txPosition && txPosition.txid === this.txId && txPosition.position) { + this.loadingPosition = false; this.mempoolPosition = txPosition.position; this.accelerationPositions = txPosition.accelerationPositions; if (this.tx && !this.tx.status.confirmed) { @@ -390,11 +385,21 @@ export class TrackerComponent implements OnInit, OnDestroy { this.trackerStage = 'replaced'; } - if (txPosition.position?.block > 0 && this.tx.weight < 4000) { - this.accelerationEligible = true; - if (this.acceleratorAvailable && this.paymentType === 'cashapp') { + if (!this.mempoolPosition.accelerated) { + if (!this.accelerationFlowCompleted && !this.showAccelerationSummary && this.mempoolPosition.block > 0) { this.showAccelerationSummary = true; + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + }); } + if (txPosition.position?.block > 0) { + this.accelerationEligible = true; + } + } else if (this.showAccelerationSummary) { + setTimeout(() => { + this.accelerationFlowCompleted = true; + this.showAccelerationSummary = false; + }, 2000); } } } else { @@ -449,6 +454,7 @@ export class TrackerComponent implements OnInit, OnDestroy { )) .subscribe((tx: Transaction) => { if (!tx) { + this.loadingPosition = false; this.fetchCachedTx$.next(this.txId); this.seoService.logSoft404(); return; @@ -481,6 +487,7 @@ export class TrackerComponent implements OnInit, OnDestroy { } } else { this.trackerStage = 'confirmed'; + this.loadingPosition = false; this.fetchAcceleration$.next(tx.status.block_hash); this.fetchMiningInfo$.next({ hash: tx.status.block_hash, height: tx.status.block_height, txid: tx.txid }); this.transactionTime = 0; @@ -736,17 +743,23 @@ export class TrackerComponent implements OnInit, OnDestroy { return; } this.enterpriseService.goal(8); + this.accelerationFlowCompleted = false; this.showAccelerationSummary = true && this.acceleratorAvailable; - this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; + this.scrollIntoAccelPreview = true; return false; } + get isLoading(): boolean { + return this.isLoadingTx || this.loadingCachedTx || this.loadingPosition; + } + resetTransaction() { this.error = undefined; this.tx = null; this.txChanged$.next(true); this.waitingForTransaction = false; this.isLoadingTx = true; + this.loadingPosition = true; this.rbfTransaction = undefined; this.replaced = false; this.latestReplacement = ''; diff --git a/frontend/src/app/components/tracker/tracker.module.ts b/frontend/src/app/components/tracker/tracker.module.ts new file mode 100644 index 000000000..799b8cd65 --- /dev/null +++ b/frontend/src/app/components/tracker/tracker.module.ts @@ -0,0 +1,51 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { SharedModule } from '../../shared/shared.module'; +import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; +import { GraphsModule } from '../../graphs/graphs.module'; +import { TrackerComponent } from '../tracker/tracker.component'; +import { TrackerBarComponent } from '../tracker/tracker-bar.component'; +import { TransactionModule } from '../transaction/transaction.module'; + +const routes: Routes = [ + { + path: ':id', + component: TrackerComponent, + data: { + ogImage: true + } + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class TrackerRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + TrackerRoutingModule, + TransactionModule, + SharedModule, + GraphsModule, + TxBowtieModule, + ], + declarations: [ + TrackerComponent, + TrackerBarComponent, + ] +}) +export class TrackerModule { } + + + + + + diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 7b9a91c0e..da8763fa6 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -80,13 +80,27 @@

Accelerate

- + + +
-
- -
+ + + @@ -537,21 +551,23 @@ @if (eta.blocks >= 7) { - + In several hours (or more) - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) { Accelerate } } @else if (network === 'liquid' || network === 'liquidtestnet') { } @else { - + - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { - Accelerate + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) { + Accelerate } + + } diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 80caa6003..b43c63c2c 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -300,7 +300,6 @@ .accelerateDeepMempool { align-self: auto; - margin-top: 3px; margin-left: auto; background-color: var(--tertiary); @media (max-width: 995px) { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index c3421969b..3bc40ea93 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -139,7 +139,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === ''; showAccelerationSummary = false; showAccelerationDetails = false; + hasAccelerationDetails = false; + accelerationFlowCompleted = false; scrollIntoAccelPreview = false; + accelerationEligible = false; auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true; @ViewChild('graphContainer') @@ -169,6 +172,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit() { this.enterpriseService.page(); + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('cash_request_id')) { + this.showAccelerationSummary = true; + } + + if (!this.stateService.isLiquid) { + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + }); + } + this.websocketService.want(['blocks', 'mempool-blocks']); this.stateService.networkChanged$.subscribe( (network) => { @@ -397,6 +411,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) { this.tx.acceleratedBy = txPosition.position.acceleratedBy; } + + if (this.stateService.network === '') { + if (!this.mempoolPosition.accelerated) { + if (!this.accelerationFlowCompleted && !this.showAccelerationSummary) { + this.showAccelerationSummary = true; + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + }); + } + if (txPosition.position?.block > 0 && this.tx.weight < 4000) { + this.accelerationEligible = true; + } + } else if (this.showAccelerationSummary) { + setTimeout(() => { + this.closeAccelerator(); + }, 2000); + } + } } } else { this.mempoolPosition = null; @@ -681,14 +713,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { return; } - this.miningService.getMiningStats('1w').subscribe(stats => { - this.miningStats = stats; - }); - document.location.hash = '#accelerate'; this.enterpriseService.goal(8); - this.showAccelerationSummary = true && this.acceleratorAvailable; - this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; + this.accelerationFlowCompleted = false; + this.showAccelerationSummary = this.acceleratorAvailable; + this.scrollIntoAccelPreview = true; return false; } @@ -765,8 +794,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { setIsAccelerated(initialState: boolean = false) { this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))); - if (this.isAcceleration && initialState) { - this.showAccelerationSummary = false; + if (this.isAcceleration) { + if (initialState) { + this.accelerationFlowCompleted = true; + this.showAccelerationSummary = false; + } else if (this.showAccelerationSummary) { + setTimeout(() => { + this.closeAccelerator(); + }, 2000); + } } if (this.isAcceleration) { // this immediately returns cached stats if we fetched them recently @@ -835,7 +871,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.rbfReplaces = []; this.filters = []; this.showCpfpDetails = false; + this.showAccelerationDetails = false; this.accelerationInfo = null; + this.accelerationEligible = false; this.txInBlockIndex = null; this.mempoolPosition = null; this.pool = null; @@ -852,6 +890,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.stateService.markBlock$.next({}); } + closeAccelerator(): void { + this.accelerationFlowCompleted = true; + this.showAccelerationSummary = false; + } + roundToOneDecimal(cpfpTx: any): number { return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); } @@ -897,6 +940,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } } + setHasAccelerationDetails(hasDetails: boolean): void { + this.hasAccelerationDetails = hasDetails; + } + @HostListener('window:resize', ['$event']) setGraphSize(): void { this.isMobile = window.innerWidth < 850; @@ -911,6 +958,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } } + isLoggedIn(): boolean { + const auth = this.storageService.getAuth(); + return auth !== null; + } + ngOnDestroy() { this.subscription.unsubscribe(); this.fetchCpfpSubscription.unsubscribe(); diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index 01404db56..b98c33e2a 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -5,8 +5,8 @@ import { TransactionComponent } from './transaction.component'; import { SharedModule } from '../../shared/shared.module'; import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; import { GraphsModule } from '../../graphs/graphs.module'; -import { AcceleratePreviewComponent } from '../accelerate-preview/accelerate-preview.component'; -import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component'; +import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; +import { AccelerateFeeGraphComponent } from '../accelerate-checkout/accelerate-fee-graph.component'; const routes: Routes = [ { @@ -43,7 +43,12 @@ export class TransactionRoutingModule { } ], declarations: [ TransactionComponent, - AcceleratePreviewComponent, + AccelerateCheckout, + AccelerateFeeGraphComponent, + ], + exports: [ + TransactionComponent, + AccelerateCheckout, AccelerateFeeGraphComponent, ] }) diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index 2e80fd31c..cc1436e4c 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -5,8 +5,8 @@ import { MempoolBlock } from '../interfaces/websocket.interface'; import { Transaction } from '../interfaces/electrs.interface'; import { MiningService, MiningStats } from './mining.service'; import { getUnacceleratedFeeRate } from '../shared/transaction.utils'; -import { AccelerationEstimate } from '../components/accelerate-preview/accelerate-preview.component'; -import { Observable, combineLatest, map, of } from 'rxjs'; +import { AccelerationEstimate } from '../components/accelerate-checkout/accelerate-checkout.component'; +import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs'; export interface ETA { now: number, // time at which calculation performed @@ -61,7 +61,8 @@ export class EtaService { { block: 0, hashrateShare: acceleratingHashrateFraction }, ], da).time, }; - }) + }), + shareReplay() ); } diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index bdc6d18c2..0dc58b957 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -167,4 +167,20 @@ export class ServicesApiServices { requestTestnet4Coins$(address: string, sats: number) { return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' }); } + + generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable { + const params = { + product: txid, + amount: sats, + }; + return this.httpClient.post(`${SERVICES_API_PREFIX}/payments/bitcoin`, params); + } + + retreiveInvoice$(invoiceId: string): Observable { + return this.httpClient.get(`${SERVICES_API_PREFIX}/payments/bitcoin/invoice?id=${invoiceId}`); + } + + getPaymentStatus$(orderId: string): Observable { + return this.httpClient.get(`${SERVICES_API_PREFIX}/payments/bitcoin/check?order_id=${orderId}`); + } } diff --git a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts index e60c7c524..1706be24d 100644 --- a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts +++ b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts @@ -29,6 +29,7 @@ const MempoolErrors = { 'faucet_address_not_allowed': `You cannot use this address`, 'faucet_below_minimum': `Requested amount is too small`, 'faucet_above_maximum': `Requested amount is too high`, + 'payment_method_not_allowed': `You are not allowed to use this payment method`, } as { [error: string]: string }; export function isMempoolError(error: string) { diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2f7bd4dc4..c060bbbd2 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -50,8 +50,6 @@ import { BlockOverviewGraphComponent } from '../components/block-overview-graph/ import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; import { BlockFiltersComponent } from '../components/block-filters/block-filters.component'; import { AddressGroupComponent } from '../components/address-group/address-group.component'; -import { TrackerComponent } from '../components/tracker/tracker.component'; -import { TrackerBarComponent } from '../components/tracker/tracker-bar.component'; import { SearchFormComponent } from '../components/search-form/search-form.component'; import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; import { FooterComponent } from '../components/footer/footer.component'; @@ -100,7 +98,6 @@ import { MempoolErrorComponent } from './components/mempool-error/mempool-error. import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; -import { AccelerateCheckout } from '../components/accelerate-checkout/accelerate-checkout.component'; import { BlockViewComponent } from '../components/block-view/block-view.component'; import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; @@ -115,6 +112,7 @@ import { HttpErrorComponent } from '../shared/components/http-error/http-error.c import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component'; import { FaucetComponent } from '../components/faucet/faucet.component'; import { TwitterLogin } from '../components/twitter-login/twitter-login.component'; +import { BitcoinInvoiceComponent } from '../components/bitcoin-invoice/bitcoin-invoice.component'; import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives'; @@ -164,8 +162,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir BlockFiltersComponent, TransactionsListComponent, AddressGroupComponent, - TrackerComponent, - TrackerBarComponent, SearchFormComponent, AddressLabelsComponent, FooterComponent, @@ -224,12 +220,12 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir MempoolErrorComponent, AccelerationsListComponent, AccelerationStatsComponent, - AccelerateCheckout, PendingStatsComponent, HttpErrorComponent, TwitterWidgetComponent, FaucetComponent, TwitterLogin, + BitcoinInvoiceComponent, ], imports: [ CommonModule, @@ -305,8 +301,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir BlockFiltersComponent, TransactionsListComponent, AddressGroupComponent, - TrackerComponent, - TrackerBarComponent, SearchFormComponent, AddressLabelsComponent, FooterComponent, @@ -354,11 +348,11 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir MempoolErrorComponent, AccelerationsListComponent, AccelerationStatsComponent, - AccelerateCheckout, PendingStatsComponent, HttpErrorComponent, TwitterWidgetComponent, TwitterLogin, + BitcoinInvoiceComponent, MempoolBlockOverviewComponent, ClockchainComponent, diff --git a/frontend/src/resources/bitcoin-logo.png b/frontend/src/resources/bitcoin-logo.png new file mode 100644 index 000000000..5d7962d2a Binary files /dev/null and b/frontend/src/resources/bitcoin-logo.png differ diff --git a/frontend/src/resources/btcpay.svg b/frontend/src/resources/btcpay.svg new file mode 100644 index 000000000..5d8592b71 --- /dev/null +++ b/frontend/src/resources/btcpay.svg @@ -0,0 +1 @@ +btcpay3 \ No newline at end of file diff --git a/frontend/src/resources/cash-app.svg b/frontend/src/resources/cash-app.svg new file mode 100644 index 000000000..4dc645081 --- /dev/null +++ b/frontend/src/resources/cash-app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/resources/lightning-logo.png b/frontend/src/resources/lightning-logo.png new file mode 100644 index 000000000..4507ae9ad Binary files /dev/null and b/frontend/src/resources/lightning-logo.png differ