Merge simple & advanced acceleration checkout components
This commit is contained in:
parent
d7acd389bf
commit
415ad3de70
@ -1,66 +1,306 @@
|
|||||||
<div class="box 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>
|
||||||
|
|
||||||
@if (error) {
|
@if (error) {
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<app-mempool-error [error]="error"></app-mempool-error>
|
<app-mempool-error [error]="error"></app-mempool-error>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@else if (step === 'quote') {
|
||||||
@else if (step === 'cta') {
|
@if (!simpleMode) {
|
||||||
<!-- Show A/B CTAs -->
|
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||||
<div class="row mb-1">
|
<div class="row" *ngIf="showSuccess">
|
||||||
<div class="col-sm">
|
<div class="col">
|
||||||
<h1 style="font-size: larger;"><ng-content select="[slot='cta-title']"></ng-content><span class="default-slot">Accelerate your Bitcoin transaction?</span></h1>
|
<div class="alert alert-success">
|
||||||
</div>
|
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
|
||||||
</div>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md">
|
|
||||||
<div class="form-group form-check mb-2">
|
|
||||||
<input type="radio" [checked]="choosenOption === 'wait'" class="form-check-input" id="wait" name="accel" (change)="selectedOptionChanged($event)">
|
|
||||||
<label class="form-check-label d-flex flex-column" for="wait">
|
|
||||||
<span class="font-weight-bold">Wait</span>
|
|
||||||
@if (eta.blocks < 7) {
|
|
||||||
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected <app-time kind="within" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time></span>
|
|
||||||
} @else {
|
|
||||||
<span style="color: rgb(186, 186, 186); font-size: 14px;">
|
|
||||||
<span>Confirmation expected within several hours</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
</div>
|
||||||
<div class="form-group form-check mb-2">
|
|
||||||
<input type="radio" [checked]="choosenOption === 'accel'" class="form-check-input" id="accel" name="accel" (change)="selectedOptionChanged($event)">
|
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||||
<label class="form-check-label d-flex flex-column" for="accel">
|
<div class="row" *ngIf="error">
|
||||||
<span class="font-weight-bold">Accelerate</span>
|
<div class="col">
|
||||||
<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>
|
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
|
||||||
@if (!calculating) {
|
</div>
|
||||||
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>)
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
@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]="'‎' + (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 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-container *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-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" [class.group-last]="!isLoggedIn() || estimate.userBalance >= cost">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #loadingEstimate>
|
||||||
|
<div class="skeleton-loader"></div>
|
||||||
|
<br>
|
||||||
|
</ng-template>
|
||||||
|
}
|
||||||
|
@else {
|
||||||
|
<!-- Show A/B CTAs -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="form-group form-check mb-2">
|
||||||
|
<input type="radio" [checked]="choosenOption === 'wait'" class="form-check-input" id="wait" name="accel" (change)="selectedOptionChanged($event)">
|
||||||
|
<label class="form-check-label d-flex flex-column" for="wait">
|
||||||
|
<span class="font-weight-bold">Wait</span>
|
||||||
|
@if (eta.blocks < 7) {
|
||||||
|
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected <app-time kind="within" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time></span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="estimating">Calculating cost...</span>
|
<span style="color: rgb(186, 186, 186); font-size: 14px;">
|
||||||
|
<span>Confirmation expected within several hours</span>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</span>
|
</label>
|
||||||
</label>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="form-group form-check mb-2">
|
||||||
|
<input type="radio" [checked]="choosenOption === 'accel'" class="form-check-input" id="accel" name="accel" (change)="selectedOptionChanged($event)">
|
||||||
|
<label class="form-check-label d-flex flex-column" for="accel">
|
||||||
|
<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> <span class="symbol" i18n="shared.sats">sats</span></span>)
|
||||||
|
} @else {
|
||||||
|
<span class="estimating">Calculating cost...</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md pie d-none d-lg-flex" *ngIf="estimate && !forceMobile">
|
||||||
|
<small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description" *ngIf="(etaInfo$ | async) as etaInfo">Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.</small>
|
||||||
|
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md pie d-none d-lg-flex" *ngIf="estimate && !isTracker">
|
<div class="row mt-2 mb-2" [style]="(choosenOption !== 'accel' || calculating) ? 'opacity: 0.25; pointer-events: none' : ''">
|
||||||
<small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description" *ngIf="(etaInfo$ | async) as etaInfo">Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.</small>
|
<div class="col-sm d-flex flex-row justify-content-center">
|
||||||
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box>
|
<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()">
|
||||||
|
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
|
||||||
|
<span>Accelerate</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
<div class="row mt-2 mb-2" [style]="(choosenOption !== 'accel' || calculating) ? 'opacity: 0.25; pointer-events: none' : ''">
|
}
|
||||||
<div class="col-sm d-flex flex-row justify-content-center">
|
|
||||||
<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()">
|
|
||||||
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
|
|
||||||
<span>Accelerate</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
} @else if (step === 'paymentMethod') {
|
} @else if (step === 'paymentMethod') {
|
||||||
<div class="row text-center">
|
<div class="row text-center">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
@ -82,7 +322,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="row mt-2 mb-2 text-center">
|
<div class="row mt-2 mb-2 text-center">
|
||||||
<div class="col-sm d-flex flex-column">
|
<div class="col-sm d-flex flex-column">
|
||||||
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="step = 'cta'">Go Back</button>
|
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="step = 'quote'">Go Back</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -24,4 +24,134 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 330px;
|
max-width: 330px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fee-card {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: var(--bg);
|
||||||
|
|
||||||
|
.feerate {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.rate {
|
||||||
|
font-size: 0.9em;
|
||||||
|
.symbol {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-border {
|
||||||
|
border: solid 1px black;
|
||||||
|
background-color: #0c4a87;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feerate.active {
|
||||||
|
background-color: var(--primary) !important;
|
||||||
|
opacity: 1;
|
||||||
|
border: 1px solid #007fff !important;
|
||||||
|
}
|
||||||
|
.feerate:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estimateDisabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-toggle {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
&:first-child {
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
border: solid 1px black;
|
||||||
|
border-bottom: none;
|
||||||
|
background-color: #323655;
|
||||||
|
border-top-left-radius: 10px !important;
|
||||||
|
border-top-right-radius: 10px !important;
|
||||||
|
}
|
||||||
|
.tab.active {
|
||||||
|
background-color: #5d659d !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.tab:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-accelerator {
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.group-first {
|
||||||
|
td {
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.group-last, &:last-child {
|
||||||
|
td {
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.dashed-top {
|
||||||
|
border-top: 1px dashed grey;
|
||||||
|
}
|
||||||
|
&.dashed-bottom {
|
||||||
|
border-bottom: 1px dashed grey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
&:first-child {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
&.info {
|
||||||
|
color: #6c757d;
|
||||||
|
white-space: initial;
|
||||||
|
}
|
||||||
|
&.amt {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0.2em;
|
||||||
|
}
|
||||||
|
&.units {
|
||||||
|
padding-left: 0.2em;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accelerate-cols {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col.pie {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
position: relative;
|
||||||
|
top: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
white-space: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-background {
|
||||||
|
background-color: var(--bg);
|
||||||
}
|
}
|
@ -1,13 +1,42 @@
|
|||||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||||
import { Subscription, tap, of, catchError, Observable } from 'rxjs';
|
import { Subscription, tap, of, catchError, Observable } from 'rxjs';
|
||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '../../services/services-api.service';
|
||||||
import { nextRoundNumber } from '../../shared/common.utils';
|
import { nextRoundNumber } from '../../shared/common.utils';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
import { AccelerationEstimate } from '../accelerate-preview/accelerate-preview.component';
|
|
||||||
import { ETA, EtaService } from '../../services/eta.service';
|
import { ETA, EtaService } from '../../services/eta.service';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '../../interfaces/electrs.interface';
|
||||||
import { MiningStats } from '../../services/mining.service';
|
import { MiningStats } from '../../services/mining.service';
|
||||||
|
import { StorageService } from '../../services/storage.service';
|
||||||
|
|
||||||
|
export type AccelerationEstimate = {
|
||||||
|
hasAccess: boolean;
|
||||||
|
txSummary: TxSummary;
|
||||||
|
nextBlockFee: number;
|
||||||
|
targetFeeRate: number;
|
||||||
|
userBalance: number;
|
||||||
|
enoughBalance: boolean;
|
||||||
|
cost: number;
|
||||||
|
mempoolBaseFee: number;
|
||||||
|
vsizeFee: number;
|
||||||
|
pools: number[]
|
||||||
|
}
|
||||||
|
export type TxSummary = {
|
||||||
|
txid: string; // txid of the current transaction
|
||||||
|
effectiveVsize: number; // Total vsize of the dependency tree
|
||||||
|
effectiveFee: number; // Total fee of the dependency tree in sats
|
||||||
|
ancestorCount: number; // Number of ancestors
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateOption {
|
||||||
|
fee: number;
|
||||||
|
rate: number;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MIN_BID_RATIO = 1;
|
||||||
|
export const DEFAULT_BID_RATIO = 2;
|
||||||
|
export const MAX_BID_RATIO = 4;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accelerate-checkout',
|
selector: 'app-accelerate-checkout',
|
||||||
@ -20,24 +49,43 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
@Input() eta: ETA;
|
@Input() eta: ETA;
|
||||||
@Input() scrollEvent: boolean;
|
@Input() scrollEvent: boolean;
|
||||||
@Input() cashappEnabled: boolean;
|
@Input() cashappEnabled: boolean;
|
||||||
@Input() isTracker: boolean = false;
|
@Input() showDetails: boolean;
|
||||||
|
@Input() advancedEnabled: boolean = false;
|
||||||
|
@Input() forceMobile: boolean = false;
|
||||||
|
@Output() changeMode = new EventEmitter<boolean>();
|
||||||
@Output() close = new EventEmitter<null>();
|
@Output() close = new EventEmitter<null>();
|
||||||
|
|
||||||
calculating = true;
|
calculating = true;
|
||||||
choosenOption: 'wait' | 'accel';
|
choosenOption: 'wait' | 'accel';
|
||||||
error = '';
|
error = '';
|
||||||
|
math = Math;
|
||||||
|
isMobile: boolean = window.innerWidth <= 767.98;
|
||||||
|
|
||||||
step: 'paymentMethod' | 'cta' | 'checkout' | 'processing' = 'cta';
|
step: 'quote' | 'paymentMethod' | 'checkout' | 'processing' = 'quote';
|
||||||
|
simpleMode: boolean = true;
|
||||||
paymentMethod: 'cashapp' | 'btcpay';
|
paymentMethod: 'cashapp' | 'btcpay';
|
||||||
|
|
||||||
|
user: any = undefined;
|
||||||
|
|
||||||
// accelerator stuff
|
// accelerator stuff
|
||||||
square: { appId: string, locationId: string};
|
square: { appId: string, locationId: string};
|
||||||
accelerationUUID: string;
|
accelerationUUID: string;
|
||||||
|
accelerationSubscription: Subscription;
|
||||||
|
difficultySubscription: Subscription;
|
||||||
estimateSubscription: Subscription;
|
estimateSubscription: Subscription;
|
||||||
estimate: AccelerationEstimate;
|
estimate: AccelerationEstimate;
|
||||||
maxBidBoost: number; // sats
|
maxBidBoost: number; // sats
|
||||||
cost: number; // sats
|
cost: number; // sats
|
||||||
etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>;
|
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
|
// square
|
||||||
loadingCashapp = false;
|
loadingCashapp = false;
|
||||||
@ -52,8 +100,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
invoice = undefined;
|
invoice = undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
public stateService: StateService,
|
||||||
private servicesApiService: ServicesApiServices,
|
private servicesApiService: ServicesApiServices,
|
||||||
private stateService: StateService,
|
private storageService: StorageService,
|
||||||
private etaService: EtaService,
|
private etaService: EtaService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
private cd: ChangeDetectorRef
|
private cd: ChangeDetectorRef
|
||||||
@ -62,6 +111,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.user = this.storageService.getAuth()?.user ?? null;
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
||||||
this.insertSquare();
|
this.insertSquare();
|
||||||
@ -74,7 +124,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
appId: ids.squareAppId,
|
appId: ids.squareAppId,
|
||||||
locationId: ids.squareLocationId
|
locationId: ids.squareLocationId
|
||||||
};
|
};
|
||||||
if (this.step === 'cta') {
|
if (this.step === 'quote') {
|
||||||
this.fetchEstimate();
|
this.fetchEstimate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -95,7 +145,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Scroll to element id with or without setTimeout
|
* Scroll to element id with or without setTimeout
|
||||||
*/
|
*/
|
||||||
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000) {
|
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.scrollToElement(id, position);
|
this.scrollToElement(id, position);
|
||||||
}, timeout);
|
}, timeout);
|
||||||
@ -130,24 +180,100 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.error = `cannot_accelerate_tx`;
|
this.error = `cannot_accelerate_tx`;
|
||||||
return;
|
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
|
// 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));
|
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
|
||||||
const DEFAULT_BID_RATIO = 1.5;
|
|
||||||
this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO;
|
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
|
||||||
this.cost = this.maxBidBoost + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
return {
|
||||||
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate);
|
fee: this.minExtraCost * multiplier,
|
||||||
|
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
|
||||||
|
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
|
||||||
|
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
|
||||||
|
|
||||||
|
this.userBid = this.defaultBid;
|
||||||
|
if (this.userBid < this.minBidAllowed) {
|
||||||
|
this.userBid = this.minBidAllowed;
|
||||||
|
} else if (this.userBid > this.maxBidAllowed) {
|
||||||
|
this.userBid = this.maxBidAllowed;
|
||||||
|
}
|
||||||
|
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||||
|
|
||||||
this.calculating = false;
|
this.calculating = false;
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
catchError((response) => {
|
catchError((response) => {
|
||||||
|
this.estimate = undefined;
|
||||||
this.error = `cannot_accelerate_tx`;
|
this.error = `cannot_accelerate_tx`;
|
||||||
|
this.estimateSubscription.unsubscribe();
|
||||||
return of(null);
|
return of(null);
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User changed his bid
|
||||||
|
*/
|
||||||
|
setUserBid({ fee, index }: { fee: number, index: number}): void {
|
||||||
|
if (this.estimate) {
|
||||||
|
this.selectFeeRateIndex = index;
|
||||||
|
this.userBid = Math.max(0, fee);
|
||||||
|
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced mode acceleration button clicked
|
||||||
|
*/
|
||||||
|
accelerate(): void {
|
||||||
|
if (this.isLoggedIn()) {
|
||||||
|
this.accelerateWithMempoolAccount();
|
||||||
|
} else {
|
||||||
|
this.step = 'paymentMethod';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account-based acceleration request
|
||||||
|
*/
|
||||||
|
accelerateWithMempoolAccount(): void {
|
||||||
|
if (this.accelerationSubscription) {
|
||||||
|
this.accelerationSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||||
|
this.tx.txid,
|
||||||
|
this.userBid,
|
||||||
|
this.accelerationUUID
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
|
this.showSuccess = true;
|
||||||
|
this.estimateSubscription.unsubscribe();
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
if (response.status === 403 && response.error === 'not_available') {
|
||||||
|
this.error = 'waitlisted';
|
||||||
|
} else {
|
||||||
|
this.error = response.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Square
|
* Square
|
||||||
*/
|
*/
|
||||||
@ -321,4 +447,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.close.emit();
|
this.close.emit();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLoggedIn(): boolean {
|
||||||
|
const auth = this.storageService.getAuth();
|
||||||
|
return auth !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
onResize(): void {
|
||||||
|
this.isMobile = window.innerWidth <= 767.98;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<div class="fee-graph" *ngIf="tx && estimate">
|
||||||
|
<div class="column">
|
||||||
|
<ng-container *ngFor="let bar of bars">
|
||||||
|
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
|
||||||
|
<div class="fill"></div>
|
||||||
|
<div class="line">
|
||||||
|
<p class="fee-rate">
|
||||||
|
<span class="label">{{ bar.label }}</span>
|
||||||
|
<span class="rate">
|
||||||
|
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,156 @@
|
|||||||
|
.fee-graph {
|
||||||
|
height: 100%;
|
||||||
|
min-width: 120px;
|
||||||
|
width: 120px;
|
||||||
|
margin-left: 4em;
|
||||||
|
margin-right: 1.5em;
|
||||||
|
|
||||||
|
.column {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: var(--stat-box-bg);
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
min-height: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 0.75;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fee {
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
flex-grow: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
left: -4.5em;
|
||||||
|
border-top: dashed white 1.5px;
|
||||||
|
|
||||||
|
.fee-rate {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0.2em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-right: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate .symbol {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tx {
|
||||||
|
.fill {
|
||||||
|
background: var(--green);
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
.fee-rate {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fee {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.target {
|
||||||
|
.fill {
|
||||||
|
background: var(--tertiary);
|
||||||
|
}
|
||||||
|
.fee {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
.line .fee-rate {
|
||||||
|
bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.max {
|
||||||
|
cursor: pointer;
|
||||||
|
.line .fee-rate {
|
||||||
|
.label {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
bottom: 2px;
|
||||||
|
}
|
||||||
|
&.active, &:hover {
|
||||||
|
.fill {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
.fee-rate .label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.fill {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
.fee {
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover > .bar:not(:hover) {
|
||||||
|
&.target, &.max {
|
||||||
|
.fee {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.line .fee-rate .label {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.max {
|
||||||
|
.fill {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
|
||||||
|
import { tap, switchMap } from 'rxjs/operators';
|
||||||
|
import { ApiService } from '../../services/api.service';
|
||||||
|
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
|
||||||
|
|
||||||
|
interface GraphBar {
|
||||||
|
rate: number;
|
||||||
|
style: any;
|
||||||
|
class: 'tx' | 'target' | 'max';
|
||||||
|
label: string;
|
||||||
|
active?: boolean;
|
||||||
|
rateIndex?: number;
|
||||||
|
fee?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-accelerate-fee-graph',
|
||||||
|
templateUrl: './accelerate-fee-graph.component.html',
|
||||||
|
styleUrls: ['./accelerate-fee-graph.component.scss'],
|
||||||
|
})
|
||||||
|
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||||
|
@Input() tx: Transaction;
|
||||||
|
@Input() estimate: AccelerationEstimate;
|
||||||
|
@Input() showEstimate = false;
|
||||||
|
@Input() maxRateOptions: RateOption[] = [];
|
||||||
|
@Input() maxRateIndex: number = 0;
|
||||||
|
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
|
||||||
|
|
||||||
|
bars: GraphBar[] = [];
|
||||||
|
tooltipPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.initGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
this.initGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
initGraph(): void {
|
||||||
|
if (!this.tx || !this.estimate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
|
||||||
|
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
|
||||||
|
const baseHeight = baseRate / maxRate;
|
||||||
|
console.log(maxRate, baseRate, baseHeight);
|
||||||
|
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
|
||||||
|
return {
|
||||||
|
rate: option.rate,
|
||||||
|
style: this.getStyle(option.rate, maxRate, baseHeight),
|
||||||
|
class: 'max',
|
||||||
|
label: this.showEstimate ? $localize`maximum` : $localize`accelerated`,
|
||||||
|
active: option.index === this.maxRateIndex,
|
||||||
|
rateIndex: option.index,
|
||||||
|
fee: option.fee,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
|
||||||
|
bars.push({
|
||||||
|
rate: this.estimate.targetFeeRate,
|
||||||
|
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||||
|
class: 'target',
|
||||||
|
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
|
||||||
|
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bars.push({
|
||||||
|
rate: baseRate,
|
||||||
|
style: this.getStyle(baseRate, maxRate, 0),
|
||||||
|
class: 'tx',
|
||||||
|
label: '',
|
||||||
|
fee: this.estimate.txSummary.effectiveFee,
|
||||||
|
});
|
||||||
|
this.bars = bars;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyle(rate, maxRate, base) {
|
||||||
|
const top = (rate / maxRate);
|
||||||
|
return {
|
||||||
|
height: `${(top - base) * 100}%`,
|
||||||
|
bottom: base ? `${base * 100}%` : '0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(event, bar): void {
|
||||||
|
if (bar.rateIndex != null) {
|
||||||
|
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('pointermove', ['$event'])
|
||||||
|
onPointerMove(event) {
|
||||||
|
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||||
|
}
|
||||||
|
}
|
@ -117,7 +117,7 @@
|
|||||||
<div class="bottom-panel">
|
<div class="bottom-panel">
|
||||||
@if (showAccelerationSummary && !accelerationFlowCompleted) {
|
@if (showAccelerationSummary && !accelerationFlowCompleted) {
|
||||||
<ng-container *ngIf="(ETA$ | async) as eta;">
|
<ng-container *ngIf="(ETA$ | async) as eta;">
|
||||||
<app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [tx]="tx" [miningStats]="miningStats" [eta]="eta" [isTracker]="true" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout>
|
<app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [tx]="tx" [miningStats]="miningStats" [eta]="eta" [forceMobile]="true" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
} @else {
|
} @else {
|
||||||
@if (tx?.acceleration && !tx.status?.confirmed) {
|
@if (tx?.acceleration && !tx.status?.confirmed) {
|
||||||
|
@ -84,17 +84,11 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
@if (isLoggedIn()) {
|
<ng-container *ngIf="(ETA$ | async) as eta;">
|
||||||
<div class="box">
|
<app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [tx]="tx" [eta]="eta" [miningStats]="miningStats" (close)="showAccelerationSummary = false" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100">
|
||||||
<app-accelerate-preview [tx]="tx" [miningStats]="miningStats" [scrollEvent]="scrollIntoAccelPreview" [showDetails]="showAccelerationDetails"></app-accelerate-preview>
|
<span slot="cta-title">Urgent transaction? Get it confirmed faster.</span>
|
||||||
</div>
|
</app-accelerate-checkout>
|
||||||
} @else {
|
</ng-container>
|
||||||
<ng-container *ngIf="(ETA$ | async) as eta;">
|
|
||||||
<app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [tx]="tx" [eta]="eta" [miningStats]="miningStats" (close)="showAccelerationSummary = false" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100">
|
|
||||||
<span slot="cta-title">Urgent transaction? Get it confirmed faster.</span>
|
|
||||||
</app-accelerate-checkout>
|
|
||||||
</ng-container>
|
|
||||||
}
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template [ngIf]="showCpfpDetails">
|
<ng-template [ngIf]="showCpfpDetails">
|
||||||
|
@ -5,9 +5,8 @@ import { TransactionComponent } from './transaction.component';
|
|||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
|
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
|
||||||
import { GraphsModule } from '../../graphs/graphs.module';
|
import { GraphsModule } from '../../graphs/graphs.module';
|
||||||
import { AcceleratePreviewComponent } from '../accelerate-preview/accelerate-preview.component';
|
|
||||||
import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component';
|
import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component';
|
||||||
import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component';
|
import { AccelerateFeeGraphComponent } from '../accelerate-checkout/accelerate-fee-graph.component';
|
||||||
import { TrackerComponent } from '../tracker/tracker.component';
|
import { TrackerComponent } from '../tracker/tracker.component';
|
||||||
import { TrackerBarComponent } from '../tracker/tracker-bar.component';
|
import { TrackerBarComponent } from '../tracker/tracker-bar.component';
|
||||||
|
|
||||||
@ -43,7 +42,6 @@ export class TransactionRoutingModule { }
|
|||||||
TransactionComponent,
|
TransactionComponent,
|
||||||
TrackerComponent,
|
TrackerComponent,
|
||||||
TrackerBarComponent,
|
TrackerBarComponent,
|
||||||
AcceleratePreviewComponent,
|
|
||||||
AccelerateCheckout,
|
AccelerateCheckout,
|
||||||
AccelerateFeeGraphComponent,
|
AccelerateFeeGraphComponent,
|
||||||
]
|
]
|
||||||
|
@ -5,7 +5,7 @@ import { MempoolBlock } from '../interfaces/websocket.interface';
|
|||||||
import { Transaction } from '../interfaces/electrs.interface';
|
import { Transaction } from '../interfaces/electrs.interface';
|
||||||
import { MiningService, MiningStats } from './mining.service';
|
import { MiningService, MiningStats } from './mining.service';
|
||||||
import { getUnacceleratedFeeRate } from '../shared/transaction.utils';
|
import { getUnacceleratedFeeRate } from '../shared/transaction.utils';
|
||||||
import { AccelerationEstimate } from '../components/accelerate-preview/accelerate-preview.component';
|
import { AccelerationEstimate } from '../components/accelerate-checkout/accelerate-checkout.component';
|
||||||
import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs';
|
import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs';
|
||||||
|
|
||||||
export interface ETA {
|
export interface ETA {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user