Merge pull request #1317 from nymkappa/feature/pool-hashrate
Added pool hashrate chart
This commit is contained in:
		
						commit
						f0f9d33dac
					
				| @ -1,5 +1,5 @@ | ||||
| import { PoolInfo, PoolStats } from '../mempool.interfaces'; | ||||
| import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository'; | ||||
| import BlocksRepository from '../repositories/BlocksRepository'; | ||||
| import PoolsRepository from '../repositories/PoolsRepository'; | ||||
| import HashratesRepository from '../repositories/HashratesRepository'; | ||||
| import bitcoinClient from './bitcoin/bitcoin-client'; | ||||
| @ -20,25 +20,21 @@ class Mining { | ||||
|     const poolsStatistics = {}; | ||||
| 
 | ||||
|     const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval); | ||||
|     const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(null, interval); | ||||
|     const emptyBlocks: any[] = await BlocksRepository.$countEmptyBlocks(null, interval); | ||||
| 
 | ||||
|     const poolsStats: PoolStats[] = []; | ||||
|     let rank = 1; | ||||
| 
 | ||||
|     poolsInfo.forEach((poolInfo: PoolInfo) => { | ||||
|       const emptyBlocksCount = emptyBlocks.filter((emptyCount) => emptyCount.poolId === poolInfo.poolId); | ||||
|       const poolStat: PoolStats = { | ||||
|         poolId: poolInfo.poolId, // mysql row id
 | ||||
|         name: poolInfo.name, | ||||
|         link: poolInfo.link, | ||||
|         blockCount: poolInfo.blockCount, | ||||
|         rank: rank++, | ||||
|         emptyBlocks: 0 | ||||
|         emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0 | ||||
|       }; | ||||
|       for (let i = 0; i < emptyBlocks.length; ++i) { | ||||
|         if (emptyBlocks[i].poolId === poolInfo.poolId) { | ||||
|           poolStat.emptyBlocks++; | ||||
|         } | ||||
|       } | ||||
|       poolsStats.push(poolStat); | ||||
|     }); | ||||
| 
 | ||||
| @ -58,19 +54,19 @@ class Mining { | ||||
|   /** | ||||
|    * Get all mining pool stats for a pool | ||||
|    */ | ||||
|   public async $getPoolStat(interval: string | null, poolId: number): Promise<object> { | ||||
|   public async $getPoolStat(poolId: number): Promise<object> { | ||||
|     const pool = await PoolsRepository.$getPool(poolId); | ||||
|     if (!pool) { | ||||
|       throw new Error(`This mining pool does not exist`); | ||||
|     } | ||||
| 
 | ||||
|     const blockCount: number = await BlocksRepository.$blockCount(poolId, interval); | ||||
|     const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(poolId, interval); | ||||
|     const blockCount: number = await BlocksRepository.$blockCount(poolId); | ||||
|     const emptyBlocksCount = await BlocksRepository.$countEmptyBlocks(poolId); | ||||
| 
 | ||||
|     return { | ||||
|       pool: pool, | ||||
|       blockCount: blockCount, | ||||
|       emptyBlocks: emptyBlocks, | ||||
|       emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -299,6 +299,7 @@ class Server { | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y')) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y')) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all')) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/hashrate', routes.$getPoolHistoricalHashrate) | ||||
|         .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) | ||||
|  | ||||
| @ -3,11 +3,6 @@ import { DB } from '../database'; | ||||
| import logger from '../logger'; | ||||
| import { Common } from '../api/common'; | ||||
| 
 | ||||
| export interface EmptyBlocks { | ||||
|   emptyBlocks: number; | ||||
|   poolId: number; | ||||
| } | ||||
| 
 | ||||
| class BlocksRepository { | ||||
|   /** | ||||
|    * Save indexed block data in the database | ||||
| @ -100,12 +95,13 @@ class BlocksRepository { | ||||
|   /** | ||||
|    * Get empty blocks for one or all pools | ||||
|    */ | ||||
|   public async $getEmptyBlocks(poolId: number | null, interval: string | null = null): Promise<EmptyBlocks[]> { | ||||
|   public async $countEmptyBlocks(poolId: number | null, interval: string | null = null): Promise<any> { | ||||
|     interval = Common.getSqlInterval(interval); | ||||
| 
 | ||||
|     const params: any[] = []; | ||||
|     let query = `SELECT height, hash, tx_count, size, pool_id, weight, UNIX_TIMESTAMP(blockTimestamp) as timestamp
 | ||||
|     let query = `SELECT count(height) as count, pools.id as poolId
 | ||||
|       FROM blocks | ||||
|       JOIN pools on pools.id = blocks.pool_id | ||||
|       WHERE tx_count = 1`;
 | ||||
| 
 | ||||
|     if (poolId) { | ||||
| @ -117,13 +113,14 @@ class BlocksRepository { | ||||
|       query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; | ||||
|     } | ||||
| 
 | ||||
|     // logger.debug(query);
 | ||||
|     query += ` GROUP by pools.id`; | ||||
| 
 | ||||
|     const connection = await DB.pool.getConnection(); | ||||
|     try { | ||||
|       const [rows] = await connection.query(query, params); | ||||
|       connection.release(); | ||||
| 
 | ||||
|       return <EmptyBlocks[]>rows; | ||||
|       return rows; | ||||
|     } catch (e) { | ||||
|       connection.release(); | ||||
|       logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e)); | ||||
| @ -134,7 +131,7 @@ class BlocksRepository { | ||||
|   /** | ||||
|    * Get blocks count for a period | ||||
|    */ | ||||
|   public async $blockCount(poolId: number | null, interval: string | null): Promise<number> { | ||||
|   public async $blockCount(poolId: number | null, interval: string | null = null): Promise<number> { | ||||
|     interval = Common.getSqlInterval(interval); | ||||
| 
 | ||||
|     const params: any[] = []; | ||||
|  | ||||
| @ -116,6 +116,52 @@ class HashratesRepository { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns a pool hashrate history | ||||
|    */ | ||||
|    public async $getPoolWeeklyHashrate(poolId: number): Promise<any[]> { | ||||
|     const connection = await DB.pool.getConnection(); | ||||
| 
 | ||||
|     // Find hashrate boundaries
 | ||||
|     let query = `SELECT MIN(hashrate_timestamp) as firstTimestamp, MAX(hashrate_timestamp) as lastTimestamp
 | ||||
|       FROM hashrates  | ||||
|       JOIN pools on pools.id = pool_id  | ||||
|       WHERE hashrates.type = 'weekly' AND pool_id = ? AND avg_hashrate != 0 | ||||
|       ORDER by hashrate_timestamp LIMIT 1`;
 | ||||
| 
 | ||||
|     let boundaries = { | ||||
|       firstTimestamp: '1970-01-01', | ||||
|       lastTimestamp: '9999-01-01' | ||||
|     }; | ||||
|     try { | ||||
|       const [rows]: any[] = await connection.query(query, [poolId]); | ||||
|       boundaries = rows[0]; | ||||
|       connection.release(); | ||||
|     } catch (e) { | ||||
|       connection.release(); | ||||
|       logger.err('$getPoolWeeklyHashrate() error' + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
| 
 | ||||
|     // Get hashrates entries between boundaries
 | ||||
|     query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
 | ||||
|       FROM hashrates | ||||
|       JOIN pools on pools.id = pool_id | ||||
|       WHERE hashrates.type = 'weekly' AND hashrate_timestamp BETWEEN ? AND ? | ||||
|       AND pool_id = ? | ||||
|       ORDER by hashrate_timestamp`;
 | ||||
| 
 | ||||
|     try { | ||||
|       const [rows]: any[] = await connection.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, poolId]); | ||||
|       connection.release(); | ||||
| 
 | ||||
|       return rows; | ||||
|     } catch (e) { | ||||
|       connection.release(); | ||||
|       logger.err('$getPoolWeeklyHashrate() error' + (e instanceof Error ? e.message : e)); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $setLatestRunTimestamp(key: string, val: any = null) { | ||||
|     const connection = await DB.pool.getConnection(); | ||||
|     const query = `UPDATE state SET number = ? WHERE name = ?`; | ||||
| @ -136,6 +182,9 @@ class HashratesRepository { | ||||
|       const [rows] = await connection.query<any>(query, [key]); | ||||
|       connection.release(); | ||||
| 
 | ||||
|       if (rows.length === 0) { | ||||
|         return 0; | ||||
|       } | ||||
|       return rows[0]['number']; | ||||
|     } catch (e) { | ||||
|       connection.release(); | ||||
|  | ||||
| @ -538,7 +538,7 @@ class Routes { | ||||
| 
 | ||||
|   public async $getPool(req: Request, res: Response) { | ||||
|     try { | ||||
|       const stats = await mining.$getPoolStat(req.params.interval ?? null, parseInt(req.params.poolId, 10)); | ||||
|       const stats = await mining.$getPoolStat(parseInt(req.params.poolId, 10)); | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); | ||||
| @ -603,6 +603,22 @@ class Routes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getPoolHistoricalHashrate(req: Request, res: Response) { | ||||
|     try { | ||||
|       const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(parseInt(req.params.poolId, 10)); | ||||
|       const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); | ||||
|       res.json({ | ||||
|         oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp, | ||||
|         hashrates: hashrates, | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getHistoricalHashrate(req: Request, res: Response) { | ||||
|     try { | ||||
|       const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null); | ||||
|  | ||||
| @ -1,50 +1,12 @@ | ||||
| <div class="container"> | ||||
| 
 | ||||
|   <div *ngIf="poolStats$ | async as poolStats"> | ||||
|     <h1 class="m-0"> | ||||
|       <img width="50" src="{{ poolStats['logo'] }}" onError="this.src = './resources/mining-pools/default.svg'" class="mr-3"> | ||||
|   <div *ngIf="poolStats$ | async as poolStats; else loadingMain"> | ||||
|     <h1 class="m-0 mb-2"> | ||||
|       <img width="50" height="50" src="{{ poolStats['logo'] }}" onError="this.src = './resources/mining-pools/default.svg'" | ||||
|         class="mr-3"> | ||||
|       {{ poolStats.pool.name }} | ||||
|     </h1> | ||||
| 
 | ||||
|     <div class="box pl-0 bg-transparent"> | ||||
|       <div class="card-header mb-0 mb-lg-4 pr-0 pl-0"> | ||||
|         <form [formGroup]="radioGroupForm" class="formRadioGroup ml-0"> | ||||
|           <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'24h'"> 24h | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'3d'"> 3D | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'1w'"> 1W | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'1m'"> 1M | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'3m'"> 3M | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'6m'"> 6M | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'1y'"> 1Y | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'2y'"> 2Y | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'3y'"> 3Y | ||||
|             </label> | ||||
|             <label ngbButtonLabel class="btn-primary btn-sm"> | ||||
|               <input ngbButton type="radio" [value]="'all'"> ALL | ||||
|             </label> | ||||
|           </div> | ||||
|         </form> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="box"> | ||||
|       <div class="row"> | ||||
|         <div class="col-lg-9"> | ||||
| @ -54,10 +16,13 @@ | ||||
|                 <td class="col-4 col-lg-3">Addresses</td> | ||||
|                 <td class="text-truncate" *ngIf="poolStats.pool.addresses.length else noaddress"> | ||||
|                   <div class="scrollable"> | ||||
|                     <a *ngFor="let address of poolStats.pool.addresses" [routerLink]="['/address' | relativeUrl, address]">{{ address }}<br></a> | ||||
|                     <a *ngFor="let address of poolStats.pool.addresses" | ||||
|                       [routerLink]="['/address' | relativeUrl, address]">{{ address }}<br></a> | ||||
|                   </div> | ||||
|                 </td> | ||||
|                 <ng-template #noaddress><td>~</td></ng-template> | ||||
|                 <ng-template #noaddress> | ||||
|                   <td>~</td> | ||||
|                 </ng-template> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td class="col-4 col-lg-3">Coinbase Tags</td> | ||||
| @ -75,7 +40,7 @@ | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td class="col-4 col-lg-8">Empty Blocks</td> | ||||
|                 <td class="text-left">{{ poolStats.emptyBlocks.length }}</td> | ||||
|                 <td class="text-left">{{ poolStats.emptyBlocks }}</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
| @ -84,25 +49,36 @@ | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()"> | ||||
|   <div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div> | ||||
|   <div class="text-center loadingGraphs" *ngIf="isLoading"> | ||||
|     <div class="spinner-border text-light"></div> | ||||
|   </div> | ||||
| 
 | ||||
|   <table *ngIf="blocks$ | async as blocks" class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" | ||||
|     [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()"> | ||||
|     <thead> | ||||
|       <th style="width: 15%;" i18n="latest-blocks.height">Height</th> | ||||
|       <th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th> | ||||
|       <th style="width: 20%;" i18n="latest-blocks.mined">Mined</th> | ||||
|       <th style="width: 10%;" i18n="latest-blocks.reward">Reward</th> | ||||
|       <th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</th> | ||||
|       <th class="text-right" style="width: 10%; padding-right: 30px" i18n="latest-blocks.reward">Reward</th> | ||||
|       <th class="d-none d-lg-block text-right" style="width: 15%; padding-right: 40px" i18n="latest-blocks.transactions">Transactions</th> | ||||
|       <th style="width: 20%;" i18n="latest-blocks.size">Size</th> | ||||
|     </thead> | ||||
|     <tbody *ngIf="blocks$ | async as blocks"> | ||||
|     <tbody> | ||||
|       <tr *ngFor="let block of blocks"> | ||||
|         <td><a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a></td> | ||||
|         <td class="d-none d-md-block">‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td> | ||||
|         <td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td> | ||||
|         <td class=""><app-amount [satoshis]="block['reward']" digitsInfo="1.2-2" [noFiat]="true"></app-amount></td> | ||||
|         <td class="d-none d-lg-block">{{ block.tx_count | number }}</td> | ||||
|         <td> | ||||
|           <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> | ||||
|         </td> | ||||
|         <td class="text-right" style="padding-right: 30px"> | ||||
|           <app-amount [satoshis]="block['reward']" digitsInfo="1.2-2" [noFiat]="true"></app-amount> | ||||
|         </td> | ||||
|         <td class="d-none d-lg-block text-right" style="padding-right: 40px">{{ block.tx_count | number }}</td> | ||||
|         <td> | ||||
|           <div class="progress"> | ||||
|             <div class="progress-bar progress-mempool" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div> | ||||
|             <div class="progress-bar progress-mempool" role="progressbar" | ||||
|               [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div> | ||||
|             <div class="progress-text" [innerHTML]="block.size | bytes: 2"></div> | ||||
|           </div> | ||||
|         </td> | ||||
| @ -110,4 +86,53 @@ | ||||
|     </tbody> | ||||
|   </table> | ||||
| 
 | ||||
| </div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loadingMain> | ||||
|   <div> | ||||
|     <h1 class="m-0 mb-2"> | ||||
|       <img width="50" height="50" src="./resources/mining-pools/default.svg" class="mr-3"> | ||||
|       <div class="skeleton-loader"></div> | ||||
|     </h1> | ||||
| 
 | ||||
|     <div class="box"> | ||||
|       <div class="row"> | ||||
|         <div class="col-lg-9"> | ||||
|           <table class="table table-borderless table-striped" style="table-layout: fixed;"> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td class="col-4 col-lg-3">Addresses</td> | ||||
|                 <td class="text-truncate"> | ||||
|                   <div class="scrollable"> | ||||
|                     <div class="skeleton-loader"></div> | ||||
|                   </div> | ||||
|                 </td> | ||||
|                 <ng-template #noaddress> | ||||
|                   <td>~</td> | ||||
|                 </ng-template> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td class="col-4 col-lg-3">Coinbase Tags</td> | ||||
|                 <td class="text-truncate"><div class="skeleton-loader"></div></td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|         <div class="col-lg-3"> | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td class="col-4 col-lg-8">Mined Blocks</td> | ||||
|                 <td class="text-left"><div class="skeleton-loader"></div></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td class="col-4 col-lg-8">Empty Blocks</td> | ||||
|                 <td class="text-left"><div class="skeleton-loader"></div></td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| @ -18,9 +18,8 @@ | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   @media (min-width: 830px) { | ||||
|     margin-left: 2%; | ||||
|     flex-direction: row; | ||||
|     float: left; | ||||
|     float: right; | ||||
|     margin-top: 0px; | ||||
|   } | ||||
|   .btn-sm { | ||||
| @ -38,4 +37,14 @@ div.scrollable { | ||||
|   padding: 0; | ||||
|   overflow: auto; | ||||
|   max-height: 100px; | ||||
| } | ||||
| 
 | ||||
| .skeleton-loader { | ||||
|   width: 100%; | ||||
|   max-width: 90px; | ||||
| } | ||||
| 
 | ||||
| .table { | ||||
|   margin: 0px auto; | ||||
|   max-width: 900px; | ||||
| } | ||||
| @ -1,51 +1,77 @@ | ||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup } from '@angular/forms'; | ||||
| import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; | ||||
| import { distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators'; | ||||
| import { EChartsOption, graphic } from 'echarts'; | ||||
| import { BehaviorSubject, Observable } from 'rxjs'; | ||||
| import { distinctUntilChanged, map, switchMap, tap, toArray } from 'rxjs/operators'; | ||||
| import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { StateService } from 'src/app/services/state.service'; | ||||
| import { selectPowerOfTen } from 'src/app/bitcoin.utils'; | ||||
| import { formatNumber } from '@angular/common'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-pool', | ||||
|   templateUrl: './pool.component.html', | ||||
|   styleUrls: ['./pool.component.scss'], | ||||
|   styles: [` | ||||
|     .loadingGraphs { | ||||
|       position: absolute; | ||||
|       top: 50%; | ||||
|       left: calc(50% - 15px); | ||||
|       z-index: 100; | ||||
|     } | ||||
|   `],
 | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush | ||||
| }) | ||||
| export class PoolComponent implements OnInit { | ||||
|   @Input() right: number | string = 45; | ||||
|   @Input() left: number | string = 75; | ||||
| 
 | ||||
|   poolStats$: Observable<PoolStat>; | ||||
|   blocks$: Observable<BlockExtended[]>; | ||||
|   isLoading = true; | ||||
| 
 | ||||
|   chartOptions: EChartsOption = {}; | ||||
|   chartInitOptions = { | ||||
|     renderer: 'svg', | ||||
|     width: 'auto', | ||||
|     height: 'auto', | ||||
|   }; | ||||
| 
 | ||||
|   fromHeight: number = -1; | ||||
|   fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromHeight); | ||||
| 
 | ||||
|   blocks: BlockExtended[] = []; | ||||
|   poolId: number = undefined; | ||||
|   radioGroupForm: FormGroup; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(LOCALE_ID) public locale: string, | ||||
|     private apiService: ApiService, | ||||
|     private route: ActivatedRoute, | ||||
|     public stateService: StateService, | ||||
|     private formBuilder: FormBuilder, | ||||
|   ) { | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue('1w'); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.poolStats$ = combineLatest([ | ||||
|       this.route.params.pipe(map((params) => params.poolId)), | ||||
|       this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('1w')), | ||||
|     ]) | ||||
|     this.poolStats$ = this.route.params.pipe(map((params) => params.poolId)) | ||||
|       .pipe( | ||||
|         switchMap((params: any) => { | ||||
|           this.poolId = params[0]; | ||||
|         switchMap((poolId: any) => { | ||||
|           this.isLoading = true; | ||||
|           this.poolId = poolId; | ||||
|           return this.apiService.getPoolHashrate$(this.poolId) | ||||
|             .pipe( | ||||
|               switchMap((data) => { | ||||
|                 this.isLoading = false; | ||||
|                 this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate])); | ||||
|                 return poolId; | ||||
|               }), | ||||
|             ) | ||||
|         }), | ||||
|         switchMap(() => { | ||||
|           if (this.blocks.length === 0) { | ||||
|             this.fromHeightSubject.next(undefined); | ||||
|           } | ||||
|           return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w'); | ||||
|           return this.apiService.getPoolStats$(this.poolId); | ||||
|         }), | ||||
|         map((poolStats) => { | ||||
|           let regexes = '"'; | ||||
| @ -74,6 +100,96 @@ export class PoolComponent implements OnInit { | ||||
|       ) | ||||
|   } | ||||
| 
 | ||||
|   prepareChartOptions(data) { | ||||
|     this.chartOptions = { | ||||
|       animation: false, | ||||
|       color: [ | ||||
|         new graphic.LinearGradient(0, 0, 0, 0.65, [ | ||||
|           { offset: 0, color: '#F4511E' }, | ||||
|           { offset: 0.25, color: '#FB8C00' }, | ||||
|           { offset: 0.5, color: '#FFB300' }, | ||||
|           { offset: 0.75, color: '#FDD835' }, | ||||
|           { offset: 1, color: '#7CB342' } | ||||
|         ]), | ||||
|         '#D81B60', | ||||
|       ], | ||||
|       grid: { | ||||
|         right: this.right, | ||||
|         left: this.left, | ||||
|         bottom: 60, | ||||
|       }, | ||||
|       tooltip: { | ||||
|         show: !this.isMobile(), | ||||
|         trigger: 'axis', | ||||
|         axisPointer: { | ||||
|           type: 'line' | ||||
|         }, | ||||
|         backgroundColor: 'rgba(17, 19, 31, 1)', | ||||
|         borderRadius: 4, | ||||
|         shadowColor: 'rgba(0, 0, 0, 0.5)', | ||||
|         textStyle: { | ||||
|           color: '#b1b1b1', | ||||
|           align: 'left', | ||||
|         }, | ||||
|         borderColor: '#000', | ||||
|         formatter: function (data) { | ||||
|           let hashratePowerOfTen: any = selectPowerOfTen(1); | ||||
|           let hashrate = data[0].data[1]; | ||||
| 
 | ||||
|           if (this.isMobile()) { | ||||
|             hashratePowerOfTen = selectPowerOfTen(data[0].data[1]); | ||||
|             hashrate = Math.round(data[0].data[1] / hashratePowerOfTen.divider); | ||||
|           } | ||||
| 
 | ||||
|           return ` | ||||
|             <b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br> | ||||
|             <span>${data[0].marker} ${data[0].seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s</span><br> | ||||
|           `;
 | ||||
|         }.bind(this) | ||||
|       }, | ||||
|       xAxis: { | ||||
|         type: 'time', | ||||
|         splitNumber: (this.isMobile()) ? 5 : 10, | ||||
|       }, | ||||
|       yAxis: [ | ||||
|         { | ||||
|           min: function (value) { | ||||
|             return value.min * 0.9; | ||||
|           }, | ||||
|           type: 'value', | ||||
|           name: 'Hashrate', | ||||
|           axisLabel: { | ||||
|             color: 'rgb(110, 112, 121)', | ||||
|             formatter: (val) => { | ||||
|               const selectedPowerOfTen: any = selectPowerOfTen(val); | ||||
|               const newVal = Math.round(val / selectedPowerOfTen.divider); | ||||
|               return `${newVal} ${selectedPowerOfTen.unit}H/s` | ||||
|             } | ||||
|           }, | ||||
|           splitLine: { | ||||
|             show: false, | ||||
|           } | ||||
|         }, | ||||
|       ], | ||||
|       series: [ | ||||
|         { | ||||
|           name: 'Hashrate', | ||||
|           showSymbol: false, | ||||
|           symbol: 'none', | ||||
|           data: data, | ||||
|           type: 'line', | ||||
|           lineStyle: { | ||||
|             width: 2, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   isMobile() { | ||||
|     return (window.innerWidth <= 767.98); | ||||
|   } | ||||
| 
 | ||||
|   loadMore() { | ||||
|     this.fromHeightSubject.next(this.blocks[this.blocks.length - 1]?.height); | ||||
|   } | ||||
|  | ||||
| @ -136,11 +136,12 @@ export class ApiService { | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   getPoolStats$(poolId: number, interval: string | undefined): Observable<PoolStat> { | ||||
|     return this.httpClient.get<PoolStat>( | ||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}` + | ||||
|       (interval !== undefined ? `/${interval}` : '') | ||||
|     ); | ||||
|   getPoolStats$(poolId: number): Observable<PoolStat> { | ||||
|     return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}`); | ||||
|   } | ||||
| 
 | ||||
|   getPoolHashrate$(poolId: number): Observable<any> { | ||||
|     return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/hashrate`); | ||||
|   } | ||||
| 
 | ||||
|   getPoolBlocks$(poolId: number, fromHeight: number): Observable<BlockExtended[]> { | ||||
|  | ||||
| @ -38,5 +38,13 @@ do for url in / \ | ||||
| 		curl -s "https://${hostname}${url}" >/dev/null | ||||
| 	done | ||||
| 
 | ||||
| 	counter=1 | ||||
| 	while [ $counter -le 134 ] | ||||
| 	do | ||||
| 		curl -s "https://${hostname}/api/v1/mining/pool/${counter}/hashrate" >/dev/null | ||||
| 		curl -s "https://${hostname}/api/v1/mining/pool/${counter}" >/dev/null | ||||
| 		((counter++)) | ||||
| 	done | ||||
| 
 | ||||
| 	sleep 10 | ||||
| done | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user