Redesign difficulty adjustment dashboard widget
This commit is contained in:
parent
e1f0bb9901
commit
39051e94e3
@ -23,9 +23,11 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
previousTime: 1660820820,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
[ // Vector 2 (testnet)
|
||||
@ -43,11 +45,13 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
previousTime: 1660820820,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
|
||||
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
||||
|
@ -9,9 +9,11 @@ export interface DifficultyAdjustment {
|
||||
remainingBlocks: number; // Block count
|
||||
remainingTime: number; // Duration of time in ms
|
||||
previousRetarget: number; // Percent: -75 to 300
|
||||
previousTime: number; // Unix time in ms
|
||||
nextRetargetHeight: number; // Block Height
|
||||
timeAvg: number; // Duration of time in ms
|
||||
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
|
||||
expectedBlocks: number; // Block count
|
||||
}
|
||||
|
||||
export function calcDifficultyAdjustment(
|
||||
@ -32,12 +34,12 @@ export function calcDifficultyAdjustment(
|
||||
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
|
||||
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
|
||||
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
|
||||
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
|
||||
|
||||
let difficultyChange = 0;
|
||||
let timeAvgSecs = BLOCK_SECONDS_TARGET;
|
||||
let timeAvgSecs = diffSeconds / blocksInEpoch;
|
||||
// Only calculate the estimate once we have 7.2% of blocks in current epoch
|
||||
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
|
||||
timeAvgSecs = diffSeconds / blocksInEpoch;
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
|
||||
// Max increase is x4 (+300%)
|
||||
if (difficultyChange > 300) {
|
||||
@ -74,9 +76,11 @@ export function calcDifficultyAdjustment(
|
||||
remainingBlocks,
|
||||
remainingTime,
|
||||
previousRetarget,
|
||||
previousTime: DATime,
|
||||
nextRetargetHeight,
|
||||
timeAvg,
|
||||
timeOffset,
|
||||
expectedBlocks,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,87 @@
|
||||
<div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
|
||||
<div class="card-wrapper">
|
||||
<div class="card">
|
||||
<div class="card-body more-padding">
|
||||
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
||||
<div class="card-text">
|
||||
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #arrowDownDifficulty >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
</ng-template>
|
||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</div>
|
||||
</ng-template>
|
||||
<div class="symbol">
|
||||
<span i18n="difficulty-box.previous">Previous</span>:
|
||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
||||
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
|
||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #arrowDownPreviousDifficulty >
|
||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
</ng-template>
|
||||
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" *ngIf="showProgress">
|
||||
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
||||
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
|
||||
<div class="progress small-bar">
|
||||
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" *ngIf="showHalving">
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
||||
<div class="card-text">
|
||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingDifficulty>
|
||||
<div class="difficulty-skeleton loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
@ -0,0 +1,154 @@
|
||||
.difficulty-adjustment-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 76px;
|
||||
.shared-block {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
.item {
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
&:nth-child(1) {
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.difficulty-skeleton {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 14px auto 0;
|
||||
max-width: 80px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 0;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: #2d3348;
|
||||
height: 1.1rem;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.small-bar {
|
||||
height: 8px;
|
||||
top: -4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
font-size: 14px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previous-retarget-sign {
|
||||
margin-right: -2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
interface EpochProgress {
|
||||
base: string;
|
||||
change: number;
|
||||
progress: number;
|
||||
remainingBlocks: number;
|
||||
newDifficultyHeight: number;
|
||||
colorAdjustments: string;
|
||||
colorPreviousAdjustments: string;
|
||||
estimatedRetargetDate: number;
|
||||
previousRetarget: number;
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-difficulty-mining',
|
||||
templateUrl: './difficulty-mining.component.html',
|
||||
styleUrls: ['./difficulty-mining.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DifficultyMiningComponent implements OnInit {
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
difficultyEpoch$: Observable<EpochProgress>;
|
||||
|
||||
@Input() showProgress = true;
|
||||
@Input() showHalving = false;
|
||||
@Input() showTitle = true;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.difficultyEpoch$ = combineLatest([
|
||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
||||
this.stateService.difficultyAdjustment$,
|
||||
])
|
||||
.pipe(
|
||||
map(([block, da]) => {
|
||||
let colorAdjustments = '#ffffff66';
|
||||
if (da.difficultyChange > 0) {
|
||||
colorAdjustments = '#3bcc49';
|
||||
}
|
||||
if (da.difficultyChange < 0) {
|
||||
colorAdjustments = '#dc3545';
|
||||
}
|
||||
|
||||
let colorPreviousAdjustments = '#dc3545';
|
||||
if (da.previousRetarget) {
|
||||
if (da.previousRetarget >= 0) {
|
||||
colorPreviousAdjustments = '#3bcc49';
|
||||
}
|
||||
if (da.previousRetarget === 0) {
|
||||
colorPreviousAdjustments = '#ffffff66';
|
||||
}
|
||||
} else {
|
||||
colorPreviousAdjustments = '#ffffff66';
|
||||
}
|
||||
|
||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||
|
||||
const data = {
|
||||
base: `${da.progressPercent.toFixed(2)}%`,
|
||||
change: da.difficultyChange,
|
||||
progress: da.progressPercent,
|
||||
remainingBlocks: da.remainingBlocks - 1,
|
||||
colorAdjustments,
|
||||
colorPreviousAdjustments,
|
||||
newDifficultyHeight: da.nextRetargetHeight,
|
||||
estimatedRetargetDate: da.estimatedRetargetDate,
|
||||
previousRetarget: da.previousRetarget,
|
||||
blocksUntilHalving,
|
||||
timeUntilHalving,
|
||||
};
|
||||
return data;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<div
|
||||
#tooltip
|
||||
*ngIf="status"
|
||||
class="difficulty-tooltip"
|
||||
[style.visibility]="status ? 'visible' : 'hidden'"
|
||||
[style.left]="tooltipPosition.x + 'px'"
|
||||
[style.top]="tooltipPosition.y + 'px'"
|
||||
>
|
||||
<ng-container [ngSwitch]="status">
|
||||
<ng-container *ngSwitchCase="'mined'">
|
||||
<ng-container *ngIf="isAhead">
|
||||
<ng-container *ngTemplateOutlet="expected === 1 ? blocksSingular : blocksPlural; context: {$implicit: expected }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!isAhead">
|
||||
<ng-container *ngTemplateOutlet="mined === 1 ? blocksSingular : blocksPlural; context: {$implicit: mined }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'remaining'">
|
||||
<ng-container *ngTemplateOutlet="remaining === 1 ? blocksSingular : blocksPlural; context: {$implicit: remaining }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'ahead'">
|
||||
<ng-container *ngTemplateOutlet="ahead === 1 ? blocksSingular : blocksPlural; context: {$implicit: ahead }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'behind'">
|
||||
<ng-container *ngTemplateOutlet="behind === 1 ? blocksSingular : blocksPlural; context: {$implicit: behind }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'next'">
|
||||
<span i18n="@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c">next block</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
@ -0,0 +1,18 @@
|
||||
.difficulty-tooltip {
|
||||
position: fixed;
|
||||
background: rgba(#11131f, 0.95);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
|
||||
color: #b1b1b1;
|
||||
padding: 10px 15px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||
|
||||
interface EpochProgress {
|
||||
base: string;
|
||||
change: number;
|
||||
progress: number;
|
||||
minedBlocks: number;
|
||||
remainingBlocks: number;
|
||||
expectedBlocks: number;
|
||||
newDifficultyHeight: number;
|
||||
colorAdjustments: string;
|
||||
colorPreviousAdjustments: string;
|
||||
estimatedRetargetDate: number;
|
||||
previousRetarget: number;
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
}
|
||||
|
||||
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
|
||||
|
||||
@Component({
|
||||
selector: 'app-difficulty-tooltip',
|
||||
templateUrl: './difficulty-tooltip.component.html',
|
||||
styleUrls: ['./difficulty-tooltip.component.scss'],
|
||||
})
|
||||
export class DifficultyTooltipComponent implements OnChanges {
|
||||
@Input() status: string | void;
|
||||
@Input() progress: EpochProgress | void = null;
|
||||
@Input() cursorPosition: { x: number, y: number };
|
||||
|
||||
mined: number;
|
||||
ahead: number;
|
||||
behind: number;
|
||||
expected: number;
|
||||
remaining: number;
|
||||
isAhead: boolean;
|
||||
isBehind: boolean;
|
||||
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
|
||||
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
||||
let x = changes.cursorPosition.currentValue.x;
|
||||
let y = changes.cursorPosition.currentValue.y - 50;
|
||||
if (this.tooltipElement) {
|
||||
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
|
||||
x -= elementBounds.width / 2;
|
||||
x = Math.min(Math.max(x, 20), (window.innerWidth - 20 - elementBounds.width));
|
||||
}
|
||||
this.tooltipPosition = { x, y };
|
||||
}
|
||||
if ((changes.progress || changes.status) && this.progress && this.status) {
|
||||
this.remaining = this.progress.remainingBlocks;
|
||||
this.expected = this.progress.expectedBlocks;
|
||||
this.mined = this.progress.minedBlocks;
|
||||
this.ahead = Math.max(0, this.mined - this.expected);
|
||||
this.behind = Math.max(0, this.expected - this.mined);
|
||||
this.isAhead = this.ahead > 0;
|
||||
this.isBehind = this.behind > 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,81 +3,100 @@
|
||||
<div class="card">
|
||||
<div class="card-body more-padding">
|
||||
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
||||
<div class="card-text">
|
||||
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
||||
<div class="epoch-progress">
|
||||
<svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 234 9" shape-rendering="crispEdges" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#105fb0" />
|
||||
<stop offset="100%" stop-color="#9339f4" />
|
||||
</linearGradient>
|
||||
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#2486eb" />
|
||||
<stop offset="100%" stop-color="#ae6af7" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect
|
||||
*ngFor="let rect of shapes"
|
||||
[attr.x]="rect.x" [attr.y]="rect.y"
|
||||
[attr.width]="rect.w" [attr.height]="rect.h"
|
||||
class="rect {{rect.status}}"
|
||||
[class.hover]="hoverSection && rect.status === hoverSection.status"
|
||||
(pointerover)="onHover($event, rect);"
|
||||
(pointerout)="onBlur($event);"
|
||||
>
|
||||
<animate
|
||||
*ngIf="rect.status === 'next'"
|
||||
attributeType="XML"
|
||||
attributeName="fill"
|
||||
[attr.values]="'#fff;' + (rect.expected ? '#D81B60' : '#2d3348') + ';#fff'"
|
||||
dur="2s"
|
||||
repeatCount="indefinite"/>
|
||||
</rect>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #arrowDownDifficulty >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
</ng-template>
|
||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
<div class="difficulty-stats">
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
|
||||
</div>
|
||||
<div class="symbol" i18n="difficulty-box.average-block-interval">Average interval</div>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</div>
|
||||
</ng-template>
|
||||
<div class="symbol">
|
||||
<span i18n="difficulty-box.previous">Previous</span>:
|
||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
||||
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
|
||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
<div class="item">
|
||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #arrowDownPreviousDifficulty >
|
||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
<ng-template #arrowDownDifficulty >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
</ng-template>
|
||||
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
|
||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</div>
|
||||
</ng-template>
|
||||
<div class="symbol">
|
||||
<span i18n="difficulty-box.previous">Previous</span>:
|
||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
||||
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
|
||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #arrowDownPreviousDifficulty >
|
||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
</ng-template>
|
||||
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" *ngIf="showProgress">
|
||||
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
||||
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
|
||||
<div class="progress small-bar">
|
||||
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"> </div>
|
||||
<div class="item">
|
||||
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
||||
<div class="symbol">
|
||||
{{ epochData.retargetDateString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" *ngIf="showHalving">
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
||||
<div class="card-text">
|
||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingDifficulty>
|
||||
<div class="epoch-progress">
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
<div class="difficulty-skeleton loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
@ -85,3 +104,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<app-difficulty-tooltip
|
||||
*ngIf="hoverSection && (isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
[status]="hoverSection.status"
|
||||
[progress]="epochData"
|
||||
></app-difficulty-tooltip>
|
@ -1,8 +1,14 @@
|
||||
.difficulty-adjustment-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.difficulty-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 76px;
|
||||
height: 50.5px;
|
||||
.shared-block {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
@ -24,8 +30,8 @@
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
font-size: 20px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@ -33,7 +39,9 @@
|
||||
|
||||
.difficulty-skeleton {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 50.5px;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
@ -65,7 +73,7 @@
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 14px auto 0;
|
||||
margin: 10px auto 4px;
|
||||
max-width: 80px;
|
||||
}
|
||||
&:last-child {
|
||||
@ -109,7 +117,7 @@
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
min-height: 50.5px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@ -133,7 +141,7 @@
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 24px 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,4 +159,50 @@
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.epoch-progress {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.epoch-blocks {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: #2d3348;
|
||||
|
||||
.rect {
|
||||
fill: #2d3348;
|
||||
|
||||
&.behind {
|
||||
fill: #D81B60;
|
||||
}
|
||||
&.mined {
|
||||
fill: url(#diff-gradient);
|
||||
}
|
||||
&.ahead {
|
||||
fill: #1a9436;
|
||||
}
|
||||
|
||||
&.hover {
|
||||
fill: #535e84;
|
||||
&.behind {
|
||||
fill: #e94d86;
|
||||
}
|
||||
&.mined {
|
||||
fill: url(#diff-hover-gradient);
|
||||
}
|
||||
&.ahead {
|
||||
fill: #29d951;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blocks-ahead {
|
||||
color: #3bcc49;
|
||||
}
|
||||
.blocks-behind {
|
||||
color: #D81B60;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from '../..//services/state.service';
|
||||
@ -7,16 +7,33 @@ interface EpochProgress {
|
||||
base: string;
|
||||
change: number;
|
||||
progress: number;
|
||||
minedBlocks: number;
|
||||
remainingBlocks: number;
|
||||
expectedBlocks: number;
|
||||
newDifficultyHeight: number;
|
||||
colorAdjustments: string;
|
||||
colorPreviousAdjustments: string;
|
||||
estimatedRetargetDate: number;
|
||||
retargetDateString: string;
|
||||
previousRetarget: number;
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
timeAvg: number;
|
||||
}
|
||||
|
||||
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
|
||||
|
||||
interface DiffShape {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
status: BlockStatus;
|
||||
expected: boolean;
|
||||
}
|
||||
|
||||
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
|
||||
|
||||
@Component({
|
||||
selector: 'app-difficulty',
|
||||
templateUrl: './difficulty.component.html',
|
||||
@ -24,15 +41,27 @@ interface EpochProgress {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DifficultyComponent implements OnInit {
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
difficultyEpoch$: Observable<EpochProgress>;
|
||||
|
||||
@Input() showProgress = true;
|
||||
@Input() showHalving = false;
|
||||
@Input() showTitle = true;
|
||||
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
difficultyEpoch$: Observable<EpochProgress>;
|
||||
|
||||
epochStart: number;
|
||||
currentHeight: number;
|
||||
currentIndex: number;
|
||||
expectedHeight: number;
|
||||
expectedIndex: number;
|
||||
difference: number;
|
||||
shapes: DiffShape[];
|
||||
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
hoverSection: DiffShape | void;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -65,22 +94,110 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
|
||||
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
|
||||
|
||||
if (newEpochStart !== this.epochStart || newExpectedHeight !== this.expectedHeight || this.currentHeight !== this.stateService.latestBlockHeight) {
|
||||
this.epochStart = newEpochStart;
|
||||
this.expectedHeight = newExpectedHeight;
|
||||
this.currentHeight = this.stateService.latestBlockHeight;
|
||||
this.currentIndex = this.currentHeight - this.epochStart;
|
||||
this.expectedIndex = Math.min(this.expectedHeight - this.epochStart, 2106) - 1;
|
||||
this.difference = this.currentIndex - this.expectedIndex;
|
||||
|
||||
this.shapes = [];
|
||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
||||
0, Math.min(this.currentIndex, this.expectedIndex), 'mined', true
|
||||
));
|
||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
||||
this.currentIndex + 1, this.expectedIndex, 'behind', true
|
||||
));
|
||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
||||
this.expectedIndex + 1, this.currentIndex, 'ahead', false
|
||||
));
|
||||
if (this.currentIndex < 2105) {
|
||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
||||
this.currentIndex + 1, this.currentIndex + 1, 'next', (this.expectedIndex > this.currentIndex)
|
||||
));
|
||||
}
|
||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
||||
Math.max(this.currentIndex + 2, this.expectedIndex + 1), 2105, 'remaining', false
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
let retargetDateString;
|
||||
if (da.remainingBlocks > 1870) {
|
||||
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleDateString(this.locale, { month: 'long', day: 'numeric' });
|
||||
} else {
|
||||
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleTimeString(this.locale, { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
|
||||
}
|
||||
|
||||
const data = {
|
||||
base: `${da.progressPercent.toFixed(2)}%`,
|
||||
change: da.difficultyChange,
|
||||
progress: da.progressPercent,
|
||||
remainingBlocks: da.remainingBlocks,
|
||||
minedBlocks: this.currentIndex + 1,
|
||||
remainingBlocks: da.remainingBlocks - 1,
|
||||
expectedBlocks: Math.floor(da.expectedBlocks),
|
||||
colorAdjustments,
|
||||
colorPreviousAdjustments,
|
||||
newDifficultyHeight: da.nextRetargetHeight,
|
||||
estimatedRetargetDate: da.estimatedRetargetDate,
|
||||
retargetDateString,
|
||||
previousRetarget: da.previousRetarget,
|
||||
blocksUntilHalving,
|
||||
timeUntilHalving,
|
||||
timeAvg: da.timeAvg,
|
||||
};
|
||||
return data;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
blocksToShapes(start: number, end: number, status: BlockStatus, expected: boolean = false): DiffShape[] {
|
||||
const startY = start % 9;
|
||||
const startX = Math.floor(start / 9);
|
||||
const endY = (end % 9);
|
||||
const endX = Math.floor(end / 9);
|
||||
|
||||
if (startX > endX) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (startX === endX) {
|
||||
return [{
|
||||
x: startX, y: startY, w: 1, h: 1 + endY - startY, status, expected
|
||||
}];
|
||||
}
|
||||
|
||||
const shapes = [];
|
||||
shapes.push({
|
||||
x: startX, y: startY, w: 1, h: 9 - startY, status, expected
|
||||
});
|
||||
shapes.push({
|
||||
x: endX, y: 0, w: 1, h: endY + 1, status, expected
|
||||
});
|
||||
|
||||
if (startX < endX - 1) {
|
||||
shapes.push({
|
||||
x: startX + 1, y: 0, w: endX - startX - 1, h: 9, status, expected
|
||||
});
|
||||
}
|
||||
|
||||
return shapes;
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
|
||||
onHover(event, rect): void {
|
||||
this.hoverSection = rect;
|
||||
}
|
||||
|
||||
onBlur(event): void {
|
||||
this.hoverSection = null;
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
<!-- difficulty adjustment -->
|
||||
<div class="col">
|
||||
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
|
||||
<app-difficulty [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
|
||||
<app-difficulty-mining [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty-mining>
|
||||
</div>
|
||||
|
||||
<!-- pool distribution -->
|
||||
|
@ -19,6 +19,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() fixedRender = false;
|
||||
@Input() relative = false;
|
||||
@Input() forceFloorOnTimeIntervals: string[];
|
||||
@Input() fractionDigits: number = 0;
|
||||
|
||||
constructor(
|
||||
private ref: ChangeDetectorRef,
|
||||
@ -88,7 +89,12 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
} else {
|
||||
counter = Math.round(seconds / this.intervals[i]);
|
||||
}
|
||||
const dateStrings = dates(counter);
|
||||
let rounded = counter;
|
||||
if (this.fractionDigits) {
|
||||
const roundFactor = Math.pow(10,this.fractionDigits);
|
||||
rounded = Math.round((seconds / this.intervals[i]) * roundFactor) / roundFactor;
|
||||
}
|
||||
const dateStrings = dates(rounded);
|
||||
if (counter > 0) {
|
||||
switch (this.kind) {
|
||||
case 'since':
|
||||
|
@ -33,9 +33,11 @@ export interface DifficultyAdjustment {
|
||||
remainingBlocks: number;
|
||||
remainingTime: number;
|
||||
previousRetarget: number;
|
||||
previousTime: number;
|
||||
nextRetargetHeight: number;
|
||||
timeAvg: number;
|
||||
timeOffset: number;
|
||||
expectedBlocks: number;
|
||||
}
|
||||
|
||||
export interface AddressInformation {
|
||||
|
@ -58,6 +58,8 @@ import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.c
|
||||
import { StatusViewComponent } from '../components/status-view/status-view.component';
|
||||
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
|
||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
||||
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
|
||||
import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
|
||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
|
||||
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
|
||||
import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component';
|
||||
@ -133,6 +135,8 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
StatusViewComponent,
|
||||
FeesBoxComponent,
|
||||
DifficultyComponent,
|
||||
DifficultyMiningComponent,
|
||||
DifficultyTooltipComponent,
|
||||
TxBowtieGraphComponent,
|
||||
TxBowtieGraphTooltipComponent,
|
||||
TermsOfServiceComponent,
|
||||
@ -234,6 +238,8 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
StatusViewComponent,
|
||||
FeesBoxComponent,
|
||||
DifficultyComponent,
|
||||
DifficultyMiningComponent,
|
||||
DifficultyTooltipComponent,
|
||||
TxBowtieGraphComponent,
|
||||
TxBowtieGraphTooltipComponent,
|
||||
TermsOfServiceComponent,
|
||||
|
Loading…
x
Reference in New Issue
Block a user