Add expected hashrate pie chart & eta to acceleration preview
This commit is contained in:
parent
05724b9d58
commit
396b7eb3d3
@ -65,10 +65,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="row">
|
||||
<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="fee-card">
|
||||
<div class="d-flex mb-0">
|
||||
|
@ -107,6 +107,11 @@
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.col.pie {
|
||||
flex-grow: 0;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
white-space: initial;
|
||||
}
|
||||
|
@ -6,6 +6,9 @@ import { nextRoundNumber } from '../../shared/common.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { MiningStats } from '../../services/mining.service';
|
||||
import { EtaService } from '../../services/eta.service';
|
||||
import { DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../../interfaces/node-api.interface';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
txSummary: TxSummary;
|
||||
@ -40,7 +43,9 @@ export const MAX_BID_RATIO = 4;
|
||||
styleUrls: ['accelerate-preview.component.scss']
|
||||
})
|
||||
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() tx: Transaction | undefined;
|
||||
@Input() tx: Transaction;
|
||||
@Input() mempoolPosition: MempoolPosition;
|
||||
@Input() miningStats: MiningStats;
|
||||
@Input() scrollEvent: boolean;
|
||||
|
||||
math = Math;
|
||||
@ -48,7 +53,12 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
showSuccess = false;
|
||||
estimateSubscription: Subscription;
|
||||
accelerationSubscription: Subscription;
|
||||
difficultySubscription: Subscription;
|
||||
da: DifficultyAdjustment;
|
||||
estimate: any;
|
||||
hashratePercentage?: number;
|
||||
ETA?: number;
|
||||
acceleratedETA?: number;
|
||||
hasAncestors: boolean = false;
|
||||
minExtraCost = 0;
|
||||
minBidAllowed = 0;
|
||||
@ -67,6 +77,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
public stateService: StateService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private storageService: StorageService,
|
||||
private etaService: EtaService,
|
||||
private audioService: AudioService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
@ -76,16 +87,24 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
this.difficultySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.accelerationUUID = window.crypto.randomUUID();
|
||||
this.difficultySubscription = this.stateService.difficultyAdjustment$.subscribe(da => {
|
||||
this.da = da;
|
||||
this.updateETA();
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.scrollEvent) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
if (changes.miningStats || changes.mempoolPosition) {
|
||||
this.updateETA();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@ -113,6 +132,8 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
}
|
||||
|
||||
this.updateETA();
|
||||
|
||||
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
|
||||
|
||||
// Make min extra fee at least 50% of the current tx fee
|
||||
@ -157,6 +178,36 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
).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
|
||||
*/
|
||||
|
@ -1,3 +1,6 @@
|
||||
@if (chartOnly) {
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
} @else {
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
@ -12,23 +15,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="pie-chart" rowspan="2">
|
||||
<div class="chart-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>
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -38,4 +25,25 @@
|
||||
</td>
|
||||
</tr>
|
||||
</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() accelerationInfo: Acceleration;
|
||||
@Input() miningStats: MiningStats;
|
||||
@Input() pools: number[];
|
||||
@Input() chartOnly: boolean = false;
|
||||
|
||||
acceleratedByPercentage: string = '';
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartOptions: EChartsOption;
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
@ -28,12 +30,13 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
constructor() {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.tx && (this.tx.acceleratedBy || this.accelerationInfo) && this.miningStats) {
|
||||
this.prepareChartOptions();
|
||||
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
|
||||
if (pools && this.miningStats) {
|
||||
this.prepareChartOptions(pools);
|
||||
}
|
||||
}
|
||||
|
||||
getChartData() {
|
||||
getChartData(poolList: number[]) {
|
||||
const data: object[] = [];
|
||||
const pools: { [id: number]: SinglePoolStats } = {};
|
||||
for (const pool of this.miningStats.pools) {
|
||||
@ -73,7 +76,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
});
|
||||
|
||||
let totalAcceleratedHashrate = 0;
|
||||
for (const poolId of (this.accelerationInfo?.pools || this.tx.acceleratedBy || [])) {
|
||||
for (const poolId of poolList || []) {
|
||||
const pool = pools[poolId];
|
||||
if (!pool) {
|
||||
continue;
|
||||
@ -96,7 +99,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
return data;
|
||||
}
|
||||
|
||||
prepareChartOptions() {
|
||||
prepareChartOptions(pools: number[]) {
|
||||
this.chartOptions = {
|
||||
animation: false,
|
||||
grid: {
|
||||
@ -113,7 +116,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '100%',
|
||||
data: this.getChartData(),
|
||||
data: this.getChartData(pools),
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -83,7 +83,7 @@
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
|
@ -682,6 +682,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||
this.miningStats = stats;
|
||||
});
|
||||
|
||||
document.location.hash = '#accelerate';
|
||||
this.enterpriseService.goal(8);
|
||||
this.showAccelerationSummary = true && this.acceleratorAvailable;
|
||||
|
@ -5,6 +5,8 @@ import { TransactionComponent } from './transaction.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
|
||||
import { GraphsModule } from '../../graphs/graphs.module';
|
||||
import { AcceleratePreviewComponent } from '../accelerate-preview/accelerate-preview.component';
|
||||
import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -36,6 +38,8 @@ export class TransactionRoutingModule { }
|
||||
],
|
||||
declarations: [
|
||||
TransactionComponent,
|
||||
AcceleratePreviewComponent,
|
||||
AccelerateFeeGraphComponent,
|
||||
]
|
||||
})
|
||||
export class TransactionModule { }
|
||||
|
@ -116,38 +116,52 @@ export class EtaService {
|
||||
if (!accelerationPositions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Define parameters**
|
||||
- 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
|
||||
**Overall expected confirmation time**
|
||||
- $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
|
||||
*/
|
||||
const pools: { [id: number]: SinglePoolStats } = {};
|
||||
for (const pool of miningStats.pools) {
|
||||
pools[pool.poolUniqueId] = pool;
|
||||
}
|
||||
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
|
||||
const positions = [unacceleratedPosition, ...accelerationPositions];
|
||||
const max = unacceleratedPosition.block; // by definition, assuming no negative fee deltas or out of band txs
|
||||
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 = accelerationPositions.reduce((total, pos) => total + (pos.block <= i ? pools[pos.poolId].lastEstimatedHashrate : 0), 0) / miningStats.lastEstimatedHashrate;
|
||||
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)
|
||||
@ -165,6 +179,5 @@ export class EtaService {
|
||||
wait: eta,
|
||||
blocks: Math.ceil(eta / da.adjustedTimeAvg),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,8 +96,6 @@ import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
|
||||
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.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 { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
|
||||
import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component';
|
||||
@ -212,8 +210,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
GeolocationComponent,
|
||||
TestnetAlertComponent,
|
||||
GlobalFooterComponent,
|
||||
AcceleratePreviewComponent,
|
||||
AccelerateFeeGraphComponent,
|
||||
CalculatorComponent,
|
||||
BitcoinsatoshisPipe,
|
||||
BlockViewComponent,
|
||||
@ -355,8 +351,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
TestnetAlertComponent,
|
||||
PreviewTitleComponent,
|
||||
GlobalFooterComponent,
|
||||
AcceleratePreviewComponent,
|
||||
AccelerateFeeGraphComponent,
|
||||
MempoolErrorComponent,
|
||||
AccelerationsListComponent,
|
||||
AccelerationStatsComponent,
|
||||
|
Loading…
x
Reference in New Issue
Block a user