From 0ebe0a5dc9985866c1c2efb2a5455fac5d6dffef Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 16 Mar 2023 16:13:11 +0900 Subject: [PATCH] Add new stats in mining pool page --- backend/src/api/mining/mining.ts | 6 + backend/src/repositories/BlocksRepository.ts | 49 ++++++++ .../components/pool/pool-preview.component.ts | 5 - .../app/components/pool/pool.component.html | 106 ++++++++++-------- .../app/components/pool/pool.component.scss | 13 ++- .../src/app/components/pool/pool.component.ts | 10 +- .../src/app/interfaces/node-api.interface.ts | 4 +- 7 files changed, 128 insertions(+), 65 deletions(-) diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index 8b4abb0d6..58626df65 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -13,6 +13,7 @@ import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; import PricesRepository from '../../repositories/PricesRepository'; import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory'; import { IEsploraApi } from '../bitcoin/esplora-api.interface'; +import database from '../../database'; class Mining { private blocksPriceIndexingRunning = false; @@ -141,6 +142,9 @@ class Mining { const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w'); const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w'); + const avgHealth = await BlocksRepository.$getAvgBlockHealthPerPoolId(pool.id); + const totalReward = await BlocksRepository.$getTotalRewardForPoolId(pool.id); + let currentEstimatedHashrate = 0; try { currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h); @@ -162,6 +166,8 @@ class Mining { }, estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h), reportedHashrate: null, + avgBlockHealth: avgHealth, + totalReward: totalReward, }; } diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index f2d0a283e..04dcd4b56 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -330,6 +330,55 @@ class BlocksRepository { } } + /** + * Get average block health for all blocks for a single pool + */ + public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> { + const params: any[] = []; + const query = ` + SELECT AVG(blocks_audits.match_rate) AS avg_match_rate + FROM blocks + JOIN blocks_audits ON blocks.height = blocks_audits.height + WHERE blocks.pool_id = ? + `; + params.push(poolId); + + try { + const [rows] = await DB.query(query, params); + if (!rows[0] || !rows[0].avg_match_rate) { + return 0; + } + return Math.round(rows[0].avg_match_rate * 100) / 100; + } catch (e) { + logger.err(`Cannot get average block health for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + + /** + * Get average block health for all blocks for a single pool + */ + public async $getTotalRewardForPoolId(poolId: number): Promise<number> { + const params: any[] = []; + const query = ` + SELECT sum(reward) as total_reward + FROM blocks + WHERE blocks.pool_id = ? + `; + params.push(poolId); + + try { + const [rows] = await DB.query(query, params); + if (!rows[0] || !rows[0].total_reward) { + return 0; + } + return rows[0].total_reward; + } catch (e) { + logger.err(`Cannot get total reward for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + /** * Get the oldest indexed block */ diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts index 277bacb33..0431686d6 100644 --- a/frontend/src/app/components/pool/pool-preview.component.ts +++ b/frontend/src/app/components/pool/pool-preview.component.ts @@ -86,11 +86,6 @@ export class PoolPreviewComponent implements OnInit { regexes += regex + '", "'; } poolStats.pool.regexes = regexes.slice(0, -3); - poolStats.pool.addresses = poolStats.pool.addresses; - - if (poolStats.reportedHashrate) { - poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; - } this.openGraphService.waitOver('pool-stats-' + this.slug); diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 0ae32ccb8..d7c791db9 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -37,13 +37,13 @@ <!-- Addresses desktop --> <tr *ngIf="!isMobile()" class="taller-row"> <td class="label addresses" i18n="mining.addresses">Addresses</td> - <td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 25px"> - <a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]" class="first-address"> + <td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 15px"> + <a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]"> {{ poolStats.pool.addresses[0] }} </a> <div> <div #collapse="ngbCollapse" [(ngbCollapse)]="gfg"> - <a *ngFor="let address of poolStats.pool.addresses | slice: 1" + <a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1" [routerLink]="['/address' | relativeUrl, address]">{{ address }}<br></a> </div> @@ -67,13 +67,13 @@ [attr.aria-expanded]="!gfg" aria-controls="collapseExample"> <span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }}) </button> - <a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]"> - {{ poolStats.pool.addresses[0] | shortenString: 40 }} + <a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]"> + {{ poolStats.pool.addresses[0] | shortenString: 30 }} </a> <div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%"> - <a *ngFor="let address of poolStats.pool.addresses | slice: 1" + <a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1" [routerLink]="['/address' | relativeUrl, address]">{{ - address | shortenString: 40 }}<br></a> + address | shortenString: 30 }}<br></a> </div> </div> </td> @@ -88,22 +88,25 @@ <!-- Hashrate desktop --> <tr *ngIf="!isMobile()" class="taller-row"> - <td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td> <td class="data"> <table class="table table-xs table-data"> <thead> <tr> - <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th> - <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th> - <th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck">Avg Health</th> </tr> </thead> <tbody> - <td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td> - <ng-template *ngIf="poolStats.luck; else noreported"> - <td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td> - <td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td> - </ng-template> + <td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td> + <td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td> + <td class="text-center"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99" + [class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75" + *ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span> + <ng-template #nullHealth> + <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> + </ng-template> + </td> </tbody> </table> </td> @@ -111,49 +114,46 @@ <!-- Hashrate mobile --> <tr *ngIf="isMobile()"> <td colspan="2"> - <span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span> <table class="table table-xs table-data"> <thead> <tr> - <th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th> - <th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th> - <th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck">Avg Health</th> </tr> </thead> <tbody> - <td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td> - <ng-template *ngIf="poolStats.luck; else noreported"> - <td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td> - <td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td> - </ng-template> + <td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td> + <td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td> + <td class="text-center"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99" + [class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75" + *ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span> + <ng-template #nullHealth> + <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> + </ng-template> + </td> </tbody> </table> </td> </tr> - <ng-template #noreported> - <td>~</td> - <td>~</td> - </ng-template> - <!-- Mined blocks desktop --> <tr *ngIf="!isMobile()" class="taller-row"> - <td class="label" i18n="mining.mined-blocks">Mined blocks</td> <td class="data"> <table class="table table-xs table-data"> <thead> <tr> - <th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th> - <th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th> - <th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th> </tr> </thead> <tbody> - <td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + <td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td> - <td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + <td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td> - <td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + <td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td> </tbody> </table> @@ -162,21 +162,20 @@ <!-- Mined blocks mobile --> <tr *ngIf="isMobile()"> <td colspan=2> - <span class="label" i18n="mining.mined-blocks">Mined blocks</span> <table class="table table-xs table-data"> <thead> <tr> - <th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th> - <th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th> - <th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th> + <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th> </tr> </thead> <tbody> - <td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + <td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td> - <td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + <td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td> - <td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + <td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td> </tbody> </table> @@ -213,8 +212,9 @@ <th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th> <th class="mined" i18n="latest-blocks.mined">Mined</th> <th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th> + <th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health">Health</th> <th class="reward text-right" i18n="latest-blocks.reward">Reward</th> - <th class="fees text-right" i18n="latest-blocks.fees">Fees</th> + <th *ngIf="!auditAvailable" class="fees text-right" i18n="latest-blocks.fees">Fees</th> <th class="txs text-right" i18n="dashboard.txs">TXs</th> <th class="size" i18n="latest-blocks.size">Size</th> </thead> @@ -234,10 +234,24 @@ {{ block.extras.coinbaseRaw | hex2ascii }} </span> </td> + <td *ngIf="auditAvailable" class="health text-right"> + <a + class="health-badge badge" + [class.badge-success]="block.extras.matchRate >= 99" + [class.badge-warning]="block.extras.matchRate >= 75 && block.extras.matchRate < 99" + [class.badge-danger]="block.extras.matchRate < 75" + [routerLink]="block.extras.matchRate != null ? ['/block/' | relativeUrl, block.id] : null" + [state]="{ data: { block: block } }" + *ngIf="block.extras.matchRate != null; else nullHealth" + >{{ block.extras.matchRate }}%</a> + <ng-template #nullHealth> + <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> + </ng-template> + </td> <td class="reward text-right"> <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount> </td> - <td class="fees text-right"> + <td *ngIf="!auditAvailable" class="fees text-right"> <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount> </td> <td class="txs text-right"> diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index 9103f38f5..21468773f 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -68,6 +68,11 @@ div.scrollable { vertical-align: top; padding-top: 25px; } +.addresses-data { + vertical-align: top; + font-family: monospace; + font-size: 14px; +} .data { text-align: right; @@ -100,7 +105,7 @@ div.scrollable { @media (max-width: 875px) { padding-left: 50px; } - @media (max-width: 650px) { + @media (max-width: 685px) { display: none; } } @@ -118,7 +123,7 @@ div.scrollable { padding-right: 10px; } @media (max-width: 875px) { - padding-right: 50px; + padding-right: 20px; } @media (max-width: 567px) { padding-right: 10px; @@ -186,10 +191,6 @@ div.scrollable { .block-count-title { color: #4a68b9; font-size: 14px; - text-align: left; - @media (max-width: 767.98px) { - text-align: center; - } } .table-data tr { diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 56b8bd392..85fd028ef 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { EChartsOption, graphic } from 'echarts'; -import { BehaviorSubject, Observable, timer } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; @@ -35,6 +35,8 @@ export class PoolComponent implements OnInit { blocks: BlockExtended[] = []; slug: string = undefined; + auditAvailable = false; + loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height); constructor( @@ -44,6 +46,7 @@ export class PoolComponent implements OnInit { public stateService: StateService, private seoService: SeoService, ) { + this.auditAvailable = this.stateService.env.AUDIT; } ngOnInit(): void { @@ -74,11 +77,6 @@ export class PoolComponent implements OnInit { regexes += regex + '", "'; } poolStats.pool.regexes = regexes.slice(0, -3); - poolStats.pool.addresses = poolStats.pool.addresses; - - if (poolStats.reportedHashrate) { - poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; - } return Object.assign({ logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg' diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index cad623f9f..8d8f30863 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -107,8 +107,8 @@ export interface PoolStat { '1w': number, }; estimatedHashrate: number; - reportedHashrate: number; - luck?: number; + avgBlockHealth: number; + totalReward: number; } export interface BlockExtension {