Added hashrate chart

This commit is contained in:
nymkappa 2022-02-19 22:09:35 +09:00
parent 6fe8f6fa1e
commit 358604ad85
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
8 changed files with 284 additions and 12 deletions

View File

@ -87,6 +87,19 @@ class Mining {
}
}
/**
* Return the historical hashrates and oldest indexed block timestamp
*/
public async $getHistoricalHashrates(interval: string | null): Promise<object> {
const hashrates = await HashratesRepository.$get(interval);
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
return {
hashrates: hashrates,
oldestIndexedBlockTimestamp: oldestBlock.getTime(),
}
}
/**
*
*/
@ -97,7 +110,7 @@ class Mining {
this.hashrateIndexingStarted = true;
const totalIndexed = await BlocksRepository.$blockCount(null, null);
const indexedTimestamp = await HashratesRepository.$getAllTimestamp();
const indexedTimestamp = (await HashratesRepository.$get(null)).map(hashrate => hashrate.timestamp);
const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const lastMidnight = new Date();
@ -114,7 +127,12 @@ class Mining {
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp, toTimestamp
);
let lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight);
let lastBlockHashrate = 0;
if (blockStats.blockCount > 0) {
lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
blockStats.lastBlockHeight);
}
if (toTimestamp % 864000 === 0) {
const progress = Math.round((totalIndexed - blockStats.lastBlockHeight) / totalIndexed * 100);
@ -130,6 +148,8 @@ class Mining {
toTimestamp -= 86400;
}
logger.info(`Hashrates indexing completed`);
}
}

View File

@ -285,7 +285,9 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty);
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate);
}
if (config.BISQ.ENABLED) {

View File

@ -1,3 +1,4 @@
import { Common } from '../api/common';
import { DB } from '../database';
import logger from '../logger';
@ -30,12 +31,22 @@ class HashratesRepository {
/**
* Returns an array of all timestamp we've already indexed
*/
public async $getAllTimestamp(): Promise<number[]> {
public async $get(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(`SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp from hashrates`);
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate
FROM hashrates`;
if (interval) {
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
const [rows]: any[] = await connection.query(query);
connection.release();
return rows.map(val => val.timestamp);
return rows;
}
}

View File

@ -586,6 +586,18 @@ class Routes {
}
}
public async $getHistoricalHashrate(req: Request, res: Response) {
try {
const stats = await mining.$getHistoricalHashrates(req.params.interval ?? null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(stats);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlock(req.params.hash);

View File

@ -1 +1,53 @@
<p>hashrate-chart works!</p>
<div [class]="widget === false ? 'container-xl' : ''">
<div *ngIf="hashrateObservable$ | 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="(hashrateObservable$ | async) as diffChanges">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/hashrate' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 90">
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/hashrate' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 180">
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/hashrate' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 365">
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/hashrate' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 730">
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/hashrate' | 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/hashrate' | 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.hashrate">Difficulty</th>
<th i18n="mining.change">Change</th>
</tr>
</thead>
<tbody *ngIf="(hashrateObservable$ | async) as diffChanges">
<tr *ngFor="let diffChange of diffChanges.data">
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
<td>&lrm;{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td class="d-none d-md-block">{{ formatNumber(diffChange.hashrate, 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>

View File

@ -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;
}

View File

@ -1,15 +1,173 @@
import { Component, OnInit } from '@angular/core';
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-hashrate-chart',
templateUrl: './hashrate-chart.component.html',
styleUrls: ['./hashrate-chart.component.scss']
styleUrls: ['./hashrate-chart.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 38%;
left: calc(50% - 15px);
z-index: 100;
}
`],
})
export class HashrateChartComponent implements OnInit {
@Input() widget: boolean = false;
constructor() { }
radioGroupForm: FormGroup;
ngOnInit(): void {
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg'
};
hashrateObservable$: 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.hashrate:hashrate`);
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith('1y'),
switchMap((timespan) => {
return this.apiService.getHistoricalHashrate$(timespan)
.pipe(
tap(data => {
this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]));
this.isLoading = false;
}),
map(data => {
const availableTimespanDay = (
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000)
) / 3600 / 24;
return {
availableTimespanDay: availableTimespanDay,
data: data.hashrates
};
}),
);
}),
share()
);
}
prepareChartOptions(data) {
this.chartOptions = {
title: {
text: this.widget? '' : $localize`:@@mining.hashrate:Hashrate`,
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 powerOfTen = {
exa: Math.pow(10, 18),
peta: Math.pow(10, 15),
terra: Math.pow(10, 12),
giga: Math.pow(10, 9),
mega: Math.pow(10, 6),
kilo: Math.pow(10, 3),
}
let selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
if (val < powerOfTen.mega) {
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
} else if (val < powerOfTen.giga) {
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
} else if (val < powerOfTen.terra) {
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
} else if (val < powerOfTen.peta) {
selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
} else if (val < powerOfTen.exa) {
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
}
const newVal = val / selectedPowerOfTen.divider;
return `${newVal} ${selectedPowerOfTen.unit}`
}
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
},
},
series: {
showSymbol: false,
data: data,
type: 'line',
smooth: false,
lineStyle: {
width: 3,
},
areaStyle: {},
},
dataZoom: this.widget ? null : [{
type: 'inside',
realtime: true,
zoomLock: true,
zoomOnMouseWheel: true,
moveOnMouseMove: true,
maxSpan: 100,
minSpan: 10,
}, {
showDetail: false,
show: true,
type: 'slider',
brushSelect: false,
realtime: true,
bottom: 0,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
},
}],
};
}
isMobile() {
return (window.innerWidth <= 767.98);
}
}

View File

@ -156,4 +156,11 @@ export class ApiService {
(interval !== undefined ? `/${interval}` : '')
);
}
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
(interval !== undefined ? `/${interval}` : '')
);
}
}