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 });
 | 
			
		||||
 | 
			
		||||
    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  |
 | 
			
		||||
      // |        |      |        |        |       a|
 | 
			
		||||
      // |        | flip |        | rotate |     c  |
 | 
			
		||||
      // |  c     |  --> |     c  |  -->   |        |
 | 
			
		||||
      // |a______b|        |_______a|      |_______b|
 | 
			
		||||
      // |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="box" *ngIf="!error">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <ng-template [ngIf]="!isLoadingBlock">
 | 
			
		||||
        <div class="col-sm">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
@ -68,11 +69,103 @@
 | 
			
		||||
                <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>
 | 
			
		||||
                <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>
 | 
			
		||||
              <ng-template [ngIf]="webGlEnabled">
 | 
			
		||||
                <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>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </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>
 | 
			
		||||
@ -131,10 +224,36 @@
 | 
			
		||||
            </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