Add visualization to mined blocks
This commit is contained in:
		
							parent
							
								
									225decd286
								
							
						
					
					
						commit
						7f4c6352ba
					
				| @ -15,6 +15,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit { | ||||
|   @Input() isLoading: boolean; | ||||
|   @Input() resolution: number; | ||||
|   @Input() blockLimit: number; | ||||
|   @Input() orientation = 'left'; | ||||
|   @Input() flip = true; | ||||
|   @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); | ||||
| 
 | ||||
|   @ViewChild('blockCanvas') | ||||
| @ -67,9 +69,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   replace(transactions: TransactionStripped[], direction: string): void { | ||||
|   replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void { | ||||
|     if (this.scene) { | ||||
|       this.scene.replace(transactions, direction); | ||||
|       this.scene.replace(transactions || [], direction, sort); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -139,8 +141,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit { | ||||
|     if (this.scene) { | ||||
|       this.scene.resize({ width: this.displayWidth, height: this.displayHeight }); | ||||
|     } else { | ||||
|       this.scene = this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, | ||||
|         blockLimit: this.blockLimit, vertexArray: this.vertexArray }); | ||||
|       this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, | ||||
|         blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray }); | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -7,6 +7,8 @@ export default class BlockScene { | ||||
|   scene: { count: number, offset: { x: number, y: number}}; | ||||
|   vertexArray: FastVertexArray; | ||||
|   txs: { [key: string]: TxView }; | ||||
|   orientation: string; | ||||
|   flip: boolean; | ||||
|   width: number; | ||||
|   height: number; | ||||
|   gridWidth: number; | ||||
| @ -19,10 +21,11 @@ export default class BlockScene { | ||||
|   layout: BlockLayout; | ||||
|   dirty: boolean; | ||||
| 
 | ||||
|   constructor({ width, height, resolution, blockLimit, vertexArray }: | ||||
|       { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray } | ||||
|   constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray }: | ||||
|       { width: number, height: number, resolution: number, blockLimit: number, | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray } | ||||
|   ) { | ||||
|     this.init({ width, height, resolution, blockLimit, vertexArray }); | ||||
|     this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }); | ||||
|   } | ||||
| 
 | ||||
|   destroy(): void { | ||||
| @ -61,7 +64,7 @@ export default class BlockScene { | ||||
|   } | ||||
| 
 | ||||
|   // Reset layout and replace with new set of transactions
 | ||||
|   replace(txs: TransactionStripped[], direction: string = 'left'): void { | ||||
|   replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void { | ||||
|     const startTime = performance.now(); | ||||
|     const nextIds = {}; | ||||
|     const remove = []; | ||||
| @ -90,9 +93,15 @@ export default class BlockScene { | ||||
| 
 | ||||
|     this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); | ||||
| 
 | ||||
|     Object.values(this.txs).sort(feeRateDescending).forEach(tx => { | ||||
|       this.place(tx); | ||||
|     }); | ||||
|     if (sort) { | ||||
|       Object.values(this.txs).sort(feeRateDescending).forEach(tx => { | ||||
|         this.place(tx); | ||||
|       }); | ||||
|     } else { | ||||
|       txs.forEach(tx => { | ||||
|         this.place(this.txs[tx.txid]); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.updateAll(startTime, direction); | ||||
|   } | ||||
| @ -143,9 +152,12 @@ export default class BlockScene { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private init({ width, height, resolution, blockLimit, vertexArray }: | ||||
|       { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray } | ||||
|   private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }: | ||||
|       { width: number, height: number, resolution: number, blockLimit: number, | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray } | ||||
|   ): void { | ||||
|     this.orientation = orientation; | ||||
|     this.flip = flip; | ||||
|     this.vertexArray = vertexArray; | ||||
| 
 | ||||
|     this.scene = { | ||||
| @ -188,8 +200,8 @@ export default class BlockScene { | ||||
|       tx.update({ | ||||
|         display: { | ||||
|           position: { | ||||
|             x: tx.screenPosition.x + (direction === 'right' ? -this.width : this.width) * 1.4, | ||||
|             y: tx.screenPosition.y, | ||||
|             x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4, | ||||
|             y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4, | ||||
|             s: tx.screenPosition.s | ||||
|           }, | ||||
|           color: txColor, | ||||
| @ -237,8 +249,8 @@ export default class BlockScene { | ||||
|       tx.update({ | ||||
|         display: { | ||||
|           position: { | ||||
|             x: tx.screenPosition.x + (direction === 'right' ? this.width : -this.width) * 1.4, | ||||
|             y: this.txs[id].screenPosition.y, | ||||
|             x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4, | ||||
|             y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4, | ||||
|           } | ||||
|         }, | ||||
|         duration: 1000, | ||||
| @ -264,18 +276,42 @@ export default class BlockScene { | ||||
|       const slotSize = (position.s * this.gridSize); | ||||
|       const squareSize = slotSize - (this.unitPadding * 2); | ||||
| 
 | ||||
|       // The grid is laid out notionally left-to-right, bottom-to-top
 | ||||
|       // So we rotate 90deg counterclockwise then flip the y axis
 | ||||
|       // The grid is laid out notionally left-to-right, bottom-to-top,
 | ||||
|       // so we rotate and/or flip the y axis to match the target configuration.
 | ||||
|       //
 | ||||
|       // e.g. for flip = true, orientation = 'left':
 | ||||
|       //
 | ||||
|       //    grid                             screen
 | ||||
|       //  ________          ________        ________
 | ||||
|       // |        |        |       b|      |       a|
 | ||||
|       // |        | rotate |        | flip |     c  |
 | ||||
|       // |  c     |   -->  |      c |  --> |        |
 | ||||
|       // |a______b|        |_______a|      |_______b|
 | ||||
|       //  ________        ________          ________
 | ||||
|       // |        |      |        |        |       a|
 | ||||
|       // |        | flip |        | rotate |     c  |
 | ||||
|       // |  c     |  --> |     c  |  -->   |        |
 | ||||
|       // |a______b|      |b______a|        |_______b|
 | ||||
| 
 | ||||
|       let x = (this.gridSize * position.x) + (slotSize / 2); | ||||
|       let y = (this.gridSize * position.y) + (slotSize / 2); | ||||
|       let t; | ||||
|       if (this.flip) { | ||||
|         x = this.width - x; | ||||
|       } | ||||
|       switch (this.orientation) { | ||||
|         case 'left': | ||||
|           t = x; | ||||
|           x = this.width - y; | ||||
|           y = t; | ||||
|           break; | ||||
|         case 'right': | ||||
|           t = x; | ||||
|           x = y; | ||||
|           y = t; | ||||
|           break; | ||||
|         case 'bottom': | ||||
|           y = this.height - y; | ||||
|           break; | ||||
|       } | ||||
|       return { | ||||
|         x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize, | ||||
|         y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)), | ||||
|         x: x + this.unitPadding - (slotSize / 2), | ||||
|         y: y + this.unitPadding - (slotSize / 2), | ||||
|         s: squareSize | ||||
|       }; | ||||
|     } else { | ||||
| @ -284,11 +320,32 @@ export default class BlockScene { | ||||
|   } | ||||
| 
 | ||||
|   screenToGrid(position: Position): Position { | ||||
|     const grid = { | ||||
|       x: Math.floor((position.y - this.unitPadding) / this.gridSize), | ||||
|       y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize) | ||||
|     let x = position.x; | ||||
|     let y = this.height - position.y; | ||||
|     let t; | ||||
| 
 | ||||
|     switch (this.orientation) { | ||||
|       case 'left': | ||||
|         t = x; | ||||
|         x = y; | ||||
|         y = this.width - t; | ||||
|         break; | ||||
|       case 'right': | ||||
|         t = x; | ||||
|         x = y; | ||||
|         y = t; | ||||
|         break; | ||||
|       case 'bottom': | ||||
|         y = this.height - y; | ||||
|         break; | ||||
|     } | ||||
|     if (this.flip) { | ||||
|       x = this.width - x; | ||||
|     } | ||||
|     return { | ||||
|       x: Math.floor(x / this.gridSize), | ||||
|       y: Math.floor(y / this.gridSize) | ||||
|     }; | ||||
|     return grid; | ||||
|   } | ||||
| 
 | ||||
|   // calculates and returns the size of the tx in multiples of the grid size
 | ||||
|  | ||||
| @ -40,10 +40,11 @@ | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
| 
 | ||||
|   <ng-template [ngIf]="!isLoadingBlock && !error"> | ||||
| 
 | ||||
|     <div class="box"> | ||||
|       <div class="row"> | ||||
| 
 | ||||
|   <div class="box" *ngIf="!error"> | ||||
|     <div class="row"> | ||||
|       <ng-template [ngIf]="!isLoadingBlock"> | ||||
|         <div class="col-sm"> | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
| @ -68,73 +69,191 @@ | ||||
|                 <td i18n="block.weight">Weight</td> | ||||
|                 <td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td> | ||||
|               </tr> | ||||
|               <ng-template [ngIf]="webGlEnabled"> | ||||
|                 <tr *ngIf="block?.extras?.medianFee != undefined"> | ||||
|                   <td class="td-width" i18n="block.median-fee">Median fee</td> | ||||
|                   <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td> | ||||
|                 </tr> | ||||
|                 <ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees"> | ||||
|                   <tr> | ||||
|                     <td i18n="block.total-fees|Total fees in a block">Total fees</td> | ||||
|                     <td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees"> | ||||
|                       <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|                       <span class="fiat"> | ||||
|                         <app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat> | ||||
|                       </span> | ||||
|                     </td> | ||||
|                     <ng-template #liquidTotalFees> | ||||
|                       <td> | ||||
|                         <app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>  <app-fiat | ||||
|                           [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat> | ||||
|                       </td> | ||||
|                     </ng-template> | ||||
|                   </tr> | ||||
|                   <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                     <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td> | ||||
|                     <td> | ||||
|                       <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|                       <span class="fiat"> | ||||
|                         <app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat> | ||||
|                       </span> | ||||
|                     </td> | ||||
|                   </tr> | ||||
|                 </ng-template> | ||||
|                 <ng-template #loadingFees> | ||||
|                   <tr> | ||||
|                     <td i18n="block.total-fees|Total fees in a block">Total fees</td> | ||||
|                     <td style="width: 75%;"><span class="skeleton-loader"></span></td> | ||||
|                   </tr> | ||||
|                   <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                     <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td> | ||||
|                     <td><span class="skeleton-loader"></span></td> | ||||
|                   </tr> | ||||
|                 </ng-template> | ||||
|                 <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                   <td i18n="block.miner">Miner</td> | ||||
|                   <td *ngIf="stateService.env.MINING_DASHBOARD"> | ||||
|                     <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" | ||||
|                       [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> | ||||
|                       {{ block.extras.pool.name }} | ||||
|                     </a> | ||||
|                   </td> | ||||
|                   <td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'"> | ||||
|                     <span placement="bottom" class="badge" | ||||
|                       [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> | ||||
|                       {{ block.extras.pool.name }} | ||||
|                   </span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-template> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </ng-template> | ||||
|       <ng-template [ngIf]="isLoadingBlock"> | ||||
|         <div class="col-sm"> | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
|               <tr *ngIf="block?.extras?.medianFee != undefined"> | ||||
|                 <td class="td-width" i18n="block.median-fee">Median fee</td> | ||||
|                 <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td> | ||||
|               <tr> | ||||
|                 <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees"> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <ng-template [ngIf]="webGlEnabled"> | ||||
|                 <tr> | ||||
|                   <td i18n="block.total-fees|Total fees in a block">Total fees</td> | ||||
|                   <td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees"> | ||||
|                     <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|                     <span class="fiat"> | ||||
|                       <app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat> | ||||
|                     </span> | ||||
|                   </td> | ||||
|                   <ng-template #liquidTotalFees> | ||||
|                     <td> | ||||
|                       <app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>  <app-fiat | ||||
|                         [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat> | ||||
|                     </td> | ||||
|                   </ng-template> | ||||
|                   <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> | ||||
|                 </tr> | ||||
|                 <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                   <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td> | ||||
|                   <td> | ||||
|                     <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|                     <span class="fiat"> | ||||
|                       <app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat> | ||||
|                     </span> | ||||
|                   </td> | ||||
|                 <tr> | ||||
|                   <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                   <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                   <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|                 </tr> | ||||
|               </ng-template> | ||||
|               <ng-template #loadingFees> | ||||
|                 <tr> | ||||
|                   <td i18n="block.total-fees|Total fees in a block">Total fees</td> | ||||
|                   <td style="width: 75%;"><span class="skeleton-loader"></span></td> | ||||
|                 </tr> | ||||
|                 <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                   <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td> | ||||
|                   <td><span class="skeleton-loader"></span></td> | ||||
|                 </tr> | ||||
|               </ng-template> | ||||
|               <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                 <td i18n="block.miner">Miner</td> | ||||
|                 <td *ngIf="stateService.env.MINING_DASHBOARD"> | ||||
|                   <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" | ||||
|                     [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> | ||||
|                     {{ block.extras.pool.name }} | ||||
|                   </a> | ||||
|                 </td> | ||||
|                 <td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'"> | ||||
|                   <span placement="bottom" class="badge" | ||||
|                     [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> | ||||
|                     {{ block.extras.pool.name }} | ||||
|                 </span> | ||||
|                 </td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </ng-template> | ||||
|       <div class="col-sm" *ngIf="!webGlEnabled"> | ||||
|         <table class="table table-borderless table-striped" *ngIf="!isLoadingBlock"> | ||||
|           <tbody> | ||||
|             <tr *ngIf="block?.extras?.medianFee != undefined"> | ||||
|               <td class="td-width" i18n="block.median-fee">Median fee</td> | ||||
|               <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td> | ||||
|             </tr> | ||||
|             <ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees"> | ||||
|               <tr> | ||||
|                 <td i18n="block.total-fees|Total fees in a block">Total fees</td> | ||||
|                 <td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees"> | ||||
|                   <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|                   <span class="fiat"> | ||||
|                     <app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat> | ||||
|                   </span> | ||||
|                 </td> | ||||
|                 <ng-template #liquidTotalFees> | ||||
|                   <td> | ||||
|                     <app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>  <app-fiat | ||||
|                       [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat> | ||||
|                   </td> | ||||
|                 </ng-template> | ||||
|               </tr> | ||||
|               <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                 <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td> | ||||
|                 <td> | ||||
|                   <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|                   <span class="fiat"> | ||||
|                     <app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat> | ||||
|                   </span> | ||||
|                 </td> | ||||
|               </tr> | ||||
|             </ng-template> | ||||
|             <ng-template #loadingFees> | ||||
|               <tr> | ||||
|                 <td i18n="block.total-fees|Total fees in a block">Total fees</td> | ||||
|                 <td style="width: 75%;"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|                 <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td> | ||||
|                 <td><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|             </ng-template> | ||||
|             <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> | ||||
|               <td i18n="block.miner">Miner</td> | ||||
|               <td *ngIf="stateService.env.MINING_DASHBOARD"> | ||||
|                 <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" | ||||
|                   [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> | ||||
|                   {{ block.extras.pool.name }} | ||||
|                 </a> | ||||
|               </td> | ||||
|               <td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'"> | ||||
|                 <span placement="bottom" class="badge" | ||||
|                   [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> | ||||
|                   {{ block.extras.pool.name }} | ||||
|               </span> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|         <table class="table table-borderless table-striped" *ngIf="isLoadingBlock"> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|       <div class="col-sm chart-container" *ngIf="webGlEnabled"> | ||||
|         <app-block-overview-graph | ||||
|           #blockGraph | ||||
|           [isLoading]="isLoadingOverview" | ||||
|           [resolution]="75" | ||||
|           [blockLimit]="stateService.blockVSize" | ||||
|           [orientation]="'top'" | ||||
|           [flip]="false" | ||||
|         ></app-block-overview-graph> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|   </div> | ||||
|   <ng-template [ngIf]="!isLoadingBlock && !error"> | ||||
|     <div [hidden]="!showDetails" id="details"> | ||||
|       <br> | ||||
| 
 | ||||
| @ -223,63 +342,17 @@ | ||||
|           <div class="row"> | ||||
|             <div class="col-sm"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </div> | ||||
|             <div class="col-sm"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </ng-template> | ||||
|     <ngb-pagination class="pagination-container float-right" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination> | ||||
| 
 | ||||
|   </ng-template> | ||||
| 
 | ||||
|   <ng-template [ngIf]="isLoadingBlock && !error"> | ||||
| 
 | ||||
|     <div class="box"> | ||||
|       <div class="row"> | ||||
|         <div class="col-sm"> | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|         <div class="col-sm"> | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td colspan="2"><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </ng-template> | ||||
| 
 | ||||
|   <ng-template [ngIf]="error"> | ||||
|     <div class="text-center"> | ||||
|       <span i18n="error.general-loading-data">Error loading data.</span> | ||||
|  | ||||
| @ -148,3 +148,10 @@ h1 { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart-container{ | ||||
|   margin: 20px auto; | ||||
|   @media (min-width: 768px) { | ||||
|     margin: auto; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -2,15 +2,16 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co | ||||
| import { Location } from '@angular/common'; | ||||
| import { ActivatedRoute, ParamMap, Router } from '@angular/router'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| import { switchMap, tap, debounceTime, catchError, map } from 'rxjs/operators'; | ||||
| import { switchMap, tap, debounceTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators'; | ||||
| import { Transaction, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { Observable, of, Subscription } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { WebsocketService } from 'src/app/services/websocket.service'; | ||||
| import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; | ||||
| import { BlockExtended } from 'src/app/interfaces/node-api.interface'; | ||||
| import { BlockExtended, TransactionStripped } from 'src/app/interfaces/node-api.interface'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-block', | ||||
| @ -21,6 +22,7 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|   network = ''; | ||||
|   block: BlockExtended; | ||||
|   blockHeight: number; | ||||
|   lastBlockHeight: number; | ||||
|   nextBlockHeight: number; | ||||
|   blockHash: string; | ||||
|   isLoadingBlock = true; | ||||
| @ -28,6 +30,10 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|   latestBlocks: BlockExtended[] = []; | ||||
|   transactions: Transaction[]; | ||||
|   isLoadingTransactions = true; | ||||
|   strippedTransactions: TransactionStripped[]; | ||||
|   overviewTransitionDirection: string; | ||||
|   isLoadingOverview = true; | ||||
|   isAwaitingOverview = true; | ||||
|   error: any; | ||||
|   blockSubsidy: number; | ||||
|   fees: number; | ||||
| @ -39,13 +45,18 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|   showPreviousBlocklink = true; | ||||
|   showNextBlocklink = true; | ||||
|   transactionsError: any = null; | ||||
|   overviewError: any = null; | ||||
|   webGlEnabled = true; | ||||
| 
 | ||||
|   subscription: Subscription; | ||||
|   transactionSubscription: Subscription; | ||||
|   overviewSubscription: Subscription; | ||||
|   keyNavigationSubscription: Subscription; | ||||
|   blocksSubscription: Subscription; | ||||
|   networkChangedSubscription: Subscription; | ||||
|   queryParamsSubscription: Subscription; | ||||
| 
 | ||||
|   @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; | ||||
| 
 | ||||
|   constructor( | ||||
|     private route: ActivatedRoute, | ||||
|     private location: Location, | ||||
| @ -56,7 +67,9 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|     private websocketService: WebsocketService, | ||||
|     private relativeUrlPipe: RelativeUrlPipe, | ||||
|     private apiService: ApiService | ||||
|   ) { } | ||||
|   ) { | ||||
|     this.webGlEnabled = detectWebGL(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.websocketService.want(['blocks', 'mempool-blocks']); | ||||
| @ -85,7 +98,7 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|     this.subscription = this.route.paramMap.pipe( | ||||
|     const block$ = this.route.paramMap.pipe( | ||||
|       switchMap((params: ParamMap) => { | ||||
|         const blockHash: string = params.get('id') || ''; | ||||
|         this.block = undefined; | ||||
| @ -141,6 +154,8 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|       tap((block: BlockExtended) => { | ||||
|         this.block = block; | ||||
|         this.blockHeight = block.height; | ||||
|         const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left'; | ||||
|         this.lastBlockHeight = this.blockHeight; | ||||
|         this.nextBlockHeight = block.height + 1; | ||||
|         this.setNextAndPreviousBlockLink(); | ||||
| 
 | ||||
| @ -154,8 +169,17 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|         this.isLoadingTransactions = true; | ||||
|         this.transactions = null; | ||||
|         this.transactionsError = null; | ||||
|         this.isLoadingOverview = true; | ||||
|         this.isAwaitingOverview = true; | ||||
|         this.overviewError = true; | ||||
|         if (this.blockGraph) { | ||||
|           this.blockGraph.exit(direction); | ||||
|         } | ||||
|       }), | ||||
|       debounceTime(300), | ||||
|       shareReplay(1) | ||||
|     ); | ||||
|     this.transactionSubscription = block$.pipe( | ||||
|       switchMap((block) => this.electrsApiService.getBlockTransactions$(block.id) | ||||
|         .pipe( | ||||
|           catchError((err) => { | ||||
| @ -170,10 +194,51 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|       } | ||||
|       this.transactions = transactions; | ||||
|       this.isLoadingTransactions = false; | ||||
| 
 | ||||
|       if (!this.isAwaitingOverview && this.blockGraph && this.strippedTransactions && this.overviewTransitionDirection) { | ||||
|         this.isLoadingOverview = false; | ||||
|         this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false); | ||||
|       } | ||||
|     }, | ||||
|     (error) => { | ||||
|       this.error = error; | ||||
|       this.isLoadingBlock = false; | ||||
|       this.isLoadingOverview = false; | ||||
|     }); | ||||
| 
 | ||||
|     this.overviewSubscription = block$.pipe( | ||||
|       startWith(null), | ||||
|       pairwise(), | ||||
|       switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id) | ||||
|         .pipe( | ||||
|           catchError((err) => { | ||||
|             this.overviewError = err; | ||||
|             return of([]); | ||||
|           }), | ||||
|           switchMap((transactions) => { | ||||
|             console.log('overview loaded: ', prevBlock && prevBlock.height, block.height); | ||||
|             if (prevBlock) { | ||||
|               return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' }); | ||||
|             } else { | ||||
|               return of({ transactions, direction: 'down' }); | ||||
|             } | ||||
|           }) | ||||
|         ) | ||||
|       ), | ||||
|     ) | ||||
|     .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => { | ||||
|       this.isAwaitingOverview = false; | ||||
|       this.strippedTransactions = transactions; | ||||
|       this.overviewTransitionDirection = direction; | ||||
|       if (!this.isLoadingTransactions && this.blockGraph) { | ||||
|         this.isLoadingOverview = false; | ||||
|         this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false); | ||||
|       } | ||||
|     }, | ||||
|     (error) => { | ||||
|       this.error = error; | ||||
|       this.isLoadingOverview = false; | ||||
|       this.isAwaitingOverview = false; | ||||
|     }); | ||||
| 
 | ||||
|     this.networkChangedSubscription = this.stateService.networkChanged$ | ||||
| @ -203,7 +268,8 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.stateService.markBlock$.next({}); | ||||
|     this.subscription.unsubscribe(); | ||||
|     this.transactionSubscription.unsubscribe(); | ||||
|     this.overviewSubscription.unsubscribe(); | ||||
|     this.keyNavigationSubscription.unsubscribe(); | ||||
|     this.blocksSubscription.unsubscribe(); | ||||
|     this.networkChangedSubscription.unsubscribe(); | ||||
| @ -303,3 +369,9 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function detectWebGL() { | ||||
|   const canvas = document.createElement('canvas'); | ||||
|   const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); | ||||
|   return (gl && gl instanceof WebGLRenderingContext); | ||||
| } | ||||
|  | ||||
| @ -3,5 +3,7 @@ | ||||
|   [isLoading]="isLoading$ | async" | ||||
|   [resolution]="75" | ||||
|   [blockLimit]="stateService.blockVSize" | ||||
|   (txPreviewEvent)="onTxPreview($event)"> | ||||
| </app-block-overview-graph> | ||||
|   [orientation]="'left'" | ||||
|   [flip]="true" | ||||
|   (txPreviewEvent)="onTxPreview($event)" | ||||
| ></app-block-overview-graph> | ||||
|  | ||||
| @ -128,6 +128,13 @@ export interface BlockExtended extends Block { | ||||
|   extras?: BlockExtension; | ||||
| } | ||||
| 
 | ||||
| export interface TransactionStripped { | ||||
|   txid: string; | ||||
|   fee: number; | ||||
|   vsize: number; | ||||
|   value: number; | ||||
| } | ||||
| 
 | ||||
| export interface RewardStats { | ||||
|   startBlock: number; | ||||
|   endBlock: number; | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { HttpClient, HttpParams } from '@angular/common/http'; | ||||
| import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended, RewardStats } from '../interfaces/node-api.interface'; | ||||
| import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, | ||||
|          PoolsStats, PoolStat, BlockExtended, TransactionStripped, RewardStats } from '../interfaces/node-api.interface'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { StateService } from './state.service'; | ||||
| import { WebsocketResponse } from '../interfaces/websocket.interface'; | ||||
| @ -158,6 +159,10 @@ export class ApiService { | ||||
|     return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash); | ||||
|   } | ||||
| 
 | ||||
|   getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> { | ||||
|     return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary'); | ||||
|   } | ||||
| 
 | ||||
|   getHistoricalHashrate$(interval: string | undefined): Observable<any> { | ||||
|     return this.httpClient.get<any[]>( | ||||
|         this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` + | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user