From 08e19a612cdb7fa28d5a5896a0236b235f5943a1 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 9 Apr 2022 01:07:13 +0900 Subject: [PATCH] Add block fees graph --- backend/src/api/mining.ts | 23 +++ backend/src/index.ts | 1 + backend/src/repositories/BlocksRepository.ts | 29 +++ backend/src/routes.ts | 16 ++ frontend/src/app/app-routing.module.ts | 65 ++---- frontend/src/app/app.module.ts | 2 + .../block-fees-graph.component.html | 61 ++++++ .../block-fees-graph.component.scss | 135 ++++++++++++ .../block-fees-graph.component.ts | 195 ++++++++++++++++++ .../components/graphs/graphs.component.html | 4 + .../hashrate-chart.component.html | 2 +- frontend/src/app/services/api.service.ts | 7 + 12 files changed, 489 insertions(+), 51 deletions(-) create mode 100644 frontend/src/app/components/block-fees-graph/block-fees-graph.component.html create mode 100644 frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss create mode 100644 frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 482a34511..e88f21983 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -5,6 +5,7 @@ import HashratesRepository from '../repositories/HashratesRepository'; import bitcoinClient from './bitcoin/bitcoin-client'; import logger from '../logger'; import blocks from './blocks'; +import { Common } from './common'; class Mining { hashrateIndexingStarted = false; @@ -13,6 +14,28 @@ class Mining { constructor() { } + /** + * Get historical block reward and total fee + */ + public async $getHistoricalBlockFees(interval: string | null = null): Promise { + let timeRange: number; + switch (interval) { + case '3y': timeRange = 43200; break; // 12h + case '2y': timeRange = 28800; break; // 8h + case '1y': timeRange = 28800; break; // 8h + case '6m': timeRange = 10800; break; // 3h + case '3m': timeRange = 7200; break; // 2h + case '1m': timeRange = 1800; break; // 30min + case '1w': timeRange = 300; break; // 5min + case '24h': timeRange = 1; break; + default: timeRange = 86400; break; // 24h + } + + interval = Common.getSqlInterval(interval); + + return await BlocksRepository.$getHistoricalBlockFees(timeRange, interval); + } + /** * Generate high level overview of the pool ranks and general stats */ diff --git a/backend/src/index.ts b/backend/src/index.ts index 9f0e80bd0..591afcfb4 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -316,6 +316,7 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees) ; } diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index ff40414a2..a58d689e9 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -439,6 +439,35 @@ class BlocksRepository { connection.release(); } + + /** + * Get the historical averaged block reward and total fees + */ + public async $getHistoricalBlockFees(div: number, interval: string | null): Promise { + let connection; + try { + connection = await DB.getConnection(); + + let query = `SELECT CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, + CAST(AVG(fees) as INT) as avg_fees + FROM blocks`; + + if (interval !== null) { + query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`; + + const [rows]: any = await connection.query(query); + connection.release(); + + return rows; + } catch (e) { + connection.release(); + logger.err('$getHistoricalBlockFees() error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } } export default new BlocksRepository(); diff --git a/backend/src/routes.ts b/backend/src/routes.ts index d558e3061..c2ddac72c 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -638,6 +638,22 @@ class Routes { } } + public async $getHistoricalBlockFees(req: Request, res: Response) { + try { + const blockFees = await mining.$getHistoricalBlockFees(req.params.interval ?? null); + 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, + blockFees: blockFees, + }); + } 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/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index d46da5696..0ff1ee006 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -33,6 +33,7 @@ import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/ import { MiningStartComponent } from './components/mining-start/mining-start.component'; import { GraphsComponent } from './components/graphs/graphs.component'; import { BlocksList } from './components/blocks-list/blocks-list.component'; +import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component'; let routes: Routes = [ { @@ -117,6 +118,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/block-fees', + component: BlockFeesGraphComponent, + } ], }, { @@ -211,18 +216,6 @@ let routes: Routes = [ path: 'blocks', component: BlocksList, }, - { - path: 'hashrate', - component: HashrateChartComponent, - }, - { - path: 'hashrate/pools', - component: HashrateChartPoolsComponent, - }, - { - path: 'pools', - component: PoolRankingComponent, - }, { path: 'pool', children: [ @@ -259,6 +252,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/block-fees', + component: BlockFeesGraphComponent, + } ] }, { @@ -347,18 +344,6 @@ let routes: Routes = [ path: 'blocks', component: BlocksList, }, - { - path: 'hashrate', - component: HashrateChartComponent, - }, - { - path: 'hashrate/pools', - component: HashrateChartPoolsComponent, - }, - { - path: 'pools', - component: PoolRankingComponent, - }, { path: 'pool', children: [ @@ -395,6 +380,10 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/block-fees', + component: BlockFeesGraphComponent, + } ] }, { @@ -507,19 +496,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'mempool', component: StatisticsComponent, - }, - { - path: 'mining/hashrate-difficulty', - component: HashrateChartComponent, - }, - { - path: 'mining/pools-dominance', - component: HashrateChartPoolsComponent, - }, - { - path: 'mining/pools', - component: PoolRankingComponent, - }, + } ] }, { @@ -639,19 +616,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'mempool', component: StatisticsComponent, - }, - { - path: 'mining/hashrate-difficulty', - component: HashrateChartComponent, - }, - { - path: 'mining/pools-dominance', - component: HashrateChartPoolsComponent, - }, - { - path: 'mining/pools', - component: PoolRankingComponent, - }, + } ] }, { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 807c88ade..4536a2ff1 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -80,6 +80,7 @@ import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments- import { BlocksList } from './components/blocks-list/blocks-list.component'; import { RewardStatsComponent } from './components/reward-stats/reward-stats.component'; import { DataCyDirective } from './data-cy.directive'; +import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component'; @NgModule({ declarations: [ @@ -141,6 +142,7 @@ import { DataCyDirective } from './data-cy.directive'; BlocksList, DataCyDirective, RewardStatsComponent, + BlockFeesGraphComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html new file mode 100644 index 000000000..88c07e208 --- /dev/null +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html @@ -0,0 +1,61 @@ +
+ +
+ Block fees +
+
+ + + + + + + + + +
+
+
+ +
+
+
+
+
+ +
+ + +
+
+
Hashrate
+

+ +

+
+
+
Difficulty
+

+ +

+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss new file mode 100644 index 000000000..54dbe5fad --- /dev/null +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss @@ -0,0 +1,135 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } +} + +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.full-container { + padding: 0px 15px; + width: 100%; + min-height: 500px; + height: calc(100% - 150px); + @media (max-width: 992px) { + height: 100%; + padding-bottom: 100px; + }; +} + +.chart { + width: 100%; + height: 100%; + padding-bottom: 20px; + padding-right: 10px; + @media (max-width: 992px) { + padding-bottom: 25px; + } + @media (max-width: 829px) { + padding-bottom: 50px; + } + @media (max-width: 767px) { + padding-bottom: 25px; + } + @media (max-width: 629px) { + padding-bottom: 55px; + } + @media (max-width: 567px) { + padding-bottom: 55px; + } +} +.chart-widget { + width: 100%; + height: 100%; + max-height: 270px; +} + +.formRadioGroup { + margin-top: 6px; + display: flex; + flex-direction: column; + @media (min-width: 1130px) { + position: relative; + top: -65px; + } + @media (min-width: 830px) and (max-width: 1130px) { + position: relative; + top: 0px; + } + @media (min-width: 830px) { + flex-direction: row; + float: right; + margin-top: 0px; + } + .btn-sm { + font-size: 9px; + @media (min-width: 830px) { + font-size: 14px; + } + } +} + +.pool-distribution { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + display: inline-block; + margin: 0px auto 20px; + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } +} + +.skeleton-loader { + width: 100%; + display: block; + max-width: 80px; + margin: 15px auto 3px; +} diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts new file mode 100644 index 000000000..6a729d4f6 --- /dev/null +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts @@ -0,0 +1,195 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { EChartsOption, graphic } 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'; +import { formatterXAxisLabel } from 'src/app/shared/graphs.utils'; + +@Component({ + selector: 'app-block-fees-graph', + templateUrl: './block-fees-graph.component.html', + styleUrls: ['./block-fees-graph.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockFeesGraphComponent implements OnInit { + @Input() tableOnly = false; + @Input() widget = false; + @Input() right: number | string = 45; + @Input() left: number | string = 75; + + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + statsObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private apiService: ApiService, + private formBuilder: FormBuilder + ) { + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); + this.radioGroupForm.controls.dateSpan.setValue('1y'); + } + + ngOnInit(): void { + if (!this.widget) { + this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`); + } + + this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith('1y'), + switchMap((timespan) => { + this.timespan = timespan; + this.isLoading = true; + return this.apiService.getHistoricalBlockFees$(timespan) + .pipe( + tap((data: any) => { + this.prepareChartOptions({ + blockFees: data.blockFees.map(val => [val.timestamp * 1000, val.avg_fees / 100000000]), + }); + this.isLoading = false; + }), + map((data: any) => { + const availableTimespanDay = ( + (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp) + ) / 3600 / 24; + + return { + availableTimespanDay: availableTimespanDay, + }; + }), + ); + }), + share() + ); + } + + 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' } + ]), + ], + grid: { + top: 30, + bottom: 80, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile() || !this.widget, + 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: (ticks) => { + const tick = ticks[0]; + const feesString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC`; + return ` + ${tick.axisValueLabel}
+ ${feesString} + `; + } + }, + xAxis: { + name: formatterXAxisLabel(this.locale, this.timespan), + nameLocation: 'middle', + nameTextStyle: { + padding: [10, 0, 0, 0], + }, + type: 'time', + splitNumber: this.isMobile() ? 5 : 10, + }, + yAxis: [ + { + type: 'value', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${val} BTC`; + } + }, + splitLine: { + show: false, + } + }, + ], + series: [ + { + zlevel: 0, + name: 'Fees', + showSymbol: false, + symbol: 'none', + data: data.blockFees, + type: 'line', + lineStyle: { + width: 2, + }, + }, + ], + dataZoom: this.widget ? null : [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 10, + moveOnMouseMove: false, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + } + }, + }], + }; + } + + isMobile() { + return (window.innerWidth <= 767.98); + } +} diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index 55397bec7..97654beb5 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -16,6 +16,10 @@ [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty"> Hashrate & Difficulty + + Block Fees + 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 4e9c66495..4107f1554 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -19,7 +19,7 @@
- Hashrate & Difficulty + Hashrate & Difficulty