From ad2dcc46e44e54e9beeed8beab323e62a07af9f6 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 8 Mar 2022 12:50:47 +0100 Subject: [PATCH] Added pool hashrate chart --- backend/src/index.ts | 1 + .../src/repositories/HashratesRepository.ts | 35 +++++ backend/src/routes.ts | 16 +++ .../app/components/pool/pool.component.html | 95 ++++++------- .../app/components/pool/pool.component.scss | 3 +- .../src/app/components/pool/pool.component.ts | 129 +++++++++++++++++- frontend/src/app/services/api.service.ts | 4 + 7 files changed, 228 insertions(+), 55 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 4ede865a6..fb358b1f3 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -299,6 +299,7 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all')) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/hashrate/:interval', routes.$getPoolHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks', routes.$getPoolBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks/:height', routes.$getPoolBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index 3523004d5..a217dcc16 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -116,6 +116,41 @@ class HashratesRepository { } } + /** + * Returns a pool hashrate history + */ + public async $getPoolWeeklyHashrate(interval: string | null, poolId: number): Promise { + interval = Common.getSqlInterval(interval); + + const connection = await DB.pool.getConnection(); + + let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName + FROM hashrates + JOIN pools on pools.id = pool_id`; + + if (interval) { + query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() + AND hashrates.type = 'weekly' + AND pool_id = ${poolId}`; + } else { + query += ` WHERE hashrates.type = 'weekly' + AND pool_id = ${poolId}`; + } + + query += ` ORDER by hashrate_timestamp`; + + try { + const [rows]: any[] = await connection.query(query); + connection.release(); + + return rows; + } catch (e) { + connection.release(); + logger.err('$getPoolWeeklyHashrate() error' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $setLatestRunTimestamp(key: string, val: any = null) { const connection = await DB.pool.getConnection(); const query = `UPDATE state SET number = ? WHERE name = ?`; diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 6b1a365b6..69dde1a59 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -603,6 +603,22 @@ class Routes { } } + public async $getPoolHistoricalHashrate(req: Request, res: Response) { + try { + const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(req.params.interval ?? null, parseInt(req.params.poolId, 10)); + const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json({ + oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp, + hashrates: hashrates, + }); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public async $getHistoricalHashrate(req: Request, res: Response) { try { const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null); diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 43bc647e8..8478aa566 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -1,47 +1,34 @@
-

- - {{ poolStats.pool.name }} -

- -
-
-
-
- - - - - - - - - - -
-
+
+

+ + {{ poolStats.pool.name }} +

+
+
+
+
+ + + + + +
+
+
@@ -54,10 +41,13 @@ Addresses - ~ + + ~ + Coinbase Tags @@ -84,7 +74,13 @@
- +
+
+
+
+ +
@@ -97,12 +93,17 @@ - - + + diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index 271696a39..65f16b6b8 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -18,9 +18,8 @@ display: flex; flex-direction: column; @media (min-width: 830px) { - margin-left: 2%; flex-direction: row; - float: left; + float: right; margin-top: 0px; } .btn-sm { diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 9d094dce0..7cc909abd 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -1,11 +1,14 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { EChartsOption, graphic } from 'echarts'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, startWith, switchMap, tap, toArray } 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'; +import { selectPowerOfTen } from 'src/app/bitcoin.utils'; +import { formatNumber } from '@angular/common'; @Component({ selector: 'app-pool', @@ -14,34 +17,57 @@ import { StateService } from 'src/app/services/state.service'; changeDetection: ChangeDetectionStrategy.OnPush }) export class PoolComponent implements OnInit { + @Input() right: number | string = 45; + @Input() left: number | string = 75; + poolStats$: Observable; blocks$: Observable; + isLoading = true; + + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + width: 'auto', + height: 'auto', + }; fromHeight: number = -1; fromHeightSubject: BehaviorSubject = new BehaviorSubject(this.fromHeight); blocks: BlockExtended[] = []; poolId: number = undefined; - radioGroupForm: FormGroup; constructor( + @Inject(LOCALE_ID) public locale: string, 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'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: 'all' }); + this.radioGroupForm.controls.dateSpan.setValue('all'); } ngOnInit(): void { this.poolStats$ = combineLatest([ this.route.params.pipe(map((params) => params.poolId)), - this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('1w')), + this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('all')), ]) .pipe( switchMap((params: any) => { this.poolId = params[0]; + return this.apiService.getPoolHashrate$(this.poolId, params[1] ?? 'all') + .pipe( + switchMap((data) => { + this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate])); + return params; + }), + toArray(), + ) + }), + switchMap((params: any) => { if (this.blocks.length === 0) { this.fromHeightSubject.next(undefined); } @@ -55,6 +81,7 @@ export class PoolComponent implements OnInit { poolStats.pool.regexes = regexes.slice(0, -3); poolStats.pool.addresses = poolStats.pool.addresses; + this.isLoading = false; return Object.assign({ logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg' }, poolStats); @@ -74,6 +101,96 @@ export class PoolComponent implements OnInit { ) } + prepareChartOptions(data) { + this.chartOptions = { + animation: false, + color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + '#D81B60', + ], + grid: { + right: this.right, + left: this.left, + bottom: 60, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + align: 'left', + }, + borderColor: '#000', + formatter: function (data) { + let hashratePowerOfTen: any = selectPowerOfTen(1); + let hashrate = data[0].data[1]; + + if (this.isMobile()) { + hashratePowerOfTen = selectPowerOfTen(data[0].data[1]); + hashrate = Math.round(data[0].data[1] / hashratePowerOfTen.divider); + } + + return ` + ${data[0].axisValueLabel}
+ ${data[0].marker} ${data[0].seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s
+ `; + }.bind(this) + }, + xAxis: { + type: 'time', + splitNumber: (this.isMobile()) ? 5 : 10, + }, + yAxis: [ + { + min: function (value) { + return value.min * 0.9; + }, + type: 'value', + name: 'Hashrate', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + const selectedPowerOfTen: any = selectPowerOfTen(val); + const newVal = Math.round(val / selectedPowerOfTen.divider); + return `${newVal} ${selectedPowerOfTen.unit}H/s` + } + }, + splitLine: { + show: false, + } + }, + ], + series: [ + { + name: 'Hashrate', + showSymbol: false, + symbol: 'none', + data: data, + type: 'line', + lineStyle: { + width: 2, + }, + }, + ], + }; + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + loadMore() { this.fromHeightSubject.next(this.blocks[this.blocks.length - 1]?.height); } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 5548780b1..fcac428a9 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -143,6 +143,10 @@ export class ApiService { ); } + getPoolHashrate$(poolId: number, interval: string | undefined): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/hashrate/${interval}`); + } + getPoolBlocks$(poolId: number, fromHeight: number): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/blocks` +
Height Timestamp
{{ block.height }} ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} + + + + {{ block.tx_count | number }}
-
+