diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index b16a406cb..8b277da57 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -1,4 +1,4 @@ -import { PoolInfo, PoolStats } from '../mempool.interfaces'; +import { PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces'; import BlocksRepository from '../repositories/BlocksRepository'; import PoolsRepository from '../repositories/PoolsRepository'; import HashratesRepository from '../repositories/HashratesRepository'; @@ -70,6 +70,13 @@ class Mining { }; } + /** + * Get miner reward stats + */ + public async $getRewardStats(blockCount: number): Promise { + return await BlocksRepository.$getBlockStats(blockCount); + } + /** * [INDEXING] Generate weekly mining pool hashrate history */ diff --git a/backend/src/index.ts b/backend/src/index.ts index d4b55e078..d5bf0e59e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -312,6 +312,7 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate) .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) ; } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index b5cbb7e11..15d1ad618 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -209,3 +209,9 @@ export interface IDifficultyAdjustment { timeAvg: number; timeOffset: number; } + +export interface RewardStats { + totalReward: number; + totalFee: number; + totalTx: number; +} diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 40c705bdb..33cb727d9 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -354,6 +354,9 @@ class BlocksRepository { } } + /** + * Return oldest blocks height + */ public async $getOldestIndexedBlockHeight(): Promise { const connection = await DB.getConnection(); try { @@ -367,6 +370,29 @@ class BlocksRepository { throw e; } } + + /** + * Get general block stats + */ + public async $getBlockStats(blockCount: number): Promise { + let connection; + try { + connection = await DB.getConnection(); + + // We need to use a subquery + const query = `SELECT SUM(reward) as totalReward, SUM(fees) as totalFee, SUM(tx_count) as totalTx + FROM (SELECT reward, fees, tx_count FROM blocks ORDER by height DESC LIMIT ${blockCount}) as sub`; + + const [rows]: any = await connection.query(query); + connection.release(); + + return rows[0]; + } catch (e) { + connection.release(); + logger.err('$getBlockStats() 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 2f4cdff3a..b14ea6ac4 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -935,6 +935,15 @@ class Routes { res.status(500).end(); } } + + public async $getRewardStats(req: Request, res: Response) { + try { + const response = await mining.$getRewardStats(parseInt(req.params.blockCount)) + res.json(response); + } catch (e) { + res.status(500).end(); + } + } } export default new Routes(); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index cb59c19dc..807c88ade 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -78,6 +78,7 @@ import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-st import { GraphsComponent } from './components/graphs/graphs.component'; import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components'; import { BlocksList } from './components/blocks-list/blocks-list.component'; +import { RewardStatsComponent } from './components/reward-stats/reward-stats.component'; import { DataCyDirective } from './data-cy.directive'; @NgModule({ @@ -139,6 +140,7 @@ import { DataCyDirective } from './data-cy.directive'; DifficultyAdjustmentsTable, BlocksList, DataCyDirective, + RewardStatsComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html index cdcd03319..674d0bc44 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html @@ -4,64 +4,18 @@
-
Reward stats
+
+ Reward stats  + (144 blocks) +
-
-
-
Miners Reward
-
- -
in the last 8 blocks
-
-
-
-
Reward Per Tx
-
- {{ rewardStats.rewardPerTx | amountShortener }} - sats/tx -
in the last 8 blocks
-
-
-
-
Average Fee
-
- {{ rewardStats.feePerTx | amountShortener}} - sats/tx -
in the last 8 blocks
-
-
-
+
- -
-
-
Miners Reward
-
-
-
-
-
-
-
Reward Per Tx
-
-
-
-
-
-
-
Average Fee
-
-
-
-
-
-
-
diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss index 6d87f0a57..d744e285d 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss @@ -59,42 +59,6 @@ padding-bottom: 3px; } -.reward-container { - display: flex; - flex-direction: row; - justify-content: space-around; - height: 76px; - .shared-block { - color: #ffffff66; - font-size: 12px; - } - .item { - display: table-cell; - padding: 0 5px; - width: 100%; - &:nth-child(1) { - display: none; - @media (min-width: 485px) { - display: table-cell; - } - @media (min-width: 768px) { - display: none; - } - @media (min-width: 992px) { - display: table-cell; - } - } - } - .card-text { - font-size: 22px; - margin-top: -9px; - position: relative; - } - .card-text.skeleton { - margin-top: 0px; - } -} - .more-padding { padding: 18px; } diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index ce92ed56c..cfd1eafbd 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -14,14 +14,8 @@ import { WebsocketService } from 'src/app/services/websocket.service'; export class MiningDashboardComponent implements OnInit { private blocks = []; - public $rewardStats: Observable; - public totalReward = 0; - public rewardPerTx = '~'; - public feePerTx = '~'; - constructor( private seoService: SeoService, - public stateService: StateService, private websocketService: WebsocketService, ) { this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`); @@ -29,21 +23,5 @@ export class MiningDashboardComponent implements OnInit { ngOnInit(): void { this.websocketService.want(['blocks', 'mempool-blocks']); - - this.$rewardStats = this.stateService.blocks$.pipe( - map(([block]) => { - this.blocks.unshift(block); - this.blocks = this.blocks.slice(0, 8); - const totalTx = this.blocks.reduce((acc, b) => acc + b.tx_count, 0); - const totalFee = this.blocks.reduce((acc, b) => acc + b.extras?.totalFees ?? 0, 0); - const totalReward = this.blocks.reduce((acc, b) => acc + b.extras?.reward ?? 0, 0); - - return { - 'totalReward': totalReward, - 'rewardPerTx': Math.round(totalReward / totalTx), - 'feePerTx': Math.round(totalFee / totalTx), - }; - }) - ); } } diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.html b/frontend/src/app/components/reward-stats/reward-stats.component.html new file mode 100644 index 000000000..861921ca6 --- /dev/null +++ b/frontend/src/app/components/reward-stats/reward-stats.component.html @@ -0,0 +1,119 @@ +
+
+
+
Miners Reward
+
+
+ +
+ + + +
+
+
+
Reward Per Tx
+
+
+ {{ rewardStats.rewardPerTx | amountShortener }} + sats/tx +
+ + + +
+
+
+
Average Fee
+
+
{{ rewardStats.feePerTx | amountShortener }} + sats/tx +
+ + + +
+
+
+
+ + +
+
+
Low priority
+
+
+
+
+
+
+
Medium priority
+
+
+
+
+
+
+
High priority
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.scss b/frontend/src/app/components/reward-stats/reward-stats.component.scss new file mode 100644 index 000000000..460db5e4b --- /dev/null +++ b/frontend/src/app/components/reward-stats/reward-stats.component.scss @@ -0,0 +1,85 @@ +.card-title { + color: #4a68b9; + font-size: 10px; + margin-bottom: 4px; + font-size: 1rem; +} + +.card-text { + font-size: 22px; + span { + font-size: 11px; + position: relative; + top: -2px; + display: inline-flex; + } + .green-color { + display: block; + } +} + +.fee-estimation-container { + display: flex; + justify-content: space-between; + @media (min-width: 376px) { + flex-direction: row; + } + .item { + max-width: 150px; + margin: 0; + width: -webkit-fill-available; + @media (min-width: 376px) { + margin: 0 auto 0px; + } + &:first-child{ + display: none; + @media (min-width: 485px) { + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + &:last-child { + margin-bottom: 0; + } + .card-text span { + color: #ffffff66; + font-size: 12px; + top: 0px; + } + .fee-text{ + border-bottom: 1px solid #ffffff1c; + width: fit-content; + margin: auto; + line-height: 1.45; + padding: 0px 2px; + } + .fiat { + display: block; + font-size: 14px !important; + } + } +} + +.loading-container{ + min-height: 76px; +} + +.card-text { + .skeleton-loader { + width: 100%; + display: block; + &:first-child { + max-width: 90px; + margin: 15px auto 3px; + } + &:last-child { + margin: 10px auto 3px; + max-width: 55px; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.ts b/frontend/src/app/components/reward-stats/reward-stats.component.ts new file mode 100644 index 000000000..dd466985e --- /dev/null +++ b/frontend/src/app/components/reward-stats/reward-stats.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, skip, switchMap } from 'rxjs/operators'; +import { ApiService } from 'src/app/services/api.service'; +import { StateService } from 'src/app/services/state.service'; + +@Component({ + selector: 'app-reward-stats', + templateUrl: './reward-stats.component.html', + styleUrls: ['./reward-stats.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RewardStatsComponent implements OnInit { + public $rewardStats: Observable; + + constructor(private apiService: ApiService, private stateService: StateService) { } + + ngOnInit(): void { + this.$rewardStats = this.stateService.blocks$ + .pipe( + // (we always receives some blocks at start so only trigger for the last one) + skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1), + switchMap(() => { + return this.apiService.getRewardStats$() + .pipe( + map((stats) => { + return { + totalReward: stats.totalReward, + rewardPerTx: stats.totalReward / stats.totalTx, + feePerTx: stats.totalFee / stats.totalTx, + }; + }) + ); + }) + ); + } +} diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index c8118add7..786fd6687 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -115,3 +115,9 @@ export interface BlockExtension { export interface BlockExtended extends Block { extras?: BlockExtension; } + +export interface RewardStats { + totalReward: number; + totalFee: number; + totalTx: number; +} diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index cf510c449..92068c44e 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended } from '../interfaces/node-api.interface'; +import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended, RewardStats } from '../interfaces/node-api.interface'; import { Observable } from 'rxjs'; import { StateService } from './state.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; @@ -174,4 +174,8 @@ export class ApiService { (interval !== undefined ? `/${interval}` : '') ); } + + getRewardStats$(blockCount: number = 144): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`); + } } diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer index 38db73215..f9b89b7fa 100755 --- a/production/nginx-cache-warmer +++ b/production/nginx-cache-warmer @@ -33,6 +33,7 @@ do for url in / \ '/api/v1/mining/hashrate/pools/2y' \ '/api/v1/mining/hashrate/pools/3y' \ '/api/v1/mining/hashrate/pools/all' \ + '/api/v1/mining/reward-stats/144' \ do curl -s "https://${hostname}${url}" >/dev/null