Move reward stats to component - Add /api/v1/mining/reward-stats/{blockCount}

This commit is contained in:
nymkappa 2022-03-22 12:34:29 +09:00
parent 5c629dfe98
commit 2644f2fb07
No known key found for this signature in database
GPG key ID: E155910B16E8BD04
15 changed files with 202 additions and 110 deletions

View file

@ -1,4 +1,4 @@
import { PoolInfo, PoolStats } from '../mempool.interfaces'; import { PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces';
import BlocksRepository from '../repositories/BlocksRepository'; import BlocksRepository from '../repositories/BlocksRepository';
import PoolsRepository from '../repositories/PoolsRepository'; import PoolsRepository from '../repositories/PoolsRepository';
import HashratesRepository from '../repositories/HashratesRepository'; import HashratesRepository from '../repositories/HashratesRepository';
@ -70,6 +70,13 @@ class Mining {
}; };
} }
/**
* Get miner reward stats
*/
public async $getRewardStats(blockCount: number): Promise<RewardStats> {
return await BlocksRepository.$getBlockStats(blockCount);
}
/** /**
* [INDEXING] Generate weekly mining pool hashrate history * [INDEXING] Generate weekly mining pool hashrate history
*/ */

View file

@ -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/pools/:interval', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate) .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/hashrate/:interval', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
; ;
} }

View file

@ -209,3 +209,9 @@ export interface IDifficultyAdjustment {
timeAvg: number; timeAvg: number;
timeOffset: number; timeOffset: number;
} }
export interface RewardStats {
totalReward: number;
totalFee: number;
totalTx: number;
}

View file

@ -354,6 +354,9 @@ class BlocksRepository {
} }
} }
/**
* Return oldest blocks height
*/
public async $getOldestIndexedBlockHeight(): Promise<number> { public async $getOldestIndexedBlockHeight(): Promise<number> {
const connection = await DB.getConnection(); const connection = await DB.getConnection();
try { try {
@ -367,6 +370,29 @@ class BlocksRepository {
throw e; throw e;
} }
} }
/**
* Get general block stats
*/
public async $getBlockStats(blockCount: number): Promise<any> {
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(); export default new BlocksRepository();

View file

@ -935,6 +935,15 @@ class Routes {
res.status(500).end(); 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(); export default new Routes();

View file

@ -78,6 +78,7 @@ import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-st
import { GraphsComponent } from './components/graphs/graphs.component'; import { GraphsComponent } from './components/graphs/graphs.component';
import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components'; import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components';
import { BlocksList } from './components/blocks-list/blocks-list.component'; 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 { DataCyDirective } from './data-cy.directive';
@NgModule({ @NgModule({
@ -139,6 +140,7 @@ import { DataCyDirective } from './data-cy.directive';
DifficultyAdjustmentsTable, DifficultyAdjustmentsTable,
BlocksList, BlocksList,
DataCyDirective, DataCyDirective,
RewardStatsComponent,
], ],
imports: [ imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }), BrowserModule.withServerTransition({ appId: 'serverApp' }),

View file

@ -8,60 +8,11 @@
<div class="card-wrapper"> <div class="card-wrapper">
<div class="card" style="height: 123px"> <div class="card" style="height: 123px">
<div class="card-body more-padding"> <div class="card-body more-padding">
<div class="reward-container" *ngIf="$rewardStats | async as rewardStats; else loadingReward"> <app-reward-stats></app-reward-stats>
<div class="item">
<h5 class="card-title" i18n="mining.rewards">Miners Reward</h5>
<div class="card-text">
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
<div class="card-text">
{{ rewardStats.rewardPerTx | amountShortener }}
<span class="symbol">sats/tx</span>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
<div class="card-text">
{{ rewardStats.feePerTx | amountShortener}}
<span class="symbol">sats/tx</span>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<ng-template #loadingReward>
<div class="reward-container">
<div class="item">
<h5 class="card-title" i18n="mining.rewards">Miners Reward</h5>
<div class="card-text skeleton">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
<div class="card-text skeleton">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
<div class="card-text skeleton">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>
<!-- difficulty adjustment --> <!-- difficulty adjustment -->
<div class="col"> <div class="col">

View file

@ -59,42 +59,6 @@
padding-bottom: 3px; 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 { .more-padding {
padding: 18px; padding: 18px;
} }

View file

@ -14,14 +14,8 @@ import { WebsocketService } from 'src/app/services/websocket.service';
export class MiningDashboardComponent implements OnInit { export class MiningDashboardComponent implements OnInit {
private blocks = []; private blocks = [];
public $rewardStats: Observable<any>;
public totalReward = 0;
public rewardPerTx = '~';
public feePerTx = '~';
constructor( constructor(
private seoService: SeoService, private seoService: SeoService,
public stateService: StateService,
private websocketService: WebsocketService, private websocketService: WebsocketService,
) { ) {
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`); this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
@ -29,21 +23,5 @@ export class MiningDashboardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']); 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),
};
})
);
} }
} }

View file

@ -0,0 +1,51 @@
<div class="reward-container" *ngIf="$rewardStats | async as rewardStats; else loadingReward">
<div class="item">
<h5 class="card-title" i18n="mining.rewards">Miners Reward</h5>
<div class="card-text">
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
<div class="symbol">in the past 144 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
<div class="card-text">
{{ rewardStats.rewardPerTx | amountShortener }}
<span class="symbol">sats/tx</span>
<div class="symbol">in the past 144 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
<div class="card-text">
{{ rewardStats.feePerTx | amountShortener}}
<span class="symbol">sats/tx</span>
<div class="symbol">in the past 144 blocks</div>
</div>
</div>
</div>
<ng-template #loadingReward>
<div class="reward-container">
<div class="item">
<h5 class="card-title" i18n="mining.rewards">Miners Reward</h5>
<div class="card-text skeleton">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
<div class="card-text skeleton">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
<div class="card-text skeleton">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View file

@ -0,0 +1,57 @@
.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;
}
}
.card-text {
font-size: 22px;
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}

View file

@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.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<any>;
constructor(private apiService: ApiService) { }
ngOnInit(): void {
this.$rewardStats = this.apiService.getRewardStats$()
.pipe(
map((res) => {
return {
totalReward: res.totalReward,
rewardPerTx: res.totalReward / res.totalTx,
feePerTx: res.totalFee / res.totalTx,
};
})
);
}
}

View file

@ -115,3 +115,9 @@ export interface BlockExtension {
export interface BlockExtended extends Block { export interface BlockExtended extends Block {
extras?: BlockExtension; extras?: BlockExtension;
} }
export interface RewardStats {
totalReward: number;
totalFee: number;
totalTx: number;
}

View file

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; 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 { Observable } from 'rxjs';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface'; import { WebsocketResponse } from '../interfaces/websocket.interface';
@ -174,4 +174,8 @@ export class ApiService {
(interval !== undefined ? `/${interval}` : '') (interval !== undefined ? `/${interval}` : '')
); );
} }
getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
}
} }

View file

@ -33,6 +33,7 @@ do for url in / \
'/api/v1/mining/hashrate/pools/2y' \ '/api/v1/mining/hashrate/pools/2y' \
'/api/v1/mining/hashrate/pools/3y' \ '/api/v1/mining/hashrate/pools/3y' \
'/api/v1/mining/hashrate/pools/all' \ '/api/v1/mining/hashrate/pools/all' \
'/api/v1/mining/reward-stats/144' \
do do
curl -s "https://${hostname}${url}" >/dev/null curl -s "https://${hostname}${url}" >/dev/null