diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 7ac27d0f4..c4107e426 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 23; + private static currentVersion = 24; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -243,6 +243,11 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"'); } + + if (databaseSchemaVersion < 24 && isBitcoin == true) { + await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`'); + await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits')); + } } catch (e) { throw e; } @@ -567,6 +572,19 @@ class DatabaseMigration { ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + private getCreateBlocksAuditsTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS blocks_audits ( + time timestamp NOT NULL, + hash varchar(65) NOT NULL, + height int(10) unsigned NOT NULL, + missing_txs JSON NOT NULL, + added_txs JSON NOT NULL, + match_rate float unsigned NOT NULL, + PRIMARY KEY (hash), + INDEX (height) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + public async $truncateIndexedData(tables: string[]) { const allowedTables = ['blocks', 'hashrates', 'prices']; diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 83712a143..e9fe6bae7 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -10,11 +10,22 @@ import { escape } from 'mysql2'; import indexer from '../indexer'; import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; import config from '../config'; +import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; class Mining { constructor() { } + /** + * Get historical block predictions match rate + */ + public async $getBlockPredictionsHistory(interval: string | null = null): Promise { + return await BlocksAuditsRepository.$getBlockPredictionsHistory( + this.getTimeRange(interval), + Common.getSqlInterval(interval) + ); + } + /** * Get historical block total fee */ diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 3c5b830cc..300341ef5 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -16,6 +16,7 @@ import transactionUtils from './transaction-utils'; import rbfCache from './rbf-cache'; import difficultyAdjustment from './difficulty-adjustment'; import feeApi from './fee-api'; +import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; class WebsocketHandler { private wss: WebSocket.Server | undefined; @@ -416,17 +417,40 @@ class WebsocketHandler { if (_mempoolBlocks[0]) { const matches: string[] = []; + const added: string[] = []; + const missing: string[] = []; + for (const txId of txIds) { if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) { matches.push(txId); + } else { + added.push(txId); } delete _memPool[txId]; } - matchRate = Math.round((matches.length / (txIds.length - 1)) * 100); + for (const txId of _mempoolBlocks[0].transactionIds) { + if (matches.includes(txId) || added.includes(txId)) { + continue; + } + missing.push(txId); + } + + matchRate = Math.round((Math.max(0, matches.length - missing.length - added.length) / txIds.length * 100) * 100) / 100; mempoolBlocks.updateMempoolBlocks(_memPool); mBlocks = mempoolBlocks.getMempoolBlocks(); mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); + + if (Common.indexingEnabled()) { + BlocksAuditsRepository.$saveAudit({ + time: block.timestamp, + height: block.height, + hash: block.id, + addedTxs: added, + missingTxs: missing, + matchRate: matchRate, + }); + } } if (block.extras) { diff --git a/backend/src/index.ts b/backend/src/index.ts index b86e45029..ff0209294 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -28,6 +28,7 @@ import { Common } from './api/common'; import poolsUpdater from './tasks/pools-updater'; import indexer from './indexer'; import priceUpdater from './tasks/price-updater'; +import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; class Server { private wss: WebSocket.Server | undefined; @@ -292,6 +293,7 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', routes.$getDifficultyAdjustments) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', routes.$getHistoricalBlockPrediction) ; } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 983434564..477c6a920 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -22,6 +22,15 @@ export interface PoolStats extends PoolInfo { emptyBlocks: number; } +export interface BlockAudit { + time: number, + height: number, + hash: string, + missingTxs: string[], + addedTxs: string[], + matchRate: number, +} + export interface MempoolBlock { blockSize: number; blockVSize: number; diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts new file mode 100644 index 000000000..31d8ec785 --- /dev/null +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -0,0 +1,51 @@ +import DB from '../database'; +import logger from '../logger'; +import { BlockAudit } from '../mempool.interfaces'; + +class BlocksAuditRepositories { + public async $saveAudit(audit: BlockAudit): Promise { + try { + await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate) + VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), + JSON.stringify(audit.addedTxs), audit.matchRate]); + } catch (e: any) { + if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart + logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`); + } else { + logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + } + + public async $getBlockPredictionsHistory(div: number, interval: string | null): Promise { + try { + let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`; + + if (interval !== null) { + query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${div} ORDER BY height`; + + const [rows] = await DB.query(query); + return rows; + } catch (e: any) { + logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getPredictionsCount(): Promise { + try { + const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`); + return rows[0].count; + } catch (e: any) { + logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } +} + +export default new BlocksAuditRepositories(); + diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 3676aa49a..57c64ddf5 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -27,6 +27,7 @@ import BlocksRepository from './repositories/BlocksRepository'; import HashratesRepository from './repositories/HashratesRepository'; import difficultyAdjustment from './api/difficulty-adjustment'; import DifficultyAdjustmentsRepository from './repositories/DifficultyAdjustmentsRepository'; +import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; class Routes { constructor() {} @@ -743,6 +744,20 @@ class Routes { } } + public async $getHistoricalBlockPrediction(req: Request, res: Response) { + try { + const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval); + const blockCount = await BlocksAuditsRepository.$getPredictionsCount(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate])); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public async getBlock(req: Request, res: Response) { try { const block = await blocks.$getBlock(req.params.hash); diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 6951accb2..b62f586a4 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -171,7 +171,7 @@ let routes: Routes = [ { path: 'block', component: StartComponent, - children: [ + children: [ { path: ':id', component: BlockComponent @@ -258,7 +258,7 @@ let routes: Routes = [ { path: 'block', component: StartComponent, - children: [ + children: [ { path: ':id', component: BlockComponent @@ -361,7 +361,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'block', component: StartComponent, - children: [ + children: [ { path: ':id', component: BlockComponent @@ -465,7 +465,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'block', component: StartComponent, - children: [ + children: [ { path: ':id', component: BlockComponent diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss index 007a2cd06..5aaf8a91b 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss @@ -79,57 +79,3 @@ } } } - -.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-fee-rates-graph/block-fee-rates-graph.component.ts b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts index e5ee42608..37243bd4a 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts @@ -174,7 +174,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { align: 'left', }, borderColor: '#000', - formatter: function (data) { + formatter: function(data) { if (data.length <= 0) { return ''; } 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 index 007a2cd06..5aaf8a91b 100644 --- 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 @@ -79,57 +79,3 @@ } } } - -.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-predictions-graph/block-predictions-graph.component.html b/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.html new file mode 100644 index 000000000..79806eb2b --- /dev/null +++ b/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.html @@ -0,0 +1,52 @@ + + +
+
+ Block Predictions Accuracy + +
+
+ + + + + + + + + + +
+
+
+ +
+
+
+
+
+ +
diff --git a/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.scss b/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.scss new file mode 100644 index 000000000..5aaf8a91b --- /dev/null +++ b/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.scss @@ -0,0 +1,81 @@ +.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: 991px) { + position: relative; + top: -65px; + } + @media (min-width: 830px) and (max-width: 991px) { + 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; + } + } +} diff --git a/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.ts b/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.ts new file mode 100644 index 000000000..b2ec7116c --- /dev/null +++ b/frontend/src/app/components/block-predictions-graph/block-predictions-graph.component.ts @@ -0,0 +1,289 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, 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'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils'; +import { StorageService } from 'src/app/services/storage.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from 'src/app/services/state.service'; + +@Component({ + selector: 'app-block-predictions-graph', + templateUrl: './block-predictions-graph.component.html', + styleUrls: ['./block-predictions-graph.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockPredictionsGraphComponent implements OnInit { + @Input() right: number | string = 45; + @Input() left: number | string = 75; + + miningWindowPreference: string; + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + statsObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + chartInstance: any = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private apiService: ApiService, + private formBuilder: FormBuilder, + private storageService: StorageService, + private zone: NgZone, + private route: ActivatedRoute, + private stateService: StateService, + private router: Router, + ) { + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); + this.radioGroupForm.controls.dateSpan.setValue('1y'); + } + + ngOnInit(): void { + this.seoService.setTitle($localize`Block predictions accuracy`); + this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); + + this.route + .fragment + .subscribe((fragment) => { + if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) { + this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); + } + }); + + this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith(this.radioGroupForm.controls.dateSpan.value), + switchMap((timespan) => { + this.storageService.setValue('miningWindowPreference', timespan); + this.timespan = timespan; + this.isLoading = true; + return this.apiService.getHistoricalBlockPrediction$(timespan) + .pipe( + tap((response) => { + this.prepareChartOptions(response.body); + this.isLoading = false; + }), + map((response) => { + return { + blockCount: parseInt(response.headers.get('x-total-count'), 10), + }; + }), + ); + }), + share() + ); + } + + prepareChartOptions(data) { + this.chartOptions = { + animation: false, + grid: { + top: 30, + bottom: 80, + right: this.right, + left: this.left, + }, + 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: (ticks) => { + let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10) * 1000)}
`; + tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data.value, this.locale, '1.2-2')}%
`; + + if (['24h', '3d'].includes(this.timespan)) { + tooltip += `` + $localize`At block: ${ticks[0].data.block}` + ``; + } else { + tooltip += `` + $localize`Around block: ${ticks[0].data.block}` + ``; + } + + return tooltip; + } + }, + xAxis: { + name: formatterXAxisLabel(this.locale, this.timespan), + nameLocation: 'middle', + nameTextStyle: { + padding: [10, 0, 0, 0], + }, + type: 'category', + boundaryGap: false, + axisLine: { onZero: true }, + axisLabel: { + formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)), + align: 'center', + fontSize: 11, + lineHeight: 12, + hideOverlap: true, + padding: [0, 5], + }, + data: data.map(prediction => prediction[0]) + }, + yAxis: [ + { + type: 'value', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${val}%`; + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, + }, + ], + series: [ + { + zlevel: 0, + name: $localize`Match rate`, + data: data.map(prediction => ({ + value: prediction[2], + block: prediction[1], + itemStyle: { + color: this.getPredictionColor(prediction[2]) + } + })), + type: 'bar', + barWidth: '90%', + }, + ], + dataZoom: [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 5, + 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, + } + }, + }], + }; + } + + colorGradient(fadeFraction, rgbColor1, rgbColor2, rgbColor3) { + let color1 = rgbColor1; + let color2 = rgbColor2; + let fade = fadeFraction; + + // Do we have 3 colors for the gradient? Need to adjust the params. + if (rgbColor3) { + fade = fade * 2; + + // Find which interval to use and adjust the fade percentage + if (fade >= 1) { + fade -= 1; + color1 = rgbColor2; + color2 = rgbColor3; + } + } + + const diffRed = color2.red - color1.red; + const diffGreen = color2.green - color1.green; + const diffBlue = color2.blue - color1.blue; + + const gradient = { + red: Math.floor(color1.red + (diffRed * fade)), + green: Math.floor(color1.green + (diffGreen * fade)), + blue: Math.floor(color1.blue + (diffBlue * fade)), + }; + + return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')'; + } + + getPredictionColor(matchRate) { + return this.colorGradient( + Math.pow((100 - matchRate) / 100, 0.5), + {red: 67, green: 171, blue: 71}, + {red: 253, green: 216, blue: 53}, + {red: 244, green: 0, blue: 0}, + ); + } + + onChartInit(ec) { + this.chartInstance = ec; + + this.chartInstance.on('click', (e) => { + this.zone.run(() => { + if (['24h', '3d'].includes(this.timespan)) { + const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.data.block}`); + this.router.navigate([url]); + } + }); + }); + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + + onSaveChart() { + // @ts-ignore + const prevBottom = this.chartOptions.grid.bottom; + const now = new Date(); + // @ts-ignore + this.chartOptions.grid.bottom = 40; + this.chartOptions.backgroundColor = '#11131f'; + this.chartInstance.setOption(this.chartOptions); + download(this.chartInstance.getDataURL({ + pixelRatio: 2, + excludeComponents: ['dataZoom'], + }), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`); + // @ts-ignore + this.chartOptions.grid.bottom = prevBottom; + this.chartOptions.backgroundColor = 'none'; + this.chartInstance.setOption(this.chartOptions); + } +} diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss index 007a2cd06..5aaf8a91b 100644 --- a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss @@ -79,57 +79,3 @@ } } } - -.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-sizes-weights-graph/block-sizes-weights-graph.component.scss b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss index 86c1f8ec3..a47f63923 100644 --- a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss +++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss @@ -79,57 +79,3 @@ } } } - -.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; -} \ No newline at end of file diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index 7b417a4ac..aca62a4dd 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -18,6 +18,8 @@ [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">Block Rewards Block Sizes and Weights + Blocks Predictions Accuracy 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 0790a5c95..0e9e05167 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -71,7 +71,7 @@ export class HashrateChartComponent implements OnInit { this.miningWindowPreference = '1y'; } else { this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`); - this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); + this.miningWindowPreference = this.miningService.getDefaultTimespan('3m'); } this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index 5d17ae43a..8cdc51608 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -22,6 +22,7 @@ import { DashboardComponent } from '../dashboard/dashboard.component'; import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component'; import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component'; import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; +import { BlockPredictionsGraphComponent } from '../components/block-predictions-graph/block-predictions-graph.component'; import { CommonModule } from '@angular/common'; @NgModule({ @@ -47,6 +48,7 @@ import { CommonModule } from '@angular/common'; LbtcPegsGraphComponent, HashrateChartComponent, HashrateChartPoolsComponent, + BlockPredictionsGraphComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 5f2e89b59..fd3efaba4 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { BlockPredictionsGraphComponent } from '../components/block-predictions-graph/block-predictions-graph.component'; import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component'; import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component'; import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component'; @@ -92,6 +93,10 @@ const routes: Routes = [ path: '', redirectTo: 'mempool', }, + { + path: 'mining/block-predictions', + component: BlockPredictionsGraphComponent, + }, ] }, { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 5efa745d1..a0b3d8ff7 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -221,6 +221,13 @@ export class ApiService { ); } + getHistoricalBlockPrediction$(interval: string | undefined) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/predictions` + + (interval !== undefined ? `/${interval}` : ''), { observe: 'response' } + ); + } + getRewardStats$(blockCount: number = 144): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`); }