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 {