From 66a88b8422ff299f5ffe0ee98cc337f287a25098 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 26 Jun 2024 18:35:36 +0900 Subject: [PATCH 01/45] [accelerator] accelerate with lightning --- .../accelerate-checkout.component.html | 70 ++++---- .../accelerate-checkout.component.scss | 8 + .../accelerate-checkout.component.ts | 61 +++++-- .../bitcoin-invoice.component.html | 89 +++++++++++ .../bitcoin-invoice.component.scss | 149 ++++++++++++++++++ .../bitcoin-invoice.component.ts | 94 +++++++++++ .../components/tracker/tracker.component.html | 4 +- .../components/tracker/tracker.component.ts | 8 +- .../src/app/services/services-api.service.ts | 15 ++ frontend/src/app/shared/shared.module.ts | 3 + 10 files changed, 453 insertions(+), 48 deletions(-) create mode 100644 frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html create mode 100644 frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss create mode 100644 frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts 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..40089ddbf 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -58,12 +58,24 @@ - } - - @else if (step === 'checkout') { - + + } @else if (step === 'paymentMethod') {
+

Select your payment method

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

Confirm your payment

@@ -76,36 +88,40 @@ - @if (!loadingCashapp) { + @if (paymentMethod === 'cashapp') { + @if (!loadingCashapp) { +
+
+
+ Total additional cost
+ + Pay + + with + +
+
+
+
+ } +
- Total additional cost
- - Pay - - with - -
+
+ @if (loadingCashapp) { +
+ Loading payment method... +
+
+ }
+ } @else if (paymentMethod === 'btcpay' && invoice?.btcpayInvoiceId) { + } -
-
-
-
- @if (loadingCashapp) { -
- Loading payment method... -
-
- } -
-
-
-
@@ -118,7 +134,7 @@ @else if (step === 'processing') {
-

Confirm your payment

+

Confirming your payment

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..268f03f93 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,11 @@ .estimating { color: var(--green) } + +.paymentMethod { + padding: 10px; + background-color: var(--secondary); + border-radius: 15px; + border: 2px solid var(--bg); + cursor: pointer; +} \ 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..ba867d096 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -16,12 +16,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: number | null = null; @Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad'; @Input() scrollEvent: boolean; + @Input() cashappEnabled: boolean; @Output() close = new EventEmitter(); calculating = true; choosenOption: 'wait' | 'accelerate' = 'wait'; error = ''; + step: 'paymentMethod' | 'cta' | 'checkout' | 'processing' = 'cta'; + paymentMethod: 'cashapp' | 'btcpay'; + // accelerator stuff square: { appId: string, locationId: string}; accelerationUUID: string; @@ -38,7 +42,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { cashAppPay: any; cashAppSubscription: Subscription; conversionsSubscription: Subscription; - step: 'cta' | 'checkout' | 'processing' = 'cta'; + + // btcpay + loadingBtcpayInvoice = false; + invoice = undefined; constructor( private servicesApiService: ServicesApiServices, @@ -77,19 +84,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ngOnChanges(changes: SimpleChanges): void { if (changes.scrollEvent) { - this.scrollToPreview('acceleratePreviewAnchor', 'start'); + this.scrollToElement('acceleratePreviewAnchor', 'start'); } } /** * Scroll to element id with or without setTimeout */ - scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) { + scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000) { 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(); @@ -111,7 +118,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.calculating = true; this.estimateSubscription = this.servicesApiService.estimate$(this.txid).pipe( tap((response) => { - this.calculating = false; if (response.status === 204) { this.error = `cannot_accelerate_tx`; } else { @@ -126,6 +132,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO; this.cost = this.maxBidBoost + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate); + this.calculating = false; + this.cd.markForCheck(); } }), @@ -265,19 +273,48 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ); } + /** + * BTCPay + */ + async requestBTCPayInvoice() { + this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.txid).subscribe({ + next: (response) => { + this.invoice = response; + this.cd.markForCheck(); + this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 500); + }, + error: (response) => { + console.log(response); + } + }); + } + /** * UI events */ enableCheckoutPage() { + this.step = 'paymentMethod'; + } + selectPaymentMethod(paymentMethod: 'cashapp' | 'btcpay') { this.step = 'checkout'; - this.loadingCashapp = true; - this.insertSquare(); - this.setupSquare(); + 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(): void { - this.close.emit(); + closeModal(timeout: number = 0): void { + setTimeout(() => { + this.step = 'processing'; + this.cd.markForCheck(); + this.close.emit(); + }, timeout); } } 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..dabaf991e --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -0,0 +1,89 @@ +
+ + + 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. + + +
+ +
+ +
+
+ + + +
+
+ +
+ + + +
+ + + +
+ +
+ +
+ +
+
+

{{ invoice.amount }} BTC

+ +
+ + + +
+ + + +
+ +
+ +
+ +
+
+ +

{{ invoice.amount * 100_000_000 }} sats

+ +
+ + + +
+ + + +
+
+
+ +
+ +
+
+

{{ invoice.amount }} BTC

+ +
+ +

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..7582b70f0 --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss @@ -0,0 +1,149 @@ +.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; +} + +.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..2e12f54ba --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -0,0 +1,94 @@ +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription, 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, OnDestroy { + @Input() invoiceId: string; + @Input() redirect = true; + @Output() completed = new EventEmitter(); + + paymentForm: FormGroup; + requestSubscription: Subscription | undefined; + paymentStatusSubscription: Subscription | undefined; + invoice: 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) => { + const invoiceId = paramMap.get('invoiceId') ?? this.invoiceId; + if (invoiceId) { + this.paymentStatusSubscription = this.apiService.retreiveInvoice$(invoiceId).pipe( + tap((invoice: any) => { + this.invoice = invoice; + this.invoice.amount = invoice.btcDue ?? (invoice.cryptoInfo.length ? parseFloat(invoice.cryptoInfo[0].totalDue) : 0) ?? 0; + + if (this.invoice.amount > 0) { + this.paymentStatus = 2; + } else { + this.paymentStatus = 4; + } + }), + switchMap(() => this.apiService.getPaymentStatus$(this.invoice.id) + .pipe( + retry({ delay: () => timer(2000)}) + ) + ), + ).subscribe({ + next: ((result) => { + this.paymentStatus = 3; + this.completed.emit(); + }), + }); + } + }) + ).subscribe(); + } + + bypassSecurityTrustUrl(text: string): SafeUrl { + return this.sanitizer.bypassSecurityTrustUrl(text); + } +} diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 571c02f96..1380990df 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -75,7 +75,7 @@ } @else { } - @if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { + @if (!showAccelerationSummary && isMobile && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { Accelerate } @@ -116,7 +116,7 @@
@if (showAccelerationSummary && !accelerationFlowCompleted) { - + } @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..508c8db19 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -107,7 +107,6 @@ export class TrackerComponent implements OnInit, OnDestroy { now = Date.now(); da$: Observable; isMobile: boolean; - paymentType: 'bitcoin' | 'cashapp' = 'bitcoin'; trackerStage: TrackerStage = 'waiting'; @@ -158,9 +157,6 @@ export class TrackerComponent implements OnInit, OnDestroy { 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'; - } const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('cash_request_id')) { this.showAccelerationSummary = true; @@ -390,11 +386,9 @@ export class TrackerComponent implements OnInit, OnDestroy { this.trackerStage = 'replaced'; } + this.showAccelerationSummary = true; if (txPosition.position?.block > 0 && this.tx.weight < 4000) { this.accelerationEligible = true; - if (this.acceleratorAvailable && this.paymentType === 'cashapp') { - this.showAccelerationSummary = true; - } } } } else { diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index bdc6d18c2..534f45b4e 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -167,4 +167,19 @@ 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): Observable { + const params = { + product: txid + }; + 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/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2f7bd4dc4..e3f219aba 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -115,6 +115,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'; @@ -230,6 +231,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir TwitterWidgetComponent, FaucetComponent, TwitterLogin, + BitcoinInvoiceComponent, ], imports: [ CommonModule, @@ -359,6 +361,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir HttpErrorComponent, TwitterWidgetComponent, TwitterLogin, + BitcoinInvoiceComponent, MempoolBlockOverviewComponent, ClockchainComponent, From 790e76ab26ed3fb262dc66ea3545ae409a6963b9 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 26 Jun 2024 18:38:19 +0900 Subject: [PATCH 02/45] [accelerator] add payment methods assets --- frontend/src/resources/btcpay.svg | 1 + frontend/src/resources/cash-app.svg | 1 + 2 files changed, 2 insertions(+) create mode 100644 frontend/src/resources/btcpay.svg create mode 100644 frontend/src/resources/cash-app.svg 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 From 4445fe408b947d833a022ca2030334c5735a0788 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 27 Jun 2024 02:02:35 +0000 Subject: [PATCH 03/45] Add simple mode checkout to main transaction page --- .../accelerate-checkout.component.html | 52 ++++++++++--------- .../accelerate-checkout.component.scss | 10 ++++ .../accelerate-checkout.component.ts | 22 ++++---- .../components/tracker/tracker.component.html | 4 +- .../transaction/transaction.component.html | 14 +++-- .../transaction/transaction.component.ts | 7 +++ .../transaction/transaction.module.ts | 6 +++ frontend/src/app/shared/shared.module.ts | 9 ---- 8 files changed, 77 insertions(+), 47 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 40089ddbf..aba494d0e 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -1,4 +1,4 @@ -
+
@if (error) {
@@ -10,18 +10,33 @@
-

Accelerate your Bitcoin transaction?

+

Accelerate your Bitcoin transaction?

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

Select your payment method

+

Select your payment method

@@ -76,14 +78,14 @@
-

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) }}
@@ -134,7 +136,7 @@ @else if (step === 'processing') {
-

Confirming your payment

+

Confirming your payment

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 268f03f93..af11f6c2b 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -14,4 +14,14 @@ 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; } \ 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 ba867d096..9e37cb67f 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -5,7 +5,9 @@ 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'; @Component({ selector: 'app-accelerate-checkout', @@ -13,10 +15,12 @@ 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; @Input() cashappEnabled: boolean; + @Input() isTracker: boolean = false; @Output() close = new EventEmitter(); calculating = true; @@ -116,7 +120,7 @@ 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) => { if (response.status === 204) { this.error = `cannot_accelerate_tx`; @@ -213,13 +217,13 @@ 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'} }); @@ -235,7 +239,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, @@ -277,7 +281,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * BTCPay */ async requestBTCPayInvoice() { - this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.txid).subscribe({ + this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid).subscribe({ next: (response) => { this.invoice = response; this.cd.markForCheck(); diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 1380990df..c0f77c424 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -116,7 +116,9 @@
@if (showAccelerationSummary && !accelerationFlowCompleted) { - + + + } @else { @if (tx?.acceleration && !tx.status?.confirmed) {
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 43ead974d..65c859e5c 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -84,9 +84,17 @@
-
- -
+ @if (isLoggedIn()) { +
+ +
+ } @else { + + + Urgent transaction? Get it confirmed faster. + + + } diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 570242a9a..c84eb8787 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -140,6 +140,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { showAccelerationSummary = false; showAccelerationDetails = 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') @@ -397,6 +398,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) { this.tx.acceleratedBy = txPosition.position.acceleratedBy; } + this.accelerationEligible = txPosition?.position?.block > 0 && this.tx?.weight < 4000; } } else { this.mempoolPosition = null; @@ -910,6 +912,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 eb663c9ac..ac09067de 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -6,7 +6,10 @@ 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 { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component'; +import { TrackerComponent } from '../tracker/tracker.component'; +import { TrackerBarComponent } from '../tracker/tracker-bar.component'; const routes: Routes = [ { @@ -38,7 +41,10 @@ export class TransactionRoutingModule { } ], declarations: [ TransactionComponent, + TrackerComponent, + TrackerBarComponent, AcceleratePreviewComponent, + AccelerateCheckout, AccelerateFeeGraphComponent, ] }) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index e3f219aba..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'; @@ -165,8 +162,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir BlockFiltersComponent, TransactionsListComponent, AddressGroupComponent, - TrackerComponent, - TrackerBarComponent, SearchFormComponent, AddressLabelsComponent, FooterComponent, @@ -225,7 +220,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir MempoolErrorComponent, AccelerationsListComponent, AccelerationStatsComponent, - AccelerateCheckout, PendingStatsComponent, HttpErrorComponent, TwitterWidgetComponent, @@ -307,8 +301,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir BlockFiltersComponent, TransactionsListComponent, AddressGroupComponent, - TrackerComponent, - TrackerBarComponent, SearchFormComponent, AddressLabelsComponent, FooterComponent, @@ -356,7 +348,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir MempoolErrorComponent, AccelerationsListComponent, AccelerationStatsComponent, - AccelerateCheckout, PendingStatsComponent, HttpErrorComponent, TwitterWidgetComponent, From 9fe44bd6ba1d801dcb38e3f0f728015c2eb64448 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 27 Jun 2024 06:09:23 +0000 Subject: [PATCH 04/45] more simple acceleration UI adjustments --- .../accelerate-checkout.component.html | 37 ++++++++++++------- .../accelerate-checkout.component.ts | 2 +- .../bitcoin-invoice.component.html | 2 +- frontend/src/app/services/eta.service.ts | 5 ++- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index aba494d0e..b559f3a09 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -15,10 +15,10 @@
-
+
- +
- +
- Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.
-
+
+
} @else if (step === 'checkout') { @@ -127,8 +139,7 @@
- Changed your mind? - +
} 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 9e37cb67f..f646b7c5a 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -24,7 +24,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Output() close = new EventEmitter(); calculating = true; - choosenOption: 'wait' | 'accelerate' = 'wait'; + choosenOption: 'wait' | 'accelerate'; error = ''; step: 'paymentMethod' | 'cta' | 'checkout' | 'processing' = 'cta'; 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 dabaf991e..22205973b 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -46,7 +46,7 @@ - +
diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index 2e80fd31c..3dc396a55 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -6,7 +6,7 @@ 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 { 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() ); } From d7acd389bf43c8877de8968302ccfd7870d937be Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 27 Jun 2024 06:42:31 +0000 Subject: [PATCH 05/45] fix scrolljacking by #accelerate fragment --- .../accelerate-checkout.component.html | 8 ++++---- .../accelerate-checkout/accelerate-checkout.component.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index b559f3a09..1b1ca6ca4 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -18,7 +18,7 @@
- +
- -
-
+
+ +
+
+
+
+
+ +
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 + +
+ @if (isLoggedIn()) { + Maximum acceleration cost + } @else { + Acceleration cost + } + + + {{ cost | number }} + + + sats + + + +
Available balance + {{ estimate.userBalance | number }} + + sats + + + +
+
+ +
+
+
+
+
+ +
+ + +
+
+
+ } + @else { + +
+
+

Accelerate your Bitcoin transaction?

+
+
+ + +
+
+
+ + + +
+
+
+
+ + +
+
+
+ Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. +
-
- Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. - +
+
+ +
-
-
-
- -
-
- - + + } } @else if (step === 'paymentMethod') {
@@ -82,7 +322,7 @@
- +
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 af11f6c2b..e03f223ca 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -24,4 +24,134 @@ 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; +} + +.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; + position: relative; + top: -15px; +} + +.item { + white-space: initial; +} + +.table-background { + background-color: var(--bg); } \ 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 8714373d0..3d720e757 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,13 +1,42 @@ -import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges } from '@angular/core'; +import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Subscription, tap, of, catchError, Observable } 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 { 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 AccelerationEstimate = { + hasAccess: boolean; + 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-checkout', @@ -20,24 +49,43 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: ETA; @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean; - @Input() isTracker: boolean = false; + @Input() showDetails: boolean; + @Input() advancedEnabled: boolean = false; + @Input() forceMobile: boolean = false; + @Output() changeMode = new EventEmitter(); @Output() close = new EventEmitter(); calculating = true; choosenOption: 'wait' | 'accel'; error = ''; + math = Math; + isMobile: boolean = window.innerWidth <= 767.98; - step: 'paymentMethod' | 'cta' | 'checkout' | 'processing' = 'cta'; + step: 'quote' | 'paymentMethod' | 'checkout' | 'processing' = 'quote'; + 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; @@ -52,8 +100,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { invoice = undefined; constructor( + public stateService: StateService, private servicesApiService: ServicesApiServices, - private stateService: StateService, + private storageService: StorageService, private etaService: EtaService, private audioService: AudioService, private cd: ChangeDetectorRef @@ -62,6 +111,7 @@ 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.insertSquare(); @@ -74,7 +124,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { appId: ids.squareAppId, locationId: ids.squareLocationId }; - if (this.step === 'cta') { + if (this.step === 'quote') { this.fetchEstimate(); } }); @@ -95,7 +145,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { /** * Scroll to element id with or without setTimeout */ - scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000) { + scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { setTimeout(() => { this.scrollToElement(id, position); }, timeout); @@ -130,24 +180,100 @@ 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; + 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.isLoggedIn()) { + this.accelerateWithMempoolAccount(); + } else { + this.step = 'paymentMethod'; + } + } + + /** + * 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(); + }, + error: (response) => { + if (response.status === 403 && response.error === 'not_available') { + this.error = 'waitlisted'; + } else { + this.error = response.error; + } + } + }); + } + /** * Square */ @@ -321,4 +447,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.close.emit(); }, timeout); } + + isLoggedIn(): boolean { + const auth = this.storageService.getAuth(); + return auth !== null; + } + + @HostListener('window:resize', ['$event']) + onResize(): void { + this.isMobile = window.innerWidth <= 767.98; + } } diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html new file mode 100644 index 000000000..fe0718ecc --- /dev/null +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html @@ -0,0 +1,21 @@ +
+
+ +
+
+
+

+ {{ bar.label }} + + + +

+
+
+ {{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} sat +
+
+
+
+
+
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss new file mode 100644 index 000000000..919fdec4a --- /dev/null +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss @@ -0,0 +1,156 @@ +.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-checkout/accelerate-fee-graph.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts new file mode 100644 index 000000000..c41cb2f87 --- /dev/null +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts @@ -0,0 +1,100 @@ +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-checkout.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() showEstimate = false; + @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; + console.log(maxRate, baseRate, baseHeight); + const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => { + return { + rate: option.rate, + style: this.getStyle(option.rate, maxRate, baseHeight), + class: 'max', + label: this.showEstimate ? $localize`maximum` : $localize`accelerated`, + 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/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index c0f77c424..faa2db793 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/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 65c859e5c..87477c5d7 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -84,17 +84,11 @@
- @if (isLoggedIn()) { -
- -
- } @else { - - - Urgent transaction? Get it confirmed faster. - - - } + + + 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 ac09067de..b536b3045 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -5,9 +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 { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; -import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component'; +import { AccelerateFeeGraphComponent } from '../accelerate-checkout/accelerate-fee-graph.component'; import { TrackerComponent } from '../tracker/tracker.component'; import { TrackerBarComponent } from '../tracker/tracker-bar.component'; @@ -43,7 +42,6 @@ export class TransactionRoutingModule { } TransactionComponent, TrackerComponent, TrackerBarComponent, - AcceleratePreviewComponent, AccelerateCheckout, AccelerateFeeGraphComponent, ] diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index 3dc396a55..cc1436e4c 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -5,7 +5,7 @@ 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 { AccelerationEstimate } from '../components/accelerate-checkout/accelerate-checkout.component'; import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs'; export interface ETA { From 473da82caa57c2e63cce9744d971ba74e41cd6a9 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 27 Jun 2024 12:56:49 +0000 Subject: [PATCH 07/45] acceleration estimate payment methods field --- .../accelerate-checkout/accelerate-checkout.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 3d720e757..2a7f0d067 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -9,6 +9,8 @@ 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; @@ -19,7 +21,8 @@ export type AccelerationEstimate = { cost: number; mempoolBaseFee: number; vsizeFee: number; - pools: number[] + pools: number[]; + availablePaymentMethods: PaymentMethod[]; } export type TxSummary = { txid: string; // txid of the current transaction From c75afe20cd391f552592af7a409034f6166cca45 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 28 Jun 2024 07:02:12 +0000 Subject: [PATCH 08/45] More acceleration checkout refactoring --- frontend/src/app/app-routing.module.ts | 5 +- .../accelerate-checkout.component.html | 691 +++++++++--------- .../accelerate-checkout.component.scss | 17 + .../accelerate-checkout.component.ts | 87 ++- .../accelerate-fee-graph.component.html | 21 - .../accelerate-fee-graph.component.scss | 156 ---- .../accelerate-fee-graph.component.ts | 98 --- .../accelerate-preview.component.html | 239 ------ .../accelerate-preview.component.scss | 132 ---- .../accelerate-preview.component.ts | 250 ------- .../bitcoin-invoice.component.html | 42 +- .../bitcoin-invoice.component.scss | 1 + .../bitcoin-invoice.component.ts | 1 + .../components/tracker/tracker.component.html | 2 +- .../app/components/tracker/tracker.module.ts | 51 ++ .../transaction/transaction.component.html | 3 +- .../transaction/transaction.module.ts | 9 +- .../src/app/services/services-api.service.ts | 5 +- 18 files changed, 532 insertions(+), 1278 deletions(-) delete mode 100644 frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.html delete mode 100644 frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss delete mode 100644 frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts delete mode 100644 frontend/src/app/components/accelerate-preview/accelerate-preview.component.html delete mode 100644 frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss delete mode 100644 frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts create mode 100644 frontend/src/app/components/tracker/tracker.module.ts 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); } From 254d9625586c755f6a17196b54496b1d19411863 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 27 Jun 2024 18:49:37 +0900 Subject: [PATCH 09/45] [accelerator] add new error message --- .../shared/components/mempool-error/mempool-error.component.ts | 1 + 1 file changed, 1 insertion(+) 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) { From e158c1068850680860348dc890398242b338e43a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 28 Jun 2024 13:29:44 +0000 Subject: [PATCH 10/45] [accelerator] fix duplicate invoice request --- .../accelerate-checkout/accelerate-checkout.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index ee4e84518..1a7039c92 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -233,7 +233,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; - if (this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { + if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { this.loadingBtcpayInvoice = true; this.requestBTCPayInvoice(); } From 5872b2c46b9be5fcc8671329d329d941db570788 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 28 Jun 2024 13:35:34 +0000 Subject: [PATCH 11/45] [accelerator] fix success/failure messages --- .../accelerate-checkout.component.html | 24 +++++++------------ .../accelerate-checkout.component.ts | 1 + 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index b8b9a4283..7a7dab8cf 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -1,27 +1,21 @@
- @if (error) { -
- -
- } - @else if (step === 'quote') { - -
-
-
- Transaction has now been submitted to mining pools for acceleration. -
+
+
+
+ Transaction has now been submitted to mining pools for acceleration.
+
- -
+ @if (error) { +
- + } + @else if (step === 'quote') {
{ if (response.status === 403 && response.error === 'not_available') { From 193c41cb81f760cab5c649ee1c4c10bcc8493dda Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 04:10:47 +0000 Subject: [PATCH 12/45] Fix pizza tracker loading state --- .../accelerate-checkout.component.html | 2 +- .../components/tracker/tracker.component.html | 7 ++++++- .../components/tracker/tracker.component.ts | 18 ++++++++++-------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 7a7dab8cf..5a57b492b 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -335,7 +335,7 @@ } @else { Loading invoice... -
+
}
@if (canPayWithCashapp) { diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 03375c44d..64277e3e0 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -115,7 +115,12 @@
- @if (showAccelerationSummary && !accelerationFlowCompleted) { + @if (isLoading) { +
+
+
+   + } @else if (showAccelerationSummary && !accelerationFlowCompleted) { diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 508c8db19..234d89adc 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; @@ -148,13 +149,6 @@ 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 === ''; const urlParams = new URLSearchParams(window.location.search); @@ -361,6 +355,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) { @@ -443,6 +438,7 @@ export class TrackerComponent implements OnInit, OnDestroy { )) .subscribe((tx: Transaction) => { if (!tx) { + this.loadingPosition = false; this.fetchCachedTx$.next(this.txId); this.seoService.logSoft404(); return; @@ -475,6 +471,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; @@ -735,12 +732,17 @@ export class TrackerComponent implements OnInit, OnDestroy { 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 = ''; From 48bdae4e781f0223e9b63e5e36e8a269ba3ffd7f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 04:11:02 +0000 Subject: [PATCH 13/45] [accelerator] hide pizza tracker CTA when irrelevant --- frontend/src/app/components/tracker/tracker.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 234d89adc..00051350b 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -381,9 +381,11 @@ export class TrackerComponent implements OnInit, OnDestroy { this.trackerStage = 'replaced'; } - this.showAccelerationSummary = true; - if (txPosition.position?.block > 0 && this.tx.weight < 4000) { - this.accelerationEligible = true; + if (!this.mempoolPosition.accelerated) { + this.showAccelerationSummary = true; + if (txPosition.position?.block > 0 && this.tx.weight < 4000) { + this.accelerationEligible = true; + } } } } else { From d62300ccff98b6f8ddd4184ba809cee88ffe75eb Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 06:06:11 +0000 Subject: [PATCH 14/45] [accelerator] add acceleration paid screen, fix end state --- .../accelerate-checkout.component.html | 221 ++++++++++-------- .../accelerate-checkout.component.ts | 14 +- .../components/tracker/tracker.component.html | 2 +- .../components/tracker/tracker.component.ts | 4 + .../transaction/transaction.component.html | 2 +- .../transaction/transaction.component.ts | 10 +- 6 files changed, 146 insertions(+), 107 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 5a57b492b..a77347cdf 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -240,119 +240,141 @@ } @else if (step === 'summary') { - -
-
-

Accelerate your Bitcoin transaction?

+ + +
+
+

Accelerate your Bitcoin transaction?

+
-
-
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + +
+
+
+
+ +
+
+
+ +
-
- - +
+
+
+ + } @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.
-
-
-
-
- - } @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... -
+ } @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 (canPayWithCashapp) { -
-

OR

+ } + @if (cashappEnabled) { +
+

Pay with

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

Pay with

- -
- } +
+
+ } + + +
+
+
+
+
- } +
@if (showSummary) {
@@ -413,7 +435,6 @@
} - @else if (step === 'processing') {
@@ -434,4 +455,20 @@
} + @else if (step === 'paid') { +
+
+

Accelerating your transaction

+
+
+ +
+
+
+

Confirming your acceleration with our mining pool partners...

+
+
+
+
+ }
\ 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 6af453706..a580360a5 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -41,7 +41,7 @@ export const MIN_BID_RATIO = 1; export const DEFAULT_BID_RATIO = 2; export const MAX_BID_RATIO = 4; -type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing'; +type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid'; @Component({ selector: 'app-accelerate-checkout', @@ -58,7 +58,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() forceSummary: boolean = false; @Input() forceMobile: boolean = false; @Output() changeMode = new EventEmitter(); - @Output() close = new EventEmitter(); calculating = true; choosenOption: 'wait' | 'accel'; @@ -294,7 +293,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; this.estimateSubscription.unsubscribe(); - this.closeModal(2000); + this.moveToStep('paid') }, error: (response) => { if (response.status === 403 && response.error === 'not_available') { @@ -409,7 +408,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')}`, '')); @@ -457,13 +456,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { selectedOptionChanged(event) { this.choosenOption = event.target.id; } - closeModal(timeout: number = 0): void { - setTimeout(() => { - this._step = 'processing'; - this.cd.markForCheck(); - this.close.emit(); - }, timeout); - } isLoggedIn(): boolean { const auth = this.storageService.getAuth(); diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 64277e3e0..f65e1a204 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -122,7 +122,7 @@   } @else if (showAccelerationSummary && !accelerationFlowCompleted) { - + } @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 00051350b..b9c2b5ae7 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -386,6 +386,10 @@ export class TrackerComponent implements OnInit, OnDestroy { if (txPosition.position?.block > 0 && this.tx.weight < 4000) { this.accelerationEligible = true; } + } else if (this.showAccelerationSummary) { + setTimeout(() => { + this.accelerationFlowCompleted = true; + }, 2000); } } } else { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index cb20abe1e..0e67887dc 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -84,7 +84,7 @@
- + Urgent transaction? Get it confirmed faster. diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index c4b4a6af1..1e37ca3fc 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -767,8 +767,14 @@ 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.showAccelerationSummary = false; + } else if (this.showAccelerationSummary) { + setTimeout(() => { + this.showAccelerationSummary = false; + }, 2000); + } } if (this.isAcceleration) { // this immediately returns cached stats if we fetched them recently From 84d4eaa93267ccd5ebc55f1dafd951bee89f5f5a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 06:08:58 +0000 Subject: [PATCH 15/45] remove stray console.log --- .../accelerate-checkout/accelerate-fee-graph.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts index c41cb2f87..d85d2ee46 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts @@ -48,7 +48,6 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges { const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate)); const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize; const baseHeight = baseRate / maxRate; - console.log(maxRate, baseRate, baseHeight); const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => { return { rate: option.rate, From 3720d67666979977338cd73f7c108685d136ae16 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 07:04:08 +0000 Subject: [PATCH 16/45] [accelerator] waitlisted & preview-only screens --- .../accelerate-checkout.component.html | 29 ++++++++++++------- .../accelerate-checkout.component.ts | 26 ++++++++++++----- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index a77347cdf..f23b808de 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -32,8 +32,8 @@
-
-
You are currently on the waitlist
+
+
You are currently on the waitlist
@if (showDetails) { @@ -177,7 +177,7 @@ - + @if (isLoggedIn()) { Maximum acceleration cost @@ -219,10 +219,17 @@
- + @if (isLoggedIn()) { + + } @else { + + }
@@ -248,7 +255,7 @@
-
+
@@ -328,14 +335,14 @@
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { -
-
} @else { -
+

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

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 a580360a5..7de6a4a04 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -232,6 +232,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; + if (!this.canPay && this.advancedEnabled && this.step !== 'quote') { + this.moveToStep('quote'); + } + if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { this.loadingBtcpayInvoice = true; this.requestBTCPayInvoice(); @@ -266,14 +270,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * Advanced mode acceleration button clicked */ accelerate(): void { - if (this.isLoggedIn()) { - if (this.step !== 'summary') { - this.moveToStep('summary'); + if (this.canPay) { + if (this.isLoggedIn()) { + if (this.step !== 'summary') { + this.moveToStep('summary'); + } else { + this.accelerateWithMempoolAccount(); + } } else { - this.accelerateWithMempoolAccount(); + this.moveToStep('checkout'); } - } else { - this.moveToStep('checkout'); } } @@ -471,11 +477,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get canPayWithCashapp() { - return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('bitcoin'); + return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp'); } get canPayWithBalance() { - return this.isLoggedIn() && this.estimate?.availablePaymentMethods?.includes('balance'); + return this.isLoggedIn() && this.estimate?.availablePaymentMethods?.includes('balance') && this.estimate?.hasAccess; + } + + get canPay() { + return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp; } get showSummary() { From c249da790152e65c12b97ffb9ccbeafeeb86f77f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 07:13:43 +0000 Subject: [PATCH 17/45] [accelerator] pizza tracker waitlisted & preview-only screens --- .../accelerate-checkout.component.html | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index f23b808de..85e62b783 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -32,7 +32,7 @@
-
+
You are currently on the waitlist
@@ -224,7 +224,7 @@ Accelerate - } @else { + } @else if (!canPayWithBitcoin && !canPayWithCashapp) {
+
+
You are currently on the waitlist for Mempool Accelerator™
+
+
@@ -292,12 +296,19 @@
-
+
- + @if (isLoggedIn()) { + + } @else if (!canPayWithBitcoin && !canPayWithCashapp) { + + }
From 277f8f7bfdd9c79b91bca6685d828567a6dba3f2 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 07:45:10 +0000 Subject: [PATCH 18/45] [accelerator] restore missing sparkles button --- .../accelerate-checkout/accelerate-checkout.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 85e62b783..6dccfc060 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -298,12 +298,12 @@
- @if (isLoggedIn()) { + @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { - } @else if (!canPayWithBitcoin && !canPayWithCashapp) { + } @else { - } @else if (!canPayWithBitcoin && !canPayWithCashapp) { + } @else {
@if (!minimal) { -

{{ invoice.amount }} BTC

+

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

} @@ -66,7 +66,7 @@
@if (!minimal) { -

{{ invoice.amount * 100_000_000 }} sats

+

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

} @@ -74,8 +74,8 @@
@@ -86,7 +86,7 @@
@if (!minimal) { -

{{ invoice.amount }} BTC

+

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

} 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 6f2d4b36c..395e1e41d 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -65,9 +65,7 @@ export class BitcoinInvoiceComponent implements OnInit, OnDestroy { this.paymentStatusSubscription = this.apiService.retreiveInvoice$(invoiceId).pipe( tap((invoice: any) => { this.invoice = invoice; - this.invoice.amount = invoice.btcDue ?? (invoice.cryptoInfo.length ? parseFloat(invoice.cryptoInfo[0].totalDue) : 0) ?? 0; - - if (this.invoice.amount > 0) { + if (this.invoice.btcDue > 0) { this.paymentStatus = 2; } else { this.paymentStatus = 4; From 776404dbdea891a65b69a7296e06898e838052ec Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 09:17:08 +0000 Subject: [PATCH 21/45] [accelerator] Pro for everyone --- .../accelerate-checkout.component.html | 89 +++++++++++-------- .../accelerate-checkout.component.ts | 19 +--- .../components/tracker/tracker.component.html | 2 +- .../components/tracker/tracker.component.ts | 11 ++- .../transaction/transaction.component.html | 5 +- .../transaction/transaction.component.ts | 30 +++++-- 6 files changed, 91 insertions(+), 65 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 0cd278f25..11ecd2b7e 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -107,25 +107,49 @@ - - 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 - - - + @if (isLoggedIn()) { + + 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 + + + + } + @else { + + + Target rate + + {{ maxRateOptions[selectFeeRateIndex].rate | number : '1.0-0' }} + + sat/vB + + + + Extra fee required + + + {{ maxRateOptions[selectFeeRateIndex].fee | number }} + + + sats + + + + } @@ -219,17 +243,10 @@
- @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { - - } @else { - - } +
@@ -280,7 +297,7 @@
- @if (showSummary) { -
-
- -
+
+
+
- } +
} @else if (step === 'cashapp') {
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 7de6a4a04..6c37663d0 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -55,7 +55,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; @Input() advancedEnabled: boolean = false; - @Input() forceSummary: boolean = false; @Input() forceMobile: boolean = false; @Output() changeMode = new EventEmitter(); @@ -122,10 +121,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.moveToStep('processing'); this.insertSquare(); this.setupSquare(); - } else if (this.isLoggedIn() || this.forceSummary) { - this.moveToStep('summary'); } else { - this.moveToStep('checkout'); + this.moveToStep('summary'); } this.servicesApiService.setupSquare$().subscribe(ids => { @@ -232,10 +229,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; - if (!this.canPay && this.advancedEnabled && this.step !== 'quote') { - this.moveToStep('quote'); - } - if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { this.loadingBtcpayInvoice = true; this.requestBTCPayInvoice(); @@ -272,11 +265,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { accelerate(): void { if (this.canPay) { if (this.isLoggedIn()) { - if (this.step !== 'summary') { - this.moveToStep('summary'); - } else { - this.accelerateWithMempoolAccount(); - } + this.accelerateWithMempoolAccount(); } else { this.moveToStep('checkout'); } @@ -488,10 +477,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp; } - 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/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index f65e1a204..5199897fc 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -122,7 +122,7 @@   } @else if (showAccelerationSummary && !accelerationFlowCompleted) { - + } @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 b9c2b5ae7..88a929bd7 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -151,6 +151,10 @@ export class TrackerComponent implements OnInit, OnDestroy { this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; + 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; @@ -382,7 +386,12 @@ export class TrackerComponent implements OnInit, OnDestroy { } if (!this.mempoolPosition.accelerated) { - this.showAccelerationSummary = true; + if (!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; } diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 0e67887dc..58322a72d 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -547,11 +547,8 @@ } @else if (network === 'liquid' || network === 'liquidtestnet') { } @else { - + - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { - Accelerate - } } diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 1e37ca3fc..7b12b3c7b 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -170,6 +170,15 @@ 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; + } + + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + }); + this.websocketService.want(['blocks', 'mempool-blocks']); this.stateService.networkChanged$.subscribe( (network) => { @@ -398,7 +407,22 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) { this.tx.acceleratedBy = txPosition.position.acceleratedBy; } - this.accelerationEligible = txPosition?.position?.block > 0 && this.tx?.weight < 4000; + + if (!this.mempoolPosition.accelerated) { + if (!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.showAccelerationSummary = false; + }, 2000); + } } } else { this.mempoolPosition = null; @@ -683,10 +707,6 @@ 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; From c5fc47683470d6b194df22e1d44a00ab6f46bd2f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 29 Jun 2024 09:21:39 +0000 Subject: [PATCH 22/45] [accelerator] no autoscroll to checkout --- .../accelerate-checkout.component.ts | 28 ------------------- .../components/tracker/tracker.component.html | 2 +- .../transaction/transaction.component.html | 2 +- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 6c37663d0..30ce7a606 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -52,7 +52,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() tx: Transaction; @Input() miningStats: MiningStats; @Input() eta: ETA; - @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @@ -139,12 +138,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } } - ngOnChanges(changes: SimpleChanges): void { - if (changes.scrollEvent) { - this.scrollToElement('acceleratePreviewAnchor', 'start'); - } - } - moveToStep(step: CheckoutStep) { this._step = step; if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { @@ -160,26 +153,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } } - /** - * Scroll to element id with or without setTimeout - */ - scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { - setTimeout(() => { - this.scrollToElement(id, position); - }, timeout); - } - scrollToElement(id: string, position: ScrollLogicalPosition) { - const acceleratePreviewAnchor = document.getElementById(id); - if (acceleratePreviewAnchor) { - this.cd.markForCheck(); - acceleratePreviewAnchor.scrollIntoView({ - behavior: 'smooth', - inline: position, - block: position, - }); - } - } - /** * Accelerator */ @@ -437,7 +410,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { next: (response) => { this.invoice = response; this.cd.markForCheck(); - this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 500); }, error: (response) => { console.log(response); diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 5199897fc..417038539 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -122,7 +122,7 @@   } @else if (showAccelerationSummary && !accelerationFlowCompleted) { - + } @else { @if (tx?.acceleration && !tx.status?.confirmed) { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 58322a72d..1c83bba40 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -84,7 +84,7 @@
- + Urgent transaction? Get it confirmed faster. From f68c8cc621cafa4e474e2e3ed0d990a48e093625 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 01:46:11 +0000 Subject: [PATCH 23/45] [accelerator] restore scroll events, remove eta button --- .../accelerate-checkout.component.ts | 28 +++++++++++++++++++ .../components/tracker/tracker.component.html | 6 ++-- .../components/tracker/tracker.component.ts | 2 +- .../transaction/transaction.component.html | 6 ++-- .../transaction/transaction.component.ts | 2 +- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 30ce7a606..6a20f6ff7 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -52,6 +52,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() tx: Transaction; @Input() miningStats: MiningStats; @Input() eta: ETA; + @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @@ -138,6 +139,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } } + ngOnChanges(changes: SimpleChanges): void { + if (changes.scrollEvent && this.scrollEvent) { + this.scrollToElement('acceleratePreviewAnchor', 'start'); + } + } + moveToStep(step: CheckoutStep) { this._step = step; if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { @@ -153,6 +160,26 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } } + /** + * Scroll to element id with or without setTimeout + */ + scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { + setTimeout(() => { + this.scrollToElement(id, position); + }, timeout); + } + scrollToElement(id: string, position: ScrollLogicalPosition) { + const acceleratePreviewAnchor = document.getElementById(id); + if (acceleratePreviewAnchor) { + this.cd.markForCheck(); + acceleratePreviewAnchor.scrollIntoView({ + behavior: 'smooth', + inline: position, + block: position, + }); + } + } + /** * Accelerator */ @@ -410,6 +437,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { next: (response) => { this.invoice = response; this.cd.markForCheck(); + this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 500); }, error: (response) => { console.log(response); diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 417038539..6bb0950b5 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 && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { +
@@ -122,7 +122,7 @@   } @else if (showAccelerationSummary && !accelerationFlowCompleted) { - + } @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 88a929bd7..397f98805 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -743,7 +743,7 @@ export class TrackerComponent implements OnInit, OnDestroy { } this.enterpriseService.goal(8); this.showAccelerationSummary = true && this.acceleratorAvailable; - this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; + this.scrollIntoAccelPreview = true; return false; } diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 1c83bba40..5a2c1510a 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -84,7 +84,7 @@
- + Urgent transaction? Get it confirmed faster. @@ -540,9 +540,9 @@ @if (eta.blocks >= 7) { In several hours (or more) - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { + } @else if (network === 'liquid' || network === 'liquidtestnet') { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 7b12b3c7b..74f49c69b 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -710,7 +710,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { document.location.hash = '#accelerate'; this.enterpriseService.goal(8); this.showAccelerationSummary = true && this.acceleratorAvailable; - this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; + this.scrollIntoAccelPreview = true; return false; } From 2798b43913e1cf056a3b1bd7cfa581bc786ab127 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 02:40:58 +0000 Subject: [PATCH 24/45] [accelerator] adjust h1 labels --- .../src/app/components/transaction/transaction.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 5a2c1510a..4a56f2c99 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -78,14 +78,14 @@
-

Accelerate

+

Transaction stuck?

- Urgent transaction? Get it confirmed faster. + Mempool Accelerator™ fixes this.
From bf37affe47be152cb6de536e700a9e5a3bd48a3d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 03:20:33 +0000 Subject: [PATCH 25/45] [accelerator] fiat limits --- .../accelerate-checkout.component.html | 2 +- .../accelerate-checkout/accelerate-checkout.component.ts | 2 +- .../src/app/components/tracker/tracker.component.html | 2 +- frontend/src/app/components/tracker/tracker.component.ts | 7 +++++-- .../components/transaction/transaction.component.html | 4 ++-- .../app/components/transaction/transaction.component.ts | 9 ++++++++- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 11ecd2b7e..2ffb94294 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -391,7 +391,7 @@
} } - @if (cashappEnabled) { + @if (canPayWithCashapp) {

Pay with

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 6a20f6ff7..c69da8524 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -466,7 +466,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get canPayWithCashapp() { - return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp'); + return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp') && this.cost < 400000; } get canPayWithBalance() { diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 6bb0950b5..a0f242d46 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -120,7 +120,7 @@
  - } @else if (showAccelerationSummary && !accelerationFlowCompleted) { + } @else if (showAccelerationSummary) { diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 397f98805..57eecee16 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -386,18 +386,19 @@ export class TrackerComponent implements OnInit, OnDestroy { } if (!this.mempoolPosition.accelerated) { - if (!this.showAccelerationSummary) { + 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.tx.weight < 4000) { + if (txPosition.position?.block > 0) { this.accelerationEligible = true; } } else if (this.showAccelerationSummary) { setTimeout(() => { this.accelerationFlowCompleted = true; + this.showAccelerationSummary = false; }, 2000); } } @@ -742,6 +743,7 @@ export class TrackerComponent implements OnInit, OnDestroy { return; } this.enterpriseService.goal(8); + this.accelerationFlowCompleted = false; this.showAccelerationSummary = true && this.acceleratorAvailable; this.scrollIntoAccelPreview = true; return false; @@ -777,6 +779,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.auditStatus = null; this.accelerationPositions = null; this.accelerationEligible = false; + this.accelerationFlowCompleted = false; this.trackerStage = 'waiting'; document.body.scrollTo(0, 0); this.leaveTransaction(); diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 4a56f2c99..eecae86f7 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -540,9 +540,9 @@ @if (eta.blocks >= 7) { In several hours (or more) - + } } @else if (network === 'liquid' || network === 'liquidtestnet') { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 74f49c69b..0f86701ff 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -139,6 +139,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === ''; showAccelerationSummary = false; showAccelerationDetails = 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; @@ -409,7 +410,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } if (!this.mempoolPosition.accelerated) { - if (!this.showAccelerationSummary) { + if (!this.accelerationFlowCompleted && !this.showAccelerationSummary) { this.showAccelerationSummary = true; this.miningService.getMiningStats('1w').subscribe(stats => { this.miningStats = stats; @@ -420,6 +421,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } } else if (this.showAccelerationSummary) { setTimeout(() => { + this.accelerationFlowCompleted = true; this.showAccelerationSummary = false; }, 2000); } @@ -709,6 +711,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { document.location.hash = '#accelerate'; this.enterpriseService.goal(8); + this.accelerationFlowCompleted = false; this.showAccelerationSummary = true && this.acceleratorAvailable; this.scrollIntoAccelPreview = true; return false; @@ -789,9 +792,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))); if (this.isAcceleration) { if (initialState) { + this.accelerationFlowCompleted = true; this.showAccelerationSummary = false; } else if (this.showAccelerationSummary) { setTimeout(() => { + this.accelerationFlowCompleted = true; this.showAccelerationSummary = false; }, 2000); } @@ -864,6 +869,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.filters = []; this.showCpfpDetails = false; this.accelerationInfo = null; + this.accelerationEligible = false; + this.accelerationFlowCompleted = false; this.txInBlockIndex = null; this.mempoolPosition = null; this.pool = null; From d059c5ca27c23f0d892fe798d281ccb4c7aed845 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 03:43:28 +0000 Subject: [PATCH 26/45] [accelerator] slim summary screen --- .../accelerate-checkout.component.html | 59 +++++++------------ .../accelerate-checkout.component.scss | 13 ++++ .../accelerate-checkout.component.ts | 10 +--- .../transaction/transaction.component.html | 14 ++++- 4 files changed, 47 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 2ffb94294..73b209593 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -266,36 +266,23 @@ @else if (step === 'summary') { -
-
-

Accelerate your Bitcoin transaction?

+ @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.
-
-
-
- @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { - - } @else { - - } -
+ @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { + + } @else { + + }
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 1fdb51086..da4dc8079 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -171,4 +171,17 @@ .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; + } } \ 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 c69da8524..4f8c5b163 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -56,10 +56,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() cashappEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; + @Input() noCTA: boolean = false; @Output() changeMode = new EventEmitter(); calculating = true; - choosenOption: 'wait' | 'accel'; + armed = false; error = ''; math = Math; isMobile: boolean = window.innerWidth <= 767.98; @@ -445,13 +446,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }); } - /** - * UI events - */ - selectedOptionChanged(event) { - this.choosenOption = event.target.id; - } - isLoggedIn(): boolean { const auth = this.storageService.getAuth(); return auth !== null; diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index eecae86f7..29ef8bfba 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -84,9 +84,17 @@
- - Mempool Accelerator™ fixes this. - + From 110b7a934cfe07db89edc677b4ac79417698edaf Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 04:57:00 +0000 Subject: [PATCH 27/45] [accelerator] buttons --- .../accelerate-checkout.component.html | 6 +++--- .../app/components/tracker/tracker.component.ts | 1 - .../transaction/transaction.component.html | 11 +++++++++-- .../transaction/transaction.component.ts | 14 ++++++++------ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 73b209593..d96c8af36 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -269,7 +269,7 @@ @if (!noCTA) {
-

Accelerate your Bitcoin transaction?

+

Transaction stuck?

} @@ -284,7 +284,7 @@
- Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.
From 84e1ac31c27dbbf4963d3c07940c7391d5af5a61 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 05:51:13 +0000 Subject: [PATCH 30/45] [accelerator] fix stray margin --- .../accelerate-checkout/accelerate-checkout.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index dff691c95..86f008a61 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -35,7 +35,7 @@
@if (showDetails) { -
Your transaction
+
Your transaction
From caf7011df527e7d6cdf08aa9ee4b5403b71c8a39 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 06:51:01 +0000 Subject: [PATCH 31/45] [accelerator] checkbox error hint --- .../accelerate-checkout.component.html | 14 +++++------ .../accelerate-checkout.component.scss | 25 ++++++++++++++++++- .../accelerate-checkout.component.ts | 15 ++++++++--- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 86f008a61..301303c89 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -29,7 +29,7 @@ -
+
You are currently on the waitlist
@@ -280,12 +280,12 @@
You are currently on the waitlist for Mempool Accelerator™
-
+
- +
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { -
+
} @else { -
+

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

@@ -490,12 +490,12 @@ @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { - } @else { - 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 da4dc8079..5cfd153bd 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -59,7 +59,11 @@ box-shadow: none !important; } -.estimateDisabled { +.grayOut { + opacity: 0.5; +} + +.disabled { opacity: 0.5; pointer-events: none; } @@ -184,4 +188,23 @@ @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 49eb990fa..791dc1ebc 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -63,6 +63,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { calculating = true; armed = false; + misfire = false; error = ''; math = Math; isMobile: boolean = window.innerWidth <= 767.98; @@ -149,6 +150,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { moveToStep(step: CheckoutStep) { this._step = step; + this.misfire = false; if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { this.fetchEstimate(); } @@ -266,11 +268,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * Advanced mode acceleration button clicked */ accelerate(): void { - if (this.canPay) { - if (this.isLoggedIn()) { - this.accelerateWithMempoolAccount(); + if (this.canPay && !this.calculating) { + if ((!this.armed && this.step === 'summary')) { + this.misfire = true; } else { - this.moveToStep('checkout'); + if (this.isLoggedIn()) { + this.accelerateWithMempoolAccount(); + } else { + this.armed = true; + this.moveToStep('checkout'); + } } } } From e3abdf4b4f666b132bc2588ec962e18282a78b22 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 06:53:56 +0000 Subject: [PATCH 32/45] [accelerator] revert titles --- .../accelerate-checkout/accelerate-checkout.component.html | 2 +- .../src/app/components/transaction/transaction.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 301303c89..d4db46258 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -271,7 +271,7 @@ @if (!noCTA) {
-

Transaction stuck?

+

Accelerate your Bitcoin transaction?

} diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 47c368dfc..da8763fa6 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -78,7 +78,7 @@
-

Transaction stuck?

+

Accelerate

From 1e820a0fc8dc3e0d1b5d9519a3eb995a7ea37eae Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 06:55:32 +0000 Subject: [PATCH 33/45] [accelerator] soft enforce referrer --- .../accelerate-checkout/accelerate-checkout.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 791dc1ebc..61b03d553 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -469,7 +469,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get canPayWithCashapp() { - return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp') && this.cost < 400000; + return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp') && this.cost < 400000 && this.stateService.referrer === 'https://cash.app/'; } get canPayWithBalance() { From 3ecc8ae8cfcec38543fd29ac2a30efeba2afc2ab Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 07:17:15 +0000 Subject: [PATCH 34/45] [accelerator] ln qr --- .../components/bitcoin-invoice/bitcoin-invoice.component.html | 4 ++-- .../components/bitcoin-invoice/bitcoin-invoice.component.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) 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 4e158e3e4..790f046f7 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -14,7 +14,7 @@ -
+
HDdSHVG@r+9edq<=r^GvVZLRavtyfrzvHLe ziZYTu_6TT{`G;Hem;B4|y#RL;j^iK=2vE>d)G;yAGgwKSH!LOY=1wlI1{gq(I#71Y2QS+0UE?}Aj>7|2 zAn>obh)l*K%Dr7b>+_~SfGit&KPwV4Sja3#R97KElCwIh+$GYa(IRVS6lzW?iZuUC z9GDeUHN*oxnwLgxc~l+c))mI3xMlDOsJqM4`$0N{5>DTHgyaECt!*7o{c{n+Lz4 zrVBKprUsRPFiF=Zfv%M~Sulp*{&BZ>z5#XM(yAIYa8`5eHpu!KN)7AP8t2lp4=hd! zt%yWMJy$KAXd~93@%tmaxFFOyPWVrxCIvo=jLpbl?rqYD+CHq{ zQ>fF#dLM5Wg&s*FW48xyqtcsJA=ib+fw7Ojqf9)FXdU_c2NqEu@qFIxBU6P>p2KSu z{i;>Og1m;i%AMw0YqvK<7t1eeBd|DVEdy%H#K5KMX$oepAZ74aF9J(66}(EW_1g+4 z?w_lz?5~xO6j*_v{$_3I_&cxQk)OiemOe0|8VejLc0V#HaZ@$w!Q>KcV^C@^j@ zpxL8iULkQnH5?-YZJi#c-w9N^AKP<}Y!X|QtH9aF9s{F5%z7u?KLz$zd;0`TX3*Az! zv>V-10SD;lNr#wTNdZGrIAqV#UXzu(NBm+|u}Kfv!9pxgg0x|pW ze4OEd&yvB)@YRBA*)IA##mUbYghG@bn$>7lio%X7ob%Z&j-_FK)d9S%fvy4w+AH8l zH-U)Xrk{(q7fMAH0*Y5nt}~6ydV;v@)m}2={I0&mi*C}3W#9mEY1b2tsuu}GoVd4WPTUjsqpl)op`o^ zXTbJ12EjB0C|7i?USG!7!)YHHKlx#yi~Ntx1aA4|H5;(3#o?5VhG8I$ld8Yb&ju){ zev>VlOFl;&;l7Q=&3B0E3Q*#eG~@%9&f5$zR|Kv}ZRDZ}N|77n8hj;I>>-nvyYT4| zC=$%}B8c?zqY&iig^Y+d7&re=;)f~tF9AjUNuVX;JM;V5))vxF3PyWC%9pR0O#L5D zc{qy8t9MgRSQ`tadY^+hP3_)h&d)`2bxYIf!eWLQ5Pbc4wTT>sRGh@5x-Ac z1*#_@P#r47ciV{C{m;~*+`!lT5#pH70a`g|&i9WCe`i}L&u*$M{-ux@ z;Q`kbRHYV*_2|>05cfc%N4~S{m1JUj;n3O-d%QI(T5t3P@U~1AYnBWv{sy0UAGP|j z7Ht9wn*>bG>!c3A?Yhw|^Z}G%dC$y3aouh^9VMnN&bl3vj^sjQ6YV~3Kfj(VH-7{O z{a~MYfnP`b=f^;jQ1cNE46Jb-eR33)jogVNvI;iu>Ex+Tel!2Z*kjWhFr-XB3C z90w{UkBRF+>e76dPIc0$ec<~cc}}!tu+gIy*K|R>*0#w6)bmv2A3nhjj&d9Ut==a7 z`{@^G<<&r3*_!>#e>Umd&l+t@>UQ2o;#DmA28(*eA31!O-aq=vkLheJwkcH@y{mld zQK_^y-aBz^w8 zo0YY+wcD{3cti=ADoFFOn_ff1Tp;pIFa9+~itP#4koa!;{FN+~}q#vHO zE|OX7A}2ve>=N&I+g-jLm|+C*MW-{=mA}?z7ed8?Mem3$p|EVAIi*d+XS~%gI8<97 zmw2zocPX#(r9x{P0FroIS( ztG?^nZAapI@X>>P6_L6WCH=ihr-PMf&M|Gswyj@va?qIL(WFvXCbODlzr00q0Oa?d zK8iF=n4Q?(n4fA|mYz`Ry&S(+QvcIN53V`s9QoUo=iGm2_SykuPgqF}UJ2|Yp)iAJ-XdX$z(fApU!z} zNj8|pUvoeO&oRID{I>317(e8DFFX(6A{kdSEdt7|ie%3fAbGS-O34+5jXx>b<0ERZ z3XhqeBYH)%Z_|f^y3uL1Pr|y+b|?2R5BW&;73i-cYM8sb+|j2LbKlE0K6}sBlgu`x z$|)S3alMH@^BpBXArxJglXtd=O~%cuPZiLQ((voJj^SBmKRRk~hA``9xLM5BA=1jZp0h(U1Dyv4 zPdXL%DdEz#f=_%*eqYemf}%#=hFWcL@)ggY)ZEU07Q70PstX*VC$)|f4hMJ#>-gu}U@J~XgMHQgU33C;W295Xsvh7J+ zCHojwS$s!-{jEeI8Dc!9pYua{>H#x?+{o2XRW{x%AM++&0Unx?M#Ziu`zu<-Ac@nt zC96FLmxmRfhz>SoOG@5IY#8Tpd(Q^tU{S3RvG0r{2dyY^LjQ!#>P;2Q+q?raCVKtg z;9as#n}F!ine!20r;B9{#PBTWJ7Q->+hr@V4*4t^6=Y)Dd*9?ELa8RDN=-NEmJ&x^ zM@vdp-|zb-&n)wc4_0={nVRIZ;8y=@b<>yGpzx(xDV(e9iV^(p7PWc0xvR6~@OYC} z+3bHU&FQM~a<)ZJRKWo0A7P|dPez={2^vH~pRQ#bXHfYiLl6O5C++^&=ygfH(mz{} zmZ{NH^RxbX0uh+5FCv_)YF-I0_TA>IU-2g| z2S4xypvCl?!Ds&y+jz>6xJObOD}g;iZDds5o|gmddD4I7$tj5R z9z>sStqyru?xv^FnaBvzez@>h@LyLIrRgt$yYG_syCSD{zSAF}W{_XbuiEwmYo%iQ zIB=)1ywhbj&y1M^n!4w3ln7I78jwSN39-ue0DqGC%aXtLFmX3|YBeIv;1PS^b7Lb| zwvs%I+ND42tt&qW9j@FJ-!)&bOBPCK%AvxXRp@+BHoCfmC0F}0VZ=O4ya$uRSot8K zp<=SlyWY6J9YB?=4r208V9PvcLH5h_1#onyjDn}V$l)QJ>r@?b@J&@7b*JneA12Ai z+{P*+uzMZ%_s+qe?X9 zJy<4p_P^T3Q{hS*tP3Wa#BMz9zpCaJs>k-AGNSbO@oN51*kw8};>6`+5$^%TH|87M ze}?O7x{PH_Y3FoH9q5566wdz#Z^AhHTX07^x zS@3QUoK&#(G}b8>(k zm9kx$%a3A*vs^mI#1H({_sl z`#~1Iz|~j$pw(YXKkFD)lJ}6C6u2s+Jc=*CpQ#=jD##;Eh4MynApTbVc$mN%8s2<@ z_I>tBL^1N4+4VCytAl&@xGRjL{KD#oDH-9fd*So3!IY_8@HvR2w@v@)2<%-jg0EAl zw)`FR*p6;~uwWjL9lN<$+YvvW<+OPYuKE^aHtcA=%E+xOKm zyub~pF(-GQkToILm%rybrIb}k1Abelc8$dz$lI7Pex|-L(c7IEd!QEaFtu z;P{h3IKB!|`pvit#H;W_6TmDI8fLg~nP!aSYMiq(mHn)UrcdHPKOeFG3cD=CZ1*cg zXdI5}kkhYIDD0PGZ|V|+LbPaE{Yo;ElXY%`TM_OME-6&ok#?Jsh(=FSXNvIQqHTK( z_$ubz9%ECARW`Tes;~6y8yHN1gh$)=St_aw;!(#^K_y>9cLa7dZ1h@v4OFdd!keR9 z7VY7Wt%Ln{y=S8sztGc|8}(ScQZ|$(Ku-4P$RcX%5tk1LnHUnjcpr~p$T0+#r*z93 z>b57G+UXF<6HMmO%xw4&aVmho)SPs|X8VOXD-|?X*RpZ=x{!a&%!RPs+PX^7apF^@>;PX%11s}NADpy0t>oO}x zA-)iyGad>1Ryyz4P=Fnw8{MPr29tb&&42xLj*Tt`Gb|HQ{^#C(`Zfi~LX}QCUbnte z?7c|^&mVa6{72U7M0OCHaA?DA<67lpE3ofgt{<<}ac(F1P&EfJc5al}fD`ypS!<7l z)vM`$Y@iz`wEliSDy_(x6$K zp^0tH8nNiqvAQ|N|NHCVZPARIK{>#ZPus_qxFfK+a9Ww}3+Zw}@)$Uz4Pm;a!y{i+ z+%xP*BIahXsRI&n)?2rkW&SWuVb>|-z$#|eLe?x*nxkMfD;ca0c55is73=+QBysM0 zT6RpPR7ekL-sCM;Ffmok1b+bbc6M<54z$RsG~_9y#LNwW0W#JQ0SMxbEBS- zCsa~fWwPI~QiS}pWn%i%$dkO0*kz+GDw5yXW=_&DiRrT1Xpio#)^G9`GxQ$Cxqkaj z{n$pm2niQ|w@eeg;8V0$);9fPDQ!GBXVv*Y#IXk3ebK- zW$e`7By}Ljrw+AVqUH%lk}kQ@yK(v?jskDNh5SK?mHM4L6^ z8Uu_!RUGAh%S#!o8vQAf5NYYB)7I{MQSIX2re_PLgt_U}!LgTtBOJJ&dRo+1?~fa= z?UZXIvI^;9Dt4(8ruxZf_+p~ofZ5p9AF0o1Q(N=P7qx5UtBpJx99!PI`I$8I_QYjo zndR5LL9YM!{pxar-L@E+2lPE`6}{s3-uSfJ2B-^`*OBqShC2g{iZ_4aj!f7Y%{DxZgEvnV9zC*HQf2^cJI!^|DqSXs(1sQ z$i4nF3^3`4^`#oN>YoCGTqV-(8#5=3J%6)YxTOMTjB$4m972E3lAtdfc&TKrw>Q}J zb?DM=0$2zpxBb1-aW`IBzcAii34I45odym*TV&jOR$p1H!K%}>bWCF>#;kbeBvH)I zKsTd8TUe?Q_6H?=J?1~V2#5)%wK7IYOW2*sRMb>(S1{b#t5|-i9En_@3I+(hE zb2+oH>k4UUl{rcza)xF;;=KYc}g+9fl0k zp7km?jyIqoh_w8>fnRK7m$RJJ#d?!Yx=!1zioNQ92ho-sbXRIWN9cCuM+O@9URUTj z$-pyR11mHELR(o5F%snZceblJmNGx|I5(ml{>nH@txIT?rS>m6&~jtI#!9NF&nBMm zi&gJ*mNUFKVI`82GEP{2B>Q?Xb^IVVnM=4QFmR`_Napv*rPlKic!+Zk%tHD73@ltk zW$d0YMnDN?fwQLd#D;7E(`(#WCpwwhpB=191CKs3$t?3RAFw-53&i8opzNd4^S3h|0I_|3M)Vmz~ zNd3EtTAxcv^%O$FRZ9U==Q-cIu;cj^z35=nT@3kn>>LQ<{~^uhZRSo}EUTb2ZC+jA znPnFtd*R=EuN%``EIGU$#Pql^o_FV$7MB-U=JY$xl#%Ul*ogpd3yZ_|ZaTSDh%pedFkP530c9FqsnkvV5|dzT(_G^3#R<8WZEULVj6)I={htS75q37j(CUvG;5Y( zoUV2< z0^|$H^Gkx{i17Lkk)P73gNJ69%TErl~t*wH1zrSO7vgW)FHk3cJy2iy2&t0)Le}fA`Dq-K4 z^Ja}4=6*SVuoV3vmGRQ^t80kk4z>VY#Y3TpCO2-LV1w7NKtNypeyui7fBjCsNa& zqwD$jYP@XdZLIuM5j9l%K&Q(0-Msg*$;x}j-DixovI9ozjBwTak zvy6){mT$1=!dV@vY|AXTnZ;f4Xnsai1)|jQCo_v2^Mi4&9kkG)y z4P_E&y{T4%L9O%t56hao{lv=P6}aT6lTTUFIw^tdg)zWX{N#4YdrVDfr2I!WbYi|i zJl$~zZ5%`KuI;#(^mmEPPrJXS=yDnSUL8wLlZ)mK?G zkt0map0jVi`%VV9?7-V=1Ibmb$8B=U06b7fz5`%VSXSm5djSY~lo!g}WBVrw6ZLGi z8?=Zf&O-aMvBQ!NmRwRZI0LpBjl_Ej${1E==?V*1P~{-af2Jo=8+_!(?W`zD-es@z z>HcVUNAwM7637%ai43_&aP-lEP)My{OS{i4DOxrw0wdZ|mPI%Gw5)WJaBQ;f{@u#J zht}qZXcUXdRC9a+ndFa`A7=(|SASqg^H}8TO{y@;cAkUXt`NBBW6suHAJuB$m5UF- z2_weQ?PG=Qe`gus_28@cGwYQSgT~-<_TMC-ag_JZ4+tfAHJU??MvXtoNZE5YqPcwcqtD#MEhL%%3VK?V?3B# z1dbd1(-v1-ZsJg$L`8bP$FZ8-whaUP_u~@^aN}TrXm$~pa;&9 zeYWOckLmg!8o0z3EK-{15wPqHx=ggQFOUl4w1AsI?VKfEPhwS6uep|%JG zw2kLDEPp?mu`5_%t6-(zEow*$dg}_z$J@nz7~w|MFm}A@Pbq`KSwl%z7Vlxi+`dD1 z!96);JD0(3uXqiDGJpaG5`&|qm)Sl%@|7tJrO4f*N%U*$yaA$%P!;um=hzDATVy-~ z-`yTt4#2_-;V&_u$qYHTQvWp_e%y$a1ruMh4=sp_Sq-kQ`R5 zJS~!$!wpYSIFOV*LG(HGzQ1`M{r%O`n-t>C)-v34x^Z-&ILP7t*ptSDjr0lD_?=Yd zHok2`JPPoG048Q~;&EEe%%(HKy5VT_?oIl5kJ#TNk9UtRk>^^$N2#2N=ME($%mqM@ zd_p3Qz`kVqXvJ2xjx*_q6r|BHhLtqty-#hv@p*fE^kEl33e03R68FdSjy1EYZU)8I zTTph|1{AK|@j1Hj;hMpN(C9|*#HW#Ou;ibbzTc|LM5kc&UgAxIfP$;1n;v^qeXeq_ z?6Znv4#l5prc*FF-xhM8JM<~-FF%5{*i_9=o~HeSJWh$>(y8zwmY{#UWA50?Nq{R& zmlFWRZ2mKvc~gVS`kQ-0`uJz(}x9 zti26ATISc^$4-A+yI&Klp7M~B@ZaTlxg2hN_F!FrMWzrWKil?4Y3o61FN0iHrT{SC zl{$Runlz-m^uiI%%}d9FYI8S!TsHJB-do23tS#EKsav~P$lrSgVHP#a&ln6bO59hx za$NXC)0#a%!a&P=oh8~7IxV|&L(i{}cRU+|NLar;Mu2rWZ7iz+;CtuDzUpOxTJ89~ z>bmKgaf!j;90)b>^rGRdg+(*ZxVkcH5+wCU&v$1u0iQR#)xPn;qpN>>f_ov!SeNprI3!k_1H=5cBN4VDt~+V6-hG*zPswf>@K{A<06RDH@>YOL1;fXHxKEBEtnC= zuLzAgchpI6p84{OLPrQFEu_LoNoH^&wWff(Yu#`xvx{f4Wg%ecMR0;65qi{oxxhQS z#jX-X$`ewE7t_((Ff0QER7678W<~kjkwhCUlf216qL5E&n%2)hWc;+Ed42Zljl4Wf z;}yK>mBAQ^O1mXT+_^Vc|-SYqcJdhu4S$)Q8aJ=L(cSSvmjy>F`GuA3EwZP zFQz|#;nxWZhwGF-tnn!y5HVP$t$#P+v+xRY|{_^ zsL)W_{wfsK!gOB%a=lzh48(=fu1nPBHnnH84q!#TM0R zEa5iFs}2NsCSA2WW`fk-Oq$m^pA#kAT_)Q9pOAR@?u!b1=LuF7Wb~+XQVVsElbxt5 z*bI%1a=*ttZUmNHog|KtDl<#xua&o6D$4s(z5omWu?v+W+qnh(kxF!nz397&8uZQ98;|+A@Wp7dqbLS_E zu-9BpiNeR21|vFRuQP;SV|C#m)_ge07B{k!J-Erd)LRfeW6DL`qJiL1A@5_!NqUvF ztsw5Ls@S0`xk4hMqzw|JTph1ot4LTOemAW+$Zt>9BJyW;&XTbGNQ2K+h>S@{?{(un zwDpQg#mn!Fss7RDw>VWSEM7E&Qb}}tFu2+9i1zAGN28O)>EVq+soDxR3ov69_^@xaN{UtkY2lijlQRmP4j%G zg^SUXOtw2XivX_mwBZv&`S)8I@4L zIum(YS9V0rmfxzp>TPU}Evpvt0NfOiCX{qs!ei+M2X~voSUd;EY&>z%2O z_QE&f`HkG{-rE{v5x#VBa%!auyA)8E^QnXj$+sMIhzkYr=%r8k8X3U6CHgZMb=JNMA;|84|!7tiaAHpR=F;~7;CQHr(qKNdr$OkbbSphHhXdRibyZt_`w$+ zEt)vuPR+({e>BHFO2O-CDuwklS<~sA=Tc!T>Kl6&)!h-_-4-`}qvf~0^iArFS(fn( zVkmfnk(kFEY2;V3&icE&6z}H=b#6sG{mJe^r5`jN^~*q?@Aa!#UWra=TK?i~@jwVA zfJXA|Tz;6aLtqi>k@5KP)ikgx{uv$!G7slv>L*da&4)CLkeJGjkmI;o%dqh?@2cG7 zqhr0~AFq}SO(kB-$g zYJ3Z^hZ3SlvnZ*~1tX1qcqXjFnfUtZkt^@X6H3=^C3HElS|X<{qCNepZ~YjCm(N05IedpL5d#u0oRrevRs8tSYB*4 ztv&hXTLxxasug({Te>8vJ=avtCMVX<^FDq51{?Xw(}SpPEm5CCfg%3f$v5U=u=%UuHnV~6(}Vb@BU zL(Ftd;?;^DAE@h|5~-~%t8)2wJGz<+{&h!E<@+=={nl@yL68Xb-ESFsRpdcRiH_&ss_YpmvgUutmAJJ4p`$N_2GB)@hDRV$ObIQYg~`F<;I5wTugD%F#G(~D&H z7X{Q6VaZjkmVT8*fxDdW_G?Shlad!oJnzK&<0W+qq>6<$g4|^yt{<_=>KmgHt^Ujf z-ENUjJbpV3V+GL11j)=)iQ9d8Q;X=z%`PLOThQOMV}S`o5{vn@_WS+ZGR5Q5A7iUSj zzZA7?^G9YijLr0r?c&rJB?3?i_RVKwcY&{zj5MF_p4UC1@Jv2;Gx35vZ#jkZLBxOp zr5L5vYNNG;RnL_Le?osN`#&D4#I@}uv1Nq0#!D)KXa>IQ%gLe~e%!2`O>+?EW(oG8rouYJ! zdpI-G2@{WT+d(I8aWQ@OYHv!wq%t<@rb_m;3R9GqqK_P&{YnnOb_iT$0KPvWK4-+> zQ&;X|Hsez@#I5h%=LfjXd`X6sjjM%xqQ?bpNU(Fclrcio2;iQzA|L1kC2p^#Vz2-S9M-s2qr1p&tr$1${|)#6$IUv5v#LpL3KJ;VfLzzMB7WKmiKm zi+N(zOR}-LXS8`u@l8oNuhT_QQ=a^EP0O$C(ebvxbfF8$(FpQUtnc+;^$0I$3kTif z-M2Cgu2H=)Hrf(SKGCFwcb76mGD|FUzb<9LMT;qzGDbhv>-~XAlO8Hj3qV{?YU;TzCQA(N4&ztJf@sE6#@hxhhpUcx-QhblfENeT~0$>hj^e&1( z?$GbMwxoX+U;s7&=Y5Rz(s}b@?wy*i)|F62+XP+?&rYtibBN9}hf`zVyOms!Qkh^A zOf9#3@!uIhsWo8(UK}7TqEg@0b)CobM^s*k9drKI!T^!}BU1KArjl)0qoqgqwqY4R zU(@d{+b56c=DwzQRaNyD90kFGGclsCo;mN`Aqj|)FHJVY+}26Mdly6=!Hl=eYL$TiKWh;GP_dw_Fr#lJgN?i;J40u#fO2N`I2<6t$An{EnBE!^>jTv%-@+= zmR|VEdGXWbm-J5O&?A&SaS@HF9<@3PZ#EiQ)Y;Fddx9ZVN-iIR*x2r$hJzJhN*^CwX;at}We zS(YvO=KJKN;T`XG(h2z5eGQS*fpctb)iaHNpXV;Kk3X>RvquqN7P0HE)dB*Tp*T;e zryQ5vy8B5o+&^#4Lo{TFm>f3eBT+)VKi>wH@oPT3v_$^Ox0w*NsIPKph+65(v|Y=~ z>M%}#^)(MI(`}?50e=9Ku;v84Fv2xdwaTC3Sf!lqO;!r2GlH>6NFunikuIhhKZTrU~DzHD@*P3D>D(?2E4SDfOe zPPU68)E7Vi1O>XEdaA!xkU_YL)_wjn>xBXX42!jL^T=Au1f@PAoV6Ek4heyZ3@px> z*q=K~$B_uWl>RNkrJWbqnzOpDYg!f9Lutpv$%N#pao6-$BvnxLgw zcW)l>F*K-Di{2gy4KYKA~g_7r!y}GhemYfhZh)N(%0LaDzZ#7{JMu<|JoP& zm6tk&w7`^bPDpyzcOvSs8MS58!d>J3k@W4QgIP3ah%7a*3hwZh)SZ*(D4;m&fdyj# z)L|i=k3Xu-H8I&GU*2%zi}$xY^Vzf-fi7an;$?#WJ$HH}Z{X{^AnnKK)nRnCdHm^w#gYrloUZl=3LxMtMIW^KPxc1Qug<;YUEV(ytkKv%l zW5nsZ%ENDZ8&d-ZbrfGOQlDWsZG=RQs5%aX%zvM-|gkhbV^r_!LsAyD_e?x z$3@oA48u${6EL>F)KI?L`PZ}huP@w)Ft)VRyQ98a5r{t`5}Wlu^WNvC!KbT6Hv6#b5RYzq%t{TUjho3E&M0K8C9)Yh`)j{hD86eEBqzT*o&3b1{0ADExCG)tB-79I_NsvH5TIR`+I29c$#6NGx~{+?y^H5Y zMVLDSk{6XImqP#0Z1zQB;u=|znPoqH&9$dhK8MC7*aYF82aV_;APB}X{yjNB+K@hj zRXvV~k+lx;bX!yr$crdi_C)hWaJElc3ZOy%1WF9nCKydL9E@0C`Lw7-KRtPP$UzG3 z{mOJs$D6-DApHMK6DlXU;WCbcI_OywcRC*$^I}K9-p^m_h05{9?~TiMdFIb0`=@s? zLOf`bEdxqNT>EDprFnBvjd0avQgGzgZ7?=cB4c_>bbw`wDoj}Uk<*qJ4_vVSTM#~0 zuptZ9#(j(E)#sc{1=&GNg&2FW8`!+2>2Lx=+A~Clt?K1mGQD6eING>I>Gpn zLcNWKf;g0Az>hY;fxXjm?XJwh|7yQVycy-9+s!ut5G*Laq48cmq@~QNq*|sz-i&yQ z5zqzEpi~} zEa#s|ki1Q9(KfJ9FTHSdG=*y~A)_V{&D3zjpo44`)CXM9UHM8=7~H z>Nd%J>U#(%RNDd{$J+lqm4|1053hFom`q=~!Crgs06B)ZbYOfp;ND;@FDz6oIRJm8 z!K|}@uG2B>zyfD~y%wmmh=G)-&-r@Tj#uL!{h$#7aoG}_T`y9aD!~c}vf946?j`6o zX%rW@xem8ayyue(m6+Qzk20$>`?9{%5#-yaXc1-w#C7Xu;r90-GVJ^z=|(U^j4TRf z4f%Ay*lUgpE6DKm_&E`y=%)y^Z~YUOnGu1zNLEnk=|q5zSXx;=?e0j>^kZ%~kvq}y z{UtzTH$qNs6N|?-(KJSZMN{FOT9$&w1I|12bs@|J$$s(K5^3)don#>e!waL`l<(Mx zGp_bl?pBr@l?6c#HMsG(z_CDyzp0Gl1=ZU*7q-gtc~S1oX0LfT;q7%mOL4! zSO}nHA6jx8ct`49Yhc5znx|TPFrgrB9HARt#E0@^;(8;qpN2C?gOv5 zB{StSzoi&(@18TR6M$nfO*_HM$Dz97PkWh%yGEbeq<#bkY;i(wPBri-b^m0u>;a=k z_kYH@fK3s$o~0CfVyyQ#-L_)HzY*7sMm{jAUXW#yzirVxzr-Gy@!y)03?v*2M9l=K zl|K^I2Q>{J5D5fJnIybiIItseVAts9qPt9nA$yBq;V)9dXp%O<*8-1G=FtDoHa+&Nx}gQWON7u{9U zYo;LJhG<}DPkcTUKo(wOhcmG)<0s8p##t^pZp^Sl0xk8qoQ0&O2ftoCglsey&5=$J zA5oYq+K8AUzs+IEslVgFgQPs?_ux!not!)3cOwMuf(!l}P2mjGUt9{cHw-0P0@q~5 zjYfFK4>nRF!7iF|7uFrlFozvGm3JaGd0k?1`;=4znHBkW@bZ;orEfJ@61l&YtsZ6; zMv37XIH7&AYA2ji8)%bH>l{Et_Z6EkZXfCC&=oTX3tQpXU6ZjC^L z+<%R&{k`4gl*i7@ zc+ap%;M`n6TEq8_7|jv8n`J#jFDEeKz?|r*wZOP7$#Nu+KD8|k%tsUF&z@n;*8=7D zn$}2RmyzwKm6VG{wP9u1B-n?IpzP1m<7An4s{r5WN&j!xs2IJ>t}XandK;z4B{(S>y0Wd*>E;!G-b&6xp=kjSFzt_?jv=w%G(J zfB9?1?OP*-)qiw6={(x~yIKP}q>muBdIOv;J!E5rY{ac5t97DUC&{62)MC~v{Z7pw z1h-R-S}Z+UJh6VUW12(yYn>TJbj%-K&WeO<{<1RoR8>-@4+wg{0j{?y8W`N0b^gSsQO&^-G@ zpIpg6&dj@Oyy{2$v&)O{nne$@-vqq9rKjhh+B|*AHkMGdX=43oNX%`=E%izZxbg!> z54p)}H+f)rh+Fn&us8*Al+(7;ak;ULLB`2uJCD6+2J9r*{Zv3=n1_YR?yLe z&g$o7$ggOHg`Ozdl)mS1Zeg?X#qO$`zd7KX9y*T-X_eLHH7|$@PFgXUJX@U zYq;jW@l4pN-+E(5eMpjLCnWDu#B8j{EPJ-+8R@@UCwwIvcUSc+6R$2*JR{{`p>uGs zRF}&8Tt`P^%fR!k2eAr#I|Hp1V~JxWqF6}Og;WgHO-njSG8#Ru>mNpk?;Jkpb3C9n zYU{DviSqop&K}S5#ejko-(<;8OcySg5Y~TRUnAaWdD;RfxWjzls7&$TtNPgTnH86N z$k;P)X-9qb)VcLT0RT)s537~SAiqK_4StW%YgGsQE{g&qofEXF-CRg&{^DV|6-l^c8 zCF`>uPwPtD@PRT@x#<`eXbS&c@;k}l;%&ZTnhns->@(|n;V{@m$d`ul@VQT`W}nzY z8j?AAQo4GRR>bfC+C7*&rVFn9$8(dMg9^vhP-7TC-Iw#D){ltrpuL(g;?l|fp%^5y zF+>Ayq1)Tvm#jFvXpKL&E1>o;Li<$FdwyoN#@96xPkHfo+p8I#AuR?(r%v%9jP8Pd zs1Fu%(Vor-lSq8yrMH|9kM*)B7tYOg`o}=HL)C$=DHSqI*&e2>YB$^Z2Ft$7By|^} zb}!ADeSo#M>r*_XsCvo!5w58l1SE+2u-KE!K$YVG)AKMF&!H!`2hRXZKPXu4 z)Pfc%`1t!|JBE**j2h(~>TVb^WR%qkt`lrK)P%l|R(n8b)8v!VU~i7)%7Vv*^e6aUJ9C(|uEZ{eg{947g)lZ{yiv z#n=8p;vDU`D_}T4RxP~sR&UU(6KlO1OMNN2g%p)71b(CA2NGXCr@3}KAZ6F+*C=@) zq`u*F?F=k2Ax8;Z!M}LgXN-BuGYIg~Yq#x9RjFlf-*f({035*a-}2h!y>8)FE8=;n z4B*G$d~_80qKg$>{Z1;%r3*`TTQy3u}l2Y;pCTq@qE|L8EhWj;Y5066BP^1Gn6!|A;=u_+4Jgp zm)p)vh-~sr8PgTwkE45(H$Q8Cv8St7)tbp&i~D#zjYr=ay*nnz3AAg4E-%+)tVtod)F!^7AAyQ|LdL>RFeAVX(=dkHP+ zF{C?ENI)G!Ru~ia7*U4L$9qmGi#?>-dN5O#)Nce+v0#E}`-_1(wJHalL+DWt5hBJA zn^0%gt8kg&dXgW*$G*B7IRRtYmti=jV38X37&=t5dVv6QS~+~!cLekBY`c7@)vZ~B zan0czpv9k%^Mu-Cg;MjMIo4O)lpty-D`vkKw>;phY8~OO&3(L5S$zOIc83m+Q14hN zaa}(y^7ToTu!1Hig#Vx0y|uqMoD$`G==w(bu~k(I<5onr1p%Q4<~SK}_YWv91%e+K zAHRG+IQyiJeUDpyB@Oy`_Jo9B&PHy)-fNpX=4rlkiKPts`XA=WBXx-6_eEyVt~uFU zq#JxUx_?|Pnp(|Ix!98g@e%I3c*@aZ1qD0rvyZ8g^{{C zzAqdYqN>xmkA5H{y+&ZPj)sOao1#gM)>zi#=R#ohYaiUVz`@$**>2-dTHNW6(an|%&Gfd;XVaGIvc-^b zo)1xFEIabwqy1f?_bsbS=?dh1$lb0OnhD)#?!3zu0YZi;-Y4%7gOrix=TO5RK?%=|N32j4-{F4+nXK=)0RTxyB zK4MT@tnVo!DKk6~Ln!?ad%3G&b>PFfNAwrNgAj?5MayH?Mzfp~*4urnAoQ8?%n@$>bmfs+lB!w^Wl(Ks z-B_D+9P9hh3vUkJ;ri@os*1{Od$jmGGS1!3lH7DuC3GT7Sx3h6*QT(RCp)Zs@aH%0 z<6s+uBq(I}LU$C|(sFW~KI+E*>uMae+UvJGtFnCN+6)M}gg2(#|F+Z$@i&yO3equU z>lnK^Xfa*Re>CoCvSY$`br>TKtqdHD6$AJn_n5BZe@Pq==Uh%1>F2J@jFkv{)yt`@ z;~P4$)=lt_g@r0*Ux6G0!e>vQi58kOi9jK!w~=KpojD(#YrPzrDffFwpLlwThJfg- zQZ!tJ_rj)Mli%Z2&bkX5VRsXbH=pO=30^9!7c6p{*JoHzMKYTYM)GcQqC?SBOkdSFl)3tluntxR0K6SI> z*Q(|5lBXEN;DsuHIv&A12Omxb50M0X4g=Lbq5*j&K@h76!U@_;wrQ~piawgoS{~{Z z(eO9pnP=C<&Ya|XJV#j_Q-F0v!czj^9256 zC^JSbjQ-)#6{Q3I9Uuptsvy4(;VO}N+7$TYc(bS)C1c%SU~~T{f%oWvAcq(~t5@K? zZaNti2cZ$}%4&-|Cu)BY#hUom-}>MPZEGU3l`T@C`%2&ZqDr_3{1f=BZGJ1wXl3`U z=~lwnv9u|MPDDrk^;#8}KWb<{I9jY)K`_nA~#vz}^235|fnp|CBue zW#L*<00h#8aP*8JMirQ~i zwbp=s7L1*yEXb7jTl|@P;1ECzm}N)LI&%#MkXJ5J(QSvdbpb`~{`N`$9k*z#t>00+ zCa?4RL6jF0-*BG1bKD-GUi-=MhRyUMWd98`4ydzhE;<}XuNY2^WuyF#t@10uN#0C_ z0ztx8L@Mjr8&fMS_2@#OsO5btBgiX|;jm5}ZE5P}T|r`^4)HR!K~k!D-0LiO8N zjDVH18fEIF9%=!puW+e6(0+pF)Y+*^8#8tT18}QQwb=CQNNWW*vdIkd5APOnd z#LbO*(-a)B2gmIBtaSQoMCAoxlxcLk2o9o}Qg}#2w0F5=NnTqJvj1&`Z&Fo9gII`( z+>>k6A2G{cfM?>zbWGs8nRVGI9k%apXvZm!9!2LuiUpTRd7C4E!Ks#czP#l3 zoEQb2FLI#7l)|X9j~xjL(pn0LzlrycV-+}6+$9kGN6S*E9}Bt#Z_+ZrQrY`{$74*E zZia!Lw(0a7INaEQlfju}14J<$vtopiCvCP!1= zuI)fpZm^HQQG-Xy>XV0f*G&juW8bmHkvabBciLn}<*Zd}IS0m6uPjD6zEwXo)3+{l zAP_Tk03E1&tEeh@3v|}Omw?sRqXbpHQ0{1pN{#B_?hx`T@r&*f8FAk=86d8xO-pD{ zW#M`}*I==C;LnHD3^vSyDU_9)RSw@z++4m=CTo|$O#@+OJ}Pjh3hj*(o2a=u(z%N$ z8uVX{r_KXaX4(`_f?EVam9zly*bDbjHk&W@&yDat6i9o`py&~1w=}8d8-_s;72N^U zBKXj@w|h9>wq*t9906^n?nf{?RLuEzJ>s#&Qy^mVG>TB4_u*(6K=zTK+S?9KL>9#g$z>J|6k zyQ3-eI<+TR&=T&{>IDK`SH!IhfsgOML*KE7TM0ppFbSGGw&DOCH`U-)(sZV_epic&p8_+(r z?9q0{mz?riyBg)kZ)HM0q0*%UO2n#_F{J4bpbnxz9D0aY`HFSV;Cw6Sj!q zi3n;F59Q3n7ALl+#4S3`$Chx|1Cr=gmM(U(-nwVe(D3V?j5D;4jl-V**118hOL^M@ zPx(>ec2gRKzo=WmpGQzTGcNQpGZUP85TqV%S{!+fnwFXjWa*a~6bn~HHJ@@#@qzTt zZ47M`q%|#w)f{&ElmW~GG`%#xq^!QxKSux4=k!rZONcpk@c2gnLoj2{z6uHks|5y~ zYoCu2bge3V%gPynbTEc;Qes<3+`D20J_?463V)nVH32S z2XeNrtyI(2zq@qYCP89ubc{V(p8_53`Mbyof|ja~Wf!)&gSs$qhKriJO^XSUsgJoS zmvP+GQ8l(rN0J!n=74LH8^3oX^m^NTqc#1%&fxml*sXBTs_M`WW?)py$ zV!Te{(Vq~3l&L9i%LGZxp|iBc*LF87g#tPCmXG2)phzn`ndgob{P-<{@QH8~q$tiE|Bi&~P_7)1h|ogB0$u)-F>Bk<7P)m(kri;|4i zI2zMk`kwSLdjWhW@MJPb3nF){B$EOJ;Cj*UKA&x_eq1zYevh)iW#97HZzeQ8fiw2T#y9VcydEaPJ(Qs!zr+^&||q zt;Vyx)K7Bm?~2Hn=GwD9X{RU#sxQIhEMMNdo#TUuLGV?m2RQCIQU z@ZJkgd8P{XH#!;<%O2@hzCQ4(Irc!=R-p2$F%u7;J*zub7FX zip!>i4|3UabneYC@4M}dhWb?f4tlEQ2kgfDuOAl6_9ll{8R{L}r%!kPm~3_OZ{}vG z*WD~KT(KstJxmh+ztrpglUS}lZPs;!kqRBatBlHsk$y9t|A`QTsyE5&!=OLUDc=i) zBEpO>-%6AYwkmL**=IYQcT4TF&le}&p9&>sd;l*nsFL=mPTTr`0b>B3%->SU3WmLD z6%YBvV&Ket;xQcvbb5JZpMuT5)6Ao9YLV_^#J=%cJ>v$rNMiHsseQ(Z|5Rhpo3hC+ zoypyPi5vIgwpI`EeED~c@O_5mjgn-Pm;c*bbH{8k?-M4u7sAMOm0fmvRZ1_O1wCl4v=T|+SkOv$o_Nc% zcV_rgPgxJ?V{eAXpQbf9>CB~E?`Fh$r7OdU*psclKMS?#iGSHY^Z|c*A`8a@MHmGV z(pP_4W%>Qs!VKZ|Hd^OgS>3^EFP$nS9;hB7@U+s~f?I!PgfoY6{}o}=ksl=gT`~J! z8qco-sYlNQJ_r&#lHWKJR%G^9lJ!Awp6)5Ma_$Z?qt<1feRIch&~{kFZasESI7E2G zyr>-Y?=OQ-l*0L(F0XmNcB_5NaW?~hWdE{==$Bs{p2edwL-}6bD;j#TytagviPp#o z?lQbg(Ee6&u75F~Aen&F&caDtJIIo!g?gvm+OZ#{<<<6A7c{+UV(4do)9Xd_D1}S*YM>xdrivpet@L4;8xv@W~F&z+qN^ZpMzoy z4@SBYn_3*pZ^1>)Z^b3mJMoOWN0SiAwYO_rUYDyxw=PuP-eJkYfzqp#`C*~;RYzU* z43FQw%%GlX{=?xay6%b6T!@}&1;+GIrB^RT1Quxo?%!g+Mx7(H!>S+rW)6M`m5Ukx zEA*uIkB#)uJ|#YVN`4z&x#=}9_4-`)z1sZMP~={5KY2_d>^bEY2+T!&R+2djAMw!V@W%Y`BjmYy*+F9wbj#%t#N4)=! zWQG0XnHTV6^p38%V+9BRCHHnlvQIAodaUHW$vcS!ItB@hrOk?a2)|ljr22H9N>1qv zpl$WF<{?S+DEy3DZ3ICH2s0uV?R`2bq5D@x2xAA^Ru17M_N04eXQCJ{sd`VTBS#z6 zJxeg@xl`mme-cl#$GJMs4z1JGte{C&=Gph?Nvz1P9-__sjP0-`Mw54Mx0qj^{ym$G zUjNGX#31opb=vI00>Aq%9pT2F{0^dBd=nSwp7mmxDht>5t@dQJLGPevL3yJt;mV$r z3<|Z2+6t?>_Zx~!Xj2|VL-AOG~9kku)$ z8a(Q*PS6pYPw&lQd?K3&hZH!9tN_;07YS)DkzU$<;n}bE7#nwD$0bX4hh3JKpTQqA z*E6k*9PW_ruk9R@OrKKPUu*ODqwE4+)6Vcv^5g7OLRQDlsh{fbC6wfDsGr7~Al`NV@3W~k4)84ug(kcYj6P7 z!K1m)+M{t{QhsKrn==dX1aYksbWeSP^^adnDyD6JA=iJUvB%Fkp(;!#a_JEa$=qG& z7&6ygIDp^U=PUBWUy}9Jv#%ghD``;!UEsCS&K3Hi;e zBL!uRu8*+@{=mv_P>2ifD@w9v3aXn9Sv=KA{6X)Y1yM!lgDa;asoW);?FqVA&$t+O zJ6;g=noPBQu-LG(+`{QwEOx^#ol{r&87aKne> zi(lS5=G4^a^V3Hfjz#QwEUexANsv@f7^*P?b^b>`n9MjkwE8GwpY{_72gr==kDd zhWu9Pe<|U{9zE%$ zI+FSaM%!N5JtpU8^R*)5Nx11m{Olu>HZc!V#QofsusDS!0405)qFZyFzn#_&?$cJe z`!BzZ2hLSd1#Lk%q`Mg1aGgcKmf(@{s&bmXgKI>lWm(s z)i)WivFKt{`loF&avkwIk^DiS_0}hHQG6%|VTJL32cCCOt)qh|=;=H;s!XN6k!eK7-%6Q#Ly-gkwo79(Y{_)DTc}jurKPzhxQuZlX} zO;6ha#QxrDc+8Gtg*s#jg85 zps-Hv!?PamvLLZTR1H>KhOSumR{WhSSv>yCx=4J~12G+ybLz1<|2joAQuc&PZIb-> z#>RSm``q0${-Ehv;rlq|PyqM*-WZN)pVHmi;V-rQ@rxR}x5JEZ9&oMdJh?v4+*F~_ zW6s-ezb8i{)QD**jNrCmpwjV&lh7?kE^05MIUy2%e(xd)Ix0V^QUxEeAJsaLspfi! zOQ#+f)-t948n3(tdNbEXYHVKTHS%MEg-iz~kQvC9*%TNPSN$8ZZv?zW$%WS=N_$y= zzC@da7S&MOtbW5D1E_r3Jc}uHKcE!Xx!d-m0qyjJlY;topDy%>Dr8;|zdmZ+w3B%AFNCjxraR}x+VdddC$EeAQX)G<{i=pX^D<ot=Zy2RYkfGkKF2Z_o5IQeRp#A?gkw-dZ+ zT`G(7_K)L-l17_;GNS7Fe37iAafQrXtoR&V#|6EKs* zut!YsmDU|+cKTZ-H8$h=#%X{l-&0K-;4`Op(IS1c}$VfW!Gm4nN{ufWKBW!S4>a%kcFO zpz13UH(5l1*p9vJb5|o-O}w5cQPsPz45RYY9;KNl0gDje8)#|{W0p&Q)pNN?%Y7#* z7-Zw=UE`s~+lTh_AqreMnvi~N(ApGg)@|)Xcljp)$8s&e_;n>G_As(Z(3_=fW#rj$9TUKe;Ntv%mKQ(lP$5tyD< zrbGl{w%XG+HRRmxWq07iP*%p_UyRKzFD87Czu^W1>V)&H^X*(q%9^VdHhRzuz4SBx zY_{3sh0gtxK(%`*#6SSJFi+_Qerc}#PH46~I>u@{?N0TXk(8TLcR>+1kU>Y-5j+;F zlzFEWPP-pdOHoE?aJ@+B#w2fwei;K*kb@%GAzXJqs#_i^HJ&`2RIqH?vutTcS~byrnA>k`>XbGQTv^ZTC2Il9H^aq+7-OAz-tE85XKCx znX2Sv+mqpey1 z7ce!g)=b-Aul3MeVrM2BjviQm{-&9zlMLo}r3>Cy+#bGzOvjdok5-3_yayi{*MEqj z3M`C&MKpQK*JoG~(wkv#0YyKy;%+o5{1zZFDy&E_ntiI%B!qv;-=(7ED~!HJ;QUH^ z@D0ld&^6#K*HAM4yWziQ$H3{7z;8vvzUDHN{d;+6CbjFxQL3OqxIH*a2CY@ZyHXV} z?%_Ohim)4*N<%!8fn48yENW*V^jCm@_n(^G#m&{WK8=#Kvy4Cg2>2iOpjB^P+|Dw* z>}3JuP+fzZTx3^XrRq*kc7RcFW7@7+Cle}O7{_S%%+hXZnLF}cW(DjOwAe1(2pent zp}3RiE@Re-K?m6u>9q?-u9sLkK}6ypQiUR|BdnI0o026={4c#!adY5=i%*AJkjMYD z#lSy7*_n@uzo8j6P&3H9uj-Bcr6=yfS~9)auOPgMgpE;&ujlt|u4VkN9)AqQGtp*l zmzu!S_l#Jx#@aEZ#HV>0-Y0fRyRlzo@#%L%rniXI9XKqwd%Nb1=W*VFWtXy;%T!)o;LJC77K0|o2^9OF$?dgl`0RvalvNV z;TT@VvQr3M9>M12!SR)DWA{xNacKs!AlIwXNBwRg1}%!Gk<1cuFKF&l2zC z+SxHS;5BWk??g$Cyr`Ll)=Dn}r_$upR1}=F?9}p5QsrsoWVamT{-%yXVkle@oDUp_ zAeutkra=?kWfr_c%pS#Zy*rWzJNvB0E<=TUEIJI`A(}Cflzy@3T=}77Q_LJH^9<4k zdEFGj%m~_{-`ExRT=~raEeh!)HwOTS6%iUiL#6 zBv&liR&(p0TX^f#Vf-rWHm?6pN>|6uNf-N}ey20Q6wSc*h(H{AyHND+M#=X35Li&g z=P_fF7jjQ*D2frBa-l+){qmtGe$g=((YvYQrD`%Uu;Ki)BX~()t;9OBt}=Sak4Li< zbyR#4F-KNg6?n>{)YQ-yQ4P#$U?t1L8QbPw!?g0yPTILjW`SaUT6N)RT*c!1r)bvt z*LB~o8}niRf&$}FC5ao+vF&x`2Aybi!Amr53Y)&71?D~Efvcb6HRsFWa82bz3?O=d z(9>6sSbn`J5t5Um`(x3^rhv(<-Kgmfr@&}sXtOa& zhH%afc6dguyRhA@9Mck?!hHM3hKRkpi!W;e{XK1`z1?Moj{!j=DF`TPu!0l1^Zqr( z+DTKKu!J~QkEN2dfYX*(&9qdDi=0SM;v?9XOJBY4ZqIzy&NfwB?#Z4wE5)LZ$(3I= zhC5Ie0)7|!r;XLgxt805oRv2KpvNuQj`GbPTG-gt~7F<@GQ zI||9RzASPcAkC0hw|IQ|4&eV0?WBC(8^w(Yszba z+vp35JYw$`rDEw7^W#qye)&pKK1R!}wtTs%teio8wR&1aG;WqJnR1>?Z!!FB^) z_-xNhahZGJLJmaZ=t0)}ysZ_Lk~8ABvCUyU>8XJ7k{(EXggU7~Fh&C;mdpijlO}Q0C;$2>2@{UjZ9Y0sG;`MP9PB%%t{s z=c>#qyZcsVKZr1gn$_dA%LM1MlW(I|HMU-(fnewnrxIxl^vz{4OK44Je%E-ab!x7C za6mIfO#sn}uF4*7zhhpvUJwqc-HI3}X|dO{FCKLZRuCGDEugk%X!}%Ygd-yM#yQs6 zz5Q(mxOo3hT~`{_)D?suP{ge)?$#435C{;XAz^8d6jT%*BtZcc2o}_UAOeyEFrf-q zD+lNinuw4EMGbo-A_#)UiiKiW0wyd65jYAVMhJCbDTO1wumAd^zuvETXXf5{GvCbn z?p%WY$4zvlm7U1mtoBIN zNnxW!;a%I}7y7G^;i+ng#x7OPUnxYG8eq9s697`p=+)6x&OIJ-n1=D2q4K_gjlF*E zgqwXHh>F$S*-e#8O*nJN+zwITcH!b1FaV65&@+ZqcE|%6_9=HCB+DK$Wt@yInJRcXdCA?u7jWh zAg=&kklbI3p?;|)q>3>pe={6(uot5{$!ZQ#;cBnG+N%dW`%?zs+EvO^;LN@Mw!}pD?45Um)vnOR z2(INj|HR1X?eOEiP|OrGft3jW15aIQK_*T?Y_-oEE&a^5DD>CJRMLDv3it20_?i5-YoRe>Q*kS}gQSDM0_*IR-W?ULpJiVr; z({o$gOvpZFd3H2&q0xm?~Eg89&pMF0vr39AUsFz6=B}VpB5)75Qf70?ZBj7vF zPZy+T^y?ZzzW)e7)(ONsbNtL5!Pgg)U6#12>Zs96ll3xw+Cz!lwx4HiXlFCz$4^ti zSaIEE3s&A>O8IdFXqiA6kx0bw@_p2nV@eS_1B~nJ)lkL<#9}C=GF7)MariAuFdX!NjMiI~ zlWlSNef9~|)|zBd{U51eHUZVPbX-Innm+EcYJd{f*S`;hibyo#InN%&%h?0rorBJY zBpgB{8sk-hK=C5 zSS+$%CA(gOu2bedkydP5y6mBSLb};BL=$Q#M5@rLTtE|2Ypbni;Q_0r6#l?sU*`UpYggTS#+7^(`br|q7$wex@9d+)}*`*An6BC_gaW#-9@ zsEDelbAC}u3X|zciZ02ZUP9$ONV(#haV(UsIs>*TJ z)AXO0Vf@EW)WzJ`&Dzm{NX^>b9Egd9k%gU+nY$!=`x^)dyvE5zL(?ngI(s^Ciwh}S z7}ak^M%WH&Hw%mctbFg+xMsv2o7)x=UhwoOJ$}xlg;CZ4l3D*kL5a3w=7>-USqS0x zUkZbR&xgay{(aR~HodlojPQq7ADr2_Ve3y{JOyK~0KB~6w?{95Fs8iLQvhusZon`4 zp8K{3=bY=$#oBoNwWsuDj;HGT%f4UM+>!Nz3HpvDCw-p~gYjf@=LVmfDDf)3hXY}w#pR_!FS!d_qZi|n4pr9cqzBVTX$p|evTi4x;QkLMFjrsTxSq|7dIrZ z71zP(`9(b9&u&+-!NtbFl!E1HI${mKr#wdtRdN=Aof=GH`G;OiPLB>8<9yBFJDKlq z3lD5f3NVZ22m3Yn8$jBH@)TDnZDptKNSf6Y_twAto{NeubjlZT;z%#de^5^8>_D z+JAF|C552$+IJT5T4$sI21Dy74%`N!V;}@%p96I`bYyvfKw2^XaTz>Vzd$7seFmDT zK9^rg@eA(wA#0;Nit#Z}$v$h%*c^UXmX>6Z`8j`P?gROd^G_nmkM-=p zw~K5WLGy;6U3S~K4zZ^VoLIz<|3z)>;=z6?0OQRnOgZuZI z82i~DnX}8yU=&c?M}SPPecGDu4?DM(L4SGdRK2ymGF&9~1FblJ&+O0--|2KSrQ-Ld?#WyZyjuQS5Xz;hrUn)LF&yBh8Le-q5|LHxl;^i7&FjOeqI&tZumzNw&?f zs{I*>$39VhJn_lTAq=F)$%lD+|BVtto`D5~jEnOw=gl>YK>bNqTIY3meOfI)4u@Z? zL^kkLhPvdYAFC(6>U80F?Nhjn1&dt))bFEMK^N$Tupe;!dkDW}m1q|nUHrtkWBuF; zi9Qv~w2F1!dVX>lPFM2z4O_&t-T*@1xVU@2r(n`DQkT!|6sy~>YzNY>^G_J`qL027 zkyR%iK&|W7Xy&=`^y-?`>M`IM`o{B@G8gdxsH_}FW`gIp0%KvmQt$~pPwEKbt6LA3JVro2Li8enIBo9k#jJG zq5ZL-4CLn-PCaT*^IM9N4PV(T0^N{7X=j9&4Cd$W(rNu8m|ymeEfS#W-6lc&ped)0 z0T=R7Sjmj)1>R?Oy)!rNyR*|@0{tDEKf^HF^1Tml)l$VgmzL!|FAoX2%XN%P3IQCO zlZaZ-?P6;W&jhK;d81GFznG9{8pA!bIRz(}2h?L|^DflCK97Ny@6Gry42Sw-*~r{C z@tG(e3O`A{%RUDBjEDmfKsT4puZ8})jDF5I4!`<;k4M9QmdXx22>IrCss=FqKpXiu z^SuY63-*YGpn!h=h<1hu+9&cosJ}BZ<$b^8ZMD98e%rtK?mpzb^qd;91Y#Ss-GCaj zf6LvYPB;bz``o+*CIP};_Dx@rzTpD(B_Myi{hvR(-Y6~nm7G@omS+6h*`@4lMI_#L zr~d8XKw$sm|5c+dVJ!p#QgybsRa5;z}klly&d4nbB+bD$EW0FL1x?y5`4L zPHhrGhY)R?gg{7TKE@zw#F%Q|OSN?I^S$11lCaoLR*0xmbWG_d)q>bWO}&%c$B?ta zMgX1#40>_m80#piHH(L1Xg%4n>@hit`w0gnyiEQwP*ykpx;{>A=1I@KdH$HDMdq#U z{X%^+ZecYr5I14R$u}5)?$!U@SRz=xKR7sEy^Hj{vCH%3wDMNOdOgD<=;o6veJXld z#HxEuh(GjA+3-Vu1^D%K!}~BhJqwKIzolIx@pVWIv%^EOp*wHoh+OaU^%X-KxWXH{ z%4j4yw}^W)FMS9sAm-R33Gjh*d7!s%OGpXI1@=J&@rX^PGqmXo#zL%k{g-$lTBz_xAC;cng6(V5;-0G9*@hT9w#zcQ9R zu)qy)6v5{QZNZa{@l)6k&3FR>zMUs<-hBG&{GEiKiQ(}0!tGsX{QPH@znoNPsMth0 z)Ef$MAUi<-V=E8AaKr|h2j28JRziYx1bnz54V>aY*EjU@GcBHD6RV;cwhsSqhoSp#EjrKx!_KCh!==&1GfOM4vJhuo_gMXY9T`O*n7*@&$@>?3^~mw zoi)}n|Fyg7?poC)dr(OdX}=&Tks6~-S8==8QcT#W($m@6#_F5-1w9eWIWm_RTo|ex z9#3ixGG5jR4)C*^c}2!sI(Q<%-$0$J7P_+FiKNU~EADD+F&0}SB6NG9cF|mk$~z!R z=37%d7iIWvV3Os_!hqGR%Zn548YictCr%1BN0cTUlnnU|#;-C%q@ zTtXV7B~G1^f>m@JMbjb@r9rg-W3HJ77D-qpyPCQ%9{Wt^^vM^aL6Mt}V?)*`#+*o{ z5~(4y(8&%A*F^-q%1`<44Zl!_RYg|oClJDYNtW3m7uhqM)^Y3*b#=<%fCIKf-8!5=p5|*4!ndipGILf?(mUn_#9u1EZtKJ ztM0JT?8GsnggSjs$_fuUjDsF5&J7M-|b|;~4IZ>Gi8HP+`u$9U; zp=m7ve&bRU-&lz%c$^{w1X`8II7=#n0;Npa{7P`ZEje6Q;kPYK>^Xm^AtP_Z#ZuA3 zE*V-gH2$`cG0Xa%8pYeHejSgK(!L#CVYK-z<2cSLro=_#Rx!wut-6(uyj`hjgfeL= zJu@hYJ&z6%?L(Ogk?sceB!)cgL9*`XEM8TMB!L-HEhn2nV7B^`RE~4QRi4S4u2oL8uw_{SLSKeX|CbBI-%=>qU~-ep52dnZacEp@L#(Np z@p1Z8R9t31*92{L?7 z(QxQ8TH`qMx<)V$3|S*fn04)$2lr1V89^(2J%TBL67aHpspFDjWt{`O^l5kXDybMZ zPmq32&XD}nGnPl&as9Pr89(%wHxyr}-%!4rJ}u-WKT(;+<}=aXJmgf5SAXHYd+e1BbWYbuhyX=fl)ZL! zh1ZmsX52Zp5dYwGQmb$3Dy-uHgO>}goUPRt0nij?QG=%rK~g6MHS*nvtCdKh&%8=1 zwEZOucFf7i%duyX3jAR6r%oG4t$-x6rd z2H@I_Zpi(9AOwsH;xmm8iN^n!cr-gN!JDLYz1ch6JMR6oTw=`C$XHU@YvYP2oNshVLLReqac{WEuVxBcPTE5@T@X57j*^d<|iU*=XABllr zr-Qho`(RjRTL)nP`&9vb#*<`);bf8Fnt|e2tl)AuGEP_jf#KpBv9gZSo)INpY7Hl+ zO`I8!Y5%SRb$1;O`KHZou9F4!JA3TvN&uf#-Z72cJRWq~Z4x2hoAB7$oyh3gt8s=2 zmx5})@>F8enQ^!^52I5MwH$hps-bM1>Sgm}o$VL9u@*aaAASx$;V@ znEZv5h&FXm3mHD?&9Su%kHu`&;id3Q^Z5@17A2a}YPMyA^_A(qve~R=MX&bEP{?P) zBA@e*&^6)27k0`~E$ML$sdXWBiCKX!3wz7u)oG|@ujc}P%R-NEww8+aNs9|piuq#d zuDQOk$qpR0KSUXe_*BMP4LJ9|>seCqcPhv&8{Lmw@R|ENuAF0x-0DI`30oe)!5L6U zrCJKf%#;O5T?P1!G@<>kr9(W*2~bb!nKwy(lT?H;yK`lj%q=_DW*NvshN_AAUe$>_ zGvpcDEV4NB2yyER{s{VQx0yk9LhuM! zzC{HAaZlg*g5UiR*Ki3-mO|WI3Cc9TbLm|v@*gT(Ix0w}GW=JpXG4|2YU{UidAGfR z9-wI^xvX3X9{$v@CC+7kRos!zH%v9!9(P>2KP8VeaZFK9gLUSmYZU~7W~hFaD`;Bi zi1#cC0yCvW87BC;S+smcXj1KXo7$I4bgluCl{2c zhPq`PiSjNYp5IO@fhWI4x(Kx)>E9;~Q_z2^TkoUCdHu0^zJ$c_6M`7Sxtq>0x;htQ zC}m;4lHpi@&lXozv!Qj3QU|-J#7QidxojY3P`;pkZ!c7d*O(!*M-QJe@h^cp{94&7 z__?!D@>?26T4pH=*%miOIZs-47Bj^WOfFJbBT&A;k`jVQ!i7MI7(~#%Gg>ZJl_2%D zQ*@YhYQ{lp=Gq{7+Ju~0*Gi6-{O7$6pyU)ln*m=6Z1RXCOi(dnw)O;}t!l)U$7_~TaGWU2t5fU*WfP0^1f z%SJ3StEy(X*|{uPM-@s;C~HkPm97qJ3=e>SA-G1(EVZ%tq!Myh@wViG`_u{mG`BeX zsx${lCdDt*b~Tn7`K~rjv#nHX?z(>*3O4vYv*yvV{pb%=;y_A16xXf*a<;+CI`cHw zI5M~@9o)1%&Nh`7MCad2>>r$}I%KIBtuo!=ix6Nle80%m75aYM{(S#Q!H_K-tzG0$ z+EguV!GB`NGK_qTzMEDpEvsTxz@1x{D~(dKUaWEhO%141+bnKeN8EtAX7ME+`KUp> z@6E2XrC4FNU1<&OA~2UGe#YyZ$vS;_L(ZE!mE=&}(W`RUt@?{YaKoGR*-7Qy~Yc4%_NEc5>z+21RmI;!s5-eM5tjJ=k<5rd{V12|6e_d+& zTKRN)SPnnVXSKU+DkP;@J0mEkTilcog!Nsr2wVy~GZonqB zT`UbNUNUrX;>|1|v9F{#@}`_9NAguccqCa)+|NIOmc0oL{qc z#9vwYQ!=Ix=s5W{r8v!V_Nj9kT3GO83a|U zt$&(VHn^PKWxK5^6}Z}2{s!`+VS>#2x;Eb}aCQP{X6IYYXqXAK{oH@IAV*SzGVQk^ zIn6_UXSrnTuUN481P;y5ewRb{k2w$~XnynV9xb6LI2=I)ONr6HL|;6?a?gm^dWKxo zg{jX7H;qeEvWUtF_zflFWPyhg&Ua6@7lkP??fyvW*u#MI>nApDADHte?|Prs9Fez$ zB>048@$sFm$gT-~->X?gaeBbLT>401E%=h*vy`v~m@|jYBEB18yU8fMePV)x!0mxB z(w-{6itmcsIb82{>9=_aLjwzNg>&w0xyrA0_hjF(H}K)kASYFUwLe=d;N$Gtki&k^ z5l_FM`;=JZU;bqIeP5i=`&%DG*~&uS|@ zoM{*`vu4I_1)4cp(JXlLc@v-fW3KsES%d6SPF<7Xp})r62HWtISI5MC!@IZ4J)zRw z>%`e|^NkT(waQfxwDU|6kPY=Fr+<{)yEnvkW$Z9S~y?N)RvbKutz#!Akv8O}SY-l03l z_2;P$?aO}VanCpGlZ37JalTql?crTZkR*@lb#Lq>iyDbXf4NgXx5m&JJW7j2)*}#s zZ(Ld4$z0-B&aQ6PUr~>+a{W7!W)SO_n|(l^kKkG;#dA&bM!A`H`iRzg{|Wh}Z;3A9 zUZx1PTF*W8&pSeop;hF#ILF!Ncu16O&+2$EhY2!UXxhU zj(2#cWIxrdx!diV2?oG$Ur}rR(3_+Ses$MUI{28s_MTpm6|6@<$+|b#`r%gtXR$Y0WwEu4D z##Q`E%hmO8*yuDrP0PjR)wFsCp6hY_Uo(Qn9J?~F-gDc^zNGvR0dM~3vlj}Dw zfuNOT>PA;QE%yN`1~Vt?AFy}h#k>6*Jf;}OQ0uy3yt$TB{WZ>Fm@$R=%KYGpd|JRo zhHkInVfj?f`QQVjr-^SRfA6y0=<4(0gwRu;?WWFz1;UqrJ$!F_2f1yGps}rQSxxO3 zrWe_JSQBRrrqIZv&^v;oX4B&!fRV_#Pq`a6Ti*OHJ3-%xGe*fnB)3@o&_5SQ<0mH zc=Y6W7AG-IQ1!XJi1{Gz-DJE-h5oLWh9 z2^J)2A=LM~)AyH_h3<-8`nZ~QjDxRYL7Z!3O(6M<0Y;6u&Q5;JgEGf$m}^4rR-kodC2I(f6&Jy1Dcn7s{n+vbOrN?8zp=HFE;XXCM(mCCvem2nMd525k z4#-^0Q>It-c^9m~vo#=g{FG?4P-{%tM+_gLq`7 z*9Z7CwUNE^9(wx{euq5fU_(`Xjm2RcLEdh3_MJdDAY;smkfV(w7S@lekBlS9ISf{j zQ0*1QxxViJSqqWmo%QnrbmF~{RX@32aCJ7v#g;1GQ0j$Nl{#ve?~iF|5Ta(lO}(C&(w&Bm+VscA%EC|H9Eni`)A9+ z=R0prd&yG;06|yNxtgBWaVgo449d3-U?LG2{D2e4DIz18%$$$nG&CEe*$6Gzxr z`-*xkUT~Z4`*!QKI8Jc$KixO^VS@Xo&G=%%1HY46+pYZQtF%B~H20Gqy#R%sK>5_6 zm@{j92Xq!`rDi2_c%~KU3!8~tGZkj3a)Q`T_ za}16DInNK=(P7r8c-e8i8a=Ik|EtQz9}K-O*wq5g+bJwzpIb&7R}uAKY&(A(QF@=_ zpfSuD3nM=n^dk$o?3VisokxwhwCPOz>=8-XtmA{<0%@V|NfNaW&V&(c!Cxl+#pfZ?N**?HAKUuP+Cmf!S_2=;fbzbnbM0+_d>GzdAo@E!WKBh9;JnU${kJsB^M#~5y zPfHIa)^37Fou@G&*3~TH)y;lufB>5W@vY*m!OCJ0o0Y>)^@LIw}8V!ZJ!!>1e6@&*DZPePp zo6=GnJ1KNV(rY#UY2?_wQ@qmVPy57W?@WT7og-`$Uc||%LS+#vY4-iZ<*eFC z%nwA12z72ue4mFf0d8|q_t}qOGXGx2`PNlsU;!UWyI<-^SEjn5x=g2_u2#M5tf0$I zp#UeJ|H}XY|7AC1<7|V2!Q)pw>$2`zU+UTWA(F+V=xc}8gN$gp{zA(llIisy&BBo@ zhijeTEkoN0$sod6ongUR?MBrGyKe70rHYs* zfX8>XiS*bG)>knr*tZk_CN%v~ds0RIhf)ge@gt;0SzyrjB5Iw`z(DoVwTeY@G^7SWa_V^GnKWSB7sn<~V74}cht9+|WUGg`)z=sSjm#95u%oxIX z<)op3sTvnWxUSM%n)`XiSb4Pj`ELtCZ#IaqpQ)RU+@(InHHs>wZ9U4l4Xh>TR*)^< zz;|c?>oNxsH&=c1Vktb-q{eaobLzK|&Q=6@eM!TmnZB5Kyf7?LU4~`KSQKVmI-Bzt zKZ;HIOA3%UF*Z($B5syfN2xxRg0r)w6?tq?eNsb|XV|Rqm^VuvTn_zG!4kRiB)9i2 zQowbT^MmAt<*2VCO6p$5T*&2OM3yW%C#I7MqEWc*9AX}h<@MyN?{COCw7QmLzcOm*{7d77;pC6;@L0gTVK6IR-JprB#3EBN|cRA2@o=0n_XfN%~ z2|@f=C`FR%9Xofxe|fR;56T!hAx#$to?np|w>Ah4hw*X~?zRRq`mS^FihlQ^N<;XQ zFtg1B&ZkjW)Xze={oNpL{V@|e*pu1Ld1{w>-Gc*j(~yk#9;bahJ)qHBu%PCw+vc(Z z-2%=-JVC9o0xj`k^C+oltZoyr=imH`XMgBTsSCNBv+vo#Jq+orxz(%6W7#dK(Y~`) z)cZyMzAE_X<<7*VZNR-_#di?93^@eDL)nZZEC;J)ZFGe0ssjyE5l>2&omIz2c_7e#384 zbz~!x;32-p!pk?hl$3Nz$_QIvGYB`Dx16qaL^ z1$*A34%qYHXT007V&A6DZ5md@PEo+VRrQ&FHb%^F3zS1!$0gk3+1^Kbzk)+3uXf&< zzwU~<@h;R$fh7r9*}zHwGtl%3Ao*P34oDDNgU_9SK0c&SfO=&9%UC~Dk{z0O>07<~ z;M8VX2afa8NQ=5C5U_A>BWN#UI;mU=X>Dk z-|pmV)zRZW@6N{HIbEcMSaeek_3V zJ`~nJC{Hc20oKs}?AN^mP*RUB7{(rCyrnf)-x&{d3#+n}DPwSMCP^`qO8DYa2$Y)> z|6Ra(oG=_;JQ`s{BT53~UVUyJNrM66x0vk+rLl@#uWfm3lAFsVnHP#eN-%W!rKluk zFg}wo)R$m9Ui*Nm$|GBj%tC>;G}fOG;YSV@G9Rwd-(qnf)JUC0L`G7CK5vbf+TeJ9isca_|_&cOccri zJQ7S0HefJq#!z8Y4}IX1{He%bq|5r3x2IrG-a|d1D`Co-R#Z%(BUSD}O{byuC^ZfX+45ZvSFjJP>l`He_1Nh>$= zj?Sw@hz-C3zx1T8qUI~bCzjO1L`(!9#j0qXCY|@h0&45{^fk|3z+cA*RsV6BfRL6jCOLs&JWwo>z9^V?Cl zp>i@JRliDch_RT}E3Ev9)JS2ANkO%}wNyx9MGKp1o{$ydsNj6z#JHc@Vwn|Ef9fOk zY)Xu{REdvq8YgU41UKHjS~t1N$lPpSlwdfk!NsCZ6*hH*MCk{PTY)^!M_|=qgUE!^ zK((j9 zV^Ug#+mOQs26kum0TQc7^p&mzH&!O<}-tuHKNX=h7^4 z@RP@XE~*~C@YMIFPybaAA)hy{$nIj@YR#s#8j?p9vu`O!b;l-v&ywF1Q1= zsSCL!dU_#ZS9M{^fFts=OF6z2(Tj}BX<^P=3+oxZ%hE+VQ{jV}9A<+6FNXMmx7eW~ zUoH(L8l4W|e*{M;A&66HAtL(AlL#fkiJ0(77MoaM#P1*#j8M9B*84p zUlNRk^B%9F7Lo=v=v0j6MnAe<2n!FzZ_XBl=o4*HkGw}7gWrYqFlwz!4yP;m2VkR*e=qtxNds!aa` zH*+11u&v9l6iOX|+2K%(#t;6$6b!crMm&Uo4Q6Z_p?ATJr9n+RtwF5?rVrBeTA{sw zGb+@CP98;Ak%tDMMe;aoQM^P2fyE1Sld2&q0y+Q09A}eMqzuP}Nci`sR*k1{3~*e=oot}AhjNx+K7WAP;(0)dP_(aK`Cbd0eNGV7`7J`-`n3}mOO z6Vv(;56`PXAt#Yb~$dU}Ae=f?RX5v#x=H8mIH<#JLVv@t;}GMU{UI zzEe6{g2B6;dyQ8;8I3<^vg@{UcQCue>MALDu~R`?UWLtLQu|r&bnjnm_ff#1-V2Sq ziWWAhYuIU5xgE+d4R&Ur;Mz`LGFER|q9pFA57Q!n$gbz9;!ZShi3?GBKu2!Si`8R3>uhdm<;+F$T%u1#(>Oa zIK>Sc+B`35= zb>GZA8z)$m2M8yIM$gc{XONoeT@rcaP;C3NuJ7tCVimPHmPd6jYF|6Kg;dNvq*uomjj?}sj z-OL$IJO*|IftTl55zKNKWsN7DL3wq;9-v}}S zU06z9Ai(|?bnvnWbk3+pry=vG9#4pAox+YFk)(;6an{V?r4A%u-OebKad&dCqI909 zdmf|!;MJCDmBu6af=J7(Nj>Nc892Yj;4Q-5W8Y37z{?z>eO=UCaR;p#_tyz4Ad4^y?6 z=2mN(dQfOj&$HCCH1AQ6(=dyOtOM$_YuBb81gganc$GONs$+-NS(Xk3dVdS)9Q*K% zI31S266X?X$4}iwQqGB8C-Gwh1`~_o-GCXmR?L7f@#E2zw0N8o#3+RQ8gw}u|1;R6 zAV8L>m~h=BkYDarJkjJZD)Z*cxnYHi+=! zK);+NEoDEWgt)G(O?bi$Q_+DjjhXT1^`I8#`h#I#JFo4I^ao%84lTzS9>2(hKln@* z!U7ZHWh_|DEQBYfthBMcV6s3Pe2AvyqxXUw=OgP8O^*q4LCjBQbRUOH&T^n|X`gqU z!ew^|(_gP*p?6aDk%_GQt?WS6-bE8Uq?B3g*S7OY4>=?S7k>e?S z-bcI+vxw#%*4g}*P{{v{MZinZ)QS)5C=g2^40HGEAuvlmEVB3O7YIui9I|)oGN?;B zyt23JRhUatg0eU3FnCMOP-)wZD?;JFNa&hPVUy(`t+Xu0F{CpQFdKW~Yu8xtx=o~a z!mGzLstJ04ky$3d}ycdBzo1-g^rWl{G8df zdvWlm%*(dI{+!)NVrDQ!b;24XAvu$g%wBZ+>t#k<3g4}mz5Zh@$2f^=B@R{509!i> zfb>?J1K4(-QS&BM9g3}D%pyZKiw#8@`DVgJZ$-gzSD|`;MZ#5ugWPe=FLyHP0tENPtFDfdSffv~#Xp|vg=U#6JZOM)0wWr0@<7JCOn>)%2X zaS_?6z#Mb3>#@rTeu?ipE{6w9c={vvM)G0t_8Tb)maPT zk>bL3S;pp3#kK+EzdVZUy*#2?)Hkoa%hb5QUCs}BVD3TtM~Q8m%Y686&ijGv@?D#dhfi^`+nFv zq};Db?XHBJR~cOTjJ4c|tZQx9=Vm$Ja-m2cfQq4*fG_3InwZk@LoGtf&gJTwY61$2 zJ3|@rq>nH-(UiE{|EkzYd070mhnWWy=1;BJVglcTLjUG9Z);UU;zlXf)|V7-wrZm% z839q4D2qhJUirrfwbj-XJ8TL|K4v*w6*$`dnUQx`opo5P+>Ouu(l!odgzT#kqcouy z3yJUQfMZ;_MSh8&^IA{768xJg^)n5}_=eiT>htx9Yaze2@lhAZTI3s+PHT}G$vl)` z@l0_XfzlK;G?V=?*5~@50L_UuCa`S*eQnVdU@1& z@!Ux0-8#1Lv5}x23U5VBj;Aw0FI7@8iAA zQnd__|13|hhLMcr^*2Y~2TrNphM!iyRDQ2QDHT9}K4Jv-kNz*JGBg1FJY5lk@@CB0 zCuRRJK7jn=FU+tzU)szkd@|cH%KsuNL&umyPqa&+D;KbQ9~L@yJ3kiEd074tmBC_E z9T?^}m4?<;a|VPLP`^Z=`8WpAr2b)*OJYN8iA?e|ra!=6Cwt^y9Ar-NVa&Y|x0j{w zxpVnvo=I>b%-QpU@E8=o;1$f19CwbZKq1j*$55)YAbwnGjQx#E2)g;7w$>e+023$c z#>-OXvNJ(a+3^??Q$3Yv6B-^8P2x+@uSXhBzc(nDC$%(vH$QWq1_a7Gxc zzG46Y{r9(B=II4;?&#QW6Y^>cD+`Xq0vPZMxrX;my_Lw4Lx%~bK{xW6#v%5!fmsvi zD>-1^81KG#LWy(8+F#S68xbu7YJJ1AO%Q=mj1_eg+Cp2z8d0oW4xpH@-=%Q+sCyDvy%;OY?$ZK}cZvT0Br1Og zfca?{*S(VNh_3(4oivj9=lC67xa$A4LZqC@T+jjevQR)zo9RPKrzv>=isZq+B(pCe{O!*uJ9s{(rJ&FY^{)P5df2GTG zKSFBS$^MJYeXVvL)*`#{OmzVu2Ev^vG*&94AC@<|VS!U14wO#= z)L$HFQ5a4!39%6ruKg7LXKVW2rWphQfmuh#6yrf@!mZYLQiiOV`N6Ls&IbKFV!?UP zo_8^uiIjbO-ELlLMBRf1R){mwiO1ro^sq0OdtimTRFmC8jNexu(Ee+Ig4(d^%(+rB zVLA#plH&Zt9ad7McbPhb_!za6>c(xQIcjTVC^mQj=Za&J(pZzO2kt?tPuAV zWmsw4$pqL~%Q_-eI1cZ9hpbAvR*6!saOQU#fxY<+y!s0}Bt1J9x0FhLotJuUntXE2 zGef+$o4lXyn9T(zVwPnm3@)2w2zk#Dq+-DDJNF)?JN5vqGOYNeWychk5;~s@KzS?;d5MbIAG9(9^s?p-9%E@WcY5Vyo+KrWXexT?@d;n= ztfctN=8IP!qzAkAe&8`^@RX+$Y&)C}Eibgp2Xq@pjOGR+!5TKjZ9%0ENS8nea_>ti z?n@~hD;R3*vphNYYn~78eeD~4H$;0j$9al0wKVQpJUH`Aw^C1S%?vG0QHlKEO1VUq z6Lwo+1>maoql&;%AU^nMK~*B3+;51bEs|t+HVW#v@(3=TpjY3&$10)P8*(G)t$Y0E z#r94Q)IC5=}NRPw#2 z4Pn{w+#M&yLE3c(Vb94ROF|9Qms_%;r@by`+h98}GpBup>V$2>a4V#tYn=R9iUwGg z8EO(sm3NtHCY_XZY<+A+*yg{$kAcQoKnaJxWof+F=>>bE5b!cEjj+R|o0p`@%bT3u z!@C@9eu=)R4znHjOmk>uNuR6Z7^7VN(w~;8SLP3xpm?3FqHdreI5N-i%a9dk9_49c z)ASmk`t88o(!5%(eexNvlDP)dqeG(X*v!dETsNyYo+^9PfS@9zw;nrC)2xFHh@F-; zLJgUNf%;C4=3=Z=F^(ffhoHykw%~gIQ~5Y{2%15Q%SB8hGE)={eaezQ9UU9bC^1qup=1qRS{2$ zv-FyY=HB%jsWL7E!t#5Y={Fag$I;w)!n@m)6 z!#u^IMViOZ5Nyc#=c(ndRXKt_@vC0o0t>NvS=AJMPNcljsj9DVmqVP|m90mqU6rU` zK}7rKJjvn_4FHvAT+zY)f;?|r$}-KSo==B))CD&vwv-XqQ?Yv#2#b4~xo}m0pY6mr z?sBN!fM?5&FhtEVNv;>yt5Obf`!7V%n*O&AtWK%EsI@2zr)3zY0=+su*-l`W#gBzd zOvz%AhV`1#W>8S3Wkbo-oZ&gZKf5=19D5GL(R@E$*J)bFS zDuqg^boC;<+Iy#pb;6j5pc!e)^i>$rO|!tni$hsmrrKJK`WnQAPYQY1y6X{(?SBAD zFM)i*4LNt$$}aEfE?cjj8>{=%qNBU}uX5XG$Fi0%wap^+O?H^Pi=@ zx6O9c${>B3==N0`vs&E!N4fn_$ucaT1tRAJ3VO6enCk4)(O0Z6QGMi7NpY5_$KrtB zgt_6Ln+<;#D^66>+aqbumTbTIx5PICn+%d)O3J>IaXkGGvpy0sb9~3nJBOepB>@Xp z71OhgI`(g-tZR`YnP8JR@sSo|TA7pHL|SoH)T<1C3PxS0?GDwqKply`oy1nfJ_GG9 ztGV{u99-t81ZkLfAl}X3q5sB=~%{?b0pJAwVwjS|1w zGZejfFUSs@TfNmX9b);Zy^FZaP8Cujj{T~N%JG#ro382qERv|cgG35&xL{R(q>qO5 zo;f+a#z@WziboYhQeyC^)s)IMT{b3g*dmm2r0y}niD%al$s~7q7bc+^%ygE*x}Rtc zqBOvwUt37%x$6?Li|W6#vVueGczdx!G^F-jFGPoC(@ZQ|vqUyewuc(ePW91&sNbheN#I&o{M5V3ds+rBXti>g%*mt z3>8ep>G=$%Az=T{+Ao^P1Xf1-OlKXsGL9J^Obd#YPKP z#SHxr_%kS>a5>mdcn;2W&b3XVtH5@ySLYFan4tM}jsJP78=dacLUWR2ffQ%$LAc8E zu~ol!*6+U87S>ESi0w!vsOP4@)4%U=ovAI0Gy92R)<;n-j5|Rmk^AL+8t9qx$=L0Z z&_tqbS=Hr|`D0$II{nH6SU{Y2}(Y@`Z%6?j<`U6b}CfaQq*nV;uH7V|3v- z5wHJg24DUOuE!Ch!Y+YjOj@(T^rEccb@Pv)dETdO z|C4hJ9J>q`*J+}W7 z$o$qcJ`>0Z!@~4p@HOl)JNxA13z(m!X?DlgbQz(fvvIJIUELX8({#Tmh>XH z^Q`B(>l`J}V{;8u1woYsHTzbW+kN`S6jQmEex3ebti5?WThG7vPqkIm(o*}{>ZXdK zwG-9WuBh6#s;z3Lb`iQ!irQ;URZ+FXz7tD{J(d!C>_HHOghX!Xnqh`=to4^?_!@_oiPXYTw@&tZIJ`7TG@GlKDDLPYCOX6`?@QnbvYY1Dasu}7eRqbe!b${L2dw)qEppA99z zuzQ-{rHDFWmsyCFjmA{#W?ozKm$F;)|3V3~5`>&P#M)=RCGYUM2LEdNYWhm^h)=R_ zz~S{x-pi7)6HP!mv1OnH1*cOeuF257yYuPh`c14+c%H-A-U!j2;GDSVc^ESW9p_MU z@XPn|>*Q+Jm%+$F=H{zUM}PP>9M-m<%5Z%(rP+GkaYtNDnK9~X$li~_i?(j(MPF%V z-V$X)sP{HXzVdtuCGvC!=EQt(^rM+I;9|+Ki~$&V1micbKIBO+bqZ7q2MF+ zblo4dn7A+p{k=tO>56|zLps{5i_S=Nbr_#{e30kh0xhYBmgXD~CEd{Y>YrC|&&S#e zlJ&;lj3q`%ZxC$fT8{5z5XNi{n)dFnGA@YfLSuL6onj;X?w-_s-K^XALUM6jBygvy1dX3WoOiND7UuA@hkb8V`x#K zndL2u`Vz%TEgL_1$m!rOTgnMHW-l<_m1N)H=Veq(p}xJ|jJ4E<9+ge;oqEyOkr-#< zL_d9~{Tbt3*-QK+_$7F^BWkjKPgv&8LHWL%%pJVuc816E?X3y_y*H5IJhf2DUK-SX)4_QA*Pgq5Am!e0n4 zcf^#=UsNj9(z3eT!1`{tLPM`#OU~4E6RYuZr*rjk=-%S=aAYjB#75+IKM(Ouv*gu^ z-p516H>Y|ZTco`bVFePBKizR3n>>>G5{qT$;1Yo3N&b0raY@@~a^e&h{KbtRIzKho zS{t0xKsPeWsqDb{N>2A6ZH#K#K_?jwg6;~?GI*(tAk2FLd|(x_=5-hVO=lx{#;db(Xe(X+_QlO%_rRH z5*xU4?v)?yVE6BHe_@Y&)_hd|Us}sXQI^wK#A=qp&{}l$)*Ym=#`g8zB9vp|jnB`~ zZKMI+eX#wDIL8r=b^TY1a1|KH4+CD9h)N?KSF$Zd7JxkT@;yzq%;15*V zWFAtrO0rKOI!qhND|63{g1p`6a0V78(??e_MEwmyUf6hzWhuteGKfl*e z*WjiV688D>$k&mix)++cI&Xhfa2}xtOqYP41vRJzlVnu7Dbj7=jzz&kD?GG_>j}h}|(m+YMZ^fee!5>!#%EZXhuxhfMh!DTeb z8&%zsBV_nMy@SrxGHPNa>dbyMQ?aYQ88knM#3vp zSKDSWUy(x}r;Kczi7fGv^eMBMS_afz;lzX{WkWCHXJ6AqoLm*Yxr}m6Ls^K}!}TSr z%1w>Q8qY3+UK#84RO>PUz}ROj;a9<@@KBqfUizccxE_TZlYl_o5~Of`?CmZs4eZsG zXP@#9gdL_o<@+8(G%JyCXVPS{Ee7tJHD3>O@2(J>EGoxu<`nnDVuW*bvpLNm_BL{+C7Rq zFQEh@WDJm6rqiPQH1KymgeZJ!8J7Y>)-PAnO3m*z0ptK7%~|s||OqJKi&W>mg^!V4p6{@m-KmgHpuTI-qhL&}#q?Y)8rxUiqAi~DZz!M{0i=KSu< zw(ymO;04=3ALf{drIZNV*N8x!{TF5d4`$?#0>-A5>*~zg1?Z{8XV+zpE4eR7WmDg( zO^0@)v*Vu(^ga6%(m?3`2~li*+nBj8=24F zE!VhR+t|UA3)3%j(1fQ<2HkU?bUu+E`@VVwd=u5}vd+4=HdFcNHP93JyDhm+4(k!W z)5djftvHtHuReIy=u;Jg z$#Uh>_xXrkSiK;Bbq>{U9$jomd4uV?E_cdyc_G;)eq&|DpG2; zJFu+=t6hRI#eEBmPzNW}b8qv5TXUjqom5K~ZVZi3uL`@?K?G$2Hh>)AGqN891z#r^ zrk@clO6vG5CR&xG*)+qE&|&#g*rslR)21Wz-bZ#A)8NBSdJD`D0zTV0of-S1#wgO4 z216Z>7uPy9z<)_BV&ocAZI2k4{NrGYZaJLqN-af{e@-rp=Yk%gf=}k*4JaESGlg)> zRG2Hn&0>|QkL3S9cuBYa%uB*t|6h2?SMCs<<3A_7WX0OTCHX%mykvClzj#TZi~kib z2@amIvHqYb!ytqr8aLw>N}F+oNJ33Z1pDW%0F;Ts8=p5eM=?sTns=J@MF;(6%XqYo zoE{xB>D~G+xIZr&s&`z9V|Qo1m|Dx+5R2&?(rRo(%>MqAHKB{keq$$o`4enAyY}ha z)Ay=MqW4AKgZ%VFc;Q&J&}DKSS@Veh-1Q@CnJcyzEI9CM>YWH=&zap|7%;x5f`^{C0P_1Zs4-dlJBUR%3XMtAdE4uFVXVh_-Y%Km65%za;+A=m6)z50ghJCCZmMv@=Px2@}bO+TzZ7WC|}>@#%pV4QLI_wPzd*cP#>as^cUS_tiD~ zKKat!tOpfCCx-pZT}*xrz3cMJ>qgUwidIIqf1fN%pp2iCF3VS+3vBqMudx}Ct>Jq{ zOpYnD8#b0)fIUxXp+Tp-FT6w>s^pn&&f;~|UE-QSmkviS~HycUU>O~ZCv>6g1HoluEITlEnYk3$_1x8KP-jV%}PkOECgW23W%il?px2~ zoB&T5ku#Zm%xkk|-nsnB&BI!b-P!$L3QpcjFW>Si4E{DN9?^K1*x;&m@@ z_)sOf+0kuRZp7#L|QtruZew& ze!`Xm(uS?E@8u_PVG@MRL}yIli7)(rb#otlR@~FpOGaax2-aeaCQqVjw+nY_oG!Fg z%Ae-*gGE)S={DSFjCy1CUDL#X^>kObn%>X)^DSGdWI?It-uF^23I@6TOb8wj)Uv;C~CtT$p_ItF!5vBgGKQa8fR1k%2L8uM~Bo%5Wn>JGB7f)AE)`jU*a zNN~=rr&b8BrMn<6!D`P;)5kF{Q-U6!xvIF&m-q9u=;fEYYQk&gQ90Ti+?RrSU$F@6 zU+J_KYSm_Mcq6g=d9+4lc@aUQdjd&n#}Ii5?7-_17|zD-}`tE&zAc20_4+;dh-0o z$#d;pO!a>ZRqiIJCcq?H&ow+=zGYDP{XtZ?rMMOgNMA|#LYeA%#DH3X!G(K%G;<=N zUGJH`VeQya*PrBadh#pJOdGf{sEkVdm>5`leP!t(f&1_&djs9Xr7g}kLNT@>LvJ>J zc;sBbk%Pgo$!-)v{ug}ef1`o}=Nqsricjoc15f{cG_&`9EBK@}~T(4@dik`_PFf*q+dpk7rwV-!EBA z-t8NmxfDU0d7ZCKViR;%k1qXesCS}Z%|_n|kNN1^>sy|7ww)p;X`!9cP?poBd_B95 zMcXGj{r6~jrFvIBnQzWd=o+ib_|hmEL_Z2st20w|TZ;|&o)Rhc7&rOtQ;y4Wp!q2; z%PWn}5%&*9?;7OViXw@WRcMJs`W2?Kh*`(1Y!X(`_ae$7=4WNQMv zNaNYR879k8IQx~|&UE9ZY8lThuMtjU)xF=|GPLe8dwef+8}G1b&zHKO)N;HV z%xXKK>p3BL@ze#Z|6MTar8CzxH$R#syfoFdXW)J8U=@&mFN5W!`Ha1A=DA_k$4`~P z-DTsw-m*KI8cg9X79tg=|t{U8wQm{nF@4#ix%s4eJaNlvACP zvmLx|-=h8AgGu3xB=b5zE z^iM`1=wP=LpYuyDKAuIFDDtm-Sh9ISp3^{&72mzUw~omZ$ybsKUKtni)n?pPKOru) zj5D2_jVG-NO;!TajY3T}Rs!=KlNXXrZz0Euy_;AVS0~erQcPa1C|Xr&;kB%s7J=r-ftCOaTXrMv-M)1gCrEo2x(YLVX5u{JYC(KdmYd zi{kg}BbTyTU6_*1X!0Z8eMlM?o3$G*^t@-vM5&WA9q)-a)AwO!LTt&dJou_yP2Z8} z9fR)Jtpw$i$>sHOt8v6$v1({YNfooxgD%@OX_H^3bBKx|)$eYphwF0XJAxu!-x|$g z$969?@63wf?GE0y+fC(#7Ka$@2*z?$$D zd5}DL%crkT5X++T>_4aO080;g-!fKc(FFEX&6}67s93j&zQ5pS7f}4o@|^AF*-+pK{1X>f%Xh!4 zHoTO{EMT@~OSsjzs|aqeH@sj{pRX;@FXhhQ82&&gx~sM9I4;hITO^8kv)sVMuyH{s z*!A4RMAXp)KhGEGZKIHYxQlT-L~rF*#93^NSp@TLc|Bd%-ER7$iRWkSj8{8CTpd4@ zr@mki%)r=gR+&-}H?ug&`YF_1u6z0XSq_j8`B>)@N*AOo(|v!=wq5}G)1EuDd?s!n zT{8Bp*#;ut)9iOX_g2}uJ$p`tnu4o)#*^jJa;A(|PVpMAe&p(rD!X6b@3m#UB${wH z#%sd0Th~Wf`oX9o-I$azIQ{1n<^EpXsZ68j{mdI7Q>V?NUx#Po)++-=qd0;ZFs}@w zMPCaC{m#?%kuKdVvyaoQmoAYie;%g`NS6kc>%$O*!%FjR{&H`}#}e+Jmz%=N8BBE3zYb6vMFr_Gl8I?q1KoEX znEnZQ3^kwUR@!iE_@rZZX?y*4k{amz&q1D;*bM9|=8=(_{Znt5E~t)h=@faF?N*;X zxdqO{p66__lK{b9yv9Mf$ML+}1PjtC(PXguA@`LE%NT;CPKG1NBEWge6`7mMN_qF( zM?iw0!9X_nrJ=j^h8;b+i4wVzV7OROP!P`m~wi`+fk5a}%3;zh< z9zhfd)Cry^Jyw7=cvrfgi}1LK1pHxLQrzv!K4$_Hb__8OGD~0~8?qtd*_k}5vL9+;5HI8vE_0$=ywUQhQ|VPBXUq)tcQRp#Cqf1ZSom;)D3O&2XqXPHfxFd2T`}A^z!=5__ZxIb4#9~z!F^- z^l5qM18(HSwJ9@*aS-%5dHTA1glIr%9sSx&^|cbl*E2#xfi9Zn9Dn@a8kpiIFt!7l ztRmyQYTJwC$MSlj+UR#9xXWw2PxSYwVS~C?)-S&NM5dD$kLNwVNh^PAILXgZ10(d^ z?xB(j=2`{IcH%nkV|5~pQfRr7N}QD|^U%_Gt;{$2HHqjg+MuSF!D0MGgVQc2d4?x> zc2}n9_7C4!4OYGka%~X9fN7Pa2XNuM{)qj$`!=yey8W+J{o_t_`vFxmDL97p@-=CT zW|4eE&h(Fid3Lz?x04iKF(kkR;-ZFt9gH8ZCdFD|OXPUI+o7bDPcmvV8HXfXEVOZ- z)&i_6;Al|zHXA0zU>A?Co&;1>P**DC0JdL81sH#$?OefO|fb#i((!|?7a zNP!OxUo^pAo_?r2Qeyken(S{*B;83Ect`N87t@*Ff|?1}|1a{4kxDDefAjc`Tu2WM z%_+lwE5pXO|0QLZZPQ&+xe{ipH6e9N>)U(LaF*LALhH=)?TVHLLsDyuUAKlkTe)^5 zYw+=EGxK_*#c>72(Y1p>K_0OGmvuOFVV&nRMS{cZUhu&bOr~e`!t8S3!IZwB>7zV| zSsZlh0EXF_LBX>-d~5*oseVYnHrA#uboFElCCCOy55?My_sN`;T2KQjvSX`Ipjue4 z5(%{jKggy&;LSMNN0wL-;|4-V7=mw{4InSgI)#NIOQ;S?I<7DXf59M`gz@QBj-v+7 zW=&cHyOb(83}S7F3L;@79OCqVkd|PmI&eH_hQUHR@SnF11hg}VgH{+Us79L*urFZY z0y?OVr3T&|vjz^I21e)snJMlth^5jLR$z3-C4_<)u56~3@1JbrZQdr55S7Nk6jZHa zFvX=71>w`)#$W}eZ|VnA$h9_$T&2`qCHj+k3HY?Y3IN%uOc_*g11TI=p`=S-mSa{~E;6M^_cpGm>?J~D_0mRftxA8*7*nOyp z0!By?HU@{+WD9uJpa99q(gPASp91@_MkbKNN0pY8tRb@p04kIZ>RU4k93ENM2X|84 zvIVw|W=520&f5}S{&{SmchbF{{HuF~8hHPgP5a->{L9wPx=r7s`7r$X%jNs3YCGq+ z!iCy8}=Ef0@W@^IPW&$GN-d~0o5Ql zHRtuDJJ5x@BMX{#qr{@fPkBh>vJayz7e~PpZ$F$L90*-dc5SQjKwLp>`JZuG<-A%t zt>2a=H~1jMqjPI-zt`l!6k2On1^KS!QPq>dTjAdws(a$U_XGcRsgn+s z)u!8_a_Nb1_=&mFU=R*xd3NfC{wbD=`l?#rPM;XC8ChY;6s@AK1iIjBCdb zQd$eC*x?a#6yTl2-K+*K1r$S8>&FQJ6eQ7_BsE;!K*f$u2ch6|2Y`Y_q(23+LTHXt zgaLp-#jM>aW)e&-QOX)e&3t_%qzFyGLv7)xIC+9 zMh4~s-pUxC>~*LmGLe$nF!&jIPd>0p{~0=Tn&mNxB!S6lNGFHoI?_zV%8pdYRP5q&8hq$xLwWp%$MZSH6LioH^oMLyS;9IMGo| zbKyIsF9gOnkhmVAZ3(sABXYUc5n>DaGCf6+DVaH@j^zacR$=5}> zqj$Ey`9oG|C~0*29D`?@$WrucNZxPoGX92)!0qL2edoczA#7awd}TI}E~J3S}5ZDe)+&dy=5W!>E&o zJM<_4N5U54?Mb{W~jlfc~$s&%#SZu64ah)xPQs;$8R%g^q9f;rURt1 z$2KB585vN+5 zC?;~hE_9H?n~;lyofK+)V@>Rs$1O;yy2-_0<hTdu`alm5 ztxvk}MNaZ12=koUeI*S=b>-N18N30iWt}>;!b8|xwvpUBgsm+jC6-ko)feJ@s@aHU z%ZLZ_$}RFAm1%nIlq5DMdw^o9-jP4% zN%3Ew?%-H0&&7e720Ebhq4{7T-co<)!Y`3M?8-tMsBnr3HLI z!r)JXck2atTw69Vnfr_rPNsrFn|^072AC@5M1oT--_!-7YIYj?-uC8XnsqPQcmL;7 zcqu^V`>J`q#SIh-4z~I{Bo^8Y9`dsx0$K{#0E|b!lNJHHZ)TM+SBZtF)U4BqZ59cq zEqHiA9;F<^6G33KvI>H13T+)kf8Dz!dQ7uC+5_BfHi-uz38ajQ@}}th&S?$|X>R@O z{-{Xwew<;A3Gp5?)4DIJYK_Ia*f@du_rAf@K$*FfMS?)`EXwa* zNXSI&5YGfe2~nT{*YDb%cEb2<)-H)7UI|RZ?`mE|yrIm5a-f3a_b?=P>jH3i4bFV^ zkhvl7aW*y;kKC-qKUj-g2Qa2RkUYS|Z)ZQRK#z|{cRS8Rnr=UF3GhG~qLCf4lJ%^`k-enylI1Z034XI0vA&RBKz{`;ExXxaTOiSxO zJGHx5b@ChRM_yY93&{01(fZL+GWxQgOG)uWvry^$$-!_Sf7uoWs-zU9tWO-%@YK|| zyX+L(yB^s=+HTgq))vXgT=9ThbJ!y(iY@hKzsP9Yu)Zx7)W3%$tofdYw&lA{i%2(; z(0*0h8H#BiQI|Sk5f@urWUK&j*HJqjix(8VDZuz_1WglDYdSjU#v;O0tZ>)HJd@2e4fkd@Z@7g*pl`pP4Rf!Zo;*O}7cnSzzxvK=nqveEO0L92-2xU_e6 zaI5^viktu@Ytn{m#9q_4Wac5nZB~@yQ8^$VDge#z8=cIxrgi=1%-rdYdB80Pt1RTm zHbEdmYb_P7OoRNW?y;#`jRZ`Bv_rpl9(tGp#~Zyg4D~xIpypcF6OAtD$>`l)4^a>~ zGDNPc?;qa}0Zlk=2~+_?#F{%Ggm-Kz(IgZpiS+1vN==!B_MTV+L#o`#c@X^byj3May`+jTa*+9Ant@qU9Gf^#zxnAkDhCh1Eq z=NzC8qLPRV9O=OkR4x_I2hz5VebVqF^po5kvfKbyUTRi~QmdCu!9p%GKLxFXD#$Bo zDW>A4(bOE#%{Mf=4ZjltpFW)9L99D9QXf%c87vpbPbrJ0z1kb8KEk)wt+fFs%g{xP zBsfEQ3;d@NUb}!3uIHa|x6*4Qd(ZJ%^YjxdUq^&~yjHs%Ea16Xzj!1m=YPV6^NxuR ziSa^%MEy?g1{I+ofLwfqZt$E*DnBKTtYuaYGbfhxZd8ANaijM0#jTMau5xi7npiN& zTiG>fykjt=kRH+~NMx$r|HY4J37Dncdm>H&sN5oHl zG|)j||4_CMoFBRl%!YiZtq|#mpZoBXDhJ7(pKS#v2dj2v3%xlDTmOm**#5FKR^k(9 z+5>lieKsr>Fy6{I79Ybj`f4e8Y+O*Mz;_g*#~O8R0l5`;o#H)t=HQ=Jp%+g)*-P|`k9oVT*Ef_=jZT^>NF>!1 z_WHF`qw^(}b3^a4Q&Zbs+~d4(_T4|DdC)QG0K0-gLLniG^2A@5MW4K48izw-8(^@0 z6h)N^0jQLf(E`1{vn4~0_EykHu@*3ypnxlraX7tCR#*E&uF6i%);oZok{+g(5^N3( zNPfxriKs2~QI^&7z2fE5R@}|jd3~i$TTQ{rp06=QLD)@!6&JK`eU(Qre>!vEnO{^` zpu<5d847|>iNR~4Bb3A4C1P{H$A+KcjF07;`PZc$Q?qaGMK;xQI22iHaG(wXH@-Ow ziR-EJ?FSvlAb$Y}^j~xC(Czc`WFK9J;XNVUB&3uXR7i;^Np8GgkUf1a1A!h&%m0M? zY+>o6^n^PLFIxqEqWnXhqq}hZ`S#&A#;J;4KGbu8UrwUXT`-DD>SrrHh8R1J1M85i zrtp61hR07sWB*OwAY<^b*y20!A?2T;;fluIehn(hy;ng*HlwT)clXFXG#7xA;Hc}Q zt|^|lv)^9Mqb5_pe2c9$6G~Jy3&E3J4E6Lweh5mU6vCs1g)3B%QLZI$Aj3%+ZY_ zAV7OM$17X|GNo6Sq&D?z48TKBTHgcQh6fj#H=RFi-GZc)oyYvfC9gZ0e5l(B#CPGY z-U5r$9dt$5Hw<}89`)ED9+4sdT*c30`P9Pw^#;Cwb`yD7V(KhpV9%pXsqVdqtSJ6I zxN|zY-EQS~o+;AaD?Iq65p`pd4La`iHLksIuyn0%(G7RCsH>*P>CWJE8|M6)&(NozPrc4)mR8_TqI*BdJx1#Vev>k!$HQt3ak++ z@vquT&J8-y^$Ht1NZs-;O~S-GsXL{`D&e%KrPlw_&GWxoB9AiP`U<*yHM;vto7HEU zkBZ@+h<^~~&TqC_dY5<>^U~Bm_yXk2qChd+fBfbw=^%~FB~@yGN(w)GTD)W6;D-`m zh_j;q3G1qDAi$jLG+?B@Yz)`7b8{9kDtJ3}zyD#rRQCG5CIm105%H>W&wsDizJJ6Q z<=@x6U-!<4SChDGg7HdvwJ%@Ic;v*tV1xamybvVeH?*9lvv~lHM?`Ma!(1C zRr%g1ZR~(AP)aFVkOxS%*q651BG*p=QdUM3o4)N$9Pj=wz-7(~fUQET093_){Px^y zAS_l?`QrlgP00G8NUHM#YSD~45SG1AGzfKSFdr}rQ9O`%8rWGJ@g9ahHr4l=8A-IucNd8H@)%g$}GVa)u9nILfBwI#yosT`>tPu`li|E`^QQ2l4(> zdG4&uSb|JsTh((D`cMJ_w8+I%W{qV2{xSKtlo8}(Hm4$jU5k@qG-E$OQlz3TYAlx{G1vU-6i1%B><@~i@$XxH^Zt?C;=aP8H&4cmDI0CZ zePs=`ix7Ga%H0(y#byT*aKTLmN9}jx?pkMgsqu-Mc9Q*M8^p#7omdx8QsAxby2Vp^ z`bUt>xC^1~HliMn zW7;d)uNGN~VpMGF1%yPv;!8&UvZ{m6UfCdfam&LYjZ#uqncY&nEoF@4KD%w8ZwGe{ zXL)9&wfH&Ateo;6TDKqV6VD7HtKf%Rx>G2X`^<=#gf+DGz_y2CLG_ z5mYi2hD5)F5P|OPB@hXrXT08|bW~?PX@ei^0{F|t1_1D z*8$vn&P*o^JLRDed+DeQIw(=;R3YbSY@MuL9jcsWVRKTx?x|j_A(4*@$|m*Cv9Jl! zidmb&8Pp}zwpc9j<62M?tqgt+5IxH)F4nR%<4JKp>HHLFvF0y>8ekRE^p>=-3s<=H zHa+uVVP{^lx42&qW#0i8w=f|*)`F6yICqD?UUbu5Vq7a)@-ylhq*_@wI#)eyV4hp4 zGvRwkXcVd<2E<~Miq=oXuf9h2zWIHy$;;@)7*i=vp9@DmQ5w1!bYuJj+UrR8k=^c? z8Ey5G&Xa|fS6WqRZ{Ra^ifE3A?}gjU+Lp}ZSH=3Hmv80G+_9d>seoI=@6fXpo_5jkmBrVxe2#WUpc*d#OMDr z6S|FPIK?-;zZ5LG_|9X$nRmvJ&mNU*)0d4KM7`^!oI<7PJ+a5v0jEJAJiVD{_drL{kHD@3v~$9MPZ z`~9sZ0hxa^@?qmQETx$ zP$tc~-zKb_eOy72$i5%=wpP%CJ?{F`Z3!MsRiu(UI!}Mwc!`ZR+ZURvScMQ&yjgS% z-;mNS3S@m$SjQ>OadbZ=m)9F1?0j+>{Em3YV662Qt3HGJr$bp1Ua{)+=VJ2wl{@N# zSCSVP71e$n;PEkoZN^lncZFoL`{bOf2QU#cQGf_3cdPNps5y0O(d*@A$=3JL=)P_5 ztQQWD%?$^~a)2#vP5S&j`k$^r2~eMhb2b%CMVw{Z%p(CZ<>Q;Rxp%gWz%kn;&q{CL zxkQ@l;EzgKi;v;cHW-H5DQKvsCB=Y%3N3em9G?v=L<-r|0*u635iPrl{K=Tq(6t4q>(XrAM(&@*hf34ZAf_S=ld4P{g}ZeR@lv zqULy7tJIb=mU3kM@I%yg=zv98u8SUUj%;$|ys2)-7TP7ebdK-0sA4vge}0n~B~Hq# zADqm*lnuG%_se(3|GIH`La{q{;;`Ss-t7wSiiUQeuJl1HPO`8$(on&qF1i1W%rULCDHL5_VBNWB#;x8V@=KFOxX-ksbQYBtNe{@ zE@g`-I5H;92QGH8(Kp(i7%`RUlvqQ4{{dN5dfwzcuhNcfz>1v9p%k|+Nx$zJr#k8& zZ47oG5bGneON#cMQEt~xtatP+%rO{K(GI-?^XI%d*!TBm$pScAi2e5x2TA(51d53J zt{DNj3OQ6gNNtEa1%2-IzSQs-f&%CosZ8jkb;XPh+##gI;wDn*7~%-TT7-NAh|C8> zpR%BBeT*=d`r69cWZN%rusQ0(pj1(1n!8NrYbu8)*#r+C^mkl&L%`KNf%_wm&=!bh zl)^jWhjQSBE9jqA^Phv-TM?l{K7FKAYx9E;Qw+#Xz*1J$p|3r%L5)*96)upuzQ?3P zG`c*80C8*jNTK-C|54uWVA>Qa{RS?a?qf$C)IO0f-qf4U(Nj%}8rtN%5xBc)0zMW}CmrtD-9FebqWtZ!*-!KhZm^hC@j%7NvzEt9_fIja?X zaWC~)PoYKfmP*U!l1k_sb)Lav`G%t7Uzp&PS>Nzp>V^!hn?y9*TAG0sTHho~z!<|L zNj5BvT?w*6<^@4Vd<3#6@jcThsi96QKMZ^P_OF@Q0RNHf{aN{`ROP9_sFPOD*=3Hr z{`x!Czxf0zIvx1$CGL0Wf2~vh2Z@sl&eqX7 zEZa2Qx`}w+t51}oJf9PS?W2G4IRT|RS^@&&N3KkQ{0_^MTR5et@k7vufv+CxC$}wy zZI#;+u$vkC%^@FW5=@by+%f3;{jXs4U9828ylH)k>T{l8?_EU)X+LC5$^(4^| zZ!rGuI4HP#S8>$7e_eXrKF$3|VGTCb3*=Q8w9uuvk%NPY54NsTTFIxKhw}qguK-Sp zUDx-n4Oq9_P}(?#{oPX8hN)ASfj2k@@uTr1tYB)HEtqGZ#f3!tV^d8CtOga!{Ymd} z9SDjU0l;oTaJ)MDSli`lamGI0<#wOkhWhc6L?b+DZY?MUb+^O-eskM(z}a{e9!05` zAq&}zEAC|y+NJ6Tv)!%}t>v69rUuL1Ec36t*)%QRmxlf18+YJ1o=$3b_JJrr-mD|y zW`fw~`eI_8%U|$2=mt?UwRt;F5}Lc6>gth8MVj-}ID%SS?h{O!W)PxlLdEzJ29UA% zW~!YvP_4p4vqtXqmJKVj5-UMIS%GoAA*D4t5VX25R6k!)Tw+|} z#5d}2HtHkvnRq*=%wZf2&j|+P<3>Co)4b#Mz5+*(Yxvs-7bamnu>F1uIVZn^blPj&;j zl2E)@cc{g+DkO|MKSd?20bx*aUb%)thR2nJMjh@mvtA zoB>ILw2D9(FJEOqn=`MX+zuScG7n}0o4>+sjX#MVkXf#QBtd*vFBXvA>A?J(gm1@Z zM2Y4<*7?cDbOQH+h;4!X&@PoXpas!E9Z*kOB8o0>7146>FmOS%S0iw*8^sscfw=Ej z(~7vdk=(806icX<|cklZxEAA^&^MviGOw?Y2zA9J8@ z-yb-lEozo@-HCy>1);CsADp2~XT1FQ0_=c4mIv9*znlj9e7WU?-v7J=dJjtf0-S*G znHA0XLjinUUuF}?*`0Sib&G7Q-k6mQo#Y3uyT5wM29|{Xm+1bj4BUV$Y+8pJ;Y6sx zJ7dB-W8gbu{E_(D9*o>nN&)(SxECy$qw;j5WH*uk%c0kAJMe^RBB^ z<<3zb;7UB?nq4|HbxZ(cx|{28#~b@5WH7{r64)8(cJ|FQ&A-mrqiW(wxFK+!x9cVD z)Uff$SA+j3f7d71|7q|?SF6Ey;FxRYnIQV-ryDX?At%f0{My>jm0uoV93<@hT2nGv<{Ll109cuy2SD&&k3j)^yd3Z!rwAK{gG^X ztSJu6sq5kaE*}GzX@Q0}hGT18_Ol>mPy;Q{@!D|gc-`XG@u>zB^HR}rXU+N4kOf>C z53rvEE`5Az0M3KdK{t~PfdG-7rmhpcz%$@0<%YW&%c0LNYfo+;4+CogBm50>8oDO@ z9hc8u0FPx_Ps`&#`=2j7XF|&>H%&nBJ1dbfea9T(z?_zv2|@J#Oz}-8q+>`d`nIzq z1-LpG{;H0u;s0#>pB3QhSQJO?WrKj7mfwg)qE-Y~1{Yv%ZziD>|2i8?&7ZOP zm~P=VS&j)Sx}P#hjv9{;{7sdlMU6!m9TyuL8^@FtsP^>nG%xSqVGcAGrQ>=a^4=GK zSj%$O#`5mxo#8O=n^B_RRD6*&z%1M6!))rztJfQE&gk&Qa2?MebiD){Hy2o=h z<;T~so{I@T*XIjKM~JkXRdL37XU@h>I~81<4X<3WO}YcNbK{m8M$tP&tWPWNn2HVK zplf)8n_cyCm)E8;4J7Q?>7OMruhM4G(HZA#m+4Mryp<{WZqm(ECC3f<@rGwbRN&ZX z?JBsM6Xfj~Urvu45qOs(OOAWn|B;z^o|MC>`!#x`yMnV3RA>E!VbjLa&u+4L*5<4y zo@Xu+p#X?*8Fsts7uqx zX_~SrSwatrwB6IQx>;MLSY+PJBhEByMTm@!5|?9*NskR&ZqwtY&k3P39q`<`at&;D z2QF~u_-vS*ixwBYAv@o=)guxjR3TQPQK6#a-Qn2~D8my*%>8l*{2l|ZEnalj8!Cs| zSb>nJ_{o(gXA)W|RO@i~LBdokmv$)gO$W9cjCmW&=q=WB`4l223xSO}UFwwRoRDZP zGy4IxQ?3f6Fa$8r3+EM9EHY>bh)AX{^v12Xph?3rC)>zRo=Iq@In94V<_b;CMt|Co z20R*W1_&9&10>MG((alN+Qk1NiczlZXax}@lZXGz#>2ZATt&w7M&=?zR`)y{*3*MN zdx}?-iFilMGae}p&OvEosB{(EejIkxRz&n>e?)P~<_x_KS2$=DeTl|1R42;XN9LUu zTrkFU7&T^ajjie}UygKBqW$GPqK&vp*2Ns)aV@AKk60w&B{^buWn(cSsTJ-(g8#8$ z3o!zDp%{wqeN}}5F`8tbN4<^75xOGs{_dV+JJeZ_cMWU-@*ZiNV6i>E4YDfB94vXp z{q~uZ?FDY{q327%dysT*ZxHA8N_kIeQd%;f*U|g#@HeB+Ev{GzMOe{!dSP`4X!|4i z@r;6S{_rraRRl5^+azjqqzdv?r+1}Vt2tqVl<@;t@v1Iy!Z~BYk z&rA`byr^@_q`-X**U&NY3bD0}tO2Ct$K^*e7-Dwg2~RE#wf5A(<%0$Q{>iX29|_s1 zyiaPvxPuNB7(2FMQir#d=7LB<<-mJ9_22F8SUi55$ znO;}+w-ls{lriQ+fWp~TU8uCnk-QUyr(B82GId`ALoWxSu$ zg!+cUn>EWGXk!}?VxDR$+t%fRH#YP@HoYyi$U=lAd_AU6zjAtnFd(+7xjOV!2@Cp( zZ8st)ada6ZLtjXu$Cg|BJB&5i(; zxJOKfMWQ39=!^g+&HN%fCN5O{?fP}_V5q%_S*CpO!HeC?_2p}2RO1M<{z6&K7Mi$- zcB%?`(f#%*YFJ2DGU9baXhUvc>~AO9#6uG5RLr)N&a$e^)8<$&lzSFcJqW#3JNK#*-NQ8bPvPYS$HTKY99f z4|Ob8H#DBr)i~d>SsoW(8i8Y%$p?CteF@(!m`EVmR&ntA-eeD4!Ndvia25jkZc&UZ z{ZHaKUo75H$MdlP2eb0~;{B%c%`{&zmmAeg5sX3GUe8!tKu2c*GF3CWtrot8cM{fu z^w3@Qu=wW*iC+F*cO{J4+FgO-{J)r*m<%4s`L06^jPTAjUsvjgKh$#-N!_8KQ#ekwo zpqhrt>`jC?uw~>@)SYX;5%;yobdc_D-`{Bww_3;T#DBX{v9FXB2d-i?UC;UpG%~#E z9K&`;>9>Gg6d(~Btx%WCVu8N=arU8>r}v`$YadU%P2w(Ondwhi6(<#DRE)WT!XrMyw$eW+b@4F|D@dR4m425>)egKYm2wD2XZ^!P#T##RY5ZGlHob9ui1yfMv zFXM0^cu_^6*GRWij_I^rP=A=24p?V_YWwMrL)IG#EuRcKzLcH^vw<^u!aF!(Uwi27cmA%4V7Rvo>4QP_}3 zw-hH8X>SoqyWwlpJ^7bjl94KR^obbBo>6A?I0p>#?V7N+8 z*bN&*>hyk!Zieh_2bVoU?l=C@NK(T-cD)zWIC9@H(HPH^5l9SiIgYzr;Y}fzYnU%x zhj{r`Rgz}v??GyO6W!Xm&DY6EHwImRR z_ENkg3_XEZwk6K0Hx_5c#f8iB?)TciYG;k1`1!zGp9@$-6H)b620B``aA?^C-w(CR z`>`)9T61EH)A>D=d0<$u4?ts=ETAeY1Vv>lc288iT4KpR`hFxj>+(at&spN_Cj%-+ zyvr9ByC51EAE>RZ;26}J{yXj2?Evejozzg(YKSOj4DcG}w7hg-u*47V1tp*glI_TS zWRAQ5@o;s)g)vGwcVm~xOn#~sC>l2lpKfhKl3wR1w_cl!r?M7MODm_fd_j_f76(|b zJ*goJhqyD7vA+AMCN9OFQdB-`Ie)Bqa=t;Mt#ekb7nHwbJXcb@&7G)HW#dwgJ~ngl z`QDw9e|te<(Z?xtymG(Wi-`=dmktf~ z$zy!}LUN`=^tCfaq+GGfc1SqJ#RY{c4*KEvu}P9`vA)PqtDc?k(2AgW!9)_? za$4%7y%(=X)1~{qX|{ai;P^a;)!XQ+6I#s2CHDa_Zdb{D9Lf2w_8%|E_~Zyex6Lgs z&RPcDP?9HwRNv@;uH$RPLKDssABI;z`y>TzcwD&(e$@$7Mrio{_{RCRnCi+~pii$e z0_5LQ^x9r(LIwpZ#^&e%Zlp3{loir?N?`BC3*K+Tp%i_b>c0c73f5;^j$@rE1{l-n z@7AnQNZ5Gn)6H014&9Iyb+ugUHC;*U!ultRyUX3F7ZiWOgqwtdn72H-%RGIhM0Sc? zF2oy&q7u_2FnltqduPw%e&TT(zd9ty8UM3Ec=-9$ljNYxGuDB0AN){!|Co>gQ(VmH zr0_DSR9M_t5?Ws2d&L6bhFncq0Da})miRj^lzxyjoAhiLV7lIZ&4{&AVAXS3&|-^VyVzEOU=`LhrL)N0pAA zPVf{RMAG#1$&RxxEHv5spB5O{R6Cr^c6@&eWJ8hkfs7eCQPhgy1b!TTA9@?6BxnG; z^S zv)9M$Sch222+LTu0JQ~RET{DwE20&qAyClc_G9!bh1P8KsJnsw^FrMI-r?I}6-cAU z;l1$DZ*36?LKIhVx)^!Q%AP;28;$!{?i&krg5tZqqV?3>JhCBY4Rqd|tQzalL5`9R z>tv})rzaeNPRZFnWUm#$7Ffk^+~pp7!?E4Hg-BGS81_b+P=i)QAJ4-$p;8z7{*{)i z#yFoJ1L`%$2xK03CbmX-+)C*oKCO{^d6)TqB(8GobO!G{pb6PjV9Z$i_gAb`(WqXv zt1~NtgBV3i?tAlWAxXB(lt}qum zY(05<(1MW|vUt42_}#|!yx%D{tQldQMu?Ws&6iU&wg{opPZ^Hhi5Jr(YYr;#bo)Sb z&bZimM+&<^(+(jp84t3c<2*_$BOj_;XGuiR{XUnz*wzu6o=S6bc;aogL$#@Thu3wg zuWq-imIr%rB|mpGreWo)N;ZW9f}0+K515VfS-eXkc@XcM3b-RF$40@dayuX0db*vlO=Ydb zSoa{-rbCg4_|i009bmIDtDU-9iA#`FP(MpeD$4yK?UOy6+o5}uK6xZfyjc9*YPbLz zmlfY~Xme&ko16PFK4-V!BhqA&N-?`Qt4mB5OlKA>644;$lsW@1+6M`|3TqfZ5u>uEu5uX)|0uHYa|e%3;!HQ6pApd@&nhL< zZmbq0`?_Hex)1DY4AfH@=FfL+GYLl1T)^vMnsXaJ7>v^0_q)e)? zf*M%{ZkH}vkgnsDIh!Wr6x7d0>^>-@Q|366pkJnvMjL3Zz|>gTD3m2+;K#_#}SkgevL^fHiSk+{n zPMHAfQ@TqdzZM~WXuC3wkEy<6N5Sd-$hWZ+?2WyCB#LdAr#W!YK_4n^A~}0=aHH}- z;o{R3W?Wdz{r2mG!$L+;T@)r&FOCq9i_<${g@^JB?x$IG;Ni^mdZ=V@h#Jgp7p+LM zZbCa1k_3d@#8Ah&uSbFT1}wlcsg*#L`sCIXNtwq7K;)n zB^cqyo36`(OWYu}AbEl>{xKhOhbr27=;whQ=(#3(D!)K}p>i7U5@G9VjZCV`xkI0E zjlf(QG3_$lu!5QSQ&wmx@=~qoXX2c?{kqnjYphJ?uj~PsM;MZc>yjy;)cZWi z6#jyE1WV;O%$+MB=;>9;jodH{p9G$*)!|;no1SR&GNU1iG_w5xE03x0K+`9?EazH9gqL#3cQ4w7q48TMpoy-B5kQ5dU;JQh2&+C9VCIJA{se} zjVteg(h&yOL0JdT@Gy9z*Bw7zDYeh%qIBiU}R6Qg+1r)ddY z@u30;_rEE29Wvs~2-GAu_YsOym6yFlmrMz%eqt*Z+vhqNdg?-ko?jz<;%!%n2drA(#Hg}T=q`96xg5D*{SkB6hub1(< zqGL;p+V?;^RZj)pxZ?w`qtlXT2m@oLc{0NnI}^IOZuUsgi>{+kxRWciDb5s-t z-ooWufpd)Tv9-{?K7&?w*UG7EXNeX*hMu^6s~}mgSh8B}7jWt9xuHUp0#1D2l38~1 zJM1T^Yo(J&`s+Sa#Ng*!!K+NVN#H^NU`c z-ssuq4pB^s1_^PEgq~gMnZTAlfk{PRW=M(ZTPXQz@GykD-4Tk+`LS%C701T(iK1c= zRlI&|v9o>eHhibxHddh-A>Y3EqkV4A;Cfu2OH6JD_(h*&N>y?8b2~>g5(3#>;KC+V zXrI$->Ux>Pu`V%+Szoq1e2p;7QfIa(Flef8zR>Fw3GHHEEtS6&t?rHX>YWCVscnU;L z)nk=QSs5RZLfLeOo^(dnN!MG7m1W#bYtTXtJk}|BC*t!OllMU_GT0e2C#)sDvt=aq z1d2*wOP%GCRWhXwUSna!a!3s@Tv@(*uzf|QhrESxY}DK&YPPbB&+c89y6djA%rn@) zr=z9M*LQE=6#m6i?Fdi0a}Sjk;Vd??iXG3w1zSBEx<|qK>0)o18bh`iZxZ|a`P}Nq z+=S&J$mui!(%71FE6&%!-POJKsZA+xL%!|AIFMhnNl66-7XoBH-71`N@=n`k~--H(K|rQ1|fX@33EF~ZYK?m zCQCx(Jz>`oDS$Pe1d@+iJ4tpV`zmoPB(4`9o2|!_V0OL_lqubCCfk@Q7ztO4*4=7g z7qTrsT~+QASp#LfrSUronVe2LbA^28K4@p&bCYb6E$%PgeDw1KFK?p$g?$@H#MUkD zOE#kLkTRM{g!8?%-OXkO{&HyA3{O#B*d-4U6TMJX-SX;b=*m#hq*=YL=m2#f?+C+A zv|=kUHIJU6c0soyTBi3Wg5$y-vZpmIl&wLkqz0XvnA5W+$M?H$Xji^gvtGe+8M5jQ z-(V)9W6d#PBuyT@VCSN1gTEZ`$oABIz9UJhj8h9=Fyr9W8(*w2`}j7AXhc}AO$MsW zf}x%5Ac+Kexd>tfO+^(t@o|Wo&LvD|Jqz1QEeS&hPXwVPK=zZkIjJ)9&GrMxM*Dh& zH)FQbLGaN*^m_}XYEzBZtUq34Q`##$3=-B2<(*psh&4@t)ep4w8Gz1+9|B&KD2G)g zGocuQ5&i0WI4AL`E%+$G)uuZ+;WJq`UoTd0fzLpEX*+w_-ZEq11Y~?GgiE~MatT?( zpYGvx0Br}IhGA=eYg_s0|B;kBfImq7Pqk}v63|}=d5HSnt zu%m-M&tk|Y&SGZX!KCoBwv@Stn9$z@>(tp12pplfZnmSfkn?>*;O_2zbCWF+0J5$R z{urD%eU&LMlxUlKg4WxJ8ug=i+o13*z$;rfW*`3Bf-723m8=wg^Cn*RHeCb+V{`_k zbWuy4oWnwdxM9wwo=h#W&O^r!LYmoeqL}3VSfYi|U2X6r}F;)u( z<@G9Ch4u;DMYULpg@0yA;|OSPKvmkN#(sFoL*Ptpn3N={3+8Ng%|*GhM%Q!vI@f1| z;r8nXZeoNOQ_(^jvV71kL26~HMJ{hcLx!k=;mj-5+?m%*4MW)@K4&Kuz8! z()rGG+1~J(x;O3Lh@FjgjX&{f^|!BX1Ym%AUWz-_t4pR?e~$QCC0m7Gj8m3c-uJ!2 z6!t{O1V7PydYaFO`Vc1i`27Gh-k9W$rA%VwyJCrte)TBo=KLiu%Kn6Dx6r*%_fEU_ zYRSpPk_owTCoz4s|%^FViq%sX&`%$Egm=a*L_ zgU`aJSDyFE~)EVJUQ_C!QPL6@8d(N_7|QnrbflG}99qb(S{p?W1CHsP#PabA6_}=nz$r#Yt1+A*nkDm~UqVRQF!b8Pj#Baz5O9o#?t>kiHpyp6Y4ZC#qL- z``BY2?V&s-X*hmi^eNJ7f982B09DBZ0o4@(4}ha8?bQ}Bv=}M4)qQ8G*@GX;;GhSU zxgeSaN&s|ugbJu2+ymK{gmGQa ztpuV#pc#7-rOpo(YDNXGJL~ae;WO9F-8aoIL35U+QNt*(0qfSf>OQX{2->sZ2W4^d zsA@ctGW?fBN<=jSoXBcir#^}1fo$v6#P0wKeJ%qH{R5M>V(Iedej)~5=dR77K)v~M zBPJ=L{hgcfWXR94wcz*mHShY`WSk#yflJQ_Dma`4fW*aofBrPyv7^CKH0!^ZT{R zk&`dNbB-;y3)gdOOh`B7L!j2St$_4Fr5MuC{wE~uH1+!E60R*CMv8<~f3X1fNSj(y zw_M4&$Bi3RmH}-#pce=bpYW*z;L{+=i^VpmsU@>g)aghB9ZS^FR1?GLgBBC~A&3nj zuokZxqsNr5T)pT}_G_UfSq|IAEj9{N>AQ^gN3ZTL!#dhvVsbsJC|SAc))o@LkE)%g zlAUTsy0E?~bty2dro#?3`f1ROjA_2hzEykeYx9%H%9s?voN2*A)oVi( zS=1FS-Njgz(%a*GU9gj}pgKA_RK*_nye@q9g6{Ku9QB#>wvRX_Z(1LE=t z+mg9?Tt|J1S%71*^KN5R4Fv?n&^J{BIeL*!$A+WfB$n#_c7&w4MqxScHbUO|xAn|Q zP#s8{;3o5$&%4O9aS}6Yz04Uq%x{GXVU3&efnz!eInu7do+<yCyj|;rGaYf|NRCmQyu!KJ0 zSwr}_hx*!;&zX$K=i=Qv-Bk4)UC(}q33~aM4E}p=uafu`O&j16Dpe|lgEFI!YI6Tl zD)ts)j{=6}*0-Lucha#;@V$^d4?{uN zrUrYYKQs8;`LEg=(ulU2qt-*F-XC~JMoI`;AX925S_>~ol_3j{ZV`v{a3!65pe=|` zUIX}bN&-`6Iota0vMu!&1D;=UG%ao5tlr9Ny+=z)qGT86mLUyv;erET`95#19EtB_ z9N5|9dp5s2pWIAeb>k5tIb&7s`9vP42VNKFszSZQZRiOt)7TGX(7JqlLL~YfOSYyX zc({^RY}!;!yLS7}Vc)%`CJ1{b_&48tEiL^=JObzs_0jQXe!5bKdfxBfY2`!*|J=_c zmCcZ;EwQ8EjgNT$alud0VH3~dtS(%DMa3C}0W%GR(Had=#+I6u8>;bj z-<;z*_9~`^lc$iOC-7WT$wUKt8`H#gEh*{0oP62)?5el$?sDtecMbBe!h+NCF4_@l z=4Fb+0K-$j;HaGvB5l>Th2V=BRUIw?#Z-jbhW}?Khe2;m83hl7gx)Z%R4lIaP(Puk zI0;6WPy5xqd{NfI3Cx1^2z6SB+{AXmtX_79SMi={>AQC%4t4s+bwvcPb`$af|<98*Pf?EooiGuDITQiniXzb#LLJ@^e|6 zyUy{0p_}=_UZqp9S6i`eb0Ljk9?(*OozJ0YK9h{!evC_?^|$IZ7^4=-Y9eU(tYs(x9z5x{cxgqN8R9NS)= zraZrueUJKY%>Ue_HU6>$zA=C64j|F!jh-1BD%d&Mv!)To_gV6hm${e{b6s1uDK5Ytq_ z$*?say;5WOjW-hHf@kMAeh;s+zUg_CI0L4`mFUAgyBw~V-|qKm+`R+Z3mbwci0y^n z2Tr)&q(nK zi|+IKe>~TJDPV4co;8ihZ|VyK_jZS|`v;>SB(#5N^9wt`jB@>*ci2#5kvGW+Bq(4c zCSh zwxPEH(<&kdNA8;)JXW>^?r(w`#Pe^*ZG&#a1n+uT+$H=U z0v}(WTwxmJPL~N^;Tct_0;JqeM4>Bj4@WsauD=`d4Z4xYzeTBjcv7Ez@dR%OBad`* z2a3K~=De>gL*F|yW~75$K&{E?imZYGJm1~fJ<)V8ZE}~9y+7*V26a^B9yugI9Ya3W z4{?M1caIu&E!8pkyG1ryZj*nky=v~d%Co7rd=u0m)?&{56R{M+H(2B(<8S}7_`3s) zI=qFqt<{5s!Nr(@@C|YAwC8G zuC1pwUM3esn!bMyn)2Dw@o2SHg*yJDig$TE1p`~nqRsYeF{Q&RPAY5~zO;m= z(4+pVQzb}wyO;_UEvI;1>-a+pCl}f(#8sBQwi~{}?AKOP#VJSOtv>aE23o9hb;RKu zl*kJRPc{i>H}_8gS|jn_>R7BCMU>5uPA>?XfGN*YM(@f#h%B5RO+69I)Zf@SqK-ce z+D{p+0Sv}`8%n4zMH7;)vyZATs6kf*BXTEMiZ7NN$+le?`vVd$d+f<2EDD?!m!c_Z zu?ukW-9aCkNR85vn36Z(c zCQX*p>w{Kx9n4@C6>IGZPk}g(kMUDn%{sudInrm@B7f^nn@_My2@`3zb$vd;ESAfb z2?J2>(*btHC%Ph`(2JBnV(Ldd{JFYBJG2kCz-|(nb@%W09a0#foG0wdNALk>@wcXw z)(Jlh49_dkPKLsC-z@op4fiSS!Fi(RzPuycB1?Cn^&JCeG*D#suS78|e5ojN&p!DZ z%Hg&*&F()KiieW}2^14XT#WiXpx&px0%-i2!TdUbWd9{9oE zekVH_Iu%U<5#HFDaXjR!oXNnR44oJbgY;yazM#%x)H_-h@2PWo$rua1HBZqn>ioW- zo>78`T;Q+pRzIBrBUy8hT)_?D8cMTAfww-RV65X_uZJg(}UUe+s@RN2b|{JrRf` zNDwB$+J^Cz_Pycx@@=26^%Hy)+UKxzLl|WU2o3`4cUo>*N{Rxe zj`l3ZW{xK2EME414!02!^>Q*cwKaDmH!-)gb`YjKYwMyUw>A@|)Z|uTQ*x3tx3ZS= zaWQ}Eqx{a)$JUhJj8aqtNytk8?7-gK&6wQF-p;{Qz)P6&Z+->9=|8erDarpPakCYs z6#wIZ>R9Y;6EN_b}j*K9sv$s z^1pv5MUcQLAr~_X0d*;vzaI{~B}{4M=H?{8%IfLq$>Pb$;^<<@%FfTv|A!k64rZ`| z+11;@&De|C!IkPyC;!Wjl)0;^i?x%RwW9<1AAXHZ9Npc7DJlOr&_6zZFPFX3KMv&J z`ge9t zX<9p&IeNO%3$gxB`CrHX%agg3v75OFm=8ZQ8wWE7_d7N|0d6({Hcp*?vj0=`Kg^XJ z&8#iF|NqT7xc_ecPolq@3$gxziNCo2A94Su)87aACAhzF{l@i61b#{UJGy@3`XvItB>o*;zj6H%fnO5;j;`Oh zeu=;@iGN4eZ(P4b;FrX|qw6=WUn1~J;@{Er8`m!p_$Bf0==zQ8mk9il_;+;u#`Q}C zeo6d0x_;yOB?7-B{vBPvas3j3UlRY0uHU$RiNG(3e@EAET)#x%m&CuL>o=}nBJfM% z-_i9O*Dn$HCGqd*`i<+C2>g=xcXa*6^-BbPN&Gvye&hNj0>32w9bLb1{Stv+693!i zLi%gChq(iIjE5(9Fozywj0pq;UZ=d2_&e};vQRYTS*H#IfB!SnP2 zq75qB0?&Iyj&OBL4`3 zH08&nm4*@LfaVOP{(=h8h9FVaGyN4GdXO?#XA>$fQXbPGOkRr!;*}SvfXWR|WUo;- zf$}>#g)Kn)&!3rQ$lU@#?izeQO}YtW-q9-{Fr?B@6FJ>TBa1I`VZfkO5J|Ebp-kg5 zC_p&aIK(5LZhE`-)y_mg79mQ~aH9~#5wOu?AE~~9a5%7!&RBq$Ye9Q0jFO2HWm5uU z1lvv1QAGja3-JY^H{yMq1PVd8R+U!p-%_vw4ZT<)HUnku42{bWsuO8BN3BF#Sa7k_ z{-KSywM1rh!Z3l6#5n0ekSU1>>9`j$wi+ul5^TzE&8Pc^g5*EFWlZh`TGb^;b^ zfKbrT*4~)zng*LmgcZp#T9OYDM7rNc>&tH;m~FY9Jx&E~Oi+usqMim9EJgwi61d&? z8`qpAwn*LhTYFPPYi>#X*H;333lfj^6#E<}hYnFT2JhHR&}V1~Sj-~4VZQ&6UPV=# zcnHEQx?c~`r{oyJ33AlWH+N}_p|*(rvf%Sn-hmBkqIlHWUi$NS$67ldE&s!wy_aU(+HSPbl2ei z;mIc)m%{X!DOu|Uq~!@-5nx1UaL=*pa(PHFP~xtv%i1atJHX+x5vm9}!Z&8c<#f<; zxc%j4OqchxjO7W^`SEMvxNrG0z8SG6ls6Nk{Wpgdn@}6~ku?M?BZ?G@;r`;D zl9MK8-~z00ICp$F-u0Y(uG^kaQFn+-iA5!(f`;pncN3Z#fZV=nf{##W0>Zq0S}$3l zw49*2Un3Z34#Kodb*YMh{#G|e(4|3mzc-YVUkV@)UEw&1H7tVj9-u#3KXibn$;j0o z>=y+(@7v}9hgkW1iw8x2j!s0J%0R%qXY;?qOWNi5?wZ+rdod08_ z86=LuLOj=?XW@gk_p&?H-Et*q==luHSGP&ZC^Z@mtc*h-aL%QLcW35J?;>E(bDBmg zEt4aen9C6pEESzP+-XABTnyF|-cSL_(#6IEy*U;L(AW8D9ptHwIH@ zj+(!qFcdXo>J(BtldzGAw06NBv5FbmH z141lq18Y7>T z>7LW6%(H1yLSwV*5_ybu?q1Fte0iqpAFvhpjNz+>Tyma)L{l$U`7})*uxubRN78as z-UrC(;3~qR;0r4KWOt@o!3z?PZ}hUs4KBs-V@*>x=!ptmB})rH_>Ad{+@0lB-gID~ z$-j{!1N&BGlT(g|a1)C3&_1|zG?EdOz*&L>-h-LT`DD9)6Uu3?9u{xl?j7PhuA=_M zm3%OtF2v-ly=ag8RY4eDl8O6!bPYT_uIi3?)Sod&}JB$fJfd7HLX z^D&X5JSu+Bd`yXknI<~}Lnf}1S}ynBf{f+CZaRiHiOo`&2RDnX!h#@&&mLckWD?@KM{*qt;<&pR-RK$@1 z6NNQ$H+lQ?8shB2z;LAz->pfot7Bj(lODu|RirL7F~#l^6OB>70bE|4ZFBU|dov`> zG~xV6A3<}2K~frRTt)d=S2@hAKhEH5<|I9hzu4B5k=I(x1Ndq;eb$jausm0Fw4@PW zl>x&}K1Wff7Hq2&(t~iYbJL<9$suGzOX=vcB5*);(g1H_!-9iL3#zO%E;TDpfmu&E z;5nO!Xw_ZbZ^~3&r$DA&miyfa>#O}wag$w! z%6oNkWnD3@1MquE3sDw z>XNd}=W_xzD>&;+SI@g zYU>#MM*YW_ZL)CSi(gQ96KZX7|8tI-N?Xj^o?2hCpyOkXZ&#qwlPhnV+$KtY5Fs1h z8&~9RZRKoqa86e$ydwLcuUxX?=9dL0NWSKy7L!zbStJ5y2vqc&1!_*=NLOAFz%qh? z{8rm%I(RkdDkBNe%$+y>$lVZnEecVI&}z^T+pGd~?IzR=)hjeo!@<*#RHbEJrw&Uv z2JBe4ny1NZWFD;oZ(!fX0`NX4eSaww=WAB*|6pu@Lt})u)eyQRPzJ7l?begm#49BM z1(!j%u?66pLy|X8X?22cz^Nbur>~vvO*hk^ExdGzec%knG3ktg#IAUG(7_ziF=+|7 zHtmd>S9Uj8Iw%!c!m5lujATm~@BhIU&^Zk;pvHlQ$a^Sw;#st zvc4i=kt&KEjby_ekNsFWjZAyRcXw+!7&&(TY`>~RUN?dA)OUe_ZUjq}W2_4iLD+Q( zIe3ZheA%bDFJgP(5bpuL{a=(%<1<5OkzGfk5TM#2X5@iejac2BqOqA!B=}Cl)aDqR zgG9qu$^cv{YFQ8Qb^hUqSJ~*sP!XJR$(hZtL4|~ZARQ-R)*Fih)H+`4T5z3%@0g$32&sE6CiRH}>I&NkWeC%;|6t&b3?U6zUAJhK;$kr33r!WX z@QL5tiaLGa$@9NpT%BN>@BT=D%l1+H!+{$zgm%qx09F_>pbLeA?KJ@#Qf6mY6eXY4 z_W9U4n_ff=3EMHHGyT9BD`7?gxNdQy4X$-#KO< z4Fy~*6W$4H;SAbY&E+Ro#6{_3v#q5e6nt4?;Ya8`n(Re=Y6CYis3wT!ZLd9+cv7H& zC#M}J+v`Q?H&$CqX9O(q8w~mn1Z;?AmKjO8d#5`1Zg)-2_Zw8J6Hee?XtWNHPD2L_ z`ql19@sQY(w{cAbEY;w1-C)&|D?@^&EdMQ3x)3M-mKO z)2iW62wV)nEde|VJ~LR(RXsb;I6nWC+RX-bRf$cedSz}kYzA#fyeVq4aH1S3qA52e z*K5T*8}4U+dC-s46^z1n#C{K?eKf+*t|6vuJ0JU=D*lX5DCh{{zW1#b#`w`6o!ebA zHVb8sS!Umkpi;nt?|$yQo47)!>J`EKJwXM-gZK_d4!y`;^c@~wp(FIEeR{NBC^>4n zKDQB;fip4rVx(MR4IflJv6U~>eXio%+g1x$_veuM{+}wYG@h-jjo&0sjpcfG2-8?bdbUU`CZDki^PLYhm@EZfmSZim&Ub=C(m(1LU<*UTN{kbCj-D%f?JRt0rmxmd^@!6M@-XLMRFlFVvpeO? zz{IO<8BwB5Q^%j%+!6%!&g}X6vQCOFCo=F3bki}$vx79q{qsXUslS7eTUUP0_3APU zXEC7gA+T;ri_h;-w)&PW2+ARJ%7xeh%Zt&msH>rdusfsZ)PbhmYmS4+6L`mNhM;O~BI4H+w*{uCB!EQ?oRjeb ziE7Ih6e(!k#Pz`pc7-abP4&KUNRDY?2xQQl%vYCcV58aJ7!S!e9f-=Uv8yGJb1jcc zZuL9Dt{NOu!qVRyor>K4+%5FP2OT;M6|S|rhVJ*Y>FUL#u+Lc+NmH8LS6)2={3+2r z_QjZ|QrH4MNvA;*2VMSwy^vr0Y|PY0Xnx`xH(OYH!R@`n8Opq)xIg6|)`h_8>I-wT zd1Kj|Rz%h!^v!gY$3ZBo90T9F(3#K#lqhgU=X17u3ekj4dK_k1$M5HlZ=I9UEPR)2 z8#@m7Zm_jF&98g020T@VAZ)3*>#1SfWQyN9eOR`OjKWM-DiMNAQ;w``IjMy@5LY{# zeHzGjEEk>Jc|d(oePx{pv{$+Ru~dT^;2f(?4})cRNAbd;;=4n~>Fs?bJHAGhaTETH z)35F>tB?INhK?5r;t#2GMj1Tgxn~U4 zfk3(bzkXV!t+nA0PSSes!@@^b#u`K+P?cKBL&Ink}`6n)jXn+N3951RmUFk6l zT+Zagr@FJ()u&)Bva0*eEBHYgyECF&)nhYz$C&%K&?LoRIb1NFR<(33Vmj3!jD4C1 z*l?S3jiE?v6uVAeTlfKmNo!T_w7#NBs1JIE}NQ4-SkJHXC z+Jk&{DkHt$+KHR3P+ya5mU%T~8LGJR`4VbQQ8Iqz8r=+-mhvP4RY)J!;;~W3S4Nz3 z3PP~)V<*D5Mi<+wq5mDfs)9Zajd;lE=K)Qw;Oa^bs;RnF?nZ z9#UB2{|u1Px#T-Ht!BR$3+9^8=+P0pGbdfB@;4V2K(6q{Keb4L;(7bNQ+DF-t|Tb) zb!(9l%7tasxy&G8FjbVXym*fg~<&K5EaXQ2~+gX+wM*b*0j zV{Y~C=+sn_LtpmO)AG&wy-BqrBjiP>!jJ*-{x}GBKG-Io-1*+58IZI(&^K4PNyt=W z-!r3sJmdCMVrfxh-SAQi=R4alRr^UI}l18RwnLW>i#WdTHA(Syyn;H zLr5j59vEpBIklzDmk#Hjg5mb{My7KCxk7Q3Jzab$acwJv-^EZd47lp#QxyBoX{#27 z@KIUJH6s|0Q3CafYfxqfprbj!M0`8nGx%?O@Aq3!*1kB@5-$}ww-t<nuJBFgGdT#OpO95wB@PBKYKS>VD<5RPshz0IG-|?Ts}WgS(D(&W6fCO)fZyqCec7 z5k8w1rLY}=!OJ5WFIh4eZg~PB1EM^TKw&~VU-xQCi1aSR769Km3tU)%I5h$=T#Kdv zR+BTb8vMs46K_glaqq~Bhj+Alt2=RaI>uHsz!?s0FSq_cg9hZm#Yr-_M)skyhADX? zW+)5+K;(^B7HTuxWqbP+zl#aO693d*>PI%4g3Qq?f*YvK50*-NWHSi_WVeej^|)cn zym1(U5Hc9f=;3sE-yh^Vr=L^YprOq0nOSQe^0$cw`KL1~?epxCk+C zNUj>xKc8iKjTUqk4{b7GOlpIxShK9`PSKCQNK$vB7tUu>sEKTUogH}$mlr_fD>9Lj zeB^3nx^S`}C`Dvc(-MZsKM^~A^~W((D9%G$Ldx$T|bQE`@S&_u1+hwHP~hvg!H z#oK824A@opn^gTnH4$%9N1}h=eWTo&QE!77$6&AfXwJ1_W6)Hnq~H&a@ra9~CH}zU z1u3_VGhx9%R(?DREg~*QF=pg{`)H?3ev!5*H|4q-??e0^GuPVVpTIw`ara&S7yW@H zk9mj+e2`spK38%jS--z2!AuEaL0teDZ@om2>r3?{k;U$rAdcCFA-G0&FH0FHI2(h? zTDD>0HVcM82Vjx`z3>pTc_;;z1kNdnv%;R3D}P8?rOEBGdf)#{Y@W82Q?$G#X Date: Sun, 30 Jun 2024 07:38:25 +0000 Subject: [PATCH 36/45] [accelerator] match loading height to actual QR --- .../accelerate-checkout/accelerate-checkout.component.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index d4db46258..decd95d3c 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -357,8 +357,10 @@

Pay {{ cost | number }} sats

} @else { - Loading invoice... -
+

Loading invoice...

+
+
+
}
@if (canPayWithCashapp) { From 0b663c1a77b1fbd922d630184c3274a2a9c67546 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 07:41:25 +0000 Subject: [PATCH 37/45] [accelerator] fix loading spinner alignment --- .../accelerate-checkout/accelerate-checkout.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index decd95d3c..db9ac6811 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -478,7 +478,7 @@
-

Confirming your acceleration with our mining pool partners...

+ Confirming your acceleration with our mining pool partners...
From f114a8ca75bb8a5fbbb382d8e241b2d62eb45903 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 08:15:20 +0000 Subject: [PATCH 38/45] [accelerator] refactor bitcoin-payment component --- .../accelerate-checkout.component.html | 2 +- .../bitcoin-invoice.component.html | 44 ++++++------ .../bitcoin-invoice.component.ts | 69 +++++++++++-------- 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index db9ac6811..a6863eb36 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -355,7 +355,7 @@
@if (invoice) {

Pay {{ cost | number }} sats

- + } @else {

Loading invoice...

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 790f046f7..5fd4f6701 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -6,7 +6,7 @@ - A transaction has been detected in the mempool fully paying for this invoice. Waiting for on-chain confirmation. + A transaction has been detected in the mempool fully paying for this invoice. Waiting for on-chain confirmation. } @@ -16,13 +16,13 @@
- -
@@ -30,63 +30,63 @@ - +
- +
- +
@if (!minimal) { -

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

+

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

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

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

+

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

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

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

+

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

}
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 e4ad1af20..b220a63b1 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -1,8 +1,8 @@ -import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'; +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, timer } from 'rxjs'; +import { Subscription, of, timer } from 'rxjs'; import { retry, switchMap, tap } from 'rxjs/operators'; import { ServicesApiServices } from '../../services/services-api.service'; @@ -11,7 +11,8 @@ import { ServicesApiServices } from '../../services/services-api.service'; templateUrl: './bitcoin-invoice.component.html', styleUrls: ['./bitcoin-invoice.component.scss'] }) -export class BitcoinInvoiceComponent implements OnInit, OnDestroy { +export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy { + @Input() invoice; @Input() invoiceId: string; @Input() redirect = true; @Input() minimal = false; @@ -20,7 +21,7 @@ export class BitcoinInvoiceComponent implements OnInit, OnDestroy { paymentForm: FormGroup; requestSubscription: Subscription | undefined; paymentStatusSubscription: Subscription | undefined; - invoice: any; + loadedInvoice: any; paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed paramMapSubscription: Subscription | undefined; invoiceSubscription: Subscription | undefined; @@ -60,35 +61,47 @@ export class BitcoinInvoiceComponent implements OnInit, OnDestroy { this.paramMapSubscription = this.activatedRoute.paramMap .pipe( tap((paramMap) => { - const invoiceId = paramMap.get('invoiceId') ?? this.invoiceId; - if (invoiceId) { - this.paymentStatusSubscription = this.apiService.retreiveInvoice$(invoiceId).pipe( - tap((invoice: any) => { - this.invoice = invoice; - if (this.invoice.btcDue > 0) { - this.paymentStatus = 2; - } else { - this.paymentStatus = 4; - } - }), - switchMap(() => this.apiService.getPaymentStatus$(this.invoice.id) - .pipe( - retry({ delay: () => timer(2000)}) - ) - ), - ).subscribe({ - next: ((result) => { - this.paymentStatus = 3; - this.completed.emit(); - }), - }); - } + 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.btcpayInvoiceId || 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.invoice?.addresses || {}).filter(k => k === 'BTC_LightningLike'); + return Object.keys(this.loadedInvoice?.addresses || {}).filter(k => k === 'BTC_LightningLike'); } bypassSecurityTrustUrl(text: string): SafeUrl { From 35d0e7fae700013c8aee62c9814267368e540ca8 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 08:39:32 +0000 Subject: [PATCH 39/45] [accelerator] rerefactor bitcoin-payment component --- .../accelerate-checkout.component.html | 2 +- .../accelerate-checkout.component.ts | 23 +++++++++++-------- .../bitcoin-invoice.component.ts | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index a6863eb36..1a1977f57 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -355,7 +355,7 @@
@if (invoice) {

Pay {{ cost | number }} sats

- + } @else {

Loading invoice...

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 61b03d553..661b78c11 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; -import { Subscription, tap, of, catchError, Observable } from 'rxjs'; +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'; @@ -443,15 +443,20 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * BTCPay */ async requestBTCPayInvoice() { - this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).subscribe({ - next: (response) => { - this.invoice = response; + 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(); - this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 500); - }, - error: (response) => { - console.log(response); - } + if (invoice) { + this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 500); + } }); } 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 b220a63b1..cb7e78ebd 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -77,7 +77,7 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy { if (this.paymentStatusSubscription) { this.paymentStatusSubscription.unsubscribe(); } - this.paymentStatusSubscription = ((this.invoice && (this.invoice.btcpayInvoiceId || this.invoice.id) === invoiceId) ? of(this.invoice) : this.apiService.retreiveInvoice$(invoiceId)).pipe( + 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) { From 9140bcb408f851236150b1f584bed14eb1a64e28 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 08:58:39 +0000 Subject: [PATCH 40/45] [accelerator] fix liquid --- .../transaction/transaction.component.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 7ad683cce..3bc40ea93 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -177,9 +177,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.showAccelerationSummary = true; } - this.miningService.getMiningStats('1w').subscribe(stats => { - this.miningStats = stats; - }); + if (!this.stateService.isLiquid) { + this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningStats = stats; + }); + } this.websocketService.want(['blocks', 'mempool-blocks']); this.stateService.networkChanged$.subscribe( @@ -410,20 +412,22 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.tx.acceleratedBy = txPosition.position.acceleratedBy; } - if (!this.mempoolPosition.accelerated) { - if (!this.accelerationFlowCompleted && !this.showAccelerationSummary) { - this.showAccelerationSummary = true; - this.miningService.getMiningStats('1w').subscribe(stats => { - this.miningStats = stats; - }); + 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); } - if (txPosition.position?.block > 0 && this.tx.weight < 4000) { - this.accelerationEligible = true; - } - } else if (this.showAccelerationSummary) { - setTimeout(() => { - this.closeAccelerator(); - }, 2000); } } } else { From a80372f3352cb1e461f2ee3c44b8a53294cbc688 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 10:04:24 +0000 Subject: [PATCH 41/45] [accelerator] play sound on invoice paid --- .../accelerate-checkout/accelerate-checkout.component.html | 2 +- .../accelerate-checkout/accelerate-checkout.component.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 1a1977f57..0f8f81165 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -355,7 +355,7 @@
@if (invoice) {

Pay {{ cost | number }} sats

- + } @else {

Loading invoice...

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 661b78c11..a4fa5c5a0 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -460,6 +460,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }); } + bitcoinPaymentCompleted(): void { + this.audioService.playSound('ascend-chime-cartoon'); + this.estimateSubscription.unsubscribe(); + this.moveToStep('paid') + } + isLoggedIn(): boolean { const auth = this.storageService.getAuth(); return auth !== null; From d76490df0c5797e8c423d6d11cdf092aa05b62b7 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 10:08:49 +0000 Subject: [PATCH 42/45] [accelerator] fresh invoice after changing bid --- .../accelerate-checkout/accelerate-checkout.component.ts | 1 + 1 file changed, 1 insertion(+) 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 a4fa5c5a0..51c8d1153 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -156,6 +156,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } if (this._step === 'checkout' && this.canPayWithBitcoin) { this.loadingBtcpayInvoice = true; + this.invoice = null; this.requestBTCPayInvoice(); } else if (this._step === 'cashapp' && this.cashappEnabled) { this.loadingCashapp = true; From ce879152fdfb6bff249ec6d3cd596c817d93eb61 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 10:09:45 +0000 Subject: [PATCH 43/45] [accelerator] don't scroll to btcpay invoice --- .../accelerate-checkout/accelerate-checkout.component.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 51c8d1153..8c0d35dd9 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -455,9 +455,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ).subscribe((invoice) => { this.invoice = invoice; this.cd.markForCheck(); - if (invoice) { - this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 500); - } }); } From da1e5c515edcdf95c3163cbf629b170a4baa844a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 30 Jun 2024 10:14:14 +0000 Subject: [PATCH 44/45] [accelerator] use invoice amount --- .../accelerate-checkout/accelerate-checkout.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 0f8f81165..a0f84e226 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -354,7 +354,7 @@ @if (canPayWithBitcoin) {
@if (invoice) { -

Pay {{ cost | number }} sats

+

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

} @else {

Loading invoice...

From de95dd9c77b32ace408002ae63669956fbbd6949 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 30 Jun 2024 22:20:44 +0900 Subject: [PATCH 45/45] removing margin causing table jump --- .../src/app/components/transaction/transaction.component.scss | 1 - 1 file changed, 1 deletion(-) 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) {