More acceleration checkout refactoring
This commit is contained in:
		
							parent
							
								
									473da82caa
								
							
						
					
					
						commit
						c75afe20cd
					
				| @ -146,8 +146,9 @@ let routes: Routes = [ | |||||||
|     data: { preload: true }, |     data: { preload: true }, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: 'tracker/:id', |     path: 'tracker', | ||||||
|     component: TrackerComponent, |     data: { networkSpecific: true }, | ||||||
|  |     loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: 'wallet', |     path: 'wallet', | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ | |||||||
|     </div> |     </div> | ||||||
|   }  |   }  | ||||||
|   @else if (step === 'quote') { |   @else if (step === 'quote') { | ||||||
|     @if (!simpleMode) { |  | ||||||
|     <span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span> |     <span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span> | ||||||
|     <div class="row" *ngIf="showSuccess"> |     <div class="row" *ngIf="showSuccess"> | ||||||
|       <div class="col"> |       <div class="col"> | ||||||
| @ -37,13 +36,14 @@ | |||||||
| 
 | 
 | ||||||
|       <ng-container *ngIf="estimate else loadingEstimate"> |       <ng-container *ngIf="estimate else loadingEstimate"> | ||||||
|         <div [class]="{estimateDisabled: error || showSuccess }"> |         <div [class]="{estimateDisabled: error || showSuccess }"> | ||||||
|  |           <button class="btn btn-sm btn-outline-info float-right" (click)="showDetails = !showDetails">Details</button> | ||||||
| 
 | 
 | ||||||
|           <div *ngIf="user && !estimate.hasAccess"> |           <div *ngIf="user && !estimate.hasAccess"> | ||||||
|             <div class="alert alert-mempool">You are currently on the waitlist</div> |             <div class="alert alert-mempool">You are currently on the waitlist</div> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           @if (showDetails) { |           @if (showDetails) { | ||||||
|               <h5 i18n="accelerator.your-transaction">Your transaction</h5> |             <h5 class="mb-4" i18n="accelerator.your-transaction">Your transaction</h5> | ||||||
|             <div class="row"> |             <div class="row"> | ||||||
|               <div class="col"> |               <div class="col"> | ||||||
|                 <small *ngIf="hasAncestors" class="form-text text-muted mb-2"> |                 <small *ngIf="hasAncestors" class="form-text text-muted mb-2"> | ||||||
| @ -163,7 +163,6 @@ | |||||||
|                     </tr> |                     </tr> | ||||||
|                   </ng-container> |                   </ng-container> | ||||||
| 
 | 
 | ||||||
|                      |  | ||||||
|                   <!-- NEXT BLOCK ESTIMATE --> |                   <!-- NEXT BLOCK ESTIMATE --> | ||||||
|                   <ng-container *ngIf="isLoggedIn()"> |                   <ng-container *ngIf="isLoggedIn()"> | ||||||
|                     <tr class="group-first"> |                     <tr class="group-first"> | ||||||
| @ -246,7 +245,7 @@ | |||||||
|       <br> |       <br> | ||||||
|     </ng-template> |     </ng-template> | ||||||
|   } |   } | ||||||
|     @else { |   @else if (step === 'summary') { | ||||||
|     <!-- Show A/B CTAs --> |     <!-- Show A/B CTAs --> | ||||||
|     <div class="row mb-1"> |     <div class="row mb-1"> | ||||||
|       <div class="col-sm"> |       <div class="col-sm"> | ||||||
| @ -262,9 +261,9 @@ | |||||||
|             <label class="form-check-label d-flex flex-column" for="wait"> |             <label class="form-check-label d-flex flex-column" for="wait"> | ||||||
|               <span class="font-weight-bold">Wait</span> |               <span class="font-weight-bold">Wait</span> | ||||||
|               @if (eta.blocks < 7) { |               @if (eta.blocks < 7) { | ||||||
|                   <span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected <app-time kind="within" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time></span> |                 <span class="checkout-text">Confirmation expected <app-time kind="within" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time></span> | ||||||
|               } @else { |               } @else { | ||||||
|                   <span style="color: rgb(186, 186, 186); font-size: 14px;"> |                 <span class="checkout-text"> | ||||||
|                   <span>Confirmation expected within several hours</span> |                   <span>Confirmation expected within several hours</span> | ||||||
|                 </span> |                 </span> | ||||||
|               } |               } | ||||||
| @ -272,11 +271,11 @@ | |||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="col-md"> |         <div class="col-md"> | ||||||
|             <div class="form-group form-check mb-2"> |           <div class="form-group form-check mb-2" *ngIf="estimate"> | ||||||
|             <input type="radio" [checked]="choosenOption === 'accel'" class="form-check-input" id="accel" name="accel" (change)="selectedOptionChanged($event)"> |             <input type="radio" [checked]="choosenOption === 'accel'" class="form-check-input" id="accel" name="accel" (change)="selectedOptionChanged($event)"> | ||||||
|             <label class="form-check-label d-flex flex-column" for="accel"> |             <label class="form-check-label d-flex flex-column" for="accel"> | ||||||
|                 <span class="font-weight-bold">Accelerate</span> |               <span><b>Accelerate</b> to ~{{ ((userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} sat/vB <button *ngIf="advancedEnabled && isLoggedIn()" class="btn btn-sm btn-accelerate btn-small-height ml-3" i18n="accelerator.customize" (click)="moveToStep('quote')">customize</button></span> | ||||||
|                 <span style="color: rgb(186, 186, 186); font-size: 14px;" *ngIf="(etaInfo$ | async) as etaInfo">Confirmation expected <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time><br> |               <span class="checkout-text" *ngIf="(etaInfo$ | async) as etaInfo">Confirmation expected <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time><br> | ||||||
|                 @if (!calculating) { |                 @if (!calculating) { | ||||||
|                   <app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) |                   <app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) | ||||||
|                 } @else { |                 } @else { | ||||||
| @ -293,40 +292,81 @@ | |||||||
|       </div> |       </div> | ||||||
|       <div class="row mt-2 mb-2" [style]="(choosenOption !== 'accel' || calculating) ? 'opacity: 0.25; pointer-events: none' : ''"> |       <div class="row mt-2 mb-2" [style]="(choosenOption !== 'accel' || calculating) ? 'opacity: 0.25; pointer-events: none' : ''"> | ||||||
|         <div class="col-sm d-flex flex-row justify-content-center"> |         <div class="col-sm d-flex flex-row justify-content-center"> | ||||||
|             <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" style="width: 200px" (click)="enableCheckoutPage()"> |           <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" style="width: 200px" (click)="accelerate()"> | ||||||
|             <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> |             <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||||
|             <span>Accelerate</span> |             <span>Accelerate</span> | ||||||
|           </button> |           </button> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </form> |     </form> | ||||||
|  |   } @else if (step === 'checkout') { | ||||||
|  |     <div class="row"> | ||||||
|  |       <div class="col-md"> | ||||||
|  |         <div class="d-flex flex-column" *ngIf="estimate"> | ||||||
|  |           <span><b>Accelerate</b> to ~{{ ((userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} sat/vB <button *ngIf="advancedEnabled && isLoggedIn()" class="btn btn-sm btn-accelerate btn-small-height ml-3" i18n="accelerator.customize" (click)="moveToStep('quote')">customize</button></span> | ||||||
|  |           <span class="checkout-text"> | ||||||
|  |             @if (!calculating) { | ||||||
|  |               For an additional <app-fiat [value]="cost"></app-fiat> (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) | ||||||
|  |             } @else { | ||||||
|  |               <span class="estimating">Calculating cost...</span> | ||||||
|             } |             } | ||||||
|   } @else if (step === 'paymentMethod') { |           </span> | ||||||
|     <div class="row text-center"> |           <span class="checkout-text" *ngIf="(etaInfo$ | async) as etaInfo"> | ||||||
|       <div class="col-sm"> |             Reducing expected confirmation time to <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time> | ||||||
|         <h1 class="mb-2" style="font-size: larger;"><ng-content select="[slot='payment-title']"></ng-content><span class="default-slot">Select your payment method</span></h1> |           </span> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     <div class="row text-center"> |       <div class="col-md pie d-none d-md-flex" *ngIf="estimate && !forceMobile"> | ||||||
|       <div class="col-sm"> |         <small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description" *ngIf="(etaInfo$ | async) as etaInfo">Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.</small> | ||||||
|         <app-fiat [value]="cost"></app-fiat> (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) |         <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="pt-2 d-flex justify-content-center position-relative"> |     @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { | ||||||
|  |       <div class="d-flex justify-content-center"> | ||||||
|  |         <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.estimateDisabled]="!canPayWithBalance" style="width: 200px" (click)="accelerate()"> | ||||||
|  |           <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||||
|  |           <span>Accelerate</span> | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|  |     } @else { | ||||||
|  |       <div class="payment-area mt-2 p-2"> | ||||||
|  |         <div class="row text-center justify-content-center mx-2" style="font-size: 14px;"> | ||||||
|  |           <p>Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank"> {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p> | ||||||
|  |         </div> | ||||||
|  |         <div class="row"> | ||||||
|  |           @if (canPayWithBitcoin) { | ||||||
|  |             <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> | ||||||
|  |               @if (invoice) { | ||||||
|  |                 <p>Pay <span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span></p> | ||||||
|  |                 <app-bitcoin-invoice style="width: 100%;" [invoiceId]="invoice.btcpayInvoiceId" [minimal]="true" (completed)="closeModal(2000)"></app-bitcoin-invoice> | ||||||
|  |               } @else { | ||||||
|  |                 <span>Loading invoice...</span> | ||||||
|  |                 <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||||
|  |               } | ||||||
|  |             </div> | ||||||
|  |             @if (canPayWithCashapp) { | ||||||
|  |               <div class="col-sm text-center flex-grow-0  d-flex flex-column justify-content-center align-items-center"> | ||||||
|  |                 <p class="text-nowrap">—<span i18n="or">OR</span>—</p> | ||||||
|  |               </div> | ||||||
|  |             } | ||||||
|  |           } | ||||||
|           @if (cashappEnabled) { |           @if (cashappEnabled) { | ||||||
|         <img class="paymentMethod mx-2" src="/resources/cash-app.svg" height=55 (click)="selectPaymentMethod('cashapp')"> |             <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> | ||||||
|       } |               <p>Pay <app-fiat [value]="cost"></app-fiat> with</p> | ||||||
|       <img class="paymentMethod mx-2" src="/resources/btcpay.svg" height=55 (click)="selectPaymentMethod('btcpay')"> |               <img class="paymentMethod mx-2" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')"> | ||||||
|             </div> |             </div> | ||||||
| 
 |           } | ||||||
|     <hr> |         </div> | ||||||
|  |       </div> | ||||||
|  |     } | ||||||
|  |     @if (showSummary) { | ||||||
|       <div class="row mt-2 mb-2 text-center"> |       <div class="row mt-2 mb-2 text-center"> | ||||||
|         <div class="col-sm d-flex flex-column"> |         <div class="col-sm d-flex flex-column"> | ||||||
|         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="step = 'quote'">Go Back</button> |           <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')">Go Back</button> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| 
 |     } | ||||||
|   } @else if (step === 'checkout') { |   } @else if (step === 'cashapp') { | ||||||
|     <!-- Show checkout page --> |     <!-- Show checkout page --> | ||||||
|     <div class="row mb-md-1 text-center"> |     <div class="row mb-md-1 text-center"> | ||||||
|       <div class="col-sm" id="confirm-payment-title"> |       <div class="col-sm" id="confirm-payment-title"> | ||||||
| @ -342,7 +382,6 @@ | |||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     @if (paymentMethod === 'cashapp') { |  | ||||||
|     @if (!loadingCashapp) { |     @if (!loadingCashapp) { | ||||||
|       <div class="row text-center mt-1"> |       <div class="row text-center mt-1"> | ||||||
|         <div class="col-sm"> |         <div class="col-sm"> | ||||||
| @ -372,14 +411,11 @@ | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     } @else if (paymentMethod === 'btcpay' && invoice?.btcpayInvoiceId) { |  | ||||||
|       <app-bitcoin-invoice [invoiceId]="invoice.btcpayInvoiceId" (completed)="closeModal(2000)"></app-bitcoin-invoice> |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     <hr> |     <hr> | ||||||
|     <div class="row mt-2 mb-2 text-center"> |     <div class="row mt-2 mb-2 text-center"> | ||||||
|       <div class="col-sm d-flex flex-column"> |       <div class="col-sm d-flex flex-column"> | ||||||
|         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="step = 'paymentMethod'">Go Back</button> |         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('checkout')">Go Back</button> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   } |   } | ||||||
| @ -404,5 +440,4 @@ | |||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   } |   } | ||||||
|    |  | ||||||
| </div> | </div> | ||||||
| @ -141,6 +141,10 @@ | |||||||
|   margin-top: 1em; |   margin-top: 1em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .payment-area { | ||||||
|  |   background: var(--bg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .col.pie { | .col.pie { | ||||||
|   flex-grow: 0; |   flex-grow: 0; | ||||||
|   padding: 0 1em; |   padding: 0 1em; | ||||||
| @ -155,3 +159,16 @@ | |||||||
| .table-background { | .table-background { | ||||||
|   background-color: var(--bg); |   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; | ||||||
|  | } | ||||||
| @ -41,6 +41,8 @@ export const MIN_BID_RATIO = 1; | |||||||
| export const DEFAULT_BID_RATIO = 2; | export const DEFAULT_BID_RATIO = 2; | ||||||
| export const MAX_BID_RATIO = 4; | export const MAX_BID_RATIO = 4; | ||||||
| 
 | 
 | ||||||
|  | type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing'; | ||||||
|  | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-accelerate-checkout', |   selector: 'app-accelerate-checkout', | ||||||
|   templateUrl: './accelerate-checkout.component.html', |   templateUrl: './accelerate-checkout.component.html', | ||||||
| @ -51,9 +53,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|   @Input() miningStats: MiningStats; |   @Input() miningStats: MiningStats; | ||||||
|   @Input() eta: ETA; |   @Input() eta: ETA; | ||||||
|   @Input() scrollEvent: boolean; |   @Input() scrollEvent: boolean; | ||||||
|   @Input() cashappEnabled: boolean; |   @Input() cashappEnabled: boolean = true; | ||||||
|   @Input() showDetails: boolean; |  | ||||||
|   @Input() advancedEnabled: boolean = false; |   @Input() advancedEnabled: boolean = false; | ||||||
|  |   @Input() forceSummary: boolean = false; | ||||||
|   @Input() forceMobile: boolean = false; |   @Input() forceMobile: boolean = false; | ||||||
|   @Output() changeMode = new EventEmitter<boolean>(); |   @Output() changeMode = new EventEmitter<boolean>(); | ||||||
|   @Output() close = new EventEmitter<null>(); |   @Output() close = new EventEmitter<null>(); | ||||||
| @ -64,8 +66,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|   math = Math; |   math = Math; | ||||||
|   isMobile: boolean = window.innerWidth <= 767.98; |   isMobile: boolean = window.innerWidth <= 767.98; | ||||||
| 
 | 
 | ||||||
|   step: 'quote' | 'paymentMethod' | 'checkout' | 'processing' = 'quote'; |   private _step: CheckoutStep = 'summary'; | ||||||
|   simpleMode: boolean = true; |   simpleMode: boolean = true; | ||||||
|  |   showDetails: boolean = false; | ||||||
|   paymentMethod: 'cashapp' | 'btcpay'; |   paymentMethod: 'cashapp' | 'btcpay'; | ||||||
| 
 | 
 | ||||||
|   user: any = undefined; |   user: any = undefined; | ||||||
| @ -117,9 +120,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|     this.user = this.storageService.getAuth()?.user ?? null; |     this.user = this.storageService.getAuth()?.user ?? null; | ||||||
|     const urlParams = new URLSearchParams(window.location.search); |     const urlParams = new URLSearchParams(window.location.search); | ||||||
|     if (urlParams.get('cash_request_id')) { // Redirected from cashapp
 |     if (urlParams.get('cash_request_id')) { // Redirected from cashapp
 | ||||||
|  |       this.moveToStep('processing'); | ||||||
|       this.insertSquare(); |       this.insertSquare(); | ||||||
|       this.setupSquare(); |       this.setupSquare(); | ||||||
|       this.step = 'processing'; |     } else if (this.isLoggedIn() || this.forceSummary) { | ||||||
|  |       this.moveToStep('summary'); | ||||||
|  |     } else { | ||||||
|  |       this.moveToStep('checkout'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.servicesApiService.setupSquare$().subscribe(ids => { |     this.servicesApiService.setupSquare$().subscribe(ids => { | ||||||
| @ -127,9 +134,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|         appId: ids.squareAppId, |         appId: ids.squareAppId, | ||||||
|         locationId: ids.squareLocationId |         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 |   * 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; |           this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; | ||||||
| 
 | 
 | ||||||
|  |           if (this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { | ||||||
|  |             this.loadingBtcpayInvoice = true; | ||||||
|  |             this.requestBTCPayInvoice(); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|           this.calculating = false; |           this.calculating = false; | ||||||
|           this.cd.markForCheck(); |           this.cd.markForCheck(); | ||||||
|         } |         } | ||||||
| @ -244,9 +268,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|    */ |    */ | ||||||
|   accelerate(): void { |   accelerate(): void { | ||||||
|     if (this.isLoggedIn()) { |     if (this.isLoggedIn()) { | ||||||
|       this.accelerateWithMempoolAccount(); |       if (this.step !== 'summary') { | ||||||
|  |         this.moveToStep('summary'); | ||||||
|       } else { |       } else { | ||||||
|       this.step = 'paymentMethod'; |         this.accelerateWithMempoolAccount(); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       this.moveToStep('checkout'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -356,7 +384,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|           button: { shape: 'semiround', size: 'small', theme: 'light'} |           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' }) |           await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' }) | ||||||
|         } |         } | ||||||
|         this.loadingCashapp = false; |         this.loadingCashapp = false; | ||||||
| @ -410,7 +438,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|    * BTCPay |    * BTCPay | ||||||
|    */ |    */ | ||||||
|   async requestBTCPayInvoice() { |   async requestBTCPayInvoice() { | ||||||
|     this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid).subscribe({ |     this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).subscribe({ | ||||||
|       next: (response) => { |       next: (response) => { | ||||||
|         this.invoice = response; |         this.invoice = response; | ||||||
|         this.cd.markForCheck(); |         this.cd.markForCheck(); | ||||||
| @ -425,27 +453,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|   /** |   /** | ||||||
|    * UI events |    * 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) { |   selectedOptionChanged(event) { | ||||||
|     this.choosenOption = event.target.id; |     this.choosenOption = event.target.id; | ||||||
|   } |   } | ||||||
|   closeModal(timeout: number = 0): void { |   closeModal(timeout: number = 0): void { | ||||||
|     setTimeout(() => { |     setTimeout(() => { | ||||||
|       this.step = 'processing'; |       this._step = 'processing'; | ||||||
|       this.cd.markForCheck(); |       this.cd.markForCheck(); | ||||||
|       this.close.emit(); |       this.close.emit(); | ||||||
|     }, timeout); |     }, timeout); | ||||||
| @ -456,6 +469,26 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | |||||||
|     return auth !== null; |     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']) |   @HostListener('window:resize', ['$event']) | ||||||
|   onResize(): void { |   onResize(): void { | ||||||
|     this.isMobile = window.innerWidth <= 767.98; |     this.isMobile = window.innerWidth <= 767.98; | ||||||
|  | |||||||
| @ -1,21 +0,0 @@ | |||||||
| <div class="fee-graph" *ngIf="tx && estimate"> |  | ||||||
|   <div class="column"> |  | ||||||
|     <ng-container *ngFor="let bar of bars"> |  | ||||||
|       <div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);"> |  | ||||||
|         <div class="fill"></div> |  | ||||||
|         <div class="line"> |  | ||||||
|           <p class="fee-rate"> |  | ||||||
|             <span class="label">{{ bar.label }}</span> |  | ||||||
|             <span class="rate"> |  | ||||||
|               <app-fee-rate [fee]="bar.rate"></app-fee-rate> |  | ||||||
|             </span> |  | ||||||
|           </p> |  | ||||||
|         </div> |  | ||||||
|         <div class="spacer"></div> |  | ||||||
|         <span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span> |  | ||||||
|         <div class="spacer"></div> |  | ||||||
|         <div class="spacer"></div> |  | ||||||
|       </div> |  | ||||||
|     </ng-container> |  | ||||||
|   </div> |  | ||||||
| </div> |  | ||||||
| @ -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; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -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 }; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,239 +0,0 @@ | |||||||
| <span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span> |  | ||||||
| <div class="row" *ngIf="showSuccess"> |  | ||||||
|   <div class="col"> |  | ||||||
|     <div class="alert alert-success"> |  | ||||||
|       Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration. |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span> |  | ||||||
| <div class="row" *ngIf="error"> |  | ||||||
|   <div class="col"> |  | ||||||
|     <app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error> |  | ||||||
|   </div> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <div class="accelerate-cols"> |  | ||||||
|   <ng-container *ngIf="!isMobile"> |  | ||||||
|     <app-accelerate-fee-graph |  | ||||||
|       [tx]="tx" |  | ||||||
|       [estimate]="estimate" |  | ||||||
|       [maxRateOptions]="maxRateOptions" |  | ||||||
|       [maxRateIndex]="selectFeeRateIndex" |  | ||||||
|       (setUserBid)="setUserBid($event)" |  | ||||||
|     ></app-accelerate-fee-graph> |  | ||||||
|   </ng-container> |  | ||||||
| 
 |  | ||||||
|   <ng-container *ngIf="estimate else loadingEstimate"> |  | ||||||
|     <div [class]="{estimateDisabled: error || showSuccess }"> |  | ||||||
| 
 |  | ||||||
|       <div *ngIf="user && !estimate.hasAccess"> |  | ||||||
|         <div class="alert alert-mempool">You are currently on the waitlist</div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <ng-template [ngIf]="showDetails"> |  | ||||||
|         <h5 i18n="accelerator.your-transaction">Your transaction</h5> |  | ||||||
|         <div class="row"> |  | ||||||
|           <div class="col"> |  | ||||||
|             <small *ngIf="hasAncestors" class="form-text text-muted mb-2"> |  | ||||||
|               <ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container> |  | ||||||
|             </small> |  | ||||||
|             <table class="table table-borderless table-border table-dark table-background table-accelerator"> |  | ||||||
|               <tbody> |  | ||||||
|                 <tr class="group-first"> |  | ||||||
|                   <td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td> |  | ||||||
|                   <td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr class="info"> |  | ||||||
|                   <td class="info" colspan=3> |  | ||||||
|                     <i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr> |  | ||||||
|                   <td class="item" i18n="accelerator.in-band-fees">In-band fees</td> |  | ||||||
|                   <td style="text-align: end;"> |  | ||||||
|                     {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr class="info group-last"> |  | ||||||
|                   <td class="info" colspan=3> |  | ||||||
|                     <i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               </tbody> |  | ||||||
|             </table> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         <br> |  | ||||||
|       </ng-template> |  | ||||||
|       <h5 *ngIf="estimate?.pools?.length" i18n="accelerator.how-much-faster">How much faster?</h5> |  | ||||||
|       <div class="row"> |  | ||||||
|         <div class="col"> |  | ||||||
|           <ng-container *ngIf="(etaInfo$ | async) as etaInfo; else loadingEstimate"> |  | ||||||
|             <small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to <strong>{{ etaInfo.hashratePercentage | number : '1.1-1' }}%</strong> of miners.</small> |  | ||||||
|             <small class="form-text text-muted mb-2" i18n="accelerator.time-estimate-description">This will reduce your expected waiting time until the first confirmation to <strong><app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></strong></small> |  | ||||||
|           </ng-container> |  | ||||||
|         </div> |  | ||||||
|         <div class="col pie"> |  | ||||||
|           <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div class="row"> |  | ||||||
|         <div class="col"> |  | ||||||
|           <div class="form-group"> |  | ||||||
|             <div class="fee-card"> |  | ||||||
|               <div class="d-flex mb-0"> |  | ||||||
|                 <ng-container *ngFor="let option of maxRateOptions"> |  | ||||||
|                   <button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)"> |  | ||||||
|                     <span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span> |  | ||||||
|                     <span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span> |  | ||||||
|                   </button> |  | ||||||
|                 </ng-container> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|    |  | ||||||
|       <h5>Summary</h5> |  | ||||||
|       <div class="row"> |  | ||||||
|         <div class="col"> |  | ||||||
|           <table class="table table-borderless table-border table-dark table-background table-accelerator"> |  | ||||||
|             <tbody> |  | ||||||
|               <!-- ESTIMATED FEE --> |  | ||||||
|               <ng-template [ngIf]="showDetails"> |  | ||||||
|                 <tr class="group-first"> |  | ||||||
|                   <td class="item" i18n="accelerator.next-block-rate">Next block market rate</td> |  | ||||||
|                   <td class="amt" style="font-size: 16px"> |  | ||||||
|                     {{ estimate.targetFeeRate | number : '1.0-0' }} |  | ||||||
|                   </td> |  | ||||||
|                   <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr class="info"> |  | ||||||
|                   <td class="info"> |  | ||||||
|                     <i><small i18n="accelerator.estimated-extra-fee-required">Estimated extra fee required</small></i> |  | ||||||
|                   </td> |  | ||||||
|                   <td class="amt"> |  | ||||||
|                     {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} |  | ||||||
|                   </td> |  | ||||||
|                   <td class="units"> |  | ||||||
|                     <span class="symbol" i18n="shared.sats">sats</span> |  | ||||||
|                     <span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|                  |  | ||||||
|                 <!-- MEMPOOL BASE FEE --> |  | ||||||
|                 <tr> |  | ||||||
|                   <td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee"> |  | ||||||
|                   <td class="info"> |  | ||||||
|                     <i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i> |  | ||||||
|                   </td> |  | ||||||
|                   <td class="amt"> |  | ||||||
|                     +{{ estimate.mempoolBaseFee | number }} |  | ||||||
|                   </td> |  | ||||||
|                   <td class="units"> |  | ||||||
|                     <span class="symbol" i18n="shared.sats">sats</span> |  | ||||||
|                     <span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               <tr class="info group-last dashed-bottom" *ngIf="estimate.vsizeFee"> |  | ||||||
|                 <td class="info"> |  | ||||||
|                   <i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i> |  | ||||||
|                 </td> |  | ||||||
|                 <td class="amt"> |  | ||||||
|                   +{{ estimate.vsizeFee | number }} |  | ||||||
|                 </td> |  | ||||||
|                 <td class="units"> |  | ||||||
|                   <span class="symbol" i18n="shared.sats">sats</span> |  | ||||||
|                   <span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span> |  | ||||||
|                 </td> |  | ||||||
|               </tr> |  | ||||||
|             </ng-template> |  | ||||||
|                |  | ||||||
|               <!-- NEXT BLOCK ESTIMATE --> |  | ||||||
|               <ng-container> |  | ||||||
|                 <tr class="group-first"> |  | ||||||
|                   <td class="item"> |  | ||||||
|                     <b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b> ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB |  | ||||||
|                   </td> |  | ||||||
|                   <td class="amt"> |  | ||||||
|                     <span style="background-color: #5E35B1" class="p-1 pl-0"> |  | ||||||
|                       {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} |  | ||||||
|                     </span> |  | ||||||
|                   </td> |  | ||||||
|                   <td class="units"> |  | ||||||
|                     <span class="symbol" i18n="shared.sats">sats</span> |  | ||||||
|                     <span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               </ng-container> |  | ||||||
| 
 |  | ||||||
|               <!-- MAX COST --> |  | ||||||
|               <ng-container> |  | ||||||
|                 <tr class="group-first" [class.group-last]="!isLoggedIn() || estimate.userBalance >= maxCost"> |  | ||||||
|                   <td class="item"> |  | ||||||
|                     <b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b>  |  | ||||||
|                   </td> |  | ||||||
|                   <td class="amt"> |  | ||||||
|                     <span style="background-color: var(--primary)" class="p-1 pl-0"> |  | ||||||
|                       {{ maxCost | number }} |  | ||||||
|                     </span> |  | ||||||
|                   </td> |  | ||||||
|                   <td class="units"> |  | ||||||
|                     <span class="symbol" i18n="shared.sats">sats</span> |  | ||||||
|                     <span class="fiat ml-1"> |  | ||||||
|                       <app-fiat [value]="maxCost" [colorClass]="isLoggedIn() && estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> |  | ||||||
|                     </span> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               </ng-container> |  | ||||||
|    |  | ||||||
|               <!-- USER BALANCE --> |  | ||||||
|               <ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost"> |  | ||||||
|                 <tr class="group-first group-last dashed-top"> |  | ||||||
|                   <td class="item" i18n="accelerator.available-balance">Available balance</td> |  | ||||||
|                   <td class="amt"> |  | ||||||
|                     {{ estimate.userBalance | number }} |  | ||||||
|                   </td> |  | ||||||
|                   <td class="units"> |  | ||||||
|                     <span class="symbol" i18n="shared.sats">sats</span> |  | ||||||
|                     <span class="fiat ml-1"> |  | ||||||
|                       <app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> |  | ||||||
|                     </span> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               </ng-container> |  | ||||||
|               <ng-container *ngIf="user && estimate?.hasAccess || !isLoggedIn()"> |  | ||||||
|                 <tr class="group-first group-last" style="border-top: 1px dashed grey"> |  | ||||||
|                   <td class="item"></td> |  | ||||||
|                   <td colspan="2"> |  | ||||||
|                     <div class="d-flex"> |  | ||||||
|                       @if (isLoggedIn()) { |  | ||||||
|                         @if (user && estimate.hasAccess) { |  | ||||||
|                           <button class="btn btn-sm btn-primary btn-success flex-grow-1" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button> |  | ||||||
|                         } |  | ||||||
|                       } @else if (stateService.isMempoolSpaceBuild) { |  | ||||||
|                         <a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1" i18n="shared.sign-in">Sign In</a> |  | ||||||
|                       } @else { |  | ||||||
|                         <a [href]="'https://mempool.space/tx/' + tx.txid + '#accelerate'" class="btn btn-purple flex-grow-1" i18n="accelerator.accelerate-on-mempoolspace">Accelerate on mempool.space</a> |  | ||||||
|                       } |  | ||||||
|                     </div> |  | ||||||
|                   </td> |  | ||||||
|                 </tr> |  | ||||||
|               </ng-container> |  | ||||||
|             </tbody> |  | ||||||
|           </table> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </ng-container> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <ng-template #loadingEstimate> |  | ||||||
|   <div class="skeleton-loader"></div> |  | ||||||
|   <br> |  | ||||||
| </ng-template> |  | ||||||
| @ -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; |  | ||||||
| } |  | ||||||
| @ -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; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,5 +1,6 @@ | |||||||
| <div class="wrapper"> | <div class="wrapper"> | ||||||
| 
 | 
 | ||||||
|  |   @if (!minimal) { | ||||||
|     <span *ngIf="paymentStatus === 3" class="valid-feedback d-block mt-5"> |     <span *ngIf="paymentStatus === 3" class="valid-feedback d-block mt-5"> | ||||||
|       Payment successful. You can close this page. |       Payment successful. You can close this page. | ||||||
|     </span> |     </span> | ||||||
| @ -7,6 +8,7 @@ | |||||||
|     <span *ngIf="paymentStatus === 4" class="valid-feedback d-block mt-5"> |     <span *ngIf="paymentStatus === 4" class="valid-feedback d-block mt-5"> | ||||||
|       A transaction <a [href]="'/tx/' + invoice.cryptoInfo[0].payments[0].id.split('-')[0]">has been detected in the mempool</a> fully paying for this invoice. Waiting for on-chain confirmation. |       A transaction <a [href]="'/tx/' + invoice.cryptoInfo[0].payments[0].id.split('-')[0]">has been detected in the mempool</a> fully paying for this invoice. Waiting for on-chain confirmation. | ||||||
|     </span> |     </span> | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   <div *ngIf="paymentStatus === 2"> |   <div *ngIf="paymentStatus === 2"> | ||||||
|      |      | ||||||
| @ -30,7 +32,7 @@ | |||||||
| 
 | 
 | ||||||
|     <ng-template [ngIf]="paymentForm.get('method')?.value === 'chain' && invoice"> |     <ng-template [ngIf]="paymentForm.get('method')?.value === 'chain' && invoice"> | ||||||
| 
 | 
 | ||||||
|         <div class="qr-wrapper"> |         <div class="qr-wrapper" [class.mt-0]="minimal"> | ||||||
|           <a [href]="bypassSecurityTrustUrl('bitcoin:' + invoice.addresses.BTC + '?amount=' + invoice.amount)" target="_blank"> |           <a [href]="bypassSecurityTrustUrl('bitcoin:' + invoice.addresses.BTC + '?amount=' + invoice.amount)" target="_blank"> | ||||||
|               <app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + invoice.addresses.BTC + '?amount=' + invoice.amount"></app-qrcode> |               <app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + invoice.addresses.BTC + '?amount=' + invoice.amount"></app-qrcode> | ||||||
|           </a> |           </a> | ||||||
| @ -42,13 +44,15 @@ | |||||||
|               <button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.BTC"></app-clipboard></button> |               <button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.BTC"></app-clipboard></button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |         @if (!minimal) { | ||||||
|           <p>{{ invoice.amount }} <span class="symbol">BTC</span></p> |           <p>{{ invoice.amount }} <span class="symbol">BTC</span></p> | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|     </ng-template> |     </ng-template> | ||||||
| 
 | 
 | ||||||
|     <ng-template [ngIf]="paymentForm.get('method')?.value === 'lightning' && invoice && invoice.addresses.BTC_LightningLike"> |     <ng-template [ngIf]="paymentForm.get('method')?.value === 'lightning' && invoice && invoice.addresses.BTC_LightningLike"> | ||||||
| 
 | 
 | ||||||
|         <div class="qr-wrapper"> |         <div class="qr-wrapper" [class.mt-0]="minimal"> | ||||||
|           <a [href]="bypassSecurityTrustUrl('lightning:' + invoice.addresses.BTC_LightningLike)" target="_blank"> |           <a [href]="bypassSecurityTrustUrl('lightning:' + invoice.addresses.BTC_LightningLike)" target="_blank"> | ||||||
|               <app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="invoice.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode> |               <app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="invoice.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode> | ||||||
|           </a> |           </a> | ||||||
| @ -61,13 +65,15 @@ | |||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|  |         @if (!minimal) { | ||||||
|           <p>{{ invoice.amount * 100_000_000 }} <span class="symbol">sats</span></p> |           <p>{{ invoice.amount * 100_000_000 }} <span class="symbol">sats</span></p> | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|     </ng-template> |     </ng-template> | ||||||
| 
 | 
 | ||||||
|     <ng-template [ngIf]="invoice && (paymentForm.get('method')?.value === 'lbtc' || paymentForm.get('method')?.value === 'tlbtc')"> |     <ng-template [ngIf]="invoice && (paymentForm.get('method')?.value === 'lbtc' || paymentForm.get('method')?.value === 'tlbtc')"> | ||||||
| 
 | 
 | ||||||
|         <div class="qr-wrapper"> |         <div class="qr-wrapper" [class.mt-0]="minimal"> | ||||||
|           <a [href]="bypassSecurityTrustUrl('liquidnetwork:' + invoice.addresses.LBTC + '?amount=' + invoice.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank"> |           <a [href]="bypassSecurityTrustUrl('liquidnetwork:' + invoice.addresses.LBTC + '?amount=' + invoice.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank"> | ||||||
|               <app-qrcode imageUrl="/resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + invoice.addresses.LBTC + '?amount=' + invoice.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode> |               <app-qrcode imageUrl="/resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + invoice.addresses.LBTC + '?amount=' + invoice.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode> | ||||||
|           </a> |           </a> | ||||||
| @ -79,11 +85,15 @@ | |||||||
|               <button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.LBTC"></app-clipboard></button> |               <button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.LBTC"></app-clipboard></button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |         @if (!minimal) { | ||||||
|           <p>{{ invoice.amount }} <span class="symbol">BTC</span></p> |           <p>{{ invoice.amount }} <span class="symbol">BTC</span></p> | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|     </ng-template> |     </ng-template> | ||||||
| 
 | 
 | ||||||
|  |     @if (!minimal) { | ||||||
|       <p>Waiting for transaction... </p> |       <p>Waiting for transaction... </p> | ||||||
|       <div class="spinner-border text-light"></div> |       <div class="spinner-border text-light"></div> | ||||||
|  |     } | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
| @ -140,6 +140,7 @@ | |||||||
| 
 | 
 | ||||||
| .wrapper { | .wrapper { | ||||||
|   text-align: center; |   text-align: center; | ||||||
|  |   width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .input-dark { | .input-dark { | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import { ServicesApiServices } from '../../services/services-api.service'; | |||||||
| export class BitcoinInvoiceComponent implements OnInit, OnDestroy { | export class BitcoinInvoiceComponent implements OnInit, OnDestroy { | ||||||
|   @Input() invoiceId: string; |   @Input() invoiceId: string; | ||||||
|   @Input() redirect = true; |   @Input() redirect = true; | ||||||
|  |   @Input() minimal = false; | ||||||
|   @Output() completed = new EventEmitter(); |   @Output() completed = new EventEmitter(); | ||||||
| 
 | 
 | ||||||
|   paymentForm: FormGroup; |   paymentForm: FormGroup; | ||||||
|  | |||||||
| @ -117,7 +117,7 @@ | |||||||
|     <div class="bottom-panel"> |     <div class="bottom-panel"> | ||||||
|       @if (showAccelerationSummary && !accelerationFlowCompleted) { |       @if (showAccelerationSummary && !accelerationFlowCompleted) { | ||||||
|         <ng-container *ngIf="(ETA$ | async) as eta;"> |         <ng-container *ngIf="(ETA$ | async) as eta;"> | ||||||
|           <app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [tx]="tx" [miningStats]="miningStats" [eta]="eta" [forceMobile]="true" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout> |           <app-accelerate-checkout *ngIf="(da$ | async) as da;" [forceSummary]="true" [cashappEnabled]="accelerationEligible" [advancedEnabled]="false" [forceMobile]="true" [tx]="tx" [miningStats]="miningStats" [eta]="eta" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|       } @else { |       } @else { | ||||||
|         @if (tx?.acceleration && !tx.status?.confirmed) { |         @if (tx?.acceleration && !tx.status?.confirmed) { | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								frontend/src/app/components/tracker/tracker.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/src/app/components/tracker/tracker.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 { } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @ -80,12 +80,11 @@ | |||||||
|       <div class="title float-left"> |       <div class="title float-left"> | ||||||
|         <h2 i18n="transaction.accelerate|Accelerate button label">Accelerate</h2> |         <h2 i18n="transaction.accelerate|Accelerate button label">Accelerate</h2> | ||||||
|       </div> |       </div> | ||||||
|       <button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="showAccelerationDetails = !showAccelerationDetails" i18n="transaction.details|Transaction Details">Details</button> |  | ||||||
| 
 | 
 | ||||||
|       <div class="clearfix"></div> |       <div class="clearfix"></div> | ||||||
| 
 | 
 | ||||||
|       <ng-container *ngIf="(ETA$ | async) as eta;"> |       <ng-container *ngIf="(ETA$ | async) as eta;"> | ||||||
|         <app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [tx]="tx" [eta]="eta" [miningStats]="miningStats" (close)="showAccelerationSummary = false" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"> |         <app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [advancedEnabled]="true" [tx]="tx" [eta]="eta" [miningStats]="miningStats" (close)="showAccelerationSummary = false" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"> | ||||||
|           <span slot="cta-title">Urgent transaction? Get it confirmed faster.</span> |           <span slot="cta-title">Urgent transaction? Get it confirmed faster.</span> | ||||||
|         </app-accelerate-checkout> |         </app-accelerate-checkout> | ||||||
|       </ng-container> |       </ng-container> | ||||||
|  | |||||||
| @ -7,8 +7,6 @@ import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; | |||||||
| import { GraphsModule } from '../../graphs/graphs.module'; | import { GraphsModule } from '../../graphs/graphs.module'; | ||||||
| import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; | import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; | ||||||
| import { AccelerateFeeGraphComponent } from '../accelerate-checkout/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'; |  | ||||||
| 
 | 
 | ||||||
| const routes: Routes = [ | const routes: Routes = [ | ||||||
|   { |   { | ||||||
| @ -40,8 +38,11 @@ export class TransactionRoutingModule { } | |||||||
|   ], |   ], | ||||||
|   declarations: [ |   declarations: [ | ||||||
|     TransactionComponent, |     TransactionComponent, | ||||||
|     TrackerComponent, |     AccelerateCheckout, | ||||||
|     TrackerBarComponent, |     AccelerateFeeGraphComponent, | ||||||
|  |   ], | ||||||
|  |   exports: [ | ||||||
|  |     TransactionComponent, | ||||||
|     AccelerateCheckout, |     AccelerateCheckout, | ||||||
|     AccelerateFeeGraphComponent, |     AccelerateFeeGraphComponent, | ||||||
|   ] |   ] | ||||||
|  | |||||||
| @ -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' }); |     return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   generateBTCPayAcceleratorInvoice$(txid: string): Observable<any> { |   generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable<any> { | ||||||
|     const params = { |     const params = { | ||||||
|       product: txid |       product: txid, | ||||||
|  |       amount: sats, | ||||||
|     }; |     }; | ||||||
|     return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/payments/bitcoin`, params); |     return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/payments/bitcoin`, params); | ||||||
|   } |   } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user