diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index fc13c2f5e..6f90ab357 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -87,6 +87,19 @@ class Mining { } } + /** + * Return the historical hashrates and oldest indexed block timestamp + */ + public async $getHistoricalHashrates(interval: string | null): Promise { + 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`); } } diff --git a/backend/src/index.ts b/backend/src/index.ts index d09196a45..4fe66bc72 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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) { diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index f55700812..837569cd8 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -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 { + public async $get(interval: string | null): Promise { + 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; } } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index df5e7cb62..1bf1c3434 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -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); diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html index e55205844..04534f176 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -1 +1,53 @@ -

hashrate-chart works!

+
+ +
+
+
+
+ +
+
+
+ + + + + + +
+
+
+ + + +
diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss index e69de29bb..c3a63e9fa 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -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; +} diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index cfbb6ba31..4739c2c30 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -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; + 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); + } } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 9a6bbc0b8..cf0ebd414 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -156,4 +156,11 @@ export class ApiService { (interval !== undefined ? `/${interval}` : '') ); } + + getHistoricalHashrate$(interval: string | undefined): Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` + + (interval !== undefined ? `/${interval}` : '') + ); + } }