Merge branch 'master' into simon/dashboard-assets
# Conflicts: # frontend/src/app/app.module.ts
This commit is contained in:
@@ -26,6 +26,9 @@ import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.com
|
||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||
import { AssetsComponent } from './components/assets/assets.component';
|
||||
import { PoolComponent } from './components/pool/pool.component';
|
||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
||||
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
|
||||
|
||||
let routes: Routes = [
|
||||
{
|
||||
@@ -56,16 +59,28 @@ let routes: Routes = [
|
||||
path: 'mempool-block/:id',
|
||||
component: MempoolBlockComponent
|
||||
},
|
||||
{
|
||||
path: 'mining',
|
||||
component: MiningDashboardComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: LatestBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/difficulty',
|
||||
component: DifficultyChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pools',
|
||||
component: PoolRankingComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pool/:poolId',
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
@@ -144,16 +159,28 @@ let routes: Routes = [
|
||||
path: 'mempool-block/:id',
|
||||
component: MempoolBlockComponent
|
||||
},
|
||||
{
|
||||
path: 'mining',
|
||||
component: MiningDashboardComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: LatestBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/difficulty',
|
||||
component: DifficultyChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pools',
|
||||
component: PoolRankingComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pool/:poolId',
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
@@ -226,16 +253,28 @@ let routes: Routes = [
|
||||
path: 'mempool-block/:id',
|
||||
component: MempoolBlockComponent
|
||||
},
|
||||
{
|
||||
path: 'mining',
|
||||
component: MiningDashboardComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: LatestBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/difficulty',
|
||||
component: DifficultyChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pools',
|
||||
component: PoolRankingComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pool/:poolId',
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
|
||||
@@ -38,6 +38,7 @@ import { TimeSpanComponent } from './components/time-span/time-span.component';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
|
||||
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
|
||||
import { PoolComponent } from './components/pool/pool.component';
|
||||
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsComponent } from './components/assets/assets.component';
|
||||
@@ -68,6 +69,8 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||
import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component';
|
||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
||||
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -97,6 +100,7 @@ import { AssetCirculationComponent } from './components/asset-circulation/asset-
|
||||
IncomingTransactionsGraphComponent,
|
||||
MempoolGraphComponent,
|
||||
PoolRankingComponent,
|
||||
PoolComponent,
|
||||
LbtcPegsGraphComponent,
|
||||
AssetComponent,
|
||||
AssetsComponent,
|
||||
@@ -118,6 +122,8 @@ import { AssetCirculationComponent } from './components/asset-circulation/asset-
|
||||
AssetsFeaturedComponent,
|
||||
AssetGroupComponent,
|
||||
AssetCirculationComponent,
|
||||
MiningDashboardComponent,
|
||||
DifficultyChartComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
|
||||
@@ -21,9 +21,13 @@
|
||||
</div>
|
||||
<div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
||||
</div>
|
||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||
<a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
|
||||
{{ block.extras.pool.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingBlocksTemplate>
|
||||
|
||||
@@ -124,3 +124,20 @@
|
||||
50% {opacity: 1.0;}
|
||||
100% {opacity: 0.7;}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.animated {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.hide {
|
||||
opacity: 0;
|
||||
pointer-events : none;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { specialBlocks } from 'src/app/app.constants';
|
||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||
import { Location } from '@angular/common';
|
||||
import { config } from 'process';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blockchain-blocks',
|
||||
@@ -31,6 +32,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
arrowLeftPx = 30;
|
||||
blocksFilled = false;
|
||||
transition = '1s';
|
||||
showMiningInfo = false;
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
@@ -43,11 +45,22 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private router: Router,
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
private location: Location,
|
||||
) {
|
||||
}
|
||||
|
||||
enabledMiningInfoIfNeeded(url) {
|
||||
this.showMiningInfo = url === '/mining';
|
||||
this.cd.markForCheck(); // Need to update the view asap
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||
this.enabledMiningInfoIfNeeded(this.location.path());
|
||||
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||
}
|
||||
|
||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
||||
this.feeRounding = '1.0-1';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="text-center" class="blockchain-wrapper">
|
||||
<div class="text-center" class="blockchain-wrapper" #container>
|
||||
<div class="position-container {{ network }}">
|
||||
<span>
|
||||
<app-mempool-blocks></app-mempool-blocks>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
}
|
||||
|
||||
.blockchain-wrapper {
|
||||
overflow: hidden;
|
||||
height: 250px;
|
||||
|
||||
-webkit-user-select: none; /* Safari */
|
||||
|
||||
@@ -11,7 +11,7 @@ export class BlockchainComponent implements OnInit {
|
||||
network: string;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<div [class]="widget === false ? 'container-xl' : ''">
|
||||
|
||||
<div *ngIf="difficultyObservable$ | async" class="" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-header mb-0 mb-lg-4" [style]="widget ? 'display:none' : ''">
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(difficultyObservable$ | async) as diffChanges">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'" [routerLink]="['/mining/difficulty' | relativeUrl]" fragment="all"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless table-sm text-center" *ngIf="!widget">
|
||||
<thead>
|
||||
<tr>
|
||||
<th i18n="mining.rank">Block</th>
|
||||
<th i18n="block.timestamp">Timestamp</th>
|
||||
<th i18n="mining.difficulty">Difficulty</th>
|
||||
<th i18n="mining.change">Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody *ngIf="(difficultyObservable$ | async) as diffChanges">
|
||||
<tr *ngFor="let diffChange of diffChanges.data">
|
||||
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
|
||||
<td>‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
|
||||
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
|
||||
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-difficulty-chart',
|
||||
templateUrl: './difficulty-chart.component.html',
|
||||
styleUrls: ['./difficulty-chart.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 38%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class DifficultyChartComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg'
|
||||
};
|
||||
|
||||
difficultyObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@mining.difficulty:Difficulty`);
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const powerOfTen = {
|
||||
terra: Math.pow(10, 12),
|
||||
giga: Math.pow(10, 9),
|
||||
mega: Math.pow(10, 6),
|
||||
kilo: Math.pow(10, 3),
|
||||
}
|
||||
|
||||
this.difficultyObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith('1y'),
|
||||
switchMap((timespan) => {
|
||||
return this.apiService.getHistoricalDifficulty$(timespan)
|
||||
.pipe(
|
||||
tap(data => {
|
||||
this.prepareChartOptions(data.adjustments.map(val => [val.timestamp * 1000, val.difficulty]));
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map(data => {
|
||||
const availableTimespanDay = (
|
||||
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000)
|
||||
) / 3600 / 24;
|
||||
|
||||
const tableData = [];
|
||||
for (let i = 0; i < data.adjustments.length - 1; ++i) {
|
||||
const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100;
|
||||
let selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
|
||||
if (data.adjustments[i].difficulty < powerOfTen.mega) {
|
||||
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
|
||||
} else if (data.adjustments[i].difficulty < powerOfTen.giga) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
|
||||
} else if (data.adjustments[i].difficulty < powerOfTen.terra) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
|
||||
}
|
||||
|
||||
tableData.push(Object.assign(data.adjustments[i], {
|
||||
change: change,
|
||||
difficultyShorten: formatNumber(
|
||||
data.adjustments[i].difficulty / selectedPowerOfTen.divider,
|
||||
this.locale, '1.2-2') + selectedPowerOfTen.unit
|
||||
}));
|
||||
}
|
||||
return {
|
||||
availableTimespanDay: availableTimespanDay,
|
||||
data: tableData
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
this.chartOptions = {
|
||||
title: {
|
||||
text: this.widget? '' : $localize`:@@mining.difficulty:Difficulty`,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#FFF',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
splitNumber: this.isMobile() ? 5 : 10,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: (val) => {
|
||||
const diff = val / Math.pow(10, 12); // terra
|
||||
return diff.toString() + 'T';
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
color: '#ffffff66',
|
||||
opacity: 0.25,
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: data,
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
areaStyle: {}
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,11 @@
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
|
||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" id="btn-pools">
|
||||
<a class="nav-link" [routerLink]="['/mining/pools' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-pools" title="Mining Pools"></fa-icon></a>
|
||||
<li class="nav-item" routerLinkActive="active" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
|
||||
<a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" id="btn-graphs">
|
||||
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
|
||||
|
||||
@@ -18,7 +18,7 @@ export class MasterPageComponent implements OnInit {
|
||||
urlLanguage: string;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
) { }
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="container-xl dashboard-container">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<!-- pool distribution -->
|
||||
<div class="col">
|
||||
<div class="main-title" i18n="mining.pool-share">Mining Pools Share (1w)</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-pool-ranking [widget]=true></app-pool-ranking>
|
||||
<div class="text-center"><a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
|
||||
»</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- difficulty -->
|
||||
<div class="col">
|
||||
<div class="main-title" i18n="mining.difficulty">Difficulty (1y)</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-difficulty-chart [widget]=true></app-difficulty-chart>
|
||||
<div class="text-center"><a href="" [routerLink]="['/mining/difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
|
||||
»</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
.dashboard-container {
|
||||
padding-bottom: 60px;
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
@media (min-width: 992px) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.col {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#blockchain-container {
|
||||
position: relative;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
#blockchain-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fade-border {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mining-dashboard',
|
||||
templateUrl: './mining-dashboard.component.html',
|
||||
styleUrls: ['./mining-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MiningDashboardComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +1,48 @@
|
||||
<div class="container-xl">
|
||||
<!-- <app-difficulty [showProgress]=false [showHalving]=true></app-difficulty> -->
|
||||
<div [class]="widget === false ? 'container-xl' : ''">
|
||||
|
||||
<div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-header mb-0 mb-lg-4">
|
||||
<div class="card-header mb-0 mb-lg-4" [style]="widget === true ? 'display:none' : ''">
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1">
|
||||
<input ngbButton type="radio" [value]="'24h'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="24h"> 24h
|
||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h"> 24h
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 3">
|
||||
<input ngbButton type="radio" [value]="'3d'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="3d"> 3D
|
||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d"> 3D
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 7">
|
||||
<input ngbButton type="radio" [value]="'1w'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="1w"> 1W
|
||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w"> 1W
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 30">
|
||||
<input ngbButton type="radio" [value]="'1m'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="1m"> 1M
|
||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="3m"> 3M
|
||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="6m"> 6M
|
||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="1y"> 1Y
|
||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="2y"> 2Y
|
||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="3y"> 3Y
|
||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'" [routerLink]="['/mining/pools' | relativeUrl]" fragment="all"> ALL
|
||||
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless text-center pools-table" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50">
|
||||
<table *ngIf="widget === false" class="table table-borderless text-center pools-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="d-none d-md-block" i18n="mining.rank">Rank</th>
|
||||
@@ -58,14 +57,14 @@
|
||||
<tr *ngFor="let pool of miningStats.pools">
|
||||
<td class="d-none d-md-block">{{ pool.rank }}</td>
|
||||
<td class="text-right"><img width="25" height="25" src="{{ pool.logo }}" onError="this.src = './resources/mining-pools/default.svg'"></td>
|
||||
<td class="">{{ pool.name }}</td>
|
||||
<td class=""><a [routerLink]="[('/mining/pool/' + pool.poolId) | relativeUrl]">{{ pool.name }}</a></td>
|
||||
<td class="" *ngIf="this.poolsWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="">{{ pool['blockText'] }}</td>
|
||||
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
|
||||
</tr>
|
||||
<tr style="border-top: 1px solid #555">
|
||||
<td class="d-none d-md-block">-</td>
|
||||
<td class="text-right"><img width="25" height="25" src="./resources/mining-pools/default.svg"></td>
|
||||
<td class="d-none d-md-block"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="" i18n="mining.all-miners"><b>All miners</b></td>
|
||||
<td class="" *ngIf="this.poolsWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{ miningStats.miningUnits.hashrateUnit }}</b></td>
|
||||
<td class=""><b>{{ miningStats.blockCount }}</b></td>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { combineLatest, Observable, of } from 'rxjs';
|
||||
import { catchError, map, share, skip, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SinglePoolStats } from 'src/app/interfaces/node-api.interface';
|
||||
@@ -8,6 +9,7 @@ import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StorageService } from '../..//services/storage.service';
|
||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { chartColors } from 'src/app/app.constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pool-ranking',
|
||||
@@ -22,7 +24,9 @@ import { StateService } from '../../services/state.service';
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
export class PoolRankingComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
poolsWindowPreference: string;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
@@ -31,6 +35,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
chartInitOptions = {
|
||||
renderer: 'svg'
|
||||
};
|
||||
chartInstance: any = undefined;
|
||||
|
||||
miningStatsObservable$: Observable<MiningStats>;
|
||||
|
||||
@@ -40,14 +45,20 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
private formBuilder: FormBuilder,
|
||||
private miningService: MiningService,
|
||||
private seoService: SeoService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
|
||||
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.widget) {
|
||||
this.poolsWindowPreference = '1w';
|
||||
} else {
|
||||
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
|
||||
|
||||
// When...
|
||||
this.miningStatsObservable$ = combineLatest([
|
||||
// ...a new block is mined
|
||||
@@ -61,7 +72,9 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
startWith(this.poolsWindowPreference), // (trigger when the page loads)
|
||||
tap((value) => {
|
||||
this.storageService.setValue('poolsWindowPreference', value);
|
||||
if (!this.widget) {
|
||||
this.storageService.setValue('poolsWindowPreference', value);
|
||||
}
|
||||
this.poolsWindowPreference = value;
|
||||
})
|
||||
)
|
||||
@@ -87,9 +100,6 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
}
|
||||
|
||||
formatPoolUI(pool: SinglePoolStats) {
|
||||
pool['blockText'] = pool.blockCount.toString() + ` (${pool.share}%)`;
|
||||
return pool;
|
||||
@@ -115,9 +125,9 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
overflow: 'break',
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "#282d47",
|
||||
backgroundColor: '#282d47',
|
||||
textStyle: {
|
||||
color: "#FFFFFF",
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
formatter: () => {
|
||||
if (this.poolsWindowPreference === '24h') {
|
||||
@@ -129,8 +139,9 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
pool.blockCount.toString() + ` blocks`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
data: pool.poolId,
|
||||
} as PieSeriesOption);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
@@ -144,8 +155,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.chartOptions = {
|
||||
title: {
|
||||
text: $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
|
||||
subtext: $localize`:@@mining.pool-chart-sub-title:Estimated from the # of blocks mined`,
|
||||
text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#FFF',
|
||||
@@ -160,10 +170,11 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
top: this.isMobile() ? '5%' : '20%',
|
||||
top: this.widget ? '0%' : (this.isMobile() ? '5%' : '10%'),
|
||||
bottom: this.widget ? '0%' : (this.isMobile() ? '0%' : '5%'),
|
||||
name: 'Mining pool',
|
||||
type: 'pie',
|
||||
radius: this.isMobile() ? ['10%', '50%'] : ['20%', '80%'],
|
||||
radius: this.widget ? ['20%', '60%'] : (this.isMobile() ? ['10%', '50%'] : ['20%', '70%']),
|
||||
data: this.generatePoolsChartSerieData(miningStats),
|
||||
labelLine: {
|
||||
lineStyle: {
|
||||
@@ -180,11 +191,8 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
borderRadius: 2,
|
||||
shadowBlur: 80,
|
||||
shadowColor: 'rgba(255, 255, 255, 0.75)',
|
||||
shadowBlur: 40,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
},
|
||||
labelLine: {
|
||||
lineStyle: {
|
||||
@@ -193,10 +201,22 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
color: chartColors
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartInstance = ec;
|
||||
this.chartInstance.on('click', (e) => {
|
||||
this.router.navigate(['/mining/pool/', e.data.data]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Default mining stats if something goes wrong
|
||||
*/
|
||||
|
||||
113
frontend/src/app/components/pool/pool.component.html
Normal file
113
frontend/src/app/components/pool/pool.component.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<div class="container">
|
||||
|
||||
<div *ngIf="poolStats$ | async as poolStats">
|
||||
<h1 class="m-0">
|
||||
<img width="50" src="{{ poolStats['logo'] }}" onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
|
||||
{{ poolStats.pool.name }}
|
||||
</h1>
|
||||
|
||||
<div class="box pl-0 bg-transparent">
|
||||
<div class="card-header mb-0 mb-lg-4 pr-0 pl-0">
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup ml-0">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'24h'"> 24h
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'3d'"> 3D
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'1w'"> 1W
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'1m'"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'3m'"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'6m'"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'1y'"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'2y'"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'3y'"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<table class="table table-borderless table-striped" style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-3">Addresses</td>
|
||||
<td class="text-truncate" *ngIf="poolStats.pool.addresses.length else noaddress">
|
||||
<div class="scrollable">
|
||||
<a *ngFor="let address of poolStats.pool.addresses" [routerLink]="['/address' | relativeUrl, address]">{{ address }}<br></a>
|
||||
</div>
|
||||
</td>
|
||||
<ng-template #noaddress><td>~</td></ng-template>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-3">Coinbase Tags</td>
|
||||
<td class="text-truncate">{{ poolStats.pool.regexes }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-8">Mined Blocks</td>
|
||||
<td class="text-left">{{ poolStats.blockCount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-8">Empty Blocks</td>
|
||||
<td class="text-left">{{ poolStats.emptyBlocks.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
||||
<thead>
|
||||
<th style="width: 15%;" i18n="latest-blocks.height">Height</th>
|
||||
<th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th>
|
||||
<th style="width: 20%;" i18n="latest-blocks.mined">Mined</th>
|
||||
<th style="width: 10%;" i18n="latest-blocks.reward">Reward</th>
|
||||
<th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</th>
|
||||
<th style="width: 20%;" i18n="latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks$ | async as blocks">
|
||||
<tr *ngFor="let block of blocks">
|
||||
<td><a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a></td>
|
||||
<td class="d-none d-md-block">‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
|
||||
<td class=""><app-amount [satoshis]="block['reward']" digitsInfo="1.2-2" [noFiat]="true"></app-amount></td>
|
||||
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
41
frontend/src/app/components/pool/pool.component.scss
Normal file
41
frontend/src/app/components/pool/pool.component.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
.progress {
|
||||
background-color: #2d3348;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.d-md-block {
|
||||
display: table-cell !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.d-lg-block {
|
||||
display: table-cell !important;
|
||||
}
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 830px) {
|
||||
margin-left: 2%;
|
||||
flex-direction: row;
|
||||
float: left;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 9px;
|
||||
@media (min-width: 830px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.scrollable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
max-height: 100px;
|
||||
}
|
||||
84
frontend/src/app/components/pool/pool.component.ts
Normal file
84
frontend/src/app/components/pool/pool.component.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pool',
|
||||
templateUrl: './pool.component.html',
|
||||
styleUrls: ['./pool.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PoolComponent implements OnInit {
|
||||
poolStats$: Observable<PoolStat>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
|
||||
fromHeight: number = -1;
|
||||
fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromHeight);
|
||||
|
||||
blocks: BlockExtended[] = [];
|
||||
poolId: number = undefined;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1w');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.poolStats$ = combineLatest([
|
||||
this.route.params.pipe(map((params) => params.poolId)),
|
||||
this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('1w')),
|
||||
])
|
||||
.pipe(
|
||||
switchMap((params: any) => {
|
||||
this.poolId = params[0];
|
||||
if (this.blocks.length === 0) {
|
||||
this.fromHeightSubject.next(undefined);
|
||||
}
|
||||
return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w');
|
||||
}),
|
||||
map((poolStats) => {
|
||||
let regexes = '"';
|
||||
for (const regex of poolStats.pool.regexes) {
|
||||
regexes += regex + '", "';
|
||||
}
|
||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||
poolStats.pool.addresses = poolStats.pool.addresses;
|
||||
|
||||
return Object.assign({
|
||||
logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
||||
}, poolStats);
|
||||
})
|
||||
);
|
||||
|
||||
this.blocks$ = this.fromHeightSubject
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((fromHeight) => {
|
||||
return this.apiService.getPoolBlocks$(this.poolId, fromHeight);
|
||||
}),
|
||||
tap((newBlocks) => {
|
||||
this.blocks = this.blocks.concat(newBlocks);
|
||||
}),
|
||||
map(() => this.blocks)
|
||||
)
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.fromHeightSubject.next(this.blocks[this.blocks.length - 1]?.height);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
return block.height;
|
||||
}
|
||||
}
|
||||
@@ -54,8 +54,11 @@ export interface LiquidPegs {
|
||||
|
||||
export interface ITranslators { [language: string]: string; }
|
||||
|
||||
/**
|
||||
* PoolRanking component
|
||||
*/
|
||||
export interface SinglePoolStats {
|
||||
pooldId: number;
|
||||
poolId: number;
|
||||
name: string;
|
||||
link: string;
|
||||
blockCount: number;
|
||||
@@ -66,20 +69,35 @@ export interface SinglePoolStats {
|
||||
emptyBlockRatio: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export interface PoolsStats {
|
||||
blockCount: number;
|
||||
lastEstimatedHashrate: number;
|
||||
oldestIndexedBlockTimestamp: number;
|
||||
pools: SinglePoolStats[];
|
||||
}
|
||||
|
||||
export interface MiningStats {
|
||||
lastEstimatedHashrate: string,
|
||||
blockCount: number,
|
||||
totalEmptyBlock: number,
|
||||
totalEmptyBlockRatio: string,
|
||||
pools: SinglePoolStats[],
|
||||
lastEstimatedHashrate: string;
|
||||
blockCount: number;
|
||||
totalEmptyBlock: number;
|
||||
totalEmptyBlockRatio: string;
|
||||
pools: SinglePoolStats[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pool component
|
||||
*/
|
||||
export interface PoolInfo {
|
||||
id: number | null; // mysql row id
|
||||
name: string;
|
||||
link: string;
|
||||
regexes: string; // JSON array
|
||||
addresses: string; // JSON array
|
||||
emptyBlocks: number;
|
||||
}
|
||||
export interface PoolStat {
|
||||
pool: PoolInfo;
|
||||
blockCount: number;
|
||||
emptyBlocks: BlockExtended[];
|
||||
}
|
||||
|
||||
export interface BlockExtension {
|
||||
@@ -88,6 +106,10 @@ export interface BlockExtension {
|
||||
reward?: number;
|
||||
coinbaseTx?: Transaction;
|
||||
matchRate?: number;
|
||||
pool?: {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
stage?: number; // Frontend only
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats } from '../interfaces/node-api.interface';
|
||||
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended, BlockExtension } from '../interfaces/node-api.interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
@@ -129,7 +129,31 @@ export class ApiService {
|
||||
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
listPools$(interval: string | null) : Observable<PoolsStats> {
|
||||
return this.httpClient.get<PoolsStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools/${interval}`);
|
||||
listPools$(interval: string | undefined) : Observable<PoolsStats> {
|
||||
return this.httpClient.get<PoolsStats>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +
|
||||
(interval !== undefined ? `/${interval}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
getPoolStats$(poolId: number, interval: string | undefined): Observable<PoolStat> {
|
||||
return this.httpClient.get<PoolStat>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}` +
|
||||
(interval !== undefined ? `/${interval}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
getPoolBlocks$(poolId: number, fromHeight: number): Observable<BlockExtended[]> {
|
||||
return this.httpClient.get<BlockExtended[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/blocks` +
|
||||
(fromHeight !== undefined ? `/${fromHeight}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
getHistoricalDifficulty$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` +
|
||||
(interval !== undefined ? `/${interval}` : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface Env {
|
||||
MEMPOOL_WEBSITE_URL: string;
|
||||
LIQUID_WEBSITE_URL: string;
|
||||
BISQ_WEBSITE_URL: string;
|
||||
MINING_DASHBOARD: boolean;
|
||||
}
|
||||
|
||||
const defaultEnv: Env = {
|
||||
@@ -59,6 +60,7 @@ const defaultEnv: Env = {
|
||||
'MEMPOOL_WEBSITE_URL': 'https://mempool.space',
|
||||
'LIQUID_WEBSITE_URL': 'https://liquid.network',
|
||||
'BISQ_WEBSITE_URL': 'https://bisq.markets',
|
||||
'MINING_DASHBOARD': true
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
|
||||
Reference in New Issue
Block a user