diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 7513f259e..c406ae803 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -199,6 +199,7 @@ class Blocks { const chunkSize = 10000; let totaIndexed = 0; + let indexedThisRun = 0; while (currentBlockHeight >= lastBlockToIndex) { const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1); @@ -207,9 +208,11 @@ class Blocks { if (missingBlockHeights.length <= 0) { logger.debug(`No missing blocks between #${currentBlockHeight} to #${endBlock}`); currentBlockHeight -= chunkSize; + totaIndexed += chunkSize; continue; } + totaIndexed += chunkSize - missingBlockHeights.length; logger.debug(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`); for (const blockHeight of missingBlockHeights) { @@ -219,8 +222,10 @@ class Blocks { try { if (totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) { const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); - const blockPerSeconds = Math.round(totaIndexed / elapsedSeconds); - logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed} | elapsed: ${elapsedSeconds} seconds`); + const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); + const progress = Math.round(totaIndexed / indexingBlockAmount * 100); + const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds); + logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${elapsedSeconds} seconds | left: ~${timeLeft} seconds`); } const blockHash = await bitcoinApi.$getBlockHash(blockHeight); const block = await bitcoinApi.$getBlock(blockHash); @@ -228,6 +233,7 @@ class Blocks { const blockExtended = await this.$getBlockExtended(block, transactions); await blocksRepository.$saveBlockInDatabase(blockExtended); ++totaIndexed; + ++indexedThisRun; } catch (e) { logger.err(`Something went wrong while indexing blocks.` + e); } diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 2a978868f..beca52893 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -69,6 +69,19 @@ class Mining { emptyBlocks: emptyBlocks, }; } + + /** + * Return the historical difficulty adjustments and oldest indexed block timestamp + */ + public async $getHistoricalDifficulty(interval: string | null): Promise { + const difficultyAdjustments = await BlocksRepository.$getBlocksDifficulty(interval); + const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp()); + + return { + adjustments: difficultyAdjustments, + oldestIndexedBlockTimestamp: oldestBlock.getTime(), + } + } } export default new Mining(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 1f8575294..23c70f59d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -259,10 +259,7 @@ class Server { ; } - const indexingAvailable = - ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && - config.DATABASE.ENABLED === true; - if (indexingAvailable) { + if (Common.indexingEnabled()) { this.app .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/24h', routes.$getPools.bind(routes, '24h')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3d', routes.$getPools.bind(routes, '3d')) @@ -277,7 +274,9 @@ class Server { .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) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', 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); } if (config.BISQ.ENABLED) { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index adc3a1f31..ac0ea25bc 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -223,6 +223,30 @@ class BlocksRepository { return rows[0]; } + + /** + * Return blocks difficulty + */ + public async $getBlocksDifficulty(interval: string | null): Promise { + interval = Common.getSqlInterval(interval); + + const connection = await DB.pool.getConnection(); + + let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height + FROM blocks`; + + if (interval) { + query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + query += ` GROUP BY difficulty + ORDER BY blockTimestamp DESC`; + + const [rows]: any[] = await connection.query(query); + connection.release(); + + return rows; + } } export default new BlocksRepository(); diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 66ecebd31..4a9cb1f8f 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -575,6 +575,18 @@ class Routes { } } + public async $getHistoricalDifficulty(req: Request, res: Response) { + try { + const stats = await mining.$getHistoricalDifficulty(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/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 4018ed64e..6ce39b1d2 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -27,6 +27,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetsComponent } from './components/assets/assets.component'; import { PoolComponent } from './components/pool/pool.component'; +import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; let routes: Routes = [ { @@ -63,6 +64,10 @@ let routes: Routes = [ path: 'blocks', component: LatestBlocksComponent, }, + { + path: 'mining/difficulty', + component: DifficultyChartComponent, + }, { path: 'mining/pools', component: PoolRankingComponent, @@ -155,6 +160,10 @@ let routes: Routes = [ path: 'blocks', component: LatestBlocksComponent, }, + { + path: 'mining/difficulty', + component: DifficultyChartComponent, + }, { path: 'mining/pools', component: PoolRankingComponent, @@ -241,6 +250,10 @@ let routes: Routes = [ path: 'blocks', component: LatestBlocksComponent, }, + { + path: 'mining/difficulty', + component: DifficultyChartComponent, + }, { path: 'mining/pools', component: PoolRankingComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 20eb2ea03..15bebd033 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -68,6 +68,7 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; +import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; @NgModule({ declarations: [ @@ -118,6 +119,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent, + DifficultyChartComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html new file mode 100644 index 000000000..1cdd90576 --- /dev/null +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.html @@ -0,0 +1,53 @@ +
+ +
+
+
+
+ +
+
+
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + +
BlockTimestampDifficultyChange
{{ diffChange.height }}‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}{{ diffChange.difficultyShorten }}{{ formatNumber(diffChange.change, locale, '1.2-2') }}%
+ +
diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.scss b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts new file mode 100644 index 000000000..47e0d9ea4 --- /dev/null +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts @@ -0,0 +1,151 @@ +import { Component, Inject, 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-difficulty-chart', + templateUrl: './difficulty-chart.component.html', + styleUrls: ['./difficulty-chart.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 38%; + left: calc(50% - 15px); + z-index: 100; + } + `], +}) +export class DifficultyChartComponent implements OnInit { + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg' + }; + + difficultyObservable$: 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.difficulty:Difficulty`); + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); + this.radioGroupForm.controls.dateSpan.setValue('1y'); + } + + ngOnInit(): void { + const powerOfTen = { + terra: Math.pow(10, 12), + giga: Math.pow(10, 9), + mega: Math.pow(10, 6), + kilo: Math.pow(10, 3), + } + + this.difficultyObservable$ = this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith('1y'), + switchMap((timespan) => { + return this.apiService.getHistoricalDifficulty$(timespan) + .pipe( + tap(data => { + this.prepareChartOptions(data.adjustments.map(val => [val.timestamp * 1000, val.difficulty])); + this.isLoading = false; + }), + map(data => { + const availableTimespanDay = ( + (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000) + ) / 3600 / 24; + + const tableData = []; + for (let i = 0; i < data.adjustments.length - 1; ++i) { + const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100; + let selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; + if (data.adjustments[i].difficulty < powerOfTen.mega) { + selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling + } else if (data.adjustments[i].difficulty < powerOfTen.giga) { + selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; + } else if (data.adjustments[i].difficulty < powerOfTen.terra) { + selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; + } + + tableData.push(Object.assign(data.adjustments[i], { + change: change, + difficultyShorten: formatNumber( + data.adjustments[i].difficulty / selectedPowerOfTen.divider, + this.locale, '1.2-2') + selectedPowerOfTen.unit + })); + } + return { + availableTimespanDay: availableTimespanDay, + data: tableData + }; + }), + ); + }), + share() + ); + } + + prepareChartOptions(data) { + this.chartOptions = { + title: { + text: $localize`:@@mining.difficulty:Difficulty`, + left: 'center', + textStyle: { + color: '#FFF', + }, + }, + tooltip: { + show: true, + trigger: 'axis', + }, + axisPointer: { + type: 'line', + }, + xAxis: [ + { + type: 'time', + } + ], + yAxis: { + type: 'value', + axisLabel: { + fontSize: 11, + formatter: (val) => { + const diff = val / Math.pow(10, 12); // terra + return diff.toString() + 'T'; + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + } + }, + series: [ + { + data: data, + type: 'line', + smooth: false, + lineStyle: { + width: 3, + }, + areaStyle: {} + }, + ], + }; + } + +} diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index 1f6fdbc0e..ac59ab2d2 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -43,7 +43,7 @@ - +
diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index d1b64f190..fc5a8da60 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; @@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service'; } `], }) -export class PoolRankingComponent implements OnInit, OnDestroy { +export class PoolRankingComponent implements OnInit { poolsWindowPreference: string; radioGroupForm: FormGroup; @@ -90,9 +90,6 @@ export class PoolRankingComponent implements OnInit, OnDestroy { ); } - ngOnDestroy(): void { - } - formatPoolUI(pool: SinglePoolStats) { pool['blockText'] = pool.blockCount.toString() + ` (${pool.share}%)`; return pool; @@ -110,7 +107,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy { if (parseFloat(pool.share) < poolShareThreshold) { return; } - data.push({ + data.push({ value: pool.share, name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`), label: { @@ -118,9 +115,9 @@ export class PoolRankingComponent implements OnInit, OnDestroy { overflow: 'break', }, tooltip: { - backgroundColor: "#282d47", + backgroundColor: '#282d47', textStyle: { - color: "#FFFFFF", + color: '#FFFFFF', }, formatter: () => { if (this.poolsWindowPreference === '24h') { @@ -134,7 +131,7 @@ export class PoolRankingComponent implements OnInit, OnDestroy { } }, data: pool.poolId, - }); + } as PieSeriesOption); }); return data; } @@ -208,10 +205,10 @@ export class PoolRankingComponent implements OnInit, OnDestroy { this.chartInstance = ec; this.chartInstance.on('click', (e) => { - this.router.navigate(['/mining/pool/', e.data.data]); - }) + this.router.navigate(['/mining/pool/', e.data.data]); + }); } - + /** * Default mining stats if something goes wrong */ diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index bf468c467..9a6bbc0b8 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -129,12 +129,18 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); } - listPools$(interval: string | null) : Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools/${interval}`); + listPools$(interval: string | undefined) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` + + (interval !== undefined ? `/${interval}` : '') + ); } - getPoolStats$(poolId: number, interval: string | null): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/${interval}`); + getPoolStats$(poolId: number, interval: string | undefined): Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}` + + (interval !== undefined ? `/${interval}` : '') + ); } getPoolBlocks$(poolId: number, fromHeight: number): Observable { @@ -143,4 +149,11 @@ export class ApiService { (fromHeight !== undefined ? `/${fromHeight}` : '') ); } + + getHistoricalDifficulty$(interval: string | undefined): Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` + + (interval !== undefined ? `/${interval}` : '') + ); + } }
Rank