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 { PoolInfo, PoolStats } from '../mempool.interfaces'; | ||||||
| import BlocksRepository, { EmptyBlocks } 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'; | ||||||
| import bitcoinClient from './bitcoin/bitcoin-client'; | import bitcoinClient from './bitcoin/bitcoin-client'; | ||||||
| @ -20,25 +20,21 @@ class Mining { | |||||||
|     const poolsStatistics = {}; |     const poolsStatistics = {}; | ||||||
| 
 | 
 | ||||||
|     const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval); |     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[] = []; |     const poolsStats: PoolStats[] = []; | ||||||
|     let rank = 1; |     let rank = 1; | ||||||
| 
 | 
 | ||||||
|     poolsInfo.forEach((poolInfo: PoolInfo) => { |     poolsInfo.forEach((poolInfo: PoolInfo) => { | ||||||
|  |       const emptyBlocksCount = emptyBlocks.filter((emptyCount) => emptyCount.poolId === poolInfo.poolId); | ||||||
|       const poolStat: PoolStats = { |       const poolStat: PoolStats = { | ||||||
|         poolId: poolInfo.poolId, // mysql row id
 |         poolId: poolInfo.poolId, // mysql row id
 | ||||||
|         name: poolInfo.name, |         name: poolInfo.name, | ||||||
|         link: poolInfo.link, |         link: poolInfo.link, | ||||||
|         blockCount: poolInfo.blockCount, |         blockCount: poolInfo.blockCount, | ||||||
|         rank: rank++, |         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); |       poolsStats.push(poolStat); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -58,19 +54,19 @@ class Mining { | |||||||
|   /** |   /** | ||||||
|    * Get all mining pool stats for a pool |    * 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); |     const pool = await PoolsRepository.$getPool(poolId); | ||||||
|     if (!pool) { |     if (!pool) { | ||||||
|       throw new Error(`This mining pool does not exist`); |       throw new Error(`This mining pool does not exist`); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const blockCount: number = await BlocksRepository.$blockCount(poolId, interval); |     const blockCount: number = await BlocksRepository.$blockCount(poolId); | ||||||
|     const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(poolId, interval); |     const emptyBlocksCount = await BlocksRepository.$countEmptyBlocks(poolId); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       pool: pool, |       pool: pool, | ||||||
|       blockCount: blockCount, |       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/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/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/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', routes.$getPoolBlocks) | ||||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks/:height', 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) |         .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) | ||||||
|  | |||||||
| @ -3,11 +3,6 @@ import { DB } from '../database'; | |||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import { Common } from '../api/common'; | import { Common } from '../api/common'; | ||||||
| 
 | 
 | ||||||
| export interface EmptyBlocks { |  | ||||||
|   emptyBlocks: number; |  | ||||||
|   poolId: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class BlocksRepository { | class BlocksRepository { | ||||||
|   /** |   /** | ||||||
|    * Save indexed block data in the database |    * Save indexed block data in the database | ||||||
| @ -100,12 +95,13 @@ class BlocksRepository { | |||||||
|   /** |   /** | ||||||
|    * Get empty blocks for one or all pools |    * 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); |     interval = Common.getSqlInterval(interval); | ||||||
| 
 | 
 | ||||||
|     const params: any[] = []; |     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 |       FROM blocks | ||||||
|  |       JOIN pools on pools.id = blocks.pool_id | ||||||
|       WHERE tx_count = 1`;
 |       WHERE tx_count = 1`;
 | ||||||
| 
 | 
 | ||||||
|     if (poolId) { |     if (poolId) { | ||||||
| @ -117,13 +113,14 @@ class BlocksRepository { | |||||||
|       query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; |       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(); |     const connection = await DB.pool.getConnection(); | ||||||
|     try { |     try { | ||||||
|       const [rows] = await connection.query(query, params); |       const [rows] = await connection.query(query, params); | ||||||
|       connection.release(); |       connection.release(); | ||||||
| 
 | 
 | ||||||
|       return <EmptyBlocks[]>rows; |       return rows; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       connection.release(); |       connection.release(); | ||||||
|       logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e)); |       logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e)); | ||||||
| @ -134,7 +131,7 @@ class BlocksRepository { | |||||||
|   /** |   /** | ||||||
|    * Get blocks count for a period |    * 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); |     interval = Common.getSqlInterval(interval); | ||||||
| 
 | 
 | ||||||
|     const params: any[] = []; |     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) { |   public async $setLatestRunTimestamp(key: string, val: any = null) { | ||||||
|     const connection = await DB.pool.getConnection(); |     const connection = await DB.pool.getConnection(); | ||||||
|     const query = `UPDATE state SET number = ? WHERE name = ?`; |     const query = `UPDATE state SET number = ? WHERE name = ?`; | ||||||
| @ -136,6 +182,9 @@ class HashratesRepository { | |||||||
|       const [rows] = await connection.query<any>(query, [key]); |       const [rows] = await connection.query<any>(query, [key]); | ||||||
|       connection.release(); |       connection.release(); | ||||||
| 
 | 
 | ||||||
|  |       if (rows.length === 0) { | ||||||
|  |         return 0; | ||||||
|  |       } | ||||||
|       return rows[0]['number']; |       return rows[0]['number']; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       connection.release(); |       connection.release(); | ||||||
|  | |||||||
| @ -538,7 +538,7 @@ class Routes { | |||||||
| 
 | 
 | ||||||
|   public async $getPool(req: Request, res: Response) { |   public async $getPool(req: Request, res: Response) { | ||||||
|     try { |     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('Pragma', 'public'); | ||||||
|       res.header('Cache-control', 'public'); |       res.header('Cache-control', 'public'); | ||||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); |       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) { |   public async $getHistoricalHashrate(req: Request, res: Response) { | ||||||
|     try { |     try { | ||||||
|       const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null); |       const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null); | ||||||
|  | |||||||
| @ -1,50 +1,12 @@ | |||||||
| <div class="container"> | <div class="container"> | ||||||
| 
 | 
 | ||||||
|   <div *ngIf="poolStats$ | async as poolStats"> |   <div *ngIf="poolStats$ | async as poolStats; else loadingMain"> | ||||||
|     <h1 class="m-0"> |     <h1 class="m-0 mb-2"> | ||||||
|       <img width="50" src="{{ poolStats['logo'] }}" onError="this.src = './resources/mining-pools/default.svg'" class="mr-3"> |       <img width="50" height="50" src="{{ poolStats['logo'] }}" onError="this.src = './resources/mining-pools/default.svg'" | ||||||
|  |         class="mr-3"> | ||||||
|       {{ poolStats.pool.name }} |       {{ poolStats.pool.name }} | ||||||
|     </h1> |     </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="box"> | ||||||
|       <div class="row"> |       <div class="row"> | ||||||
|         <div class="col-lg-9"> |         <div class="col-lg-9"> | ||||||
| @ -54,10 +16,13 @@ | |||||||
|                 <td class="col-4 col-lg-3">Addresses</td> |                 <td class="col-4 col-lg-3">Addresses</td> | ||||||
|                 <td class="text-truncate" *ngIf="poolStats.pool.addresses.length else noaddress"> |                 <td class="text-truncate" *ngIf="poolStats.pool.addresses.length else noaddress"> | ||||||
|                   <div class="scrollable"> |                   <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> |                   </div> | ||||||
|                 </td> |                 </td> | ||||||
|                 <ng-template #noaddress><td>~</td></ng-template> |                 <ng-template #noaddress> | ||||||
|  |                   <td>~</td> | ||||||
|  |                 </ng-template> | ||||||
|               </tr> |               </tr> | ||||||
|               <tr> |               <tr> | ||||||
|                 <td class="col-4 col-lg-3">Coinbase Tags</td> |                 <td class="col-4 col-lg-3">Coinbase Tags</td> | ||||||
| @ -75,7 +40,7 @@ | |||||||
|               </tr> |               </tr> | ||||||
|               <tr> |               <tr> | ||||||
|                 <td class="col-4 col-lg-8">Empty Blocks</td> |                 <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> |               </tr> | ||||||
|             </tbody> |             </tbody> | ||||||
|           </table> |           </table> | ||||||
| @ -84,25 +49,36 @@ | |||||||
|     </div> |     </div> | ||||||
|   </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> |     <thead> | ||||||
|       <th style="width: 15%;" i18n="latest-blocks.height">Height</th> |       <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 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: 20%;" i18n="latest-blocks.mined">Mined</th> | ||||||
|       <th style="width: 10%;" i18n="latest-blocks.reward">Reward</th> |       <th class="text-right" style="width: 10%; padding-right: 30px" i18n="latest-blocks.reward">Reward</th> | ||||||
|       <th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</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> |       <th style="width: 20%;" i18n="latest-blocks.size">Size</th> | ||||||
|     </thead> |     </thead> | ||||||
|     <tbody *ngIf="blocks$ | async as blocks"> |     <tbody> | ||||||
|       <tr *ngFor="let block of blocks"> |       <tr *ngFor="let block of blocks"> | ||||||
|         <td><a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a></td> |         <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 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> | ||||||
|         <td class=""><app-amount [satoshis]="block['reward']" digitsInfo="1.2-2" [noFiat]="true"></app-amount></td> |           <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> | ||||||
|         <td class="d-none d-lg-block">{{ block.tx_count | number }}</td> |         </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> |         <td> | ||||||
|           <div class="progress"> |           <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 class="progress-text" [innerHTML]="block.size | bytes: 2"></div> | ||||||
|           </div> |           </div> | ||||||
|         </td> |         </td> | ||||||
| @ -111,3 +87,52 @@ | |||||||
|   </table> |   </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; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   @media (min-width: 830px) { |   @media (min-width: 830px) { | ||||||
|     margin-left: 2%; |  | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|     float: left; |     float: right; | ||||||
|     margin-top: 0px; |     margin-top: 0px; | ||||||
|   } |   } | ||||||
|   .btn-sm { |   .btn-sm { | ||||||
| @ -39,3 +38,13 @@ div.scrollable { | |||||||
|   overflow: auto; |   overflow: auto; | ||||||
|   max-height: 100px; |   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 { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; | ||||||
| import { FormBuilder, FormGroup } from '@angular/forms'; |  | ||||||
| import { ActivatedRoute } from '@angular/router'; | import { ActivatedRoute } from '@angular/router'; | ||||||
| import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; | import { EChartsOption, graphic } from 'echarts'; | ||||||
| import { distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators'; | 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 { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; | ||||||
| import { ApiService } from 'src/app/services/api.service'; | import { ApiService } from 'src/app/services/api.service'; | ||||||
| import { StateService } from 'src/app/services/state.service'; | import { StateService } from 'src/app/services/state.service'; | ||||||
|  | import { selectPowerOfTen } from 'src/app/bitcoin.utils'; | ||||||
|  | import { formatNumber } from '@angular/common'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-pool', |   selector: 'app-pool', | ||||||
|   templateUrl: './pool.component.html', |   templateUrl: './pool.component.html', | ||||||
|   styleUrls: ['./pool.component.scss'], |   styleUrls: ['./pool.component.scss'], | ||||||
|  |   styles: [` | ||||||
|  |     .loadingGraphs { | ||||||
|  |       position: absolute; | ||||||
|  |       top: 50%; | ||||||
|  |       left: calc(50% - 15px); | ||||||
|  |       z-index: 100; | ||||||
|  |     } | ||||||
|  |   `],
 | ||||||
|   changeDetection: ChangeDetectionStrategy.OnPush |   changeDetection: ChangeDetectionStrategy.OnPush | ||||||
| }) | }) | ||||||
| export class PoolComponent implements OnInit { | export class PoolComponent implements OnInit { | ||||||
|  |   @Input() right: number | string = 45; | ||||||
|  |   @Input() left: number | string = 75; | ||||||
|  | 
 | ||||||
|   poolStats$: Observable<PoolStat>; |   poolStats$: Observable<PoolStat>; | ||||||
|   blocks$: Observable<BlockExtended[]>; |   blocks$: Observable<BlockExtended[]>; | ||||||
|  |   isLoading = true; | ||||||
|  | 
 | ||||||
|  |   chartOptions: EChartsOption = {}; | ||||||
|  |   chartInitOptions = { | ||||||
|  |     renderer: 'svg', | ||||||
|  |     width: 'auto', | ||||||
|  |     height: 'auto', | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   fromHeight: number = -1; |   fromHeight: number = -1; | ||||||
|   fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromHeight); |   fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromHeight); | ||||||
| 
 | 
 | ||||||
|   blocks: BlockExtended[] = []; |   blocks: BlockExtended[] = []; | ||||||
|   poolId: number = undefined; |   poolId: number = undefined; | ||||||
|   radioGroupForm: FormGroup; |  | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|  |     @Inject(LOCALE_ID) public locale: string, | ||||||
|     private apiService: ApiService, |     private apiService: ApiService, | ||||||
|     private route: ActivatedRoute, |     private route: ActivatedRoute, | ||||||
|     public stateService: StateService, |     public stateService: StateService, | ||||||
|     private formBuilder: FormBuilder, |  | ||||||
|   ) { |   ) { | ||||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' }); |  | ||||||
|     this.radioGroupForm.controls.dateSpan.setValue('1w'); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.poolStats$ = combineLatest([ |     this.poolStats$ = this.route.params.pipe(map((params) => params.poolId)) | ||||||
|       this.route.params.pipe(map((params) => params.poolId)), |  | ||||||
|       this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('1w')), |  | ||||||
|     ]) |  | ||||||
|       .pipe( |       .pipe( | ||||||
|         switchMap((params: any) => { |         switchMap((poolId: any) => { | ||||||
|           this.poolId = params[0]; |           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) { |           if (this.blocks.length === 0) { | ||||||
|             this.fromHeightSubject.next(undefined); |             this.fromHeightSubject.next(undefined); | ||||||
|           } |           } | ||||||
|           return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w'); |           return this.apiService.getPoolStats$(this.poolId); | ||||||
|         }), |         }), | ||||||
|         map((poolStats) => { |         map((poolStats) => { | ||||||
|           let regexes = '"'; |           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() { |   loadMore() { | ||||||
|     this.fromHeightSubject.next(this.blocks[this.blocks.length - 1]?.height); |     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> { |   getPoolStats$(poolId: number): Observable<PoolStat> { | ||||||
|     return this.httpClient.get<PoolStat>( |     return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}`); | ||||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}` + |   } | ||||||
|       (interval !== undefined ? `/${interval}` : '') | 
 | ||||||
|     ); |   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[]> { |   getPoolBlocks$(poolId: number, fromHeight: number): Observable<BlockExtended[]> { | ||||||
|  | |||||||
| @ -38,5 +38,13 @@ do for url in / \ | |||||||
| 		curl -s "https://${hostname}${url}" >/dev/null | 		curl -s "https://${hostname}${url}" >/dev/null | ||||||
| 	done | 	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 | 	sleep 10 | ||||||
| done | done | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user