From 396b7eb3d30e0126e17145f12e6fa7dea4d9ca00 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 6 Jun 2024 18:26:43 +0000 Subject: [PATCH] Add expected hashrate pie chart & eta to acceleration preview --- .../accelerate-preview.component.html | 13 +++- .../accelerate-preview.component.scss | 5 ++ .../accelerate-preview.component.ts | 53 ++++++++++++++++- .../active-acceleration-box.component.html | 44 ++++++++------ .../active-acceleration-box.component.ts | 17 +++--- .../transaction/transaction.component.html | 2 +- .../transaction/transaction.component.ts | 4 ++ .../transaction/transaction.module.ts | 4 ++ frontend/src/app/services/eta.service.ts | 59 +++++++++++-------- frontend/src/app/shared/shared.module.ts | 6 -- 10 files changed, 150 insertions(+), 57 deletions(-) diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html index ae613f1a5..4c92269d8 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html @@ -65,10 +65,21 @@
+
How much faster?
+
+
+ Your transaction will be prioritized by up to {{ hashratePercentage | number : '1.1-1' }}% of miners. + This will reduce your expected waiting time until the first confirmation to +
+
+ +
+
+
How much more are you willing to pay?
- Choose the maximum extra transaction fee you're willing to pay to get into the next block. + Choose the maximum extra transaction fee you're willing to pay.
diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss index fa598f3a3..1191d882e 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss @@ -107,6 +107,11 @@ margin-top: 1em; } +.col.pie { + flex-grow: 0; + padding: 0 1em; +} + .item { white-space: initial; } diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts index 76833bb1a..6d4c88a00 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts @@ -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 */ diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html index d009a5e63..711269a47 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html @@ -1,3 +1,6 @@ +@if (chartOnly) { + +} @else { @@ -12,23 +15,7 @@ @@ -38,4 +25,25 @@ -
-
- @if (tx && (tx.acceleratedBy || accelerationInfo) && miningStats) { -
- } @else { -
-
-
- } -
+
\ No newline at end of file + +} + + +
+ @if (chartOptions && miningStats) { +
+ } @else { +
+
+
+ } +
+
\ No newline at end of file diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts index 309d2ed1f..2d94cad50 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts @@ -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), } ] }; diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index f70bd3f0e..2a2925879 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -83,7 +83,7 @@
- +
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index afbc6d62b..37c83f008 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -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; diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index a1331a463..eb663c9ac 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -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 { } diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index eba311012..467f49554 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -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), } - } } } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index ead9060ae..2f7bd4dc4 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -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,