Merge pull request #1335 from nymkappa/feature/new-blocks-page
Create new /mining/blocks page
This commit is contained in:
		
						commit
						0dbee1461d
					
				| @ -108,14 +108,23 @@ class Blocks { | |||||||
|     blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0); |     blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0); | ||||||
|     blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); |     blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); | ||||||
| 
 | 
 | ||||||
|     const stats = await bitcoinClient.getBlockStats(block.id); |  | ||||||
|     const coinbaseRaw: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); |     const coinbaseRaw: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); | ||||||
|     blockExtended.extras.coinbaseRaw = coinbaseRaw.hex; |     blockExtended.extras.coinbaseRaw = coinbaseRaw.hex; | ||||||
|  | 
 | ||||||
|  |     if (block.height === 0) { | ||||||
|  |        blockExtended.extras.medianFee = 0; // 50th percentiles
 | ||||||
|  |        blockExtended.extras.feeRange = [0, 0, 0, 0, 0, 0, 0]; | ||||||
|  |        blockExtended.extras.totalFees = 0; | ||||||
|  |        blockExtended.extras.avgFee = 0; | ||||||
|  |        blockExtended.extras.avgFeeRate = 0; | ||||||
|  |      } else { | ||||||
|  |       const stats = await bitcoinClient.getBlockStats(block.id); | ||||||
|       blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles
 |       blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles
 | ||||||
|        blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); |        blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); | ||||||
|        blockExtended.extras.totalFees = stats.totalfee; |        blockExtended.extras.totalFees = stats.totalfee; | ||||||
|        blockExtended.extras.avgFee = stats.avgfee; |        blockExtended.extras.avgFee = stats.avgfee; | ||||||
|        blockExtended.extras.avgFeeRate = stats.avgfeerate; |        blockExtended.extras.avgFeeRate = stats.avgfeerate; | ||||||
|  |      } | ||||||
| 
 | 
 | ||||||
|     if (Common.indexingEnabled()) { |     if (Common.indexingEnabled()) { | ||||||
|       let pool: PoolTag; |       let pool: PoolTag; | ||||||
| @ -336,10 +345,13 @@ class Blocks { | |||||||
| 
 | 
 | ||||||
|     await blocksRepository.$saveBlockInDatabase(blockExtended); |     await blocksRepository.$saveBlockInDatabase(blockExtended); | ||||||
| 
 | 
 | ||||||
|     return blockExtended; |     return this.prepareBlock(blockExtended); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $getBlocksExtras(fromHeight: number): Promise<BlockExtended[]> { |   public async $getBlocksExtras(fromHeight: number, limit: number = 15): Promise<BlockExtended[]> { | ||||||
|  |     // Note - This API is breaking if indexing is not available. For now it is okay because we only
 | ||||||
|  |     // use it for the mining pages, and mining pages should not be available if indexing is turned off.
 | ||||||
|  |     // I'll need to fix it before we refactor the block(s) related pages
 | ||||||
|     try { |     try { | ||||||
|       loadingIndicators.setProgress('blocks', 0); |       loadingIndicators.setProgress('blocks', 0); | ||||||
| 
 | 
 | ||||||
| @ -360,10 +372,10 @@ class Blocks { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       let nextHash = startFromHash; |       let nextHash = startFromHash; | ||||||
|       for (let i = 0; i < 10 && currentHeight >= 0; i++) { |       for (let i = 0; i < limit && currentHeight >= 0; i++) { | ||||||
|         let block = this.getBlocks().find((b) => b.height === currentHeight); |         let block = this.getBlocks().find((b) => b.height === currentHeight); | ||||||
|         if (!block && Common.indexingEnabled()) { |         if (!block && Common.indexingEnabled()) { | ||||||
|           block = this.prepareBlock(await this.$indexBlock(currentHeight)); |           block = await this.$indexBlock(currentHeight); | ||||||
|         } else if (!block) { |         } else if (!block) { | ||||||
|           block = this.prepareBlock(await bitcoinApi.$getBlock(nextHash)); |           block = this.prepareBlock(await bitcoinApi.$getBlock(nextHash)); | ||||||
|         } |         } | ||||||
| @ -383,24 +395,25 @@ class Blocks { | |||||||
|   private prepareBlock(block: any): BlockExtended { |   private prepareBlock(block: any): BlockExtended { | ||||||
|     return <BlockExtended>{ |     return <BlockExtended>{ | ||||||
|       id: block.id ?? block.hash, // hash for indexed block
 |       id: block.id ?? block.hash, // hash for indexed block
 | ||||||
|       timestamp: block?.timestamp ?? block?.blockTimestamp, // blockTimestamp for indexed block
 |       timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block
 | ||||||
|       height: block?.height, |       height: block.height, | ||||||
|       version: block?.version, |       version: block.version, | ||||||
|       bits: block?.bits, |       bits: block.bits, | ||||||
|       nonce: block?.nonce, |       nonce: block.nonce, | ||||||
|       difficulty: block?.difficulty, |       difficulty: block.difficulty, | ||||||
|       merkle_root: block?.merkle_root, |       merkle_root: block.merkle_root, | ||||||
|       tx_count: block?.tx_count, |       tx_count: block.tx_count, | ||||||
|       size: block?.size, |       size: block.size, | ||||||
|       weight: block?.weight, |       weight: block.weight, | ||||||
|       previousblockhash: block?.previousblockhash, |       previousblockhash: block.previousblockhash, | ||||||
|       extras: { |       extras: { | ||||||
|         medianFee: block?.medianFee, |         medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee, | ||||||
|         feeRange: block?.feeRange ?? [], // TODO
 |         feeRange: block.feeRange ?? block.fee_range ?? block?.extras?.feeSpan, | ||||||
|         reward: block?.reward, |         reward: block.reward ?? block?.extras?.reward, | ||||||
|  |         totalFees: block.totalFees ?? block?.fees ?? block?.extras.totalFees, | ||||||
|         pool: block?.extras?.pool ?? (block?.pool_id ? { |         pool: block?.extras?.pool ?? (block?.pool_id ? { | ||||||
|           id: block?.pool_id, |           id: block.pool_id, | ||||||
|           name: block?.pool_name, |           name: block.pool_name, | ||||||
|         } : undefined), |         } : undefined), | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|  | |||||||
| @ -277,7 +277,10 @@ class BlocksRepository { | |||||||
|     const connection = await DB.pool.getConnection(); |     const connection = await DB.pool.getConnection(); | ||||||
|     try { |     try { | ||||||
|       const [rows]: any[] = await connection.query(` |       const [rows]: any[] = await connection.query(` | ||||||
|         SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.addresses as pool_addresses, pools.regexes as pool_regexes |         SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, | ||||||
|  |         pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, | ||||||
|  |         pools.addresses as pool_addresses, pools.regexes as pool_regexes, | ||||||
|  |         previous_block_hash as previousblockhash | ||||||
|         FROM blocks |         FROM blocks | ||||||
|         JOIN pools ON blocks.pool_id = pools.id |         JOIN pools ON blocks.pool_id = pools.id | ||||||
|         WHERE height = ${height}; |         WHERE height = ${height}; | ||||||
|  | |||||||
| @ -658,7 +658,7 @@ class Routes { | |||||||
| 
 | 
 | ||||||
|   public async getBlocksExtras(req: Request, res: Response) { |   public async getBlocksExtras(req: Request, res: Response) { | ||||||
|     try { |     try { | ||||||
|       res.json(await blocks.$getBlocksExtras(parseInt(req.params.height, 10))) |       res.json(await blocks.$getBlocksExtras(parseInt(req.params.height, 10), 15)); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       res.status(500).send(e instanceof Error ? e.message : e); |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ import { MiningDashboardComponent } from './components/mining-dashboard/mining-d | |||||||
| import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component'; | import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component'; | ||||||
| import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component'; | import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component'; | ||||||
| import { MiningStartComponent } from './components/mining-start/mining-start.component'; | import { MiningStartComponent } from './components/mining-start/mining-start.component'; | ||||||
|  | import { BlocksList } from './components/blocks-list/blocks-list.component'; | ||||||
| 
 | 
 | ||||||
| let routes: Routes = [ | let routes: Routes = [ | ||||||
|   { |   { | ||||||
| @ -75,6 +76,10 @@ let routes: Routes = [ | |||||||
|         path: 'mining', |         path: 'mining', | ||||||
|         component: MiningStartComponent, |         component: MiningStartComponent, | ||||||
|         children: [ |         children: [ | ||||||
|  |           { | ||||||
|  |             path: 'blocks', | ||||||
|  |             component: BlocksList, | ||||||
|  |           }, | ||||||
|           { |           { | ||||||
|             path: 'hashrate', |             path: 'hashrate', | ||||||
|             component: HashrateChartComponent, |             component: HashrateChartComponent, | ||||||
| @ -190,6 +195,10 @@ let routes: Routes = [ | |||||||
|             path: 'mining', |             path: 'mining', | ||||||
|             component: MiningStartComponent, |             component: MiningStartComponent, | ||||||
|             children: [ |             children: [ | ||||||
|  |               { | ||||||
|  |                 path: 'blocks', | ||||||
|  |                 component: BlocksList, | ||||||
|  |               }, | ||||||
|               { |               { | ||||||
|                 path: 'hashrate', |                 path: 'hashrate', | ||||||
|                 component: HashrateChartComponent, |                 component: HashrateChartComponent, | ||||||
| @ -299,6 +308,10 @@ let routes: Routes = [ | |||||||
|             path: 'mining', |             path: 'mining', | ||||||
|             component: MiningStartComponent, |             component: MiningStartComponent, | ||||||
|             children: [ |             children: [ | ||||||
|  |               { | ||||||
|  |                 path: 'blocks', | ||||||
|  |                 component: BlocksList, | ||||||
|  |               }, | ||||||
|               { |               { | ||||||
|                 path: 'hashrate', |                 path: 'hashrate', | ||||||
|                 component: HashrateChartComponent, |                 component: HashrateChartComponent, | ||||||
|  | |||||||
| @ -76,6 +76,7 @@ import { MiningStartComponent } from './components/mining-start/mining-start.com | |||||||
| import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe'; | import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe'; | ||||||
| import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; | import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; | ||||||
| import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components'; | import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components'; | ||||||
|  | import { BlocksList } from './components/blocks-list/blocks-list.component'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|   declarations: [ |   declarations: [ | ||||||
| @ -133,6 +134,7 @@ import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments- | |||||||
|     MiningStartComponent, |     MiningStartComponent, | ||||||
|     AmountShortenerPipe, |     AmountShortenerPipe, | ||||||
|     DifficultyAdjustmentsTable, |     DifficultyAdjustmentsTable, | ||||||
|  |     BlocksList, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule.withServerTransition({ appId: 'serverApp' }), |     BrowserModule.withServerTransition({ appId: 'serverApp' }), | ||||||
|  | |||||||
| @ -0,0 +1,96 @@ | |||||||
|  | <div class="container-xl" [class]="widget ? 'widget' : ''"> | ||||||
|  |   <h1 *ngIf="!widget" class="float-left" i18n="latest-blocks.blocks">Blocks</h1> | ||||||
|  | 
 | ||||||
|  |   <div class="clearfix"></div> | ||||||
|  | 
 | ||||||
|  |   <div style="min-height: 295px"> | ||||||
|  |     <table class="table table-borderless"> | ||||||
|  |       <thead> | ||||||
|  |         <th class="height" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</th> | ||||||
|  |         <th class="pool text-left" [class]="widget ? 'widget' : ''" i18n="latest-blocks.mined-by"> | ||||||
|  |           Pool</th> | ||||||
|  |         <th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget">Timestamp</th> | ||||||
|  |         <th class="mined" i18n="latest-blocks.mined" *ngIf="!widget">Mined</th> | ||||||
|  |         <th class="reward text-right" i18n="latest-blocks.reward" [class]="widget ? 'widget' : ''"> | ||||||
|  |           Reward</th> | ||||||
|  |         <th class="fees text-right" i18n="latest-blocks.fees" *ngIf="!widget">Fees</th> | ||||||
|  |         <th class="txs text-right" i18n="latest-blocks.transactions" [class]="widget ? 'widget' : ''">Txs</th> | ||||||
|  |         <th class="size" i18n="latest-blocks.size" *ngIf="!widget">Size</th> | ||||||
|  |       </thead> | ||||||
|  |       <tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> | ||||||
|  |         <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock"> | ||||||
|  |           <td class="height " [class]="widget ? 'widget' : ''"> | ||||||
|  |             <a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height | ||||||
|  |               }}</a> | ||||||
|  |           </td> | ||||||
|  |           <td class="pool text-left" [class]="widget ? 'widget' : ''"> | ||||||
|  |             <a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]"> | ||||||
|  |               <img width="25" height="25" src="{{ block.extras.pool['logo'] }}" | ||||||
|  |                 onError="this.src = './resources/mining-pools/default.svg'"> | ||||||
|  |               <span class="pool-name">{{ block.extras.pool.name }}</span> | ||||||
|  |             </a> | ||||||
|  |           </td> | ||||||
|  |           <td class="timestamp" *ngIf="!widget"> | ||||||
|  |             ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} | ||||||
|  |           </td> | ||||||
|  |           <td class="mined" *ngIf="!widget"> | ||||||
|  |             <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> | ||||||
|  |           </td> | ||||||
|  |           <td class="reward text-right" [class]="widget ? 'widget' : ''"> | ||||||
|  |             <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount> | ||||||
|  |           </td> | ||||||
|  |           <td class="fees text-right" *ngIf="!widget"> | ||||||
|  |             <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount> | ||||||
|  |           </td> | ||||||
|  |           <td class="txs text-right" [class]="widget ? 'widget' : ''"> | ||||||
|  |             {{ block.tx_count | number }} | ||||||
|  |           </td> | ||||||
|  |           <td class="size" *ngIf="!widget"> | ||||||
|  |             <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-text" [innerHTML]="block.size | bytes: 2"></div> | ||||||
|  |             </div> | ||||||
|  |           </td> | ||||||
|  |         </tr> | ||||||
|  |       </tbody> | ||||||
|  |       <ng-template #skeleton> | ||||||
|  |         <tbody> | ||||||
|  |           <tr *ngFor="let item of skeletonLines"> | ||||||
|  |             <td class="height" [class]="widget ? 'widget' : ''"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |             <td class="pool text-left" [class]="widget ? 'widget' : ''"> | ||||||
|  |               <img width="0" height="25" style="opacity: 0"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |             <td class="timestamp" *ngIf="!widget"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |             <td class="mined" *ngIf="!widget"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |             <td class="reward text-right" [class]="widget ? 'widget' : ''"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |             <td class="fees text-right" *ngIf="!widget"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |             <td class="txs text-right" [class]="widget ? 'widget' : ''"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |             <td class="size" *ngIf="!widget"> | ||||||
|  |               <span class="skeleton-loader"></span> | ||||||
|  |             </td> | ||||||
|  |           </tr> | ||||||
|  |         </tbody> | ||||||
|  |       </ng-template> | ||||||
|  |     </table> | ||||||
|  | 
 | ||||||
|  |     <ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''" | ||||||
|  |       [collectionSize]="blocksCount" [rotate]="true" [maxSize]="5" [pageSize]="15" [(page)]="page" | ||||||
|  |       (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false"> | ||||||
|  |     </ngb-pagination> | ||||||
|  |   </div> | ||||||
|  |    | ||||||
|  | </div> | ||||||
| @ -0,0 +1,124 @@ | |||||||
|  | .container-xl { | ||||||
|  |   max-width: 1400px; | ||||||
|  |   padding-bottom: 100px; | ||||||
|  | } | ||||||
|  | .container-xl.widget { | ||||||
|  |   padding-left: 0px; | ||||||
|  |   padding-bottom: 0px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .container { | ||||||
|  |   max-width: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | td { | ||||||
|  |   padding-top: 0.7rem !important; | ||||||
|  |   padding-bottom: 0.7rem !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .clear-link { | ||||||
|  |   color: white; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .disabled { | ||||||
|  |   pointer-events: none; | ||||||
|  |   opacity: 0.5; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .progress { | ||||||
|  |   background-color: #2d3348; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .pool { | ||||||
|  |   width: 17%; | ||||||
|  | } | ||||||
|  | .pool.widget { | ||||||
|  |   width: 40%; | ||||||
|  |   @media (max-width: 576px) { | ||||||
|  |     padding-left: 30px; | ||||||
|  |     width: 60%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .pool-name { | ||||||
|  |   display: inline-block; | ||||||
|  |   vertical-align: text-top; | ||||||
|  |   padding-left: 10px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .height { | ||||||
|  |   width: 10%; | ||||||
|  |   @media (max-width: 1100px) { | ||||||
|  |     width: 10%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .height.widget { | ||||||
|  |   width: 20%; | ||||||
|  |   @media (max-width: 576px) { | ||||||
|  |     width: 10%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .timestamp { | ||||||
|  |   @media (max-width: 900px) { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .mined { | ||||||
|  |   width: 13%; | ||||||
|  |   @media (max-width: 576px) { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .txs { | ||||||
|  |   padding-right: 40px; | ||||||
|  |   @media (max-width: 1100px) { | ||||||
|  |     padding-right: 10px; | ||||||
|  |   } | ||||||
|  |   @media (max-width: 875px) { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .txs.widget { | ||||||
|  |   padding-right: 0; | ||||||
|  |   @media (max-width: 650px) { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fees { | ||||||
|  |   @media (max-width: 650px) { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .fees.widget { | ||||||
|  |   width: 20%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .reward { | ||||||
|  |   @media (max-width: 576px) { | ||||||
|  |     width: 7%; | ||||||
|  |     padding-right: 30px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .reward.widget { | ||||||
|  |   width: 20%; | ||||||
|  |   @media (max-width: 576px) { | ||||||
|  |     width: 30%; | ||||||
|  |     padding-right: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .size { | ||||||
|  |   width: 12%; | ||||||
|  |   @media (max-width: 1000px) { | ||||||
|  |     width: 15%; | ||||||
|  |   } | ||||||
|  |   @media (max-width: 650px) { | ||||||
|  |     width: 20%; | ||||||
|  |   } | ||||||
|  |   @media (max-width: 450px) { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,98 @@ | |||||||
|  | import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; | ||||||
|  | import { BehaviorSubject, combineLatest, Observable, timer } from 'rxjs'; | ||||||
|  | import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators'; | ||||||
|  | import { BlockExtended } from 'src/app/interfaces/node-api.interface'; | ||||||
|  | import { ApiService } from 'src/app/services/api.service'; | ||||||
|  | import { StateService } from 'src/app/services/state.service'; | ||||||
|  | import { WebsocketService } from 'src/app/services/websocket.service'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-blocks-list', | ||||||
|  |   templateUrl: './blocks-list.component.html', | ||||||
|  |   styleUrls: ['./blocks-list.component.scss'], | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
|  | }) | ||||||
|  | export class BlocksList implements OnInit { | ||||||
|  |   @Input() widget: boolean = false; | ||||||
|  | 
 | ||||||
|  |   blocks$: Observable<any[]> = undefined; | ||||||
|  | 
 | ||||||
|  |   isLoading = true; | ||||||
|  |   fromBlockHeight = undefined; | ||||||
|  |   paginationMaxSize: number; | ||||||
|  |   page = 1; | ||||||
|  |   lastPage = 1; | ||||||
|  |   blocksCount: number; | ||||||
|  |   fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromBlockHeight); | ||||||
|  |   skeletonLines: number[] = []; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private apiService: ApiService, | ||||||
|  |     private websocketService: WebsocketService, | ||||||
|  |     public stateService: StateService, | ||||||
|  |   ) { | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     this.websocketService.want(['blocks']); | ||||||
|  | 
 | ||||||
|  |     this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()]; | ||||||
|  |     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; | ||||||
|  | 
 | ||||||
|  |     this.blocks$ = combineLatest([ | ||||||
|  |       this.fromHeightSubject.pipe( | ||||||
|  |         switchMap((fromBlockHeight) => { | ||||||
|  |           this.isLoading = true; | ||||||
|  |           return this.apiService.getBlocks$(this.page === 1 ? undefined : fromBlockHeight) | ||||||
|  |             .pipe( | ||||||
|  |               tap(blocks => { | ||||||
|  |                 if (this.blocksCount === undefined) { | ||||||
|  |                   this.blocksCount = blocks[0].height; | ||||||
|  |                 } | ||||||
|  |                 this.isLoading = false; | ||||||
|  |               }), | ||||||
|  |               map(blocks => { | ||||||
|  |                 for (const block of blocks) { | ||||||
|  |                   // @ts-ignore: Need to add an extra field for the template
 | ||||||
|  |                   block.extras.pool.logo = `./resources/mining-pools/` + | ||||||
|  |                     block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||||
|  |                 } | ||||||
|  |                 if (this.widget) { | ||||||
|  |                   return blocks.slice(0, 5); | ||||||
|  |                 } | ||||||
|  |                 return blocks; | ||||||
|  |               }), | ||||||
|  |               retryWhen(errors => errors.pipe(delayWhen(() => timer(1000)))) | ||||||
|  |             ) | ||||||
|  |           }) | ||||||
|  |       ), | ||||||
|  |       this.stateService.blocks$ | ||||||
|  |         .pipe( | ||||||
|  |           skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1), | ||||||
|  |         ), | ||||||
|  |     ]) | ||||||
|  |       .pipe( | ||||||
|  |         scan((acc, blocks) => { | ||||||
|  |           if (this.page > 1 || acc.length === 0 || (this.page === 1 && this.lastPage !== 1)) { | ||||||
|  |             this.lastPage = this.page; | ||||||
|  |             return blocks[0]; | ||||||
|  |           } | ||||||
|  |           this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height); | ||||||
|  |           // @ts-ignore: Need to add an extra field for the template
 | ||||||
|  |           blocks[1][0].extras.pool.logo = `./resources/mining-pools/` + | ||||||
|  |             blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||||
|  |           acc.unshift(blocks[1][0]); | ||||||
|  |           acc = acc.slice(0, this.widget ? 5 : 15); | ||||||
|  |           return acc; | ||||||
|  |         }, []) | ||||||
|  |       ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   pageChange(page: number) { | ||||||
|  |     this.fromHeightSubject.next(this.blocksCount - (page - 1) * 15); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   trackByBlock(index: number, block: BlockExtended) { | ||||||
|  |     return block.height; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -95,7 +95,7 @@ | |||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <!-- pool dominance --> |     <!-- pool dominance --> | ||||||
|     <div class="col"> |     <!-- <div class="col"> | ||||||
|       <div class="card" style="height: 385px"> |       <div class="card" style="height: 385px"> | ||||||
|         <div class="card-body"> |         <div class="card-body"> | ||||||
|           <h5 class="card-title"> |           <h5 class="card-title"> | ||||||
| @ -106,6 +106,20 @@ | |||||||
|               more »</a></div> |               more »</a></div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |     </div> --> | ||||||
|  | 
 | ||||||
|  |     <!-- Latest blocks --> | ||||||
|  |     <div class="col"> | ||||||
|  |       <div class="card" style="height: 385px"> | ||||||
|  |         <div class="card-body"> | ||||||
|  |           <h5 class="card-title"> | ||||||
|  |             Latest blocks | ||||||
|  |           </h5> | ||||||
|  |           <app-blocks-list [widget]=true></app-blocks-list> | ||||||
|  |           <div><a [routerLink]="['/mining/blocks' | relativeUrl]" i18n="dashboard.view-more">View | ||||||
|  |               more »</a></div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <div class="col"> |     <div class="col"> | ||||||
| @ -115,7 +129,7 @@ | |||||||
|             Adjustments |             Adjustments | ||||||
|           </h5> |           </h5> | ||||||
|           <app-difficulty-adjustments-table></app-difficulty-adjustments-table> |           <app-difficulty-adjustments-table></app-difficulty-adjustments-table> | ||||||
|           <div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more |           <div><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more | ||||||
|               »</a></div> |               »</a></div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  | |||||||
| @ -151,6 +151,13 @@ export class ApiService { | |||||||
|       ); |       ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   getBlocks$(from: number): Observable<BlockExtended[]> { | ||||||
|  |     return this.httpClient.get<BlockExtended[]>( | ||||||
|  |       this.apiBasePath + this.apiBasePath + `/api/v1/blocks-extras` + | ||||||
|  |       (from !== undefined ? `/${from}` : ``) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getHistoricalDifficulty$(interval: string | undefined): Observable<any> { |   getHistoricalDifficulty$(interval: string | undefined): Observable<any> { | ||||||
|     return this.httpClient.get<any[]>( |     return this.httpClient.get<any[]>( | ||||||
|         this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` + |         this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` + | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user