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 @@
-
- @if (tx && (tx.acceleratedBy || accelerationInfo) && miningStats) {
-
- } @else {
-
- }
-
+
|
@@ -38,4 +25,25 @@
-
\ 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,