Merge pull request #5147 from mempool/mononaut/accelerate-preview-hashrate-pie
Acceleration preview hashrate pie chart
This commit is contained in:
commit
eedfbacf01
@ -65,10 +65,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
<h5 *ngIf="estimate?.pools?.length" i18n="accelerator.how-much-faster">How much faster?</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to {{ hashratePercentage | number : '1.1-1' }}% 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 <app-time kind="within" [time]="acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></small>
|
||||||
|
</div>
|
||||||
|
<div class="col pie">
|
||||||
|
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
<h5 i18n="accelerator.pay-how-much">How much more are you willing to pay?</h5>
|
<h5 i18n="accelerator.pay-how-much">How much more are you willing to pay?</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<small class="form-text text-muted mb-2" i18n="accelerator.transaction-fee-description">Choose the maximum extra transaction fee you're willing to pay to get into the next block.</small>
|
<small class="form-text text-muted mb-2" i18n="accelerator.transaction-fee-description">Choose the maximum extra transaction fee you're willing to pay.</small>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="fee-card">
|
<div class="fee-card">
|
||||||
<div class="d-flex mb-0">
|
<div class="d-flex mb-0">
|
||||||
|
@ -107,6 +107,11 @@
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col.pie {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
white-space: initial;
|
white-space: initial;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ import { nextRoundNumber } from '../../shared/common.utils';
|
|||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '../../services/services-api.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { MiningStats } from '../../services/mining.service';
|
||||||
|
import { EtaService } from '../../services/eta.service';
|
||||||
|
import { DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../../interfaces/node-api.interface';
|
||||||
|
|
||||||
export type AccelerationEstimate = {
|
export type AccelerationEstimate = {
|
||||||
txSummary: TxSummary;
|
txSummary: TxSummary;
|
||||||
@ -40,7 +43,9 @@ export const MAX_BID_RATIO = 4;
|
|||||||
styleUrls: ['accelerate-preview.component.scss']
|
styleUrls: ['accelerate-preview.component.scss']
|
||||||
})
|
})
|
||||||
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
|
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
@Input() tx: Transaction | undefined;
|
@Input() tx: Transaction;
|
||||||
|
@Input() mempoolPosition: MempoolPosition;
|
||||||
|
@Input() miningStats: MiningStats;
|
||||||
@Input() scrollEvent: boolean;
|
@Input() scrollEvent: boolean;
|
||||||
|
|
||||||
math = Math;
|
math = Math;
|
||||||
@ -48,7 +53,12 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
showSuccess = false;
|
showSuccess = false;
|
||||||
estimateSubscription: Subscription;
|
estimateSubscription: Subscription;
|
||||||
accelerationSubscription: Subscription;
|
accelerationSubscription: Subscription;
|
||||||
|
difficultySubscription: Subscription;
|
||||||
|
da: DifficultyAdjustment;
|
||||||
estimate: any;
|
estimate: any;
|
||||||
|
hashratePercentage?: number;
|
||||||
|
ETA?: number;
|
||||||
|
acceleratedETA?: number;
|
||||||
hasAncestors: boolean = false;
|
hasAncestors: boolean = false;
|
||||||
minExtraCost = 0;
|
minExtraCost = 0;
|
||||||
minBidAllowed = 0;
|
minBidAllowed = 0;
|
||||||
@ -67,6 +77,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private servicesApiService: ServicesApiServices,
|
private servicesApiService: ServicesApiServices,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
|
private etaService: EtaService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
private cd: ChangeDetectorRef
|
private cd: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
@ -76,16 +87,24 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
if (this.estimateSubscription) {
|
if (this.estimateSubscription) {
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
this.difficultySubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.accelerationUUID = window.crypto.randomUUID();
|
this.accelerationUUID = window.crypto.randomUUID();
|
||||||
|
this.difficultySubscription = this.stateService.difficultyAdjustment$.subscribe(da => {
|
||||||
|
this.da = da;
|
||||||
|
this.updateETA();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes.scrollEvent) {
|
if (changes.scrollEvent) {
|
||||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||||
}
|
}
|
||||||
|
if (changes.miningStats || changes.mempoolPosition) {
|
||||||
|
this.updateETA();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
@ -113,6 +132,8 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateETA();
|
||||||
|
|
||||||
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
|
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
|
||||||
|
|
||||||
// Make min extra fee at least 50% of the current tx fee
|
// Make min extra fee at least 50% of the current tx fee
|
||||||
@ -157,6 +178,36 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateETA(): void {
|
||||||
|
if (!this.mempoolPosition || !this.estimate?.pools?.length || !this.miningStats || !this.da) {
|
||||||
|
this.hashratePercentage = undefined;
|
||||||
|
this.ETA = undefined;
|
||||||
|
this.acceleratedETA = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pools: { [id: number]: SinglePoolStats } = {};
|
||||||
|
for (const pool of this.miningStats.pools) {
|
||||||
|
pools[pool.poolUniqueId] = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalAcceleratedHashrate = 0;
|
||||||
|
for (const poolId of this.estimate.pools) {
|
||||||
|
const pool = pools[poolId];
|
||||||
|
if (!pool) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
totalAcceleratedHashrate += pool.lastEstimatedHashrate;
|
||||||
|
}
|
||||||
|
const acceleratingHashrateFraction = (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)
|
||||||
|
this.hashratePercentage = acceleratingHashrateFraction * 100;
|
||||||
|
|
||||||
|
this.ETA = Date.now() + this.da.timeAvg * this.mempoolPosition.block;
|
||||||
|
this.acceleratedETA = this.etaService.calculateETAFromShares([
|
||||||
|
{ block: this.mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },
|
||||||
|
{ block: 0, hashrateShare: acceleratingHashrateFraction },
|
||||||
|
], this.da).time;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User changed his bid
|
* User changed his bid
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
@if (chartOnly) {
|
||||||
|
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||||
|
} @else {
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@ -12,23 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="pie-chart" rowspan="2">
|
<td class="pie-chart" rowspan="2">
|
||||||
<div class="chart-container">
|
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||||
@if (tx && (tx.acceleratedBy || accelerationInfo) && miningStats) {
|
|
||||||
<div
|
|
||||||
echarts
|
|
||||||
*browserOnly
|
|
||||||
class="chart"
|
|
||||||
[initOpts]="chartInitOptions"
|
|
||||||
[options]="chartOptions"
|
|
||||||
style="height: 72px; width: 72px;"
|
|
||||||
(chartInit)="onChartInit($event)"
|
|
||||||
></div>
|
|
||||||
} @else {
|
|
||||||
<div class="chart-loading">
|
|
||||||
<div class="spinner-border text-light"></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -38,4 +25,25 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ng-template #pieChart>
|
||||||
|
<div class="chart-container">
|
||||||
|
@if (chartOptions && miningStats) {
|
||||||
|
<div
|
||||||
|
echarts
|
||||||
|
*browserOnly
|
||||||
|
class="chart"
|
||||||
|
[initOpts]="chartInitOptions"
|
||||||
|
[options]="chartOptions"
|
||||||
|
style="height: 72px; width: 72px;"
|
||||||
|
(chartInit)="onChartInit($event)"
|
||||||
|
></div>
|
||||||
|
} @else {
|
||||||
|
<div class="chart-loading">
|
||||||
|
<div class="spinner-border text-light"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -15,10 +15,12 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
@Input() tx: Transaction;
|
@Input() tx: Transaction;
|
||||||
@Input() accelerationInfo: Acceleration;
|
@Input() accelerationInfo: Acceleration;
|
||||||
@Input() miningStats: MiningStats;
|
@Input() miningStats: MiningStats;
|
||||||
|
@Input() pools: number[];
|
||||||
|
@Input() chartOnly: boolean = false;
|
||||||
|
|
||||||
acceleratedByPercentage: string = '';
|
acceleratedByPercentage: string = '';
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption;
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
renderer: 'svg',
|
renderer: 'svg',
|
||||||
};
|
};
|
||||||
@ -28,12 +30,13 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (this.tx && (this.tx.acceleratedBy || this.accelerationInfo) && this.miningStats) {
|
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
|
||||||
this.prepareChartOptions();
|
if (pools && this.miningStats) {
|
||||||
|
this.prepareChartOptions(pools);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getChartData() {
|
getChartData(poolList: number[]) {
|
||||||
const data: object[] = [];
|
const data: object[] = [];
|
||||||
const pools: { [id: number]: SinglePoolStats } = {};
|
const pools: { [id: number]: SinglePoolStats } = {};
|
||||||
for (const pool of this.miningStats.pools) {
|
for (const pool of this.miningStats.pools) {
|
||||||
@ -73,22 +76,22 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let totalAcceleratedHashrate = 0;
|
let totalAcceleratedHashrate = 0;
|
||||||
for (const poolId of (this.accelerationInfo?.pools || this.tx.acceleratedBy || [])) {
|
for (const poolId of poolList || []) {
|
||||||
const pool = pools[poolId];
|
const pool = pools[poolId];
|
||||||
if (!pool) {
|
if (!pool) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
totalAcceleratedHashrate += parseFloat(pool.lastEstimatedHashrate);
|
totalAcceleratedHashrate += pool.lastEstimatedHashrate;
|
||||||
}
|
}
|
||||||
this.acceleratedByPercentage = ((totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
|
||||||
data.push(getDataItem(
|
data.push(getDataItem(
|
||||||
totalAcceleratedHashrate,
|
totalAcceleratedHashrate,
|
||||||
'var(--mainnet-alt)',
|
'var(--mainnet-alt)',
|
||||||
`${this.acceleratedByPercentage} accelerating`,
|
`${this.acceleratedByPercentage} accelerating`,
|
||||||
) as PieSeriesOption);
|
) as PieSeriesOption);
|
||||||
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate))) * 100).toFixed(1) + '%';
|
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
||||||
data.push(getDataItem(
|
data.push(getDataItem(
|
||||||
(parseFloat(this.miningStats.lastEstimatedHashrate) - totalAcceleratedHashrate),
|
(this.miningStats.lastEstimatedHashrate - totalAcceleratedHashrate),
|
||||||
'rgba(127, 127, 127, 0.3)',
|
'rgba(127, 127, 127, 0.3)',
|
||||||
`${notAcceleratedByPercentage} not accelerating`,
|
`${notAcceleratedByPercentage} not accelerating`,
|
||||||
) as PieSeriesOption);
|
) as PieSeriesOption);
|
||||||
@ -96,7 +99,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions() {
|
prepareChartOptions(pools: number[]) {
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
animation: false,
|
animation: false,
|
||||||
grid: {
|
grid: {
|
||||||
@ -113,7 +116,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
{
|
{
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: '100%',
|
radius: '100%',
|
||||||
data: this.getChartData(),
|
data: this.getChartData(pools),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRe
|
|||||||
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { EtaService } from '../../services/eta.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
|
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { feeLevels } from '../../app.constants';
|
import { feeLevels } from '../../app.constants';
|
||||||
@ -89,6 +90,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
private etaService: EtaService,
|
||||||
private themeService: ThemeService,
|
private themeService: ThemeService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
@ -437,34 +439,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.rightPosition = positionOfBlock + positionInBlock;
|
this.rightPosition = positionOfBlock + positionInBlock;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let found = false;
|
const estimatedPosition = this.etaService.mempoolPositionFromFees(this.txFeePerVSize, this.mempoolBlocks);
|
||||||
for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) {
|
this.rightPosition = estimatedPosition.block * (this.blockWidth + this.blockPadding)
|
||||||
const block = this.mempoolBlocks[txInBlockIndex];
|
+ ((estimatedPosition.vsize / this.stateService.blockVSize) * this.blockWidth)
|
||||||
for (let i = 0; i < block.feeRange.length - 1 && !found; i++) {
|
|
||||||
if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) {
|
|
||||||
const feeRangeIndex = i;
|
|
||||||
const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
|
|
||||||
|
|
||||||
const txFee = this.txFeePerVSize - block.feeRange[i];
|
|
||||||
const max = block.feeRange[i + 1] - block.feeRange[i];
|
|
||||||
const blockLocation = txFee / max;
|
|
||||||
|
|
||||||
const chunkPositionOffset = blockLocation * feeRangeChunkSize;
|
|
||||||
const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
|
|
||||||
|
|
||||||
const blockedFilledPercentage = (block.blockVSize > this.stateService.blockVSize ? this.stateService.blockVSize : block.blockVSize) / this.stateService.blockVSize;
|
|
||||||
const arrowRightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding)
|
|
||||||
+ ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
|
|
||||||
|
|
||||||
this.rightPosition = arrowRightPosition;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.txFeePerVSize >= block.feeRange[block.feeRange.length - 1]) {
|
|
||||||
this.rightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding);
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.rightPosition = Math.min(this.maxArrowPosition, this.rightPosition);
|
this.rightPosition = Math.min(this.maxArrowPosition, this.rightPosition);
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
const i = pool.blockCount.toString();
|
const i = pool.blockCount.toString();
|
||||||
if (this.miningWindowPreference === '24h') {
|
if (this.miningWindowPreference === '24h') {
|
||||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
pool.lastEstimatedHashrate.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
|
pool.lastEstimatedHashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit +
|
||||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||||
} else {
|
} else {
|
||||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
@ -291,7 +291,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
getEmptyMiningStat(): MiningStats {
|
getEmptyMiningStat(): MiningStats {
|
||||||
return {
|
return {
|
||||||
lastEstimatedHashrate: 'Error',
|
lastEstimatedHashrate: 0,
|
||||||
blockCount: 0,
|
blockCount: 0,
|
||||||
totalEmptyBlock: 0,
|
totalEmptyBlock: 0,
|
||||||
totalEmptyBlockRatio: '',
|
totalEmptyBlockRatio: '',
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="data">
|
<div class="data">
|
||||||
@if (tx && !tx.status?.confirmed && mempoolPosition?.block != null) {
|
@if (tx && !tx.status?.confirmed) {
|
||||||
<div class="field narrower mt-2">
|
<div class="field narrower mt-2">
|
||||||
<div class="label" i18n="transaction.first-seen|Transaction first seen">First seen</div>
|
<div class="label" i18n="transaction.first-seen|Transaction first seen">First seen</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
@ -68,16 +68,21 @@
|
|||||||
<div class="field narrower">
|
<div class="field narrower">
|
||||||
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<span class="justify-content-end d-flex align-items-center">
|
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||||
@if (mempoolPosition?.block >= 7) {
|
<span class="justify-content-end d-flex align-items-center">
|
||||||
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
|
@if (eta.blocks >= 7) {
|
||||||
} @else {
|
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
|
||||||
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
} @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) {
|
}
|
||||||
<a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
@if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !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>
|
}
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #etaSkeleton>
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else if (tx && tx.status?.confirmed) {
|
} @else if (tx && tx.status?.confirmed) {
|
||||||
|
@ -9,10 +9,11 @@ import {
|
|||||||
delay,
|
delay,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
tap,
|
tap,
|
||||||
map
|
map,
|
||||||
|
startWith
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '../../interfaces/electrs.interface';
|
||||||
import { of, merge, Subscription, Observable, Subject, throwError, combineLatest } from 'rxjs';
|
import { of, merge, Subscription, Observable, Subject, throwError, combineLatest, BehaviorSubject } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { CacheService } from '../../services/cache.service';
|
import { CacheService } from '../../services/cache.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@ -21,12 +22,15 @@ import { ApiService } from '../../services/api.service';
|
|||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||||
import { Filter } from '../../shared/filters.utils';
|
import { Filter } from '../../shared/filters.utils';
|
||||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration } from '../../interfaces/node-api.interface';
|
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface';
|
||||||
import { PriceService } from '../../services/price.service';
|
import { PriceService } from '../../services/price.service';
|
||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '../../services/services-api.service';
|
||||||
import { EnterpriseService } from '../../services/enterprise.service';
|
import { EnterpriseService } from '../../services/enterprise.service';
|
||||||
import { ZONE_SERVICE } from '../../injection-tokens';
|
import { ZONE_SERVICE } from '../../injection-tokens';
|
||||||
import { TrackerStage } from './tracker-bar.component';
|
import { TrackerStage } from './tracker-bar.component';
|
||||||
|
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||||
|
import { ETA, EtaService } from '../../services/eta.service';
|
||||||
|
import { getUnacceleratedFeeRate } from '../../shared/transaction.utils';
|
||||||
|
|
||||||
interface Pool {
|
interface Pool {
|
||||||
id: number;
|
id: number;
|
||||||
@ -57,6 +61,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
txId: string;
|
txId: string;
|
||||||
txInBlockIndex: number;
|
txInBlockIndex: number;
|
||||||
mempoolPosition: MempoolPosition;
|
mempoolPosition: MempoolPosition;
|
||||||
|
accelerationPositions: AccelerationPosition[];
|
||||||
isLoadingTx = true;
|
isLoadingTx = true;
|
||||||
error: any = undefined;
|
error: any = undefined;
|
||||||
loadingCachedTx = false;
|
loadingCachedTx = false;
|
||||||
@ -89,11 +94,15 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
isAcceleration: boolean = false;
|
isAcceleration: boolean = false;
|
||||||
filters: Filter[] = [];
|
filters: Filter[] = [];
|
||||||
showCpfpDetails = false;
|
showCpfpDetails = false;
|
||||||
|
miningStats: MiningStats;
|
||||||
fetchCpfp$ = new Subject<string>();
|
fetchCpfp$ = new Subject<string>();
|
||||||
fetchRbfHistory$ = new Subject<string>();
|
fetchRbfHistory$ = new Subject<string>();
|
||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
fetchAcceleration$ = new Subject<string>();
|
fetchAcceleration$ = new Subject<string>();
|
||||||
fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>();
|
fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>();
|
||||||
|
txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself)
|
||||||
|
isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself
|
||||||
|
ETA$: Observable<ETA | null>;
|
||||||
isCached: boolean = false;
|
isCached: boolean = false;
|
||||||
now = Date.now();
|
now = Date.now();
|
||||||
da$: Observable<DifficultyAdjustment>;
|
da$: Observable<DifficultyAdjustment>;
|
||||||
@ -122,6 +131,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
private etaService: EtaService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
@ -130,6 +140,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private priceService: PriceService,
|
private priceService: PriceService,
|
||||||
private enterpriseService: EnterpriseService,
|
private enterpriseService: EnterpriseService,
|
||||||
|
private miningService: MiningService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
@Inject(ZONE_SERVICE) private zoneService: any,
|
@Inject(ZONE_SERVICE) private zoneService: any,
|
||||||
@ -273,6 +284,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
this.transactionTime = tx.firstSeen || 0;
|
this.transactionTime = tx.firstSeen || 0;
|
||||||
|
|
||||||
this.fetchRbfHistory$.next(this.tx.txid);
|
this.fetchRbfHistory$.next(this.tx.txid);
|
||||||
|
this.txChanged$.next(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -354,10 +366,14 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
this.now = Date.now();
|
this.now = Date.now();
|
||||||
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
||||||
this.mempoolPosition = txPosition.position;
|
this.mempoolPosition = txPosition.position;
|
||||||
|
this.accelerationPositions = txPosition.accelerationPositions;
|
||||||
if (this.tx && !this.tx.status.confirmed) {
|
if (this.tx && !this.tx.status.confirmed) {
|
||||||
|
const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
txid: txPosition.txid,
|
txid: txPosition.txid,
|
||||||
mempoolPosition: this.mempoolPosition
|
txFeePerVSize,
|
||||||
|
mempoolPosition: this.mempoolPosition,
|
||||||
|
accelerationPositions: this.accelerationPositions,
|
||||||
});
|
});
|
||||||
this.txInBlockIndex = this.mempoolPosition.block;
|
this.txInBlockIndex = this.mempoolPosition.block;
|
||||||
|
|
||||||
@ -372,13 +388,8 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (this.replaced) {
|
if (this.replaced) {
|
||||||
this.trackerStage = 'replaced';
|
this.trackerStage = 'replaced';
|
||||||
} else if (txPosition.position?.block === 0) {
|
|
||||||
this.trackerStage = 'next';
|
|
||||||
} else if (txPosition.position?.block < 3){
|
|
||||||
this.trackerStage = 'soon';
|
|
||||||
} else {
|
|
||||||
this.trackerStage = 'pending';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
|
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
|
||||||
this.accelerationEligible = true;
|
this.accelerationEligible = true;
|
||||||
if (this.acceleratorAvailable && this.paymentType === 'cashapp') {
|
if (this.acceleratorAvailable && this.paymentType === 'cashapp') {
|
||||||
@ -388,6 +399,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.mempoolPosition = null;
|
this.mempoolPosition = null;
|
||||||
|
this.accelerationPositions = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -453,6 +465,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
this.adjustedVsize = Math.max(this.tx.weight / 4, this.sigops * 5);
|
this.adjustedVsize = Math.max(this.tx.weight / 4, this.sigops * 5);
|
||||||
}
|
}
|
||||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||||
|
this.txChanged$.next(true);
|
||||||
this.isLoadingTx = false;
|
this.isLoadingTx = false;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.loadingCachedTx = false;
|
this.loadingCachedTx = false;
|
||||||
@ -479,11 +492,13 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.fetchCpfp$.next(this.tx.txid);
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
} else {
|
} else {
|
||||||
|
const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
|
||||||
if (tx.cpfpChecked) {
|
if (tx.cpfpChecked) {
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
txFeePerVSize: tx.effectiveFeePerVsize,
|
txFeePerVSize,
|
||||||
mempoolPosition: this.mempoolPosition,
|
mempoolPosition: this.mempoolPosition,
|
||||||
|
accelerationPositions: this.accelerationPositions,
|
||||||
});
|
});
|
||||||
this.setCpfpInfo({
|
this.setCpfpInfo({
|
||||||
ancestors: tx.ancestors,
|
ancestors: tx.ancestors,
|
||||||
@ -522,6 +537,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
block_hash: block.id,
|
block_hash: block.id,
|
||||||
block_time: block.timestamp,
|
block_time: block.timestamp,
|
||||||
};
|
};
|
||||||
|
this.txChanged$.next(true);
|
||||||
this.trackerStage = 'confirmed';
|
this.trackerStage = 'confirmed';
|
||||||
this.stateService.markBlock$.next({ blockHeight: block.height });
|
this.stateService.markBlock$.next({ blockHeight: block.height });
|
||||||
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
|
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
|
||||||
@ -580,6 +596,38 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
this.txInBlockIndex = 7;
|
this.txInBlockIndex = 7;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ETA$ = combineLatest([
|
||||||
|
this.stateService.mempoolTxPosition$.pipe(startWith(null)),
|
||||||
|
this.stateService.mempoolBlocks$.pipe(startWith(null)),
|
||||||
|
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
|
||||||
|
this.isAccelerated$,
|
||||||
|
this.txChanged$,
|
||||||
|
]).pipe(
|
||||||
|
map(([position, mempoolBlocks, da, isAccelerated]) => {
|
||||||
|
return this.etaService.calculateETA(
|
||||||
|
this.network,
|
||||||
|
this.tx,
|
||||||
|
mempoolBlocks,
|
||||||
|
position,
|
||||||
|
da,
|
||||||
|
this.miningStats,
|
||||||
|
isAccelerated,
|
||||||
|
this.accelerationPositions,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
tap(eta => {
|
||||||
|
if (this.replaced) {
|
||||||
|
this.trackerStage = 'replaced'
|
||||||
|
} else if (eta?.blocks === 0) {
|
||||||
|
this.trackerStage = 'next';
|
||||||
|
} else if (eta?.blocks < 3){
|
||||||
|
this.trackerStage = 'soon';
|
||||||
|
} else {
|
||||||
|
this.trackerStage = 'pending';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadElectrsTransactionError(error: any): Observable<any> {
|
handleLoadElectrsTransactionError(error: any): Observable<any> {
|
||||||
@ -610,6 +658,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
this.hasEffectiveFeeRate = false;
|
this.hasEffectiveFeeRate = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const firstCpfp = this.cpfpInfo == null;
|
||||||
// merge ancestors/descendants
|
// merge ancestors/descendants
|
||||||
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
|
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
|
||||||
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||||
@ -625,12 +674,14 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
relatives.reduce((prev, val) => prev + val.fee, 0);
|
relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||||
} else {
|
} else {
|
||||||
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize || this.tx.effectiveFeePerVsize || this.tx.feePerVsize || (this.tx.fee / (this.tx.weight / 4));
|
||||||
}
|
}
|
||||||
if (cpfpInfo.acceleration) {
|
if (cpfpInfo.acceleration) {
|
||||||
this.tx.acceleration = cpfpInfo.acceleration;
|
this.tx.acceleration = cpfpInfo.acceleration;
|
||||||
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||||
|
this.setIsAccelerated(firstCpfp);
|
||||||
}
|
}
|
||||||
|
this.txChanged$.next(true);
|
||||||
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
if (this.cpfpInfo.adjustedVsize && this.cpfpInfo.sigops != null) {
|
if (this.cpfpInfo.adjustedVsize && this.cpfpInfo.sigops != null) {
|
||||||
@ -666,6 +717,14 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
setIsAccelerated(initialState: boolean = false) {
|
setIsAccelerated(initialState: boolean = false) {
|
||||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
||||||
|
if (this.isAcceleration) {
|
||||||
|
// this immediately returns cached stats if we fetched them recently
|
||||||
|
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||||
|
this.miningStats = stats;
|
||||||
|
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.isAccelerated$.next(this.isAcceleration);
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissAccelAlert(): void {
|
dismissAccelAlert(): void {
|
||||||
@ -685,6 +744,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
resetTransaction() {
|
resetTransaction() {
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.tx = null;
|
this.tx = null;
|
||||||
|
this.txChanged$.next(true);
|
||||||
this.waitingForTransaction = false;
|
this.waitingForTransaction = false;
|
||||||
this.isLoadingTx = true;
|
this.isLoadingTx = true;
|
||||||
this.rbfTransaction = undefined;
|
this.rbfTransaction = undefined;
|
||||||
@ -704,6 +764,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
this.mempoolPosition = null;
|
this.mempoolPosition = null;
|
||||||
this.pool = null;
|
this.pool = null;
|
||||||
this.auditStatus = null;
|
this.auditStatus = null;
|
||||||
|
this.accelerationPositions = null;
|
||||||
this.accelerationEligible = false;
|
this.accelerationEligible = false;
|
||||||
this.trackerStage = 'waiting';
|
this.trackerStage = 'waiting';
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
|
<app-accelerate-preview [tx]="tx" [miningStats]="miningStats" [mempoolPosition]="mempoolPosition" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -533,25 +533,28 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||||
<td>
|
<td>
|
||||||
@if (this.mempoolPosition?.block == null) {
|
<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 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') {
|
||||||
|
<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' : ''">
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #etaSkeleton>
|
||||||
<span class="skeleton-loader"></span>
|
<span class="skeleton-loader"></span>
|
||||||
} @else if (this.mempoolPosition.block >= 7) {
|
</ng-template>
|
||||||
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? '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') {
|
|
||||||
<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]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
|
||||||
} @else {
|
|
||||||
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
|
|
||||||
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [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>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,11 @@ import {
|
|||||||
mergeMap,
|
mergeMap,
|
||||||
tap,
|
tap,
|
||||||
map,
|
map,
|
||||||
retry
|
retry,
|
||||||
|
startWith
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '../../interfaces/electrs.interface';
|
||||||
import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
|
import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { CacheService } from '../../services/cache.service';
|
import { CacheService } from '../../services/cache.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@ -22,9 +23,9 @@ import { ApiService } from '../../services/api.service';
|
|||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||||
import { getTransactionFlags } from '../../shared/transaction.utils';
|
import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils';
|
||||||
import { Filter, toFilters } from '../../shared/filters.utils';
|
import { Filter, toFilters } from '../../shared/filters.utils';
|
||||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface';
|
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition, SinglePoolStats } from '../../interfaces/node-api.interface';
|
||||||
import { LiquidUnblinding } from './liquid-ublinding';
|
import { LiquidUnblinding } from './liquid-ublinding';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { PriceService } from '../../services/price.service';
|
import { PriceService } from '../../services/price.service';
|
||||||
@ -33,6 +34,7 @@ import { ServicesApiServices } from '../../services/services-api.service';
|
|||||||
import { EnterpriseService } from '../../services/enterprise.service';
|
import { EnterpriseService } from '../../services/enterprise.service';
|
||||||
import { ZONE_SERVICE } from '../../injection-tokens';
|
import { ZONE_SERVICE } from '../../injection-tokens';
|
||||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||||
|
import { ETA, EtaService } from '../../services/eta.service';
|
||||||
|
|
||||||
interface Pool {
|
interface Pool {
|
||||||
id: number;
|
id: number;
|
||||||
@ -106,6 +108,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
fetchAcceleration$ = new Subject<number>();
|
fetchAcceleration$ = new Subject<number>();
|
||||||
fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>();
|
fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>();
|
||||||
|
txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself)
|
||||||
|
isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself
|
||||||
|
ETA$: Observable<ETA | null>;
|
||||||
isCached: boolean = false;
|
isCached: boolean = false;
|
||||||
now = Date.now();
|
now = Date.now();
|
||||||
da$: Observable<DifficultyAdjustment>;
|
da$: Observable<DifficultyAdjustment>;
|
||||||
@ -155,6 +160,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private enterpriseService: EnterpriseService,
|
private enterpriseService: EnterpriseService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
|
private etaService: EtaService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
@Inject(ZONE_SERVICE) private zoneService: any,
|
@Inject(ZONE_SERVICE) private zoneService: any,
|
||||||
) {}
|
) {}
|
||||||
@ -281,6 +287,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.rbfInfo = rbfInfo;
|
this.rbfInfo = rbfInfo;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.txChanged$.next(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -365,7 +372,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
).subscribe(auditStatus => {
|
).subscribe(auditStatus => {
|
||||||
this.auditStatus = auditStatus;
|
this.auditStatus = auditStatus;
|
||||||
|
|
||||||
this.setIsAccelerated();
|
this.setIsAccelerated();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -375,7 +381,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.mempoolPosition = txPosition.position;
|
this.mempoolPosition = txPosition.position;
|
||||||
this.accelerationPositions = txPosition.accelerationPositions;
|
this.accelerationPositions = txPosition.accelerationPositions;
|
||||||
if (this.tx && !this.tx.status.confirmed) {
|
if (this.tx && !this.tx.status.confirmed) {
|
||||||
const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
|
const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
txid: txPosition.txid,
|
txid: txPosition.txid,
|
||||||
txFeePerVSize,
|
txFeePerVSize,
|
||||||
@ -493,6 +499,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.adjustedVsize = Math.max(this.tx.weight / 4, this.sigops * 5);
|
this.adjustedVsize = Math.max(this.tx.weight / 4, this.sigops * 5);
|
||||||
}
|
}
|
||||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||||
|
this.txChanged$.next(true);
|
||||||
this.isLoadingTx = false;
|
this.isLoadingTx = false;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.loadingCachedTx = false;
|
this.loadingCachedTx = false;
|
||||||
@ -519,7 +526,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.fetchCpfp$.next(this.tx.txid);
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
} else {
|
} else {
|
||||||
const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
|
const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
|
||||||
if (tx.cpfpChecked) {
|
if (tx.cpfpChecked) {
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
@ -566,6 +573,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
block_hash: block.id,
|
block_hash: block.id,
|
||||||
block_time: block.timestamp,
|
block_time: block.timestamp,
|
||||||
};
|
};
|
||||||
|
this.txChanged$.next(true);
|
||||||
this.stateService.markBlock$.next({ blockHeight: block.height });
|
this.stateService.markBlock$.next({ blockHeight: block.height });
|
||||||
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
|
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
|
||||||
this.audioService.playSound('wind-chimes-harp-ascend');
|
this.audioService.playSound('wind-chimes-harp-ascend');
|
||||||
@ -637,6 +645,27 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.txInBlockIndex = 7;
|
this.txInBlockIndex = 7;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ETA$ = combineLatest([
|
||||||
|
this.stateService.mempoolTxPosition$.pipe(startWith(null)),
|
||||||
|
this.stateService.mempoolBlocks$.pipe(startWith(null)),
|
||||||
|
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
|
||||||
|
this.isAccelerated$,
|
||||||
|
this.txChanged$,
|
||||||
|
]).pipe(
|
||||||
|
map(([position, mempoolBlocks, da, isAccelerated]) => {
|
||||||
|
return this.etaService.calculateETA(
|
||||||
|
this.network,
|
||||||
|
this.tx,
|
||||||
|
mempoolBlocks,
|
||||||
|
position,
|
||||||
|
da,
|
||||||
|
this.miningStats,
|
||||||
|
isAccelerated,
|
||||||
|
this.accelerationPositions,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
@ -653,6 +682,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||||
|
this.miningStats = stats;
|
||||||
|
});
|
||||||
|
|
||||||
document.location.hash = '#accelerate';
|
document.location.hash = '#accelerate';
|
||||||
this.enterpriseService.goal(8);
|
this.enterpriseService.goal(8);
|
||||||
this.showAccelerationSummary = true && this.acceleratorAvailable;
|
this.showAccelerationSummary = true && this.acceleratorAvailable;
|
||||||
@ -715,6 +748,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||||
this.setIsAccelerated(firstCpfp);
|
this.setIsAccelerated(firstCpfp);
|
||||||
}
|
}
|
||||||
|
this.txChanged$.next(true);
|
||||||
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
if (this.cpfpInfo.adjustedVsize && this.cpfpInfo.sigops != null) {
|
if (this.cpfpInfo.adjustedVsize && this.cpfpInfo.sigops != null) {
|
||||||
@ -734,8 +768,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// this immediately returns cached stats if we fetched them recently
|
// this immediately returns cached stats if we fetched them recently
|
||||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||||
this.miningStats = stats;
|
this.miningStats = stats;
|
||||||
|
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.isAccelerated$.next(this.isAcceleration);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFeatures(): void {
|
setFeatures(): void {
|
||||||
@ -780,6 +816,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.firstLoad = false;
|
this.firstLoad = false;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.tx = null;
|
this.tx = null;
|
||||||
|
this.txChanged$.next(true);
|
||||||
this.setFeatures();
|
this.setFeatures();
|
||||||
this.waitingForTransaction = false;
|
this.waitingForTransaction = false;
|
||||||
this.isLoadingTx = true;
|
this.isLoadingTx = true;
|
||||||
@ -802,6 +839,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.accelerationPositions = null;
|
this.accelerationPositions = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
this.isAcceleration = false;
|
this.isAcceleration = false;
|
||||||
|
this.isAccelerated$.next(this.isAcceleration);
|
||||||
this.leaveTransaction();
|
this.leaveTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -814,20 +852,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number {
|
|
||||||
if (accelerated) {
|
|
||||||
let ancestorVsize = tx.weight / 4;
|
|
||||||
let ancestorFee = tx.fee;
|
|
||||||
for (const ancestor of tx.ancestors || []) {
|
|
||||||
ancestorVsize += (ancestor.weight / 4);
|
|
||||||
ancestorFee += ancestor.fee;
|
|
||||||
}
|
|
||||||
return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize));
|
|
||||||
} else {
|
|
||||||
return tx.effectiveFeePerVsize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupGraph() {
|
setupGraph() {
|
||||||
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
||||||
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
||||||
@ -900,7 +924,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.urlFragmentSubscription.unsubscribe();
|
this.urlFragmentSubscription.unsubscribe();
|
||||||
this.mempoolBlocksSubscription.unsubscribe();
|
this.mempoolBlocksSubscription.unsubscribe();
|
||||||
this.mempoolPositionSubscription.unsubscribe();
|
this.mempoolPositionSubscription.unsubscribe();
|
||||||
this.mempoolBlocksSubscription.unsubscribe();
|
|
||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription.unsubscribe();
|
||||||
this.miningSubscription?.unsubscribe();
|
this.miningSubscription?.unsubscribe();
|
||||||
this.auditSubscription?.unsubscribe();
|
this.auditSubscription?.unsubscribe();
|
||||||
|
@ -5,6 +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 { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -36,6 +38,8 @@ export class TransactionRoutingModule { }
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
TransactionComponent,
|
TransactionComponent,
|
||||||
|
AcceleratePreviewComponent,
|
||||||
|
AccelerateFeeGraphComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class TransactionModule { }
|
export class TransactionModule { }
|
||||||
|
@ -140,7 +140,7 @@ export interface SinglePoolStats {
|
|||||||
emptyBlocks: number;
|
emptyBlocks: number;
|
||||||
rank: number;
|
rank: number;
|
||||||
share: number;
|
share: number;
|
||||||
lastEstimatedHashrate: string;
|
lastEstimatedHashrate: number;
|
||||||
emptyBlockRatio: string;
|
emptyBlockRatio: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -252,7 +252,7 @@ export interface MempoolPosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AccelerationPosition extends MempoolPosition {
|
export interface AccelerationPosition extends MempoolPosition {
|
||||||
pool: string;
|
poolId: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
183
frontend/src/app/services/eta.service.ts
Normal file
183
frontend/src/app/services/eta.service.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../interfaces/node-api.interface';
|
||||||
|
import { StateService } from './state.service';
|
||||||
|
import { MempoolBlock } from '../interfaces/websocket.interface';
|
||||||
|
import { Transaction } from '../interfaces/electrs.interface';
|
||||||
|
import { MiningStats } from './mining.service';
|
||||||
|
import { getUnacceleratedFeeRate } from '../shared/transaction.utils';
|
||||||
|
|
||||||
|
export interface ETA {
|
||||||
|
now: number, // time at which calculation performed
|
||||||
|
time: number, // absolute time expected (in unix epoch ms)
|
||||||
|
wait: number, // expected wait time in ms
|
||||||
|
blocks: number, // expected number of blocks (rounded up to next integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class EtaService {
|
||||||
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
mempoolPositionFromFees(feerate: number, mempoolBlocks: MempoolBlock[]): MempoolPosition {
|
||||||
|
for (let txInBlockIndex = 0; txInBlockIndex < mempoolBlocks.length; txInBlockIndex++) {
|
||||||
|
const block = mempoolBlocks[txInBlockIndex];
|
||||||
|
for (let i = 0; i < block.feeRange.length - 1; i++) {
|
||||||
|
if (feerate < block.feeRange[i + 1] && feerate >= block.feeRange[i]) {
|
||||||
|
const feeRangeIndex = i;
|
||||||
|
const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
|
||||||
|
|
||||||
|
const txFee = feerate - block.feeRange[i];
|
||||||
|
const max = block.feeRange[i + 1] - block.feeRange[i];
|
||||||
|
const blockLocation = txFee / max;
|
||||||
|
|
||||||
|
const chunkPositionOffset = blockLocation * feeRangeChunkSize;
|
||||||
|
const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
|
||||||
|
|
||||||
|
const blockedFilledPercentage = (block.blockVSize > this.stateService.blockVSize ? this.stateService.blockVSize : block.blockVSize) / this.stateService.blockVSize;
|
||||||
|
|
||||||
|
return {
|
||||||
|
block: txInBlockIndex,
|
||||||
|
vsize: (1 - feePosition) * blockedFilledPercentage * this.stateService.blockVSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (feerate >= block.feeRange[block.feeRange.length - 1]) {
|
||||||
|
// at the very front of this block
|
||||||
|
return {
|
||||||
|
block: txInBlockIndex,
|
||||||
|
vsize: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// at the very back of the last block
|
||||||
|
return {
|
||||||
|
block: mempoolBlocks.length - 1,
|
||||||
|
vsize: mempoolBlocks[mempoolBlocks.length - 1].blockVSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateETA(
|
||||||
|
network: string,
|
||||||
|
tx: Transaction,
|
||||||
|
mempoolBlocks: MempoolBlock[],
|
||||||
|
position: { txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] },
|
||||||
|
da: DifficultyAdjustment,
|
||||||
|
miningStats: MiningStats,
|
||||||
|
isAccelerated: boolean,
|
||||||
|
accelerationPositions: AccelerationPosition[],
|
||||||
|
): ETA | null {
|
||||||
|
// return this.calculateETA(tx, this.accelerationPositions, position, mempoolBlocks, da, isAccelerated)
|
||||||
|
if (!tx || !mempoolBlocks) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// use known projected position, or fall back to feerate-based estimate
|
||||||
|
const mempoolPosition = position?.position ?? this.mempoolPositionFromFees(tx.effectiveFeePerVsize || tx.feePerVsize, mempoolBlocks);
|
||||||
|
if (!mempoolPosition) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liquid block time is always 60 seconds
|
||||||
|
if (network === 'liquid' || network === 'liquidtestnet') {
|
||||||
|
return {
|
||||||
|
now,
|
||||||
|
time: now + (60_000 * (mempoolPosition.block + 1)),
|
||||||
|
wait: (60_000 * (mempoolPosition.block + 1)),
|
||||||
|
blocks: mempoolPosition.block + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// difficulty adjustment estimate is required to know avg block time on non-Liquid networks
|
||||||
|
if (!da) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAccelerated) {
|
||||||
|
const blocks = mempoolPosition.block + 1;
|
||||||
|
const wait = da.adjustedTimeAvg * (mempoolPosition.block + 1);
|
||||||
|
return {
|
||||||
|
now,
|
||||||
|
time: wait + now + da.timeOffset,
|
||||||
|
wait,
|
||||||
|
blocks,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// accelerated transactions
|
||||||
|
|
||||||
|
// mining stats are required for pool hashrate weightings
|
||||||
|
if (!miningStats) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// acceleration positions are required
|
||||||
|
if (!accelerationPositions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const pools: { [id: number]: SinglePoolStats } = {};
|
||||||
|
for (const pool of miningStats.pools) {
|
||||||
|
pools[pool.poolUniqueId] = pool;
|
||||||
|
}
|
||||||
|
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
|
||||||
|
let totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0);
|
||||||
|
const shares = [
|
||||||
|
{
|
||||||
|
block: unacceleratedPosition.block,
|
||||||
|
hashrateShare: (1 - (totalAcceleratedHashrate / miningStats.lastEstimatedHashrate)),
|
||||||
|
},
|
||||||
|
...accelerationPositions.map(pos => ({
|
||||||
|
block: pos.block,
|
||||||
|
hashrateShare: ((pools[pos.poolId].lastEstimatedHashrate) / miningStats.lastEstimatedHashrate)
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
return this.calculateETAFromShares(shares, da);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
- Let $\{C_i\}$ be the set of pools.
|
||||||
|
- $P(C_i)$ is the probability that a random block belongs to pool $C_i$.
|
||||||
|
- $N(C_i)$ is the number of blocks that need to be mined before a block by pool $C_i$ contains the given transaction.
|
||||||
|
- $H(n)$ is the proportion of hashrate for which the transaction is in mempool block ≤ $n$
|
||||||
|
- $S(n)$ is the probability of the transaction being mined in block $n$
|
||||||
|
- by definition, $S(max) = 1$ , where $max$ is the maximum depth of the transaction in any mempool, and therefore $S(n>max) = 0$
|
||||||
|
- $Q$ is the expected number of blocks before the transaction is confirmed
|
||||||
|
- $E$ is the expected time before the transaction is confirmed
|
||||||
|
|
||||||
|
- $S(i) = H(i) \times (1 - \sum_{j=0}^{i-1} S(j))$
|
||||||
|
- the probability of mining a block including the transaction at this depth, multiplied by the probability that it hasn't already been mined at an earlier depth.
|
||||||
|
- $Q = \sum_{i=0}^{max} S(i) \times (i+1)$
|
||||||
|
- number of blocks, weighted by the probability that the block includes the transaction
|
||||||
|
- $E = Q \times T$
|
||||||
|
- expected number of blocks, multiplied by the avg time per block
|
||||||
|
*/
|
||||||
|
calculateETAFromShares(shares: { block: number, hashrateShare: number }[], da: DifficultyAdjustment, now: number = Date.now()): ETA {
|
||||||
|
const max = shares.reduce((max, share) => Math.max(max, share.block), 0);
|
||||||
|
|
||||||
|
let tailProb = 0;
|
||||||
|
let Q = 0;
|
||||||
|
for (let i = 0; i < max; i++) {
|
||||||
|
// find H_i
|
||||||
|
const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0);
|
||||||
|
// find S_i
|
||||||
|
let S = H * (1 - tailProb);
|
||||||
|
// accumulate sum (S_i x i)
|
||||||
|
Q += (S * (i + 1));
|
||||||
|
// accumulate sum (S_j)
|
||||||
|
tailProb += S;
|
||||||
|
}
|
||||||
|
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
|
||||||
|
Q += (1-tailProb);
|
||||||
|
const eta = da.timeAvg * Q; // T x Q
|
||||||
|
|
||||||
|
return {
|
||||||
|
now,
|
||||||
|
time: eta + now + da.timeOffset,
|
||||||
|
wait: eta,
|
||||||
|
blocks: Math.ceil(eta / da.adjustedTimeAvg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ export interface MiningUnits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MiningStats {
|
export interface MiningStats {
|
||||||
lastEstimatedHashrate: string;
|
lastEstimatedHashrate: number;
|
||||||
blockCount: number;
|
blockCount: number;
|
||||||
totalEmptyBlock: number;
|
totalEmptyBlock: number;
|
||||||
totalEmptyBlockRatio: string;
|
totalEmptyBlockRatio: string;
|
||||||
@ -111,7 +111,7 @@ export class MiningService {
|
|||||||
const poolsStats = stats.pools.map((poolStat) => {
|
const poolsStats = stats.pools.map((poolStat) => {
|
||||||
return {
|
return {
|
||||||
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
|
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
|
||||||
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
|
lastEstimatedHashrate: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider,
|
||||||
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
||||||
logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
|
logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
|
||||||
...poolStat
|
...poolStat
|
||||||
@ -119,7 +119,7 @@ export class MiningService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastEstimatedHashrate: (stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
|
lastEstimatedHashrate: stats.lastEstimatedHashrate / hashrateDivider,
|
||||||
blockCount: stats.blockCount,
|
blockCount: stats.blockCount,
|
||||||
totalEmptyBlock: totalEmptyBlock,
|
totalEmptyBlock: totalEmptyBlock,
|
||||||
totalEmptyBlockRatio: totalEmptyBlockRatio,
|
totalEmptyBlockRatio: totalEmptyBlockRatio,
|
||||||
|
@ -96,8 +96,6 @@ import { ToggleComponent } from './components/toggle/toggle.component';
|
|||||||
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
|
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
|
||||||
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
|
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
|
||||||
import { GlobalFooterComponent } from './components/global-footer/global-footer.component';
|
import { GlobalFooterComponent } from './components/global-footer/global-footer.component';
|
||||||
import { AcceleratePreviewComponent } from '../components/accelerate-preview/accelerate-preview.component';
|
|
||||||
import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component';
|
|
||||||
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
|
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
|
||||||
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
|
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
|
||||||
import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component';
|
import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component';
|
||||||
@ -212,8 +210,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
GeolocationComponent,
|
GeolocationComponent,
|
||||||
TestnetAlertComponent,
|
TestnetAlertComponent,
|
||||||
GlobalFooterComponent,
|
GlobalFooterComponent,
|
||||||
AcceleratePreviewComponent,
|
|
||||||
AccelerateFeeGraphComponent,
|
|
||||||
CalculatorComponent,
|
CalculatorComponent,
|
||||||
BitcoinsatoshisPipe,
|
BitcoinsatoshisPipe,
|
||||||
BlockViewComponent,
|
BlockViewComponent,
|
||||||
@ -355,8 +351,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
TestnetAlertComponent,
|
TestnetAlertComponent,
|
||||||
PreviewTitleComponent,
|
PreviewTitleComponent,
|
||||||
GlobalFooterComponent,
|
GlobalFooterComponent,
|
||||||
AcceleratePreviewComponent,
|
|
||||||
AccelerateFeeGraphComponent,
|
|
||||||
MempoolErrorComponent,
|
MempoolErrorComponent,
|
||||||
AccelerationsListComponent,
|
AccelerationsListComponent,
|
||||||
AccelerationStatsComponent,
|
AccelerationStatsComponent,
|
||||||
|
@ -442,4 +442,18 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
|
|||||||
}
|
}
|
||||||
|
|
||||||
return flags;
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number {
|
||||||
|
if (accelerated) {
|
||||||
|
let ancestorVsize = tx.weight / 4;
|
||||||
|
let ancestorFee = tx.fee;
|
||||||
|
for (const ancestor of tx.ancestors || []) {
|
||||||
|
ancestorVsize += (ancestor.weight / 4);
|
||||||
|
ancestorFee += ancestor.fee;
|
||||||
|
}
|
||||||
|
return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize));
|
||||||
|
} else {
|
||||||
|
return tx.effectiveFeePerVsize;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user