[accelerator] accelerate with lightning

This commit is contained in:
nymkappa 2024-06-26 18:35:36 +09:00 committed by Mononaut
parent f1572f0038
commit 66a88b8422
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
10 changed files with 453 additions and 48 deletions

View File

@ -58,12 +58,24 @@
</div>
</div>
</form>
}
@else if (step === 'checkout') {
<!-- Show checkout page -->
} @else if (step === 'paymentMethod') {
<div class="row mb-md-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Select your payment method</h1>
</div>
</div>
<div class="pt-2 d-flex justify-content-around">
@if (cashappEnabled) {
<img class="paymentMethod" src="/resources/cash-app.svg" height=55 (click)="selectPaymentMethod('cashapp')">
}
<img class="paymentMethod" src="/resources/btcpay.svg" height=55 (click)="selectPaymentMethod('btcpay')">
</div>
} @else if (step === 'checkout') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center">
<div class="col-sm" id="confirm-payment-title">
<h1 style="font-size: larger;">Confirm your payment</h1>
</div>
</div>
@ -76,36 +88,40 @@
</div>
</div>
@if (!loadingCashapp) {
@if (paymentMethod === 'cashapp') {
@if (!loadingCashapp) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
<span><u><strong>Total additional cost</strong></u><br>
<span style="font-size: 16px" class="d-block mt-2">
Pay
<strong><app-fiat [value]="cost"></app-fiat></strong>
with
</span>
</span>
</div>
</div>
</div>
}
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
<span><u><strong>Total additional cost</strong></u><br>
<span style="font-size: 16px" class="d-block mt-2">
Pay
<strong><app-fiat [value]="cost"></app-fiat></strong>
with
</span>
</span>
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
@if (loadingCashapp) {
<div display="d-flex flex-row justify-content-center">
<span>Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div>
</div>
</div>
} @else if (paymentMethod === 'btcpay' && invoice?.btcpayInvoiceId) {
<app-bitcoin-invoice [invoiceId]="invoice.btcpayInvoiceId" (completed)="closeModal(2000)"></app-bitcoin-invoice>
}
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
@if (loadingCashapp) {
<div display="d-flex flex-row justify-content-center">
<span>Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div>
</div>
</div>
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
@ -118,7 +134,7 @@
@else if (step === 'processing') {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Confirm your payment</h1>
<h1 style="font-size: larger;">Confirming your payment</h1>
</div>
</div>

View File

@ -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;
}

View File

@ -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<null>();
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);
}
}

View File

@ -0,0 +1,89 @@
<div class="wrapper">
<span *ngIf="paymentStatus === 3" class="valid-feedback d-block mt-5">
Payment successful. You can close this page.
</span>
<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.
</span>
<div *ngIf="paymentStatus === 2">
<form [formGroup]="paymentForm">
<div class="form-group">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<!-- <label *ngIf="invoice.addresses.BTC" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'chain'}">
<input type="radio" value="chain" formControlName="method"> <fa-icon [icon]="['fas', 'link']" [fixedWidth]="true" title="Onchain"></fa-icon>
</label> -->
<label *ngIf="invoice.addresses.BTC_LightningLike" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'lightning'}">
<input type="radio" value="lightning" formControlName="method"> <fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" title="Lightning"></fa-icon>
</label>
<!-- <label *ngIf="invoice.addresses.LBTC" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'lbtc'}">
<input type="radio" value="lbtc" formControlName="method"> <fa-icon [icon]="['fas', 'tint']" [fixedWidth]="true" title="Liquid Bitcoin"></fa-icon>
</label> -->
</div>
</div>
</form>
<ng-template [ngIf]="paymentForm.get('method')?.value === 'chain' && invoice">
<div class="qr-wrapper">
<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>
</a>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control input-dark" readonly [value]="invoice.addresses.BTC">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.BTC"></app-clipboard></button>
</div>
</div>
<p>{{ invoice.amount }} <span class="symbol">BTC</span></p>
</ng-template>
<ng-template [ngIf]="paymentForm.get('method')?.value === 'lightning' && invoice">
<div class="qr-wrapper">
<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>
</a>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control input-dark" readonly [value]="invoice.addresses.BTC_LightningLike">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="invoice.addresses.BTC_LightningLike"></app-clipboard></button>
</div>
</div>
<p>{{ invoice.amount * 100_000_000 }} <span class="symbol">sats</span></p>
</ng-template>
<ng-template [ngIf]="invoice && (paymentForm.get('method')?.value === 'lbtc' || paymentForm.get('method')?.value === 'tlbtc')">
<div class="qr-wrapper">
<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>
</a>
</div>
<br>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control input-dark" readonly [value]="invoice.addresses.LBTC" />
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.LBTC"></app-clipboard></button>
</div>
</div>
<p>{{ invoice.amount }} <span class="symbol">BTC</span></p>
</ng-template>
<p>Waiting for transaction... </p>
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -75,7 +75,7 @@
} @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
}
@if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
@if (!showAccelerationSummary && isMobile && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
<a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
@ -116,7 +116,7 @@
<div class="bottom-panel">
@if (showAccelerationSummary && !accelerationFlowCompleted) {
<app-accelerate-checkout *ngIf="(da$ | async) as da;" [txid]="tx.txid" [eta]="mempoolPosition?.block >= 7 ? null : da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout>
<app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [txid]="tx.txid" [eta]="mempoolPosition?.block >= 7 ? null : da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout>
} @else {
@if (tx?.acceleration && !tx.status?.confirmed) {
<div class="progress-icon">

View File

@ -107,7 +107,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
now = Date.now();
da$: Observable<DifficultyAdjustment>;
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 {

View File

@ -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<any> {
const params = {
product: txid
};
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/payments/bitcoin`, params);
}
retreiveInvoice$(invoiceId: string): Observable<any[]> {
return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/invoice?id=${invoiceId}`);
}
getPaymentStatus$(orderId: string): Observable<any[]> {
return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/check?order_id=${orderId}`);
}
}

View File

@ -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,