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 39e659971..b8b9a4283 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -6,327 +6,367 @@ } @else if (step === 'quote') { - @if (!simpleMode) { - -
-
-
- Transaction has now been submitted to mining pools for acceleration. + +
+
+
+ Transaction has now been submitted to mining pools for acceleration. +
+
+
+ + +
+
+ +
+
+ +
+ + + + + +
+ + +
+
You are currently on the waitlist
-
-
- -
-
- -
-
- -
- - - - - -
- -
-
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 (showDetails) { +
Your transaction
+ + Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s) + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + +
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 - -
- @if (isLoggedIn()) { - Maximum acceleration cost - } @else { - Acceleration cost - } - - - {{ cost | number }} - - - sats - - - -
Available balance - {{ estimate.userBalance | number }} - - sats - - - -
-
- -
+
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)
-
-
-
- - -
-
-
- } - @else { - -
-
-

Accelerate your Bitcoin transaction?

-
-
- -
-
-
-
- - +
+ } +
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 + +
+
+
-
-
- -
+ +
+ + +
+
+
+ } + @else if (step === 'summary') { + +
+
+

Accelerate your Bitcoin transaction?

+
+
+ + +
+
+
+ + -
-
-
- Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. - + } +
-
-
- +
+
+ +
- - } - } @else if (step === 'paymentMethod') { -
-
-

Select your payment method

+
+ Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + +
-
-
-
- ({{ cost | number }} sats) +
+
+ +
-
-
- @if (cashappEnabled) { - - } - -
- -
-
-
- -
-
- + } @else if (step === 'checkout') { +
+
+
+ Accelerate to ~{{ ((userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} sat/vB + + @if (!calculating) { + For an additional ({{ cost | number }} sats) + } @else { + Calculating cost... + } + + + Reducing expected confirmation time to + +
+
+
+ Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + +
+
+ @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { +
+ +
+ } @else { +
+
+

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

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

Pay {{ cost | number }} sats

+ + } @else { + Loading invoice... +
+ } +
+ @if (canPayWithCashapp) { +
+

OR

+
+ } + } + @if (cashappEnabled) { +
+

Pay with

+ +
+ } +
+
+ } + @if (showSummary) { +
+
+ +
+
+ } + } @else if (step === 'cashapp') {
@@ -342,44 +382,40 @@
- @if (paymentMethod === 'cashapp') { - @if (!loadingCashapp) { -
-
-
- Total additional cost
- - Pay - - with - -
-
-
-
- } - + @if (!loadingCashapp) {
-
- @if (loadingCashapp) { -
- Loading payment method... -
-
- } + Total additional cost
+ + Pay + + with + +
- } @else if (paymentMethod === 'btcpay' && invoice?.btcpayInvoiceId) { - } +
+
+
+
+ @if (loadingCashapp) { +
+ Loading payment method... +
+
+ } +
+
+
+
- +
} @@ -404,5 +440,4 @@
} - -
+
\ 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 e03f223ca..1fdb51086 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -141,6 +141,10 @@ margin-top: 1em; } +.payment-area { + background: var(--bg); +} + .col.pie { flex-grow: 0; padding: 0 1em; @@ -154,4 +158,17 @@ .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; } \ 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 2a7f0d067..ee4e84518 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -41,6 +41,8 @@ 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'; + @Component({ selector: 'app-accelerate-checkout', templateUrl: './accelerate-checkout.component.html', @@ -51,9 +53,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() miningStats: MiningStats; @Input() eta: ETA; @Input() scrollEvent: boolean; - @Input() cashappEnabled: boolean; - @Input() showDetails: boolean; + @Input() cashappEnabled: boolean = true; @Input() advancedEnabled: boolean = false; + @Input() forceSummary: boolean = false; @Input() forceMobile: boolean = false; @Output() changeMode = new EventEmitter(); @Output() close = new EventEmitter(); @@ -64,8 +66,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { math = Math; isMobile: boolean = window.innerWidth <= 767.98; - step: 'quote' | 'paymentMethod' | 'checkout' | 'processing' = 'quote'; + private _step: CheckoutStep = 'summary'; simpleMode: boolean = true; + showDetails: boolean = false; paymentMethod: 'cashapp' | 'btcpay'; user: any = undefined; @@ -117,9 +120,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy { 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 if (this.isLoggedIn() || this.forceSummary) { + this.moveToStep('summary'); + } else { + this.moveToStep('checkout'); } this.servicesApiService.setupSquare$().subscribe(ids => { @@ -127,9 +134,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { appId: ids.squareAppId, locationId: ids.squareLocationId }; - if (this.step === 'quote') { - this.fetchEstimate(); - } }); } @@ -145,6 +149,21 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } } + moveToStep(step: CheckoutStep) { + this._step = step; + if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { + this.fetchEstimate(); + } + if (this._step === 'checkout' && this.canPayWithBitcoin) { + this.loadingBtcpayInvoice = true; + this.requestBTCPayInvoice(); + } else if (this._step === 'cashapp' && this.cashappEnabled) { + this.loadingCashapp = true; + this.insertSquare(); + this.setupSquare(); + } + } + /** * Scroll to element id with or without setTimeout */ @@ -214,6 +233,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; + if (this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { + this.loadingBtcpayInvoice = true; + this.requestBTCPayInvoice(); + } + this.calculating = false; this.cd.markForCheck(); } @@ -244,9 +268,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy { */ accelerate(): void { if (this.isLoggedIn()) { - this.accelerateWithMempoolAccount(); + if (this.step !== 'summary') { + this.moveToStep('summary'); + } else { + this.accelerateWithMempoolAccount(); + } } else { - this.step = 'paymentMethod'; + this.moveToStep('checkout'); } } @@ -356,7 +384,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { 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; @@ -410,7 +438,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * BTCPay */ async requestBTCPayInvoice() { - this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid).subscribe({ + this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).subscribe({ next: (response) => { this.invoice = response; this.cd.markForCheck(); @@ -425,27 +453,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { /** * UI events */ - enableCheckoutPage() { - this.step = 'paymentMethod'; - } - selectPaymentMethod(paymentMethod: 'cashapp' | 'btcpay') { - this.step = 'checkout'; - this.paymentMethod = paymentMethod; - if (paymentMethod === 'cashapp') { - this.loadingCashapp = true; - this.insertSquare(); - this.setupSquare(); - } else if (paymentMethod === 'btcpay') { - this.loadingBtcpayInvoice = true; - this.requestBTCPayInvoice(); - } - } selectedOptionChanged(event) { this.choosenOption = event.target.id; } closeModal(timeout: number = 0): void { setTimeout(() => { - this.step = 'processing'; + this._step = 'processing'; this.cd.markForCheck(); this.close.emit(); }, timeout); @@ -456,6 +469,26 @@ export class AccelerateCheckout implements OnInit, OnDestroy { 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('bitcoin'); + } + + get canPayWithBalance() { + return this.isLoggedIn() && this.estimate?.availablePaymentMethods?.includes('balance'); + } + + get showSummary() { + return this.canPayWithBalance || this.forceSummary; + } + @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-preview/accelerate-fee-graph.component.html deleted file mode 100644 index fe0718ecc..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- -
-
-
-

- {{ bar.label }} - - - -

-
-
- {{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} sat -
-
-
-
-
-
diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss b/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss deleted file mode 100644 index 919fdec4a..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss +++ /dev/null @@ -1,156 +0,0 @@ -.fee-graph { - height: 100%; - min-width: 120px; - width: 120px; - margin-left: 4em; - margin-right: 1.5em; - - .column { - width: 100%; - height: 100%; - position: relative; - background: var(--stat-box-bg); - - .bar { - position: absolute; - bottom: 0; - left: 0; - right: 0; - min-height: 30px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - .fill { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - opacity: 0.75; - pointer-events: none; - } - - .fee { - font-size: 0.9em; - opacity: 0; - pointer-events: none; - } - - .spacer { - width: 100%; - height: 1px; - flex-grow: 1; - pointer-events: none; - } - - .line { - position: absolute; - right: 0; - top: 0; - left: -4.5em; - border-top: dashed white 1.5px; - - .fee-rate { - width: 100%; - position: absolute; - left: 0; - right: 0.2em; - font-size: 0.8em; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - margin: 0; - - .label { - margin-right: .2em; - } - - .rate .symbol { - color: white; - } - } - } - - &.tx { - .fill { - background: var(--green); - } - .line { - .fee-rate { - top: 0; - } - } - .fee { - position: absolute; - opacity: 1; - z-index: 11; - } - } - - &.target { - .fill { - background: var(--tertiary); - } - .fee { - position: absolute; - opacity: 1; - z-index: 11; - } - .line .fee-rate { - bottom: 2px; - } - } - - &.max { - cursor: pointer; - .line .fee-rate { - .label { - opacity: 0; - } - bottom: 2px; - } - &.active, &:hover { - .fill { - background: var(--primary); - } - .line { - .fee-rate .label { - opacity: 1; - } - } - } - } - - &:hover { - .fill { - z-index: 10; - } - .line { - z-index: 11; - } - .fee { - opacity: 1; - z-index: 12; - } - } - } - - &:hover > .bar:not(:hover) { - &.target, &.max { - .fee { - opacity: 0; - } - .line .fee-rate .label { - opacity: 0; - } - } - &.max { - .fill { - background: none; - } - } - } - } -} \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts deleted file mode 100644 index ebfa019a1..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core'; -import { StateService } from '../../services/state.service'; -import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; -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'; - -interface GraphBar { - rate: number; - style: any; - class: 'tx' | 'target' | 'max'; - label: string; - active?: boolean; - rateIndex?: number; - fee?: number; -} - -@Component({ - selector: 'app-accelerate-fee-graph', - templateUrl: './accelerate-fee-graph.component.html', - styleUrls: ['./accelerate-fee-graph.component.scss'], -}) -export class AccelerateFeeGraphComponent implements OnInit, OnChanges { - @Input() tx: Transaction; - @Input() estimate: AccelerationEstimate; - @Input() maxRateOptions: RateOption[] = []; - @Input() maxRateIndex: number = 0; - @Output() setUserBid = new EventEmitter<{ fee: number, index: number }>(); - - bars: GraphBar[] = []; - tooltipPosition = { x: 0, y: 0 }; - - ngOnInit(): void { - this.initGraph(); - } - - ngOnChanges(): void { - this.initGraph(); - } - - initGraph(): void { - if (!this.tx || !this.estimate) { - return; - } - const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate)); - const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize; - const baseHeight = baseRate / maxRate; - const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => { - return { - rate: option.rate, - style: this.getStyle(option.rate, maxRate, baseHeight), - class: 'max', - label: $localize`maximum`, - active: option.index === this.maxRateIndex, - rateIndex: option.index, - fee: option.fee, - } - }); - if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) { - bars.push({ - rate: this.estimate.targetFeeRate, - style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight), - class: 'target', - label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(), - fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee - }); - } - bars.push({ - rate: baseRate, - style: this.getStyle(baseRate, maxRate, 0), - class: 'tx', - label: '', - fee: this.estimate.txSummary.effectiveFee, - }); - this.bars = bars; - } - - getStyle(rate, maxRate, base) { - const top = (rate / maxRate); - return { - height: `${(top - base) * 100}%`, - bottom: base ? `${base * 100}%` : '0', - } - } - - onClick(event, bar): void { - if (bar.rateIndex != null) { - this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex }); - } - } - - @HostListener('pointermove', ['$event']) - onPointerMove(event) { - this.tooltipPosition = { x: event.offsetX, y: event.offsetY }; - } -} 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 index 22205973b..1731a0e43 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -1,12 +1,14 @@
- - 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) { + + 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. + + }
@@ -30,25 +32,27 @@ -
+ - +
-

{{ invoice.amount }} BTC

+ @if (!minimal) { +

{{ invoice.amount }} BTC

+ } -
+
@@ -61,13 +65,15 @@
-

{{ invoice.amount * 100_000_000 }} sats

+ @if (!minimal) { +

{{ invoice.amount * 100_000_000 }} sats

+ }
-
+
@@ -79,11 +85,15 @@
-

{{ invoice.amount }} BTC

+ @if (!minimal) { +

{{ invoice.amount }} BTC

+ }
-

Waiting for transaction...

-
+ @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 index 7582b70f0..b88a2ef74 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss @@ -140,6 +140,7 @@ .wrapper { text-align: center; + width: 100%; } .input-dark { diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts index 2e12f54ba..6f2d4b36c 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -14,6 +14,7 @@ import { ServicesApiServices } from '../../services/services-api.service'; export class BitcoinInvoiceComponent implements OnInit, OnDestroy { @Input() invoiceId: string; @Input() redirect = true; + @Input() minimal = false; @Output() completed = new EventEmitter(); paymentForm: FormGroup; diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index faa2db793..03375c44d 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -117,7 +117,7 @@
@if (showAccelerationSummary && !accelerationFlowCompleted) { - + } @else { @if (tx?.acceleration && !tx.status?.confirmed) { 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 87477c5d7..7cba84784 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -80,12 +80,11 @@

Accelerate

-
- + Urgent transaction? Get it confirmed faster. diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index b536b3045..5429a22cc 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -7,8 +7,6 @@ import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; import { GraphsModule } from '../../graphs/graphs.module'; import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; import { AccelerateFeeGraphComponent } from '../accelerate-checkout/accelerate-fee-graph.component'; -import { TrackerComponent } from '../tracker/tracker.component'; -import { TrackerBarComponent } from '../tracker/tracker-bar.component'; const routes: Routes = [ { @@ -40,8 +38,11 @@ export class TransactionRoutingModule { } ], declarations: [ TransactionComponent, - TrackerComponent, - TrackerBarComponent, + AccelerateCheckout, + AccelerateFeeGraphComponent, + ], + exports: [ + TransactionComponent, AccelerateCheckout, AccelerateFeeGraphComponent, ] diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 534f45b4e..0dc58b957 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -168,9 +168,10 @@ export class ServicesApiServices { return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' }); } - generateBTCPayAcceleratorInvoice$(txid: string): Observable { + generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable { const params = { - product: txid + product: txid, + amount: sats, }; return this.httpClient.post(`${SERVICES_API_PREFIX}/payments/bitcoin`, params); }