Merge pull request #5233 from mempool/mononaut/hybrid-acceleration-checkout

hybrid acceleration checkout
This commit is contained in:
wiz 2024-07-01 00:27:53 +09:00 committed by GitHub
commit a9c3637c7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1458 additions and 780 deletions

View File

@ -146,8 +146,9 @@ let routes: Routes = [
data: { preload: true },
},
{
path: 'tracker/:id',
component: TrackerComponent,
path: 'tracker',
data: { networkSpecific: true },
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: 'wallet',

View File

@ -1,77 +1,412 @@
<div class="container-md card w-100" style="padding: 1em; background: var(--box-bg)" id=acceleratePreviewAnchor>
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
<div class="row mt-2" *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>
@if (error) {
<div class="mt-2">
<app-mempool-error [error]="error"></app-mempool-error>
</div>
}
@else if (step === 'cta') {
<!-- Show A/B CTAs -->
<div class="row mb-1">
<div class="col-sm">
<h1 style="font-size: larger;">Accelerate your Bitcoin transaction?</h1>
<div class="row mt-2">
<div class="col">
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
</div>
</div>
}
@else if (step === 'quote') {
<div class="accelerate-cols">
<ng-container *ngIf="!isMobile">
<app-accelerate-fee-graph
[tx]="tx"
[estimate]="estimate"
[showEstimate]="isLoggedIn()"
[maxRateOptions]="maxRateOptions"
[maxRateIndex]="selectFeeRateIndex"
(setUserBid)="setUserBid($event)"
></app-accelerate-fee-graph>
</ng-container>
<form>
<div class="row">
<div class="col-sm">
<div class="form-group form-check mb-2">
<input type="radio" class="form-check-input" id="accelerate" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="accelerate">
<span class="font-weight-bold">Accelerate</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>
@if (!calculating) {
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span>)
} @else {
<span class="estimating">Calculating cost...</span>
}
</span>
</label>
<ng-container *ngIf="estimate else loadingEstimate">
<div [class.disabled]="error || showSuccess">
<div *ngIf="isLoggedIn() && !estimate.hasAccess" style="margin-right: 5em;">
<div class="alert alert-mempool mr-6">You are currently on the waitlist</div>
</div>
@if (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]="'&lrm;' + (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>
}
<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 checkout-text 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 checkout-text 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-container *ngIf="showDetails">
@if (isLoggedIn()) {
<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>
}
@else {
<!-- TARGET FEE -->
<tr class="group-first">
<td class="item" i18n="accelerator.target-rate">Target rate</td>
<td class="amt" style="font-size: 16px">
{{ maxRateOptions[selectFeeRateIndex].rate | 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.extra-fee-required">Extra fee required</small></i>
</td>
<td class="amt">
{{ maxRateOptions[selectFeeRateIndex].fee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="maxRateOptions[selectFeeRateIndex].fee"></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-container>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="isLoggedIn()">
<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 group-last">
<td class="item">
@if (isLoggedIn()) {
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b>
} @else {
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.cost">Acceleration cost</b>
}
</td>
<td class="amt">
<span style="background-color: var(--primary)" class="p-1 pl-0">
{{ cost | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="cost" [colorClass]="isLoggedIn() && estimate.userBalance < cost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < cost">
<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 < cost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td colspan="2">
<div class="d-flex">
<ng-container *ngTemplateOutlet="accelerateButton"></ng-container>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</ng-container>
</div>
<hr>
<div class="row mt-2 mb-2 text-center">
<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)="moveToStep('summary')">Go Back</button>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group form-check mb-2">
<input type="radio" class="form-check-input" id="wait" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="wait">
<span class="font-weight-bold">Wait</span>
@if (eta) {
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected <app-time kind="within" [time]="eta" [fastRender]="false" [fixedRender]="true"></app-time></span>
} @else {
<span style="color: rgb(186, 186, 186); font-size: 14px;">
<span>Settlement expected within several hours</span>
</div>
<ng-template #loadingEstimate>
<div class="skeleton-loader"></div>
<br>
</ng-template>
}
@else if (step === 'summary') {
<ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingSummary">
<!-- Show A/B CTAs -->
@if (!noCTA) {
<div class="row mb-1">
<div class="col-sm">
<h1 style="font-size: larger;"><ng-content select="[slot='cta-title']"></ng-content><span class="default-slot">Accelerate your Bitcoin transaction?</span></h1>
</div>
</div>
}
<div *ngIf="isLoggedIn() && !estimate.hasAccess">
<div class="alert alert-mempool mr-6">You are currently on the waitlist for Mempool Accelerator&trade;</div>
</div>
<form [class.disabled]="error || showSuccess">
<div class="row summary-row">
<div>
<div class="form-group form-check mb-2">
<div class="float-right"><ng-container *ngTemplateOutlet="customizeButton"></ng-container></div>
<input type="checkbox" [checked]="armed" class="form-check-input" [class.error-shake]="misfire" id="accel" name="accel" (change)="armed = !armed; misfire = false">
<label class="form-check-label d-flex flex-column" for="accel">
<span><b>Accelerate</b> to ~{{ ((userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} sat/vB</span>
<span class="checkout-text">Confirmation expected <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time><br>
@if (!calculating) {
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span>)
} @else {
<span class="estimating">Calculating cost...</span>
}
</span>
}
</label>
</label>
</div>
</div>
<div class="pie d-none d-lg-flex" *ngIf="!forceMobile">
<small class="form-text checkout-text mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.</small>
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box>
</div>
<ng-container *ngTemplateOutlet="accelerateButton"></ng-container>
</div>
</form>
</ng-container>
<ng-template #loadingSummary>
<div class="row">
<div class="col-md">
<div class="d-flex flex-row justify-content-center align-items-center">
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
<div class="row mt-2 mb-2" [style]="(choosenOption === 'wait' || calculating) ? 'opacity: 0.25; pointer-events: none' : ''">
<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()">
</ng-template>
} @else if (step === 'checkout') {
<ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingCheckout">
<div class="row">
<div class="col-md">
<div class="d-flex flex-column">
<span><b>Accelerate</b> to ~{{ ((userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} sat/vB</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>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span>)
} @else {
<span class="estimating">Calculating cost...</span>
}
</span>
<span class="checkout-text" *ngIf="(etaInfo$ | async) as etaInfo">
Reducing expected confirmation time to <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time>
</span>
</div>
</div>
<div class="col-md pie d-none d-md-flex" *ngIf="!forceMobile">
<small class="form-text checkout-text 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-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box>
</div>
</div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="d-flex justify-content-center" [class.grayOut]="!canPayWithBalance || error || showSuccess">
<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">
<span>Accelerate</span>
</button>
</div>
} @else {
<div class="payment-area mt-2 p-2" [class.disabled]="error || showSuccess">
<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;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p>
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [invoiceId]="invoice.id" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else {
<p>Loading invoice...</p>
<div class="d-flex align-items-center justify-content-center" style="width: 100%; height: 292px;">
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
</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">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div>
}
}
@if (canPayWithCashapp) {
<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/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
</div>
}
</div>
</div>
}
</ng-container>
<ng-template #loadingCheckout>
<div class="row">
<div class="col-md">
<div class="d-flex flex-row justify-content-center align-items-center">
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
</form>
}
@else if (step === 'checkout') {
</ng-template>
<hr>
<div class="row mt-2 mb-2 text-center">
<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)="moveToStep('summary')">Go Back</button>
</div>
</div>
} @else if (step === 'cashapp') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Confirm your payment</h1>
<div class="col-sm" id="confirm-payment-title">
<h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot">Confirm your payment</span></h1>
</div>
</div>
<div class="row text-center">
<div class="col-sm">
<div class="form-group w-100" style="font-size: 14px">
Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + txid" target="_blank">{{ txid.substr(0, 10) }}..{{ txid.substr(-10) }}</a>
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>
</div>
</div>
</div>
@ -109,16 +444,14 @@
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<small>Changed your mind?</small>
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="step = 'cta'">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>
}
@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;"><ng-content select="[slot='processing-title']"></ng-content><span class="default-slot">Confirming your payment</span></h1>
</div>
</div>
@ -135,5 +468,38 @@
</div>
</div>
}
@else if (step === 'paid') {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;"><ng-content select="[slot='accelerating-title']"></ng-content><span class="default-slot">Accelerating your transaction</span></h1>
</div>
</div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
<span>Confirming your acceleration with our mining pool partners...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
}
</div>
<ng-template #customizeButton>
<button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-3" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button>
</ng-template>
<ng-template #accelerateButton>
@if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) {
<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.grayOut]="!canPay || (!armed && step === 'summary') || calculating" 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>
} @else {
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center disabled" style="width: 200px">
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
<span>Coming soon</span>
</button>
}
</ng-template>

View File

@ -7,3 +7,204 @@
.estimating {
color: var(--green)
}
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
border-radius: 15px;
border: 2px solid var(--bg);
cursor: pointer;
}
.default-slot:not(:only-child) {
display: none;
}
.pie {
display: flex;
align-items: center;
max-width: 330px;
}
.fee-card {
padding: 15px;
background-color: var(--bg);
.feerate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.rate {
font-size: 0.9em;
.symbol {
color: white;
}
}
}
}
.btn-border {
border: solid 1px black;
background-color: #0c4a87;
}
.feerate.active {
background-color: var(--primary) !important;
opacity: 1;
border: 1px solid #007fff !important;
}
.feerate:focus {
box-shadow: none !important;
}
.grayOut {
opacity: 0.5;
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
.table-toggle {
width: 100%;
margin-top: 0.5em;
}
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator {
tr {
td {
padding-top: 0;
padding-bottom: 0;
vertical-align: baseline;
}
&.group-first {
td {
padding-top: 0.75rem;
}
}
&.group-last, &:last-child {
td {
padding-bottom: 0.75rem;
}
}
&.dashed-top {
border-top: 1px dashed grey;
}
&.dashed-bottom {
border-bottom: 1px dashed grey
}
}
td {
&:first-child {
width: 100vw;
}
&.info {
color: #6c757d;
white-space: initial;
}
&.amt {
text-align: right;
padding-right: 0.2em;
}
&.units {
padding-left: 0.2em;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
.accelerate-cols {
display: flex;
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}
.payment-area {
background: var(--bg);
}
.col.pie {
flex-grow: 0;
padding: 0 1em;
position: relative;
top: -15px;
}
.item {
white-space: initial;
}
.table-background {
background-color: var(--bg);
}
.checkout-text {
color: rgb(186, 186, 186);
font-size: 14px;
}
.btn-accelerate {
background-color: var(--tertiary);
}
.btn-small-height {
line-height: 1;
}
.summary-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 2em;
flex-wrap: wrap;
@media (max-width: 640px) {
flex-direction: column;
}
}
@keyframes box-shake {
0% { transform: rotate(0deg); }
10% { transform: rotate(-8deg); }
20% { transform: rotate(8deg); }
30% { transform: rotate(-8deg); }
40% { transform: rotate(8deg); }
50% { transform: rotate(-8deg); }
60% { transform: rotate(8deg); }
70% { transform: rotate(-8deg); }
80% { transform: rotate(8deg); }
90% { transform: rotate(-8deg); }
100% { transform: rotate(0deg); }
}
.error-shake {
box-shadow: 0 0 10px 2px var(--danger);
animation: box-shake 1.5s ease-in-out;
}

View File

@ -1,11 +1,47 @@
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges } from '@angular/core';
import { Subscription, tap, of, catchError, Observable } from 'rxjs';
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '../../services/services-api.service';
import { nextRoundNumber } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { AccelerationEstimate } from '../accelerate-preview/accelerate-preview.component';
import { EtaService } from '../../services/eta.service';
import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service';
import { StorageService } from '../../services/storage.service';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
export type AccelerationEstimate = {
hasAccess: boolean;
txSummary: TxSummary;
nextBlockFee: number;
targetFeeRate: number;
userBalance: number;
enoughBalance: boolean;
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: PaymentMethod[];
}
export type TxSummary = {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface RateOption {
fee: number;
rate: number;
index: number;
}
export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid';
@Component({
selector: 'app-accelerate-checkout',
@ -13,23 +49,50 @@ import { EtaService } from '../../services/eta.service';
styleUrls: ['./accelerate-checkout.component.scss']
})
export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: number | null = null;
@Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
@Input() tx: Transaction;
@Input() miningStats: MiningStats;
@Input() eta: ETA;
@Input() scrollEvent: boolean;
@Output() close = new EventEmitter<null>();
@Input() cashappEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@Input() noCTA: boolean = false;
@Output() hasDetails = new EventEmitter<boolean>();
@Output() changeMode = new EventEmitter<boolean>();
calculating = true;
choosenOption: 'wait' | 'accelerate' = 'wait';
armed = false;
misfire = false;
error = '';
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
private _step: CheckoutStep = 'summary';
simpleMode: boolean = true;
paymentMethod: 'cashapp' | 'btcpay';
user: any = undefined;
// accelerator stuff
square: { appId: string, locationId: string};
accelerationUUID: string;
accelerationSubscription: Subscription;
difficultySubscription: Subscription;
estimateSubscription: Subscription;
estimate: AccelerationEstimate;
maxBidBoost: number; // sats
cost: number; // sats
etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>;
showSuccess = false;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
maxBidAllowed = 0;
defaultBid = 0;
userBid = 0;
selectFeeRateIndex = 1;
maxRateOptions: RateOption[] = [];
// square
loadingCashapp = false;
@ -38,11 +101,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
cashAppPay: any;
cashAppSubscription: Subscription;
conversionsSubscription: Subscription;
step: 'cta' | 'checkout' | 'processing' = 'cta';
// btcpay
loadingBtcpayInvoice = false;
invoice = undefined;
constructor(
public stateService: StateService,
private servicesApiService: ServicesApiServices,
private stateService: StateService,
private storageService: StorageService,
private etaService: EtaService,
private audioService: AudioService,
private cd: ChangeDetectorRef
@ -51,11 +118,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
ngOnInit() {
this.user = this.storageService.getAuth()?.user ?? null;
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing');
this.insertSquare();
this.setupSquare();
this.step = 'processing';
} else {
this.moveToStep('summary');
}
this.servicesApiService.setupSquare$().subscribe(ids => {
@ -63,9 +133,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
if (this.step === 'cta') {
this.fetchEstimate();
}
});
}
@ -76,20 +143,38 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
}
moveToStep(step: CheckoutStep) {
this._step = step;
this.misfire = false;
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
this.fetchEstimate();
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.loadingBtcpayInvoice = true;
this.invoice = null;
this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true;
this.insertSquare();
this.setupSquare();
}
this.hasDetails.emit(this._step === 'quote');
}
/**
* Scroll to element id with or without setTimeout
*/
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void {
setTimeout(() => {
this.scrollToPreview(id, position);
}, 1000);
this.scrollToElement(id, position);
}, timeout);
}
scrollToPreview(id: string, position: ScrollLogicalPosition) {
scrollToElement(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
@ -109,9 +194,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.estimateSubscription.unsubscribe();
}
this.calculating = true;
this.estimateSubscription = this.servicesApiService.estimate$(this.txid).pipe(
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
this.calculating = false;
if (response.status === 204) {
this.error = `cannot_accelerate_tx`;
} else {
@ -120,22 +204,113 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.error = `cannot_accelerate_tx`;
return;
}
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
}
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate, this.miningStats);
// Make min extra fee at least 50% of the current tx fee
const minExtraBoost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
const DEFAULT_BID_RATIO = 1.5;
this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO;
this.cost = this.maxBidBoost + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate);
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
return {
fee: this.minExtraCost * multiplier,
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
index,
};
});
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
this.userBid = this.defaultBid;
if (this.userBid < this.minBidAllowed) {
this.userBid = this.minBidAllowed;
} else if (this.userBid > this.maxBidAllowed) {
this.userBid = this.maxBidAllowed;
}
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice();
}
this.calculating = false;
this.cd.markForCheck();
}
}),
catchError((response) => {
this.estimate = undefined;
this.error = `cannot_accelerate_tx`;
this.estimateSubscription.unsubscribe();
return of(null);
})
).subscribe();
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}): void {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
}
}
/**
* Advanced mode acceleration button clicked
*/
accelerate(): void {
if (this.canPay && !this.calculating) {
if ((!this.armed && this.step === 'summary')) {
this.misfire = true;
} else {
if (this.isLoggedIn()) {
this.accelerateWithMempoolAccount();
} else {
this.armed = true;
this.moveToStep('checkout');
}
}
}
}
/**
* Account-based acceleration request
*/
accelerateWithMempoolAccount(): void {
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid,
this.accelerationUUID
).subscribe({
next: () => {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.estimateSubscription.unsubscribe();
this.moveToStep('paid')
},
error: (response) => {
if (response.status === 403 && response.error === 'not_available') {
this.error = 'waitlisted';
} else {
this.error = response.error;
}
}
});
}
/**
* Square
*/
@ -205,17 +380,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
amount: costUSD.toString(),
label: 'Total',
pending: true,
productUrl: `${redirectHostname}/tracker/${this.txid}`,
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
},
button: { shape: 'semiround', size: 'small', theme: 'light'}
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `${redirectHostname}/tracker/${this.txid}`,
referenceId: `accelerator-${this.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`,
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
button: { shape: 'semiround', size: 'small', theme: 'light'}
});
if (this.step === 'checkout') {
if (this.step === 'cashapp') {
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
}
this.loadingCashapp = false;
@ -227,7 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.error = error;
} else if (tokenResult.status === 'OK') {
that.servicesApiService.accelerateWithCashApp$(
that.txid,
that.tx.txid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
@ -239,7 +414,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
that.cashAppPay.destroy();
}
setTimeout(() => {
that.closeModal();
this.moveToStep('paid');
if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@ -266,18 +441,56 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
/**
* UI events
* BTCPay
*/
enableCheckoutPage() {
this.step = 'checkout';
this.loadingCashapp = true;
this.insertSquare();
this.setupSquare();
async requestBTCPayInvoice() {
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
}),
catchError(error => {
console.log(error);
return of(null);
})
).subscribe((invoice) => {
this.invoice = invoice;
this.cd.markForCheck();
});
}
selectedOptionChanged(event) {
this.choosenOption = event.target.id;
bitcoinPaymentCompleted(): void {
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();
this.moveToStep('paid')
}
closeModal(): void {
this.close.emit();
isLoggedIn(): boolean {
const auth = this.storageService.getAuth();
return auth !== null;
}
get step() {
return this._step;
}
get canPayWithBitcoin() {
return this.estimate?.availablePaymentMethods?.includes('bitcoin');
}
get canPayWithCashapp() {
return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp') && this.cost < 400000 && this.stateService.referrer === 'https://cash.app/';
}
get canPayWithBalance() {
return this.isLoggedIn() && this.estimate?.availablePaymentMethods?.includes('balance') && this.estimate?.hasAccess;
}
get canPay() {
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@ -5,7 +5,7 @@ import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar {
rate: number;
@ -25,6 +25,7 @@ interface GraphBar {
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() showEstimate = false;
@Input() maxRateOptions: RateOption[] = [];
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
@ -52,7 +53,7 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: $localize`maximum`,
label: this.showEstimate ? $localize`maximum` : $localize`accelerated`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,

View File

@ -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]="'&lrm;' + (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>

View File

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

View File

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

View File

@ -0,0 +1,99 @@
<div class="wrapper">
@if (!minimal) {
<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/' + loadedInvoice.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 *ngIf="availableMethods.length > 1" class="form-group">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<!-- <label *ngIf="loadedInvoice.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="loadedInvoice.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="loadedInvoice.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' && loadedInvoice">
<div class="qr-wrapper" [class.mt-0]="minimal">
<a [href]="bypassSecurityTrustUrl('bitcoin:' + loadedInvoice.addresses.BTC + '?amount=' + loadedInvoice.btcDue)" target="_blank">
<app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + loadedInvoice.addresses.BTC + '?amount=' + loadedInvoice.btcDue"></app-qrcode>
</a>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control input-dark" readonly [value]="loadedInvoice.addresses.BTC">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="loadedInvoice.addresses.BTC"></app-clipboard></button>
</div>
</div>
@if (!minimal) {
<p>{{ loadedInvoice.btcDue | number: '1.0-8' }} <span class="symbol">BTC</span></p>
}
</ng-template>
<ng-template [ngIf]="paymentForm.get('method')?.value === 'lightning' && loadedInvoice && loadedInvoice.addresses.BTC_LightningLike">
<div class="qr-wrapper" [class.mt-0]="minimal">
<a [href]="bypassSecurityTrustUrl('lightning:' + loadedInvoice.addresses.BTC_LightningLike)" target="_blank">
<app-qrcode imageUrl="/resources/lightning-logo.png" [size]="200" [data]="loadedInvoice.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]="loadedInvoice.addresses.BTC_LightningLike">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="loadedInvoice.addresses.BTC_LightningLike"></app-clipboard></button>
</div>
</div>
@if (!minimal) {
<p>{{ loadedInvoice.btcDue * 100_000_000 | number: '1.0-0' }} <span class="symbol">sats</span></p>
}
</ng-template>
<ng-template [ngIf]="loadedInvoice && (paymentForm.get('method')?.value === 'lbtc' || paymentForm.get('method')?.value === 'tlbtc')">
<div class="qr-wrapper" [class.mt-0]="minimal">
<a [href]="bypassSecurityTrustUrl('liquidnetwork:' + loadedInvoice.addresses.LBTC + '?amount=' + loadedInvoice.btcDue + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank">
<app-qrcode imageUrl="/resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + loadedInvoice.addresses.LBTC + '?amount=' + loadedInvoice.btcDue + '&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]="loadedInvoice.addresses.LBTC" />
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="loadedInvoice.addresses.LBTC"></app-clipboard></button>
</div>
</div>
@if (!minimal) {
<p>{{ loadedInvoice.btcDue | number: '1.0-8' }} <span class="symbol">BTC</span></p>
}
</ng-template>
@if (!minimal) {
<p>Waiting for transaction... </p>
<div class="spinner-border text-light"></div>
}
</div>
</div>

View File

@ -0,0 +1,150 @@
.form-panel {
background-color: #292b45;
padding: 20px;
}
.sponsor-page {
text-align: center;
}
.qr-wrapper {
background-color: #FFF;
padding: 10px;
display: inline-block;
padding-bottom: 5px;
margin: 20px auto 0px;
}
.info-group {
max-width: 400px;
}
.card {
width: 240px;
height: 220px;
background-color: var(--bg);
border: 2px solid var(--bg);
cursor: pointer;
position: relative;
transition: 100ms all;
margin: 30px 30px 20px 30px;
@media(min-width: 476px) {
margin: 30px 100px 20px 100px;
}
@media(min-width: 851px) {
margin: 60px 20px 40px 20px;
}
.card-title {
font-weight: bold;
span {
font-weight: 100;
}
}
&.bigger {
height: 220px;
width: 240px;
margin-top: 40px;
}
&:hover {
background-color: #5058926b;
border: 2px solid #505892;
transform: scale(1.1) translateY(-10px);
margin-top: 70px;
.card-header {
background-color: #505892;
}
}
}
.donation-form {
max-width: 280px;
margin: auto;
button {
width: 100%;
}
}
.card-header {
background-color: #171929;
}
.flex-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.middle-card {
width: 280px;
height: 260px;
margin-top: 40px;
&:hover {
margin-top: 50px;
}
}
.shiny-border {
background-color: #5058926b;
border: 2px solid #505892;
transform: scale(1.1) translateY(-10px);
margin-top: 70px;
box-shadow: 0px 0px 100px #9858ff52;
.card-header {
background-color: #505892;
}
&.middle-card {
margin-top: 50px;
}
}
.input-group {
margin: 20px auto;
}
.donation-confirmed {
h2 {
margin-top: 50px;
span {
display: block;
&:last-child {
color: #9858ff;
font-weight: bold;
font-size: 2rem;
}
}
}
.order-details {
margin-top: 50px;
span {
color: #d81b60;
margin-left: 10px;
}
}
}
.card-body {
align-items: center;
display: flex;
justify-content: center;
flex-direction: column;
height: 100%;
}
.wrapper {
text-align: center;
width: 100%;
}
.input-dark {
background-color: var(--bg);
border-color: var(--active-bg);
color: white;
}

View File

@ -0,0 +1,110 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { Subscription, of, timer } from 'rxjs';
import { retry, switchMap, tap } from 'rxjs/operators';
import { ServicesApiServices } from '../../services/services-api.service';
@Component({
selector: 'app-bitcoin-invoice',
templateUrl: './bitcoin-invoice.component.html',
styleUrls: ['./bitcoin-invoice.component.scss']
})
export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
@Input() invoice;
@Input() invoiceId: string;
@Input() redirect = true;
@Input() minimal = false;
@Output() completed = new EventEmitter();
paymentForm: FormGroup;
requestSubscription: Subscription | undefined;
paymentStatusSubscription: Subscription | undefined;
loadedInvoice: any;
paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed
paramMapSubscription: Subscription | undefined;
invoiceSubscription: Subscription | undefined;
invoiceTimeout; // Wait for angular to load all the things before making a request
constructor(
private formBuilder: FormBuilder,
private apiService: ServicesApiServices,
private sanitizer: DomSanitizer,
private activatedRoute: ActivatedRoute
) { }
ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
}
if (this.paramMapSubscription) {
this.paramMapSubscription.unsubscribe();
}
if (this.invoiceSubscription) {
this.invoiceSubscription.unsubscribe();
}
if (this.paymentStatusSubscription) {
this.paymentStatusSubscription.unsubscribe();
}
}
ngOnInit(): void {
this.paymentForm = this.formBuilder.group({
'method': 'lightning'
});
/**
* If the invoice is passed in the url, fetch it and display btcpay payment
* Otherwise get a new invoice
*/
this.paramMapSubscription = this.activatedRoute.paramMap
.pipe(
tap((paramMap) => {
this.fetchInvoice(paramMap.get('invoiceId') ?? this.invoiceId);
})
).subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
if ((changes.invoice || changes.invoiceId) && this.invoiceId) {
this.fetchInvoice(this.invoiceId);
}
}
fetchInvoice(invoiceId: string): void {
if (invoiceId) {
if (this.paymentStatusSubscription) {
this.paymentStatusSubscription.unsubscribe();
}
this.paymentStatusSubscription = ((this.invoice && this.invoice.id === invoiceId) ? of(this.invoice) : this.apiService.retreiveInvoice$(invoiceId)).pipe(
tap((invoice: any) => {
this.loadedInvoice = invoice;
if (this.loadedInvoice.btcDue > 0) {
this.paymentStatus = 2;
} else {
this.paymentStatus = 4;
}
}),
switchMap(() => this.apiService.getPaymentStatus$(this.loadedInvoice.id)
.pipe(
retry({ delay: () => timer(2000)})
)
),
).subscribe({
next: ((result) => {
this.paymentStatus = 3;
this.completed.emit();
}),
});
}
}
get availableMethods(): string[] {
return Object.keys(this.loadedInvoice?.addresses || {}).filter(k => k === 'BTC_LightningLike');
}
bypassSecurityTrustUrl(text: string): SafeUrl {
return this.sanitizer.bypassSecurityTrustUrl(text);
}
}

View File

@ -1,9 +1,10 @@
img {
position: absolute;
top: 67px;
left: 67px;
width: 65px;
height: 65px;
top: 50%;
left: 50%;
width: 42px;
height: 42px;
transform: translate(-50%, -50%);
}
.holder {

View File

@ -37,7 +37,7 @@ export class QrcodeComponent implements AfterViewInit {
return;
}
const opts: QRCode.QRCodeRenderersOptions = {
errorCorrectionLevel: 'L',
errorCorrectionLevel: 'M',
margin: 0,
color: {
dark: '#000',

View File

@ -75,9 +75,9 @@
} @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>
</ng-container>
<ng-template #etaSkeleton>
@ -115,8 +115,15 @@
</div>
<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>
@if (isLoading) {
<div class="progress-icon">
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
</div>
<span class="explainer">&nbsp;</span>
} @else if (showAccelerationSummary) {
<ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [advancedEnabled]="false" [forceMobile]="true" [tx]="tx" [miningStats]="miningStats" [eta]="eta" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout>
</ng-container>
} @else {
@if (tx?.acceleration && !tx.status?.confirmed) {
<div class="progress-icon">

View File

@ -63,8 +63,9 @@ export class TrackerComponent implements OnInit, OnDestroy {
mempoolPosition: MempoolPosition;
accelerationPositions: AccelerationPosition[];
isLoadingTx = true;
error: any = undefined;
loadingCachedTx = false;
loadingPosition = true;
error: any = undefined;
waitingForTransaction = false;
latestBlock: BlockExtended;
transactionTime = -1;
@ -107,7 +108,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
now = Date.now();
da$: Observable<DifficultyAdjustment>;
isMobile: boolean;
paymentType: 'bitcoin' | 'cashapp' = 'bitcoin';
trackerStage: TrackerStage = 'waiting';
@ -149,18 +149,12 @@ export class TrackerComponent implements OnInit, OnDestroy {
ngOnInit() {
this.onResize();
window['setStage'] = ((stage: TrackerStage) => {
this.zone.run(() => {
this.trackerStage = stage;
this.cd.markForCheck();
});
}).bind(this);
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
if (this.acceleratorAvailable && this.stateService.referrer === 'https://cash.app/') {
this.paymentType = 'cashapp';
}
this.miningService.getMiningStats('1w').subscribe(stats => {
this.miningStats = stats;
});
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) {
this.showAccelerationSummary = true;
@ -365,6 +359,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
this.now = Date.now();
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
this.loadingPosition = false;
this.mempoolPosition = txPosition.position;
this.accelerationPositions = txPosition.accelerationPositions;
if (this.tx && !this.tx.status.confirmed) {
@ -390,11 +385,21 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.trackerStage = 'replaced';
}
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
this.accelerationEligible = true;
if (this.acceleratorAvailable && this.paymentType === 'cashapp') {
if (!this.mempoolPosition.accelerated) {
if (!this.accelerationFlowCompleted && !this.showAccelerationSummary && this.mempoolPosition.block > 0) {
this.showAccelerationSummary = true;
this.miningService.getMiningStats('1w').subscribe(stats => {
this.miningStats = stats;
});
}
if (txPosition.position?.block > 0) {
this.accelerationEligible = true;
}
} else if (this.showAccelerationSummary) {
setTimeout(() => {
this.accelerationFlowCompleted = true;
this.showAccelerationSummary = false;
}, 2000);
}
}
} else {
@ -449,6 +454,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
))
.subscribe((tx: Transaction) => {
if (!tx) {
this.loadingPosition = false;
this.fetchCachedTx$.next(this.txId);
this.seoService.logSoft404();
return;
@ -481,6 +487,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
}
} else {
this.trackerStage = 'confirmed';
this.loadingPosition = false;
this.fetchAcceleration$.next(tx.status.block_hash);
this.fetchMiningInfo$.next({ hash: tx.status.block_hash, height: tx.status.block_height, txid: tx.txid });
this.transactionTime = 0;
@ -736,17 +743,23 @@ export class TrackerComponent implements OnInit, OnDestroy {
return;
}
this.enterpriseService.goal(8);
this.accelerationFlowCompleted = false;
this.showAccelerationSummary = true && this.acceleratorAvailable;
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
this.scrollIntoAccelPreview = true;
return false;
}
get isLoading(): boolean {
return this.isLoadingTx || this.loadingCachedTx || this.loadingPosition;
}
resetTransaction() {
this.error = undefined;
this.tx = null;
this.txChanged$.next(true);
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.loadingPosition = true;
this.rbfTransaction = undefined;
this.replaced = false;
this.latestReplacement = '';

View 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 { }

View File

@ -80,13 +80,27 @@
<div class="title float-left">
<h2 i18n="transaction.accelerate|Accelerate button label">Accelerate</h2>
</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>
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="closeAccelerator()" i18n="hide-diagram">Hide accelerator</button>
<button *ngIf="hasAccelerationDetails" class="btn btn-sm btn-outline-info float-right ml-2" (click)="showAccelerationDetails = !showAccelerationDetails">Details</button>
<div class="clearfix"></div>
<div class="box">
<app-accelerate-preview [tx]="tx" [miningStats]="miningStats" [scrollEvent]="scrollIntoAccelPreview" [showDetails]="showAccelerationDetails"></app-accelerate-preview>
</div>
<ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout
*ngIf="(da$ | async) as da;"
[cashappEnabled]="accelerationEligible"
[advancedEnabled]="true"
[tx]="tx"
[eta]="eta"
[miningStats]="miningStats"
[scrollEvent]="scrollIntoAccelPreview"
[showDetails]="showAccelerationDetails"
[noCTA]="true"
(hasDetails)="setHasAccelerationDetails($event)"
class="h-100 w-100"
></app-accelerate-checkout>
</ng-container>
</ng-container>
<ng-template [ngIf]="showCpfpDetails">
@ -537,21 +551,23 @@
<td>
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
@if (eta.blocks >= 7) {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') {
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
} @else if (network === 'liquid' || network === 'liquidtestnet') {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
} @else {
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') {
<a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
<span class="eta justify-content-end">
</span>
}
</ng-container>
<ng-template #etaSkeleton>

View File

@ -300,7 +300,6 @@
.accelerateDeepMempool {
align-self: auto;
margin-top: 3px;
margin-left: auto;
background-color: var(--tertiary);
@media (max-width: 995px) {

View File

@ -139,7 +139,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === '';
showAccelerationSummary = false;
showAccelerationDetails = false;
hasAccelerationDetails = false;
accelerationFlowCompleted = false;
scrollIntoAccelPreview = false;
accelerationEligible = false;
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
@ViewChild('graphContainer')
@ -169,6 +172,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit() {
this.enterpriseService.page();
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) {
this.showAccelerationSummary = true;
}
if (!this.stateService.isLiquid) {
this.miningService.getMiningStats('1w').subscribe(stats => {
this.miningStats = stats;
});
}
this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe(
(network) => {
@ -397,6 +411,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) {
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
}
if (this.stateService.network === '') {
if (!this.mempoolPosition.accelerated) {
if (!this.accelerationFlowCompleted && !this.showAccelerationSummary) {
this.showAccelerationSummary = true;
this.miningService.getMiningStats('1w').subscribe(stats => {
this.miningStats = stats;
});
}
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
this.accelerationEligible = true;
}
} else if (this.showAccelerationSummary) {
setTimeout(() => {
this.closeAccelerator();
}, 2000);
}
}
}
} else {
this.mempoolPosition = null;
@ -681,14 +713,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.miningService.getMiningStats('1w').subscribe(stats => {
this.miningStats = stats;
});
document.location.hash = '#accelerate';
this.enterpriseService.goal(8);
this.showAccelerationSummary = true && this.acceleratorAvailable;
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
this.accelerationFlowCompleted = false;
this.showAccelerationSummary = this.acceleratorAvailable;
this.scrollIntoAccelPreview = true;
return false;
}
@ -765,8 +794,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
setIsAccelerated(initialState: boolean = false) {
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
if (this.isAcceleration && initialState) {
this.showAccelerationSummary = false;
if (this.isAcceleration) {
if (initialState) {
this.accelerationFlowCompleted = true;
this.showAccelerationSummary = false;
} else if (this.showAccelerationSummary) {
setTimeout(() => {
this.closeAccelerator();
}, 2000);
}
}
if (this.isAcceleration) {
// this immediately returns cached stats if we fetched them recently
@ -835,7 +871,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.rbfReplaces = [];
this.filters = [];
this.showCpfpDetails = false;
this.showAccelerationDetails = false;
this.accelerationInfo = null;
this.accelerationEligible = false;
this.txInBlockIndex = null;
this.mempoolPosition = null;
this.pool = null;
@ -852,6 +890,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.stateService.markBlock$.next({});
}
closeAccelerator(): void {
this.accelerationFlowCompleted = true;
this.showAccelerationSummary = false;
}
roundToOneDecimal(cpfpTx: any): number {
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
}
@ -897,6 +940,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
setHasAccelerationDetails(hasDetails: boolean): void {
this.hasAccelerationDetails = hasDetails;
}
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
this.isMobile = window.innerWidth < 850;
@ -911,6 +958,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
isLoggedIn(): boolean {
const auth = this.storageService.getAuth();
return auth !== null;
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.fetchCpfpSubscription.unsubscribe();

View File

@ -5,8 +5,8 @@ import { TransactionComponent } from './transaction.component';
import { SharedModule } from '../../shared/shared.module';
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
import { GraphsModule } from '../../graphs/graphs.module';
import { AcceleratePreviewComponent } from '../accelerate-preview/accelerate-preview.component';
import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component';
import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component';
import { AccelerateFeeGraphComponent } from '../accelerate-checkout/accelerate-fee-graph.component';
const routes: Routes = [
{
@ -43,7 +43,12 @@ export class TransactionRoutingModule { }
],
declarations: [
TransactionComponent,
AcceleratePreviewComponent,
AccelerateCheckout,
AccelerateFeeGraphComponent,
],
exports: [
TransactionComponent,
AccelerateCheckout,
AccelerateFeeGraphComponent,
]
})

View File

@ -5,8 +5,8 @@ import { MempoolBlock } from '../interfaces/websocket.interface';
import { Transaction } from '../interfaces/electrs.interface';
import { MiningService, MiningStats } from './mining.service';
import { getUnacceleratedFeeRate } from '../shared/transaction.utils';
import { AccelerationEstimate } from '../components/accelerate-preview/accelerate-preview.component';
import { Observable, combineLatest, map, of } from 'rxjs';
import { AccelerationEstimate } from '../components/accelerate-checkout/accelerate-checkout.component';
import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs';
export interface ETA {
now: number, // time at which calculation performed
@ -61,7 +61,8 @@ export class EtaService {
{ block: 0, hashrateShare: acceleratingHashrateFraction },
], da).time,
};
})
}),
shareReplay()
);
}

View File

@ -167,4 +167,20 @@ export class ServicesApiServices {
requestTestnet4Coins$(address: string, sats: number) {
return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' });
}
generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable<any> {
const params = {
product: txid,
amount: sats,
};
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

@ -29,6 +29,7 @@ const MempoolErrors = {
'faucet_address_not_allowed': `You cannot use this address`,
'faucet_below_minimum': `Requested amount is too small`,
'faucet_above_maximum': `Requested amount is too high`,
'payment_method_not_allowed': `You are not allowed to use this payment method`,
} as { [error: string]: string };
export function isMempoolError(error: string) {

View File

@ -50,8 +50,6 @@ import { BlockOverviewGraphComponent } from '../components/block-overview-graph/
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
import { AddressGroupComponent } from '../components/address-group/address-group.component';
import { TrackerComponent } from '../components/tracker/tracker.component';
import { TrackerBarComponent } from '../components/tracker/tracker-bar.component';
import { SearchFormComponent } from '../components/search-form/search-form.component';
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
import { FooterComponent } from '../components/footer/footer.component';
@ -100,7 +98,6 @@ import { MempoolErrorComponent } from './components/mempool-error/mempool-error.
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component';
import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
import { AccelerateCheckout } from '../components/accelerate-checkout/accelerate-checkout.component';
import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
@ -115,6 +112,7 @@ import { HttpErrorComponent } from '../shared/components/http-error/http-error.c
import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component';
import { FaucetComponent } from '../components/faucet/faucet.component';
import { TwitterLogin } from '../components/twitter-login/twitter-login.component';
import { BitcoinInvoiceComponent } from '../components/bitcoin-invoice/bitcoin-invoice.component';
import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives';
@ -164,8 +162,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BlockFiltersComponent,
TransactionsListComponent,
AddressGroupComponent,
TrackerComponent,
TrackerBarComponent,
SearchFormComponent,
AddressLabelsComponent,
FooterComponent,
@ -224,12 +220,12 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
MempoolErrorComponent,
AccelerationsListComponent,
AccelerationStatsComponent,
AccelerateCheckout,
PendingStatsComponent,
HttpErrorComponent,
TwitterWidgetComponent,
FaucetComponent,
TwitterLogin,
BitcoinInvoiceComponent,
],
imports: [
CommonModule,
@ -305,8 +301,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BlockFiltersComponent,
TransactionsListComponent,
AddressGroupComponent,
TrackerComponent,
TrackerBarComponent,
SearchFormComponent,
AddressLabelsComponent,
FooterComponent,
@ -354,11 +348,11 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
MempoolErrorComponent,
AccelerationsListComponent,
AccelerationStatsComponent,
AccelerateCheckout,
PendingStatsComponent,
HttpErrorComponent,
TwitterWidgetComponent,
TwitterLogin,
BitcoinInvoiceComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1 @@
<svg id="レイヤー_1" data-name="レイヤー 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.18 102.3"><defs><style>.cls-1{fill:#cedc21;}.cls-2{fill:#51b13e;}.cls-3{fill:#1e7a44;}.cls-4{fill:#fff;}</style></defs><title>btcpay3</title><path class="cls-1" d="M38.55,201.73a6,6,0,0,1-6-6V105.44a6,6,0,0,1,12,0v90.29A6,6,0,0,1,38.55,201.73Z" transform="translate(-32.55 -99.43)"/><path class="cls-2" d="M38.56,201.73A6,6,0,0,1,36,190.31l36.18-17.17L35,145.76a6,6,0,1,1,7.11-9.66l45.24,33.33a6,6,0,0,1-1,10.25L41.13,201.15A5.9,5.9,0,0,1,38.56,201.73Z" transform="translate(-32.55 -99.43)"/><path class="cls-1" d="M38.56,166.24A6,6,0,0,1,35,155.41L72.16,128,36,110.86A6,6,0,1,1,41.13,100l45.24,21.47a6,6,0,0,1,1,10.25L42.11,165.07A6,6,0,0,1,38.56,166.24Z" transform="translate(-32.55 -99.43)"/><polygon class="cls-3" points="12 38.46 12 63.84 29.21 51.16 12 38.46"/><rect class="cls-4" y="27.82" width="12" height="29.25"/><path class="cls-1" d="M44.55,105.44a6,6,0,0,0-12,0V181h12Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M123.92,149.86c3.84,1.09,6,4.57,6,8.93,0,6.81-4.15,10-9.82,10H107.14V132.4h11.43c5.56,0,9.87,2.65,9.87,9.56C128.44,145.44,127,148.66,123.92,149.86Zm-5.3-.89c4.1,0,7.43-1.45,7.43-7.06s-3.43-7.17-7.59-7.17h-8.88V149Zm1.3,17.41c4.15,0,7.48-2.18,7.48-7.59,0-5.82-3.79-7.58-8.52-7.58h-9.3v15.17Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M154.21,132.4v2.23h-10v34.14h-2.44V134.63h-10V132.4Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M171.3,131.88c6.19,0,11.8,3.12,13.57,10.55h-2.34c-1.66-6.08-6.55-8.32-11.28-8.32-8.57,0-13.09,7-13.09,16.47,0,10,4.52,16.37,13.14,16.37,5.1,0,9.67-2.29,11.44-9h2.33a13.6,13.6,0,0,1-13.77,11.33c-9.71,0-15.53-7.17-15.53-18.71C155.77,139.52,161.9,131.88,171.3,131.88Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M203.36,132.4c6.29,0,10.86,4.1,10.86,12,0,7.48-4.57,12-10.86,12H193.8v12.32h-2.39V132.4Zm0,21.66c4.63,0,8.42-3,8.42-9.66s-3.64-9.71-8.42-9.71H193.8v19.37Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M214.64,168.77v-.31l14.49-36.16h1.1l14.34,36.16v.31H242l-4.1-10.5H221.34l-4.1,10.5Zm15-32.16L222.17,156h14.91Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M266.13,132.4h2.6v.36L257.4,153.65v15.12h-2.49V153.65l-11.38-20.94v-.31h2.65l4.93,9.25,5,9.61h0l5-9.61Z" transform="translate(-32.55 -99.43)"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB