Compare commits
	
		
			9 Commits
		
	
	
		
			master
			...
			mononaut/m
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6efb435033 | ||
| 
						 | 
					7b3d2b983c | ||
| 
						 | 
					beb128b5e0 | ||
| 
						 | 
					2dee0b8ba6 | ||
| 
						 | 
					b14a354a06 | ||
| 
						 | 
					7b04ff72cd | ||
| 
						 | 
					62c8766e24 | ||
| 
						 | 
					3a49e528fa | ||
| 
						 | 
					a33de8bc8c | 
@ -3,7 +3,8 @@ import * as WebSocket from 'ws';
 | 
			
		||||
import {
 | 
			
		||||
  BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
 | 
			
		||||
  OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
 | 
			
		||||
  MempoolDelta, MempoolDeltaTxids
 | 
			
		||||
  MempoolDelta, MempoolDeltaTxids,
 | 
			
		||||
  TransactionCompressed
 | 
			
		||||
} from '../mempool.interfaces';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
@ -315,6 +316,7 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
 | 
			
		||||
            if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
 | 
			
		||||
              client['track-mempool-blocks'] = undefined;
 | 
			
		||||
              const index = parsedMessage['track-mempool-block'];
 | 
			
		||||
              client['track-mempool-block'] = index;
 | 
			
		||||
              const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
 | 
			
		||||
@ -324,7 +326,31 @@ class WebsocketHandler {
 | 
			
		||||
                blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-mempool-block'] = null;
 | 
			
		||||
              client['track-mempool-block'] = undefined;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-mempool-blocks'] !== undefined) {
 | 
			
		||||
            if (parsedMessage['track-mempool-blocks'].length > 0) {
 | 
			
		||||
              client['track-mempool-block'] = undefined;
 | 
			
		||||
              const indices: number[] = [];
 | 
			
		||||
              const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
 | 
			
		||||
              const updates: { index: number, sequence: number, blockTransactions: TransactionCompressed[] }[] = [];
 | 
			
		||||
              for (const i of parsedMessage['track-mempool-blocks']) {
 | 
			
		||||
                const index = parseInt(i);
 | 
			
		||||
                if (Number.isInteger(index) && index >= 0) {
 | 
			
		||||
                  indices.push(index);
 | 
			
		||||
                  updates.push({
 | 
			
		||||
                    index: index,
 | 
			
		||||
                    sequence: this.mempoolSequence,
 | 
			
		||||
                    blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              client['track-mempool-blocks'] = indices;
 | 
			
		||||
              response['projected-block-transactions'] = JSON.stringify(updates);
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-mempool-blocks'] = undefined;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@ -908,6 +934,19 @@ class WebsocketHandler {
 | 
			
		||||
            delta: mBlockDeltas[index],
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      } else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
 | 
			
		||||
        const indices = client['track-mempool-blocks'];
 | 
			
		||||
        const updates: string[] = [];
 | 
			
		||||
        for (const index of indices) {
 | 
			
		||||
          if (mBlockDeltas[index]) {
 | 
			
		||||
            updates.push(getCachedResponse(`projected-block-transactions-${index}`, {
 | 
			
		||||
              index: index,
 | 
			
		||||
              sequence: this.mempoolSequence,
 | 
			
		||||
              delta: mBlockDeltas[index],
 | 
			
		||||
            }));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        response['projected-block-transactions'] = '[' + updates.join(',') + ']';
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-rbf'] === 'all' && rbfReplacements) {
 | 
			
		||||
@ -1296,6 +1335,27 @@ class WebsocketHandler {
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
 | 
			
		||||
        const indices = client['track-mempool-blocks'];
 | 
			
		||||
        const updates: string[] = [];
 | 
			
		||||
        for (const index of indices) {
 | 
			
		||||
          if (mBlockDeltas && mBlockDeltas[index] && mBlocksWithTransactions[index]?.transactions?.length) {
 | 
			
		||||
            if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
 | 
			
		||||
              updates.push(getCachedResponse(`projected-block-transactions-full-${index}`, {
 | 
			
		||||
                index: index,
 | 
			
		||||
                sequence: this.mempoolSequence,
 | 
			
		||||
                blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
 | 
			
		||||
              }));
 | 
			
		||||
            } else {
 | 
			
		||||
              updates.push(getCachedResponse(`projected-block-transactions-delta-${index}`, {
 | 
			
		||||
                index: index,
 | 
			
		||||
                sequence: this.mempoolSequence,
 | 
			
		||||
                delta: mBlockDeltas[index],
 | 
			
		||||
              }));
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        response['projected-block-transactions'] = '[' + updates.join(',') + ']';
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-mempool-txids']) {
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
 | 
			
		||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
 | 
			
		||||
import { BlockViewComponent } from './components/block-view/block-view.component';
 | 
			
		||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
 | 
			
		||||
import { EightMempoolComponent } from './components/eight-mempool/eight-mempool.component';
 | 
			
		||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
 | 
			
		||||
import { ClockComponent } from './components/clock/clock.component';
 | 
			
		||||
import { StatusViewComponent } from './components/status-view/status-view.component';
 | 
			
		||||
@ -205,6 +206,10 @@ let routes: Routes = [
 | 
			
		||||
    path: 'view/blocks',
 | 
			
		||||
    component: EightBlocksComponent,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'view/mempool-blocks',
 | 
			
		||||
    component: EightMempoolComponent,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'status',
 | 
			
		||||
    data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
 | 
			
		||||
@ -681,10 +681,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
 | 
			
		||||
// WebGL shader attributes
 | 
			
		||||
const attribs = {
 | 
			
		||||
  offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
 | 
			
		||||
  bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
@ -707,10 +706,9 @@ varying lowp vec4 vColor;
 | 
			
		||||
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
 | 
			
		||||
// shader interpolates between start and end values at the given rate, from the given time
 | 
			
		||||
 | 
			
		||||
attribute vec2 offset;
 | 
			
		||||
attribute vec4 bounds;
 | 
			
		||||
attribute vec4 posX;
 | 
			
		||||
attribute vec4 posY;
 | 
			
		||||
attribute vec4 posR;
 | 
			
		||||
attribute vec4 colR;
 | 
			
		||||
attribute vec4 colG;
 | 
			
		||||
attribute vec4 colB;
 | 
			
		||||
@ -735,10 +733,7 @@ float interpolateAttribute(vec4 attr) {
 | 
			
		||||
void main() {
 | 
			
		||||
  vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
 | 
			
		||||
  // vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
 | 
			
		||||
 | 
			
		||||
  float radius = interpolateAttribute(posR);
 | 
			
		||||
  vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
 | 
			
		||||
 | 
			
		||||
  vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
 | 
			
		||||
  gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
 | 
			
		||||
 | 
			
		||||
  float red = interpolateAttribute(colR);
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,8 @@ export default class BlockScene {
 | 
			
		||||
  animationOffset: number;
 | 
			
		||||
  highlightingEnabled: boolean;
 | 
			
		||||
  filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  gridWidth: number;
 | 
			
		||||
@ -31,14 +33,16 @@ export default class BlockScene {
 | 
			
		||||
  animateUntil = 0;
 | 
			
		||||
  dirty: boolean;
 | 
			
		||||
 | 
			
		||||
  constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
 | 
			
		||||
      { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
 | 
			
		||||
  constructor({ x = 0, y = 0, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
 | 
			
		||||
      { x?: number, y?: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
 | 
			
		||||
  ) {
 | 
			
		||||
    this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
 | 
			
		||||
    this.init({ x, y,width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
 | 
			
		||||
  resize({ x = 0, y = 0, width = this.width, height = this.height, animate = true }: { x?: number, y?: number, width?: number, height?: number, animate: boolean }): void {
 | 
			
		||||
    this.x = x;
 | 
			
		||||
    this.y = y;
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
    this.gridSize = this.width / this.gridWidth;
 | 
			
		||||
@ -238,8 +242,8 @@ export default class BlockScene {
 | 
			
		||||
    this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
 | 
			
		||||
      { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
 | 
			
		||||
  private init({ x, y, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
 | 
			
		||||
      { x: number, y: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
 | 
			
		||||
  ): void {
 | 
			
		||||
    this.animationDuration = animationDuration || this.animationDuration || 1000;
 | 
			
		||||
@ -264,7 +268,7 @@ export default class BlockScene {
 | 
			
		||||
    this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
 | 
			
		||||
    this.gridWidth = resolution;
 | 
			
		||||
    this.gridHeight = resolution;
 | 
			
		||||
    this.resize({ width, height, animate: true });
 | 
			
		||||
    this.resize({ x, y, width, height, animate: true });
 | 
			
		||||
    this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
 | 
			
		||||
 | 
			
		||||
    this.txs = {};
 | 
			
		||||
@ -274,7 +278,7 @@ export default class BlockScene {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void {
 | 
			
		||||
    this.animateUntil = Math.max(this.animateUntil, tx.update(update));
 | 
			
		||||
    this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x, maxX: this.x + this.width, minY: this.y, maxY: this.y + this.height }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
 | 
			
		||||
@ -390,6 +394,7 @@ export default class BlockScene {
 | 
			
		||||
          position: {
 | 
			
		||||
            x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
 | 
			
		||||
            y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
 | 
			
		||||
            s: tx.screenPosition.s
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        duration: this.animationDuration,
 | 
			
		||||
@ -449,18 +454,18 @@ export default class BlockScene {
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        x: x + this.unitPadding - (slotSize / 2),
 | 
			
		||||
        y: y + this.unitPadding - (slotSize / 2),
 | 
			
		||||
        x: this.x + x + this.unitPadding - (slotSize / 2),
 | 
			
		||||
        y: this.y + y + this.unitPadding - (slotSize / 2),
 | 
			
		||||
        s: squareSize
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      return { x: 0, y: 0, s: 0 };
 | 
			
		||||
      return { x: this.x, y: this.y, s: 0 };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private screenToGrid(position: Position): Position {
 | 
			
		||||
    let x = position.x;
 | 
			
		||||
    let y = this.height - position.y;
 | 
			
		||||
    let x = position.x - this.x;
 | 
			
		||||
    let y = this.height - (position.y - this.y);
 | 
			
		||||
    let t;
 | 
			
		||||
 | 
			
		||||
    switch (this.orientation) {
 | 
			
		||||
 | 
			
		||||
@ -2,12 +2,13 @@ import { FastVertexArray } from './fast-vertex-array';
 | 
			
		||||
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
 | 
			
		||||
 | 
			
		||||
const attribKeys = ['a', 'b', 't', 'v'];
 | 
			
		||||
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
 | 
			
		||||
const updateKeys = ['x', 'y', 'r', 'g', 'b', 'a'];
 | 
			
		||||
const attributeKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
 | 
			
		||||
 | 
			
		||||
export default class TxSprite {
 | 
			
		||||
  static vertexSize = 30;
 | 
			
		||||
  static vertexSize = 28;
 | 
			
		||||
  static vertexCount = 6;
 | 
			
		||||
  static dataSize: number = (30 * 6);
 | 
			
		||||
  static dataSize: number = (28 * 6);
 | 
			
		||||
 | 
			
		||||
  vertexArray: FastVertexArray;
 | 
			
		||||
  vertexPointer: number;
 | 
			
		||||
@ -16,15 +17,26 @@ export default class TxSprite {
 | 
			
		||||
  attributes: Attributes;
 | 
			
		||||
  tempAttributes: OptionalAttributes;
 | 
			
		||||
 | 
			
		||||
  minX: number;
 | 
			
		||||
  maxX: number;
 | 
			
		||||
  minY: number;
 | 
			
		||||
  maxY: number;
 | 
			
		||||
 | 
			
		||||
  constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
 | 
			
		||||
 | 
			
		||||
  constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX: number, maxX: number, minY: number, maxY: number) {
 | 
			
		||||
    const offsetTime = params.start;
 | 
			
		||||
    this.vertexArray = vertexArray;
 | 
			
		||||
    this.vertexData = Array(VI.length).fill(0);
 | 
			
		||||
    this.vertexData = Array(TxSprite.dataSize).fill(0);
 | 
			
		||||
 | 
			
		||||
    this.updateMap = {
 | 
			
		||||
      x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.minX = minX;
 | 
			
		||||
    this.maxX = maxX;
 | 
			
		||||
    this.minY = minY;
 | 
			
		||||
    this.maxY = maxY;
 | 
			
		||||
 | 
			
		||||
    this.attributes = {
 | 
			
		||||
      x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
 | 
			
		||||
      y: { a: params.y, b: params.y, t: offsetTime, v: 0, d: 0 },
 | 
			
		||||
@ -77,11 +89,24 @@ export default class TxSprite {
 | 
			
		||||
      minDuration: minimum remaining transition duration when adjust = true
 | 
			
		||||
      temp: if true, this update is only temporary (can be reversed with 'resume')
 | 
			
		||||
  */
 | 
			
		||||
  update(params: SpriteUpdateParams): void {
 | 
			
		||||
  update(params: SpriteUpdateParams, minX?: number, maxX?: number, minY?: number, maxY?: number): void {
 | 
			
		||||
    const offsetTime = params.start || performance.now();
 | 
			
		||||
    const v = params.duration > 0 ? (1 / params.duration) : 0;
 | 
			
		||||
 | 
			
		||||
    updateKeys.forEach(key => {
 | 
			
		||||
    if (minX != null) {
 | 
			
		||||
      this.minX = minX;
 | 
			
		||||
    }
 | 
			
		||||
    if (maxX != null) {
 | 
			
		||||
      this.maxX = maxX;
 | 
			
		||||
    }
 | 
			
		||||
    if (minY != null) {
 | 
			
		||||
      this.minY = minY;
 | 
			
		||||
    }
 | 
			
		||||
    if (maxY != null) {
 | 
			
		||||
      this.maxY = maxY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    attributeKeys.forEach(key => {
 | 
			
		||||
      this.updateMap[key] = params[key];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -139,18 +164,32 @@ export default class TxSprite {
 | 
			
		||||
        ...this.tempAttributes
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    const size = attributes.s;
 | 
			
		||||
 | 
			
		||||
    // update vertex data in place
 | 
			
		||||
    // ugly, but avoids overhead of allocating large temporary arrays
 | 
			
		||||
    const vertexStride = VI.length + 2;
 | 
			
		||||
    const vertexStride = VI.length + 4;
 | 
			
		||||
    for (let vertex = 0; vertex < 6; vertex++) {
 | 
			
		||||
      this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
 | 
			
		||||
      for (let step = 0; step < VI.length; step++) {
 | 
			
		||||
      this.vertexData[vertex * vertexStride] = this.minX;
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 1] = this.minY;
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 2] = this.maxX;
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 3] = this.maxY;
 | 
			
		||||
 | 
			
		||||
      // x
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 4] = attributes[VI[0].a][VI[0].f] + (vertexOffsetFactors[vertex][0] * attributes.s.a);
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 5] = attributes[VI[1].a][VI[1].f] + (vertexOffsetFactors[vertex][0] * attributes.s.b);
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 6] = attributes[VI[2].a][VI[2].f];
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 7] = attributes[VI[3].a][VI[3].f];
 | 
			
		||||
 | 
			
		||||
      // y
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 8] = attributes[VI[4].a][VI[4].f] + (vertexOffsetFactors[vertex][1] * attributes.s.a);
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 9] = attributes[VI[5].a][VI[5].f] + (vertexOffsetFactors[vertex][1] * attributes.s.b);
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 10] = attributes[VI[6].a][VI[6].f];
 | 
			
		||||
      this.vertexData[(vertex * vertexStride) + 11] = attributes[VI[7].a][VI[7].f];
 | 
			
		||||
 | 
			
		||||
      for (let step = 8; step < VI.length; step++) {
 | 
			
		||||
        // components of each field in the vertex array are defined by an entry in VI:
 | 
			
		||||
        // VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
 | 
			
		||||
        this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
 | 
			
		||||
        this.vertexData[(vertex * vertexStride) + step + 4] = attributes[VI[step].a][VI[step].f];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
 | 
			
		||||
    returns minimum transition end time
 | 
			
		||||
  */
 | 
			
		||||
  update(params: ViewUpdateParams): number {
 | 
			
		||||
  update(params: ViewUpdateParams, { minX, maxX, minY, maxY }: { minX: number, maxX: number, minY: number, maxY: number }): number {
 | 
			
		||||
    if (params.jitter) {
 | 
			
		||||
      params.delay += (Math.random() * params.jitter);
 | 
			
		||||
    }
 | 
			
		||||
@ -115,21 +115,35 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
      this.initialised = true;
 | 
			
		||||
      this.sprite = new TxSprite(
 | 
			
		||||
        toSpriteUpdate(params),
 | 
			
		||||
        this.vertexArray
 | 
			
		||||
        this.vertexArray,
 | 
			
		||||
        minX,
 | 
			
		||||
        maxX,
 | 
			
		||||
        minY,
 | 
			
		||||
        maxY
 | 
			
		||||
      );
 | 
			
		||||
      // apply any pending hover event
 | 
			
		||||
      if (this.hover) {
 | 
			
		||||
        params.duration = Math.max(params.duration, hoverTransitionTime);
 | 
			
		||||
        this.sprite.update({
 | 
			
		||||
          ...this.hoverColor,
 | 
			
		||||
          duration: hoverTransitionTime,
 | 
			
		||||
          adjust: false,
 | 
			
		||||
          temp: true
 | 
			
		||||
        });
 | 
			
		||||
        this.sprite.update(
 | 
			
		||||
          {
 | 
			
		||||
            ...this.hoverColor,
 | 
			
		||||
            duration: hoverTransitionTime,
 | 
			
		||||
            adjust: false,
 | 
			
		||||
            temp: true
 | 
			
		||||
          },
 | 
			
		||||
          minX,
 | 
			
		||||
          maxX,
 | 
			
		||||
          minY,
 | 
			
		||||
          maxY
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      this.sprite.update(
 | 
			
		||||
        toSpriteUpdate(params)
 | 
			
		||||
        toSpriteUpdate(params),
 | 
			
		||||
        minX,
 | 
			
		||||
        maxX,
 | 
			
		||||
        minY,
 | 
			
		||||
        maxY
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    this.dirty = false;
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
 | 
			
		||||
<div class="block-overview-graph">
 | 
			
		||||
  <canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
 | 
			
		||||
  @if (!disableSpinner) {
 | 
			
		||||
    <div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
 | 
			
		||||
      <div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
 | 
			
		||||
      <div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  }
 | 
			
		||||
  <app-block-overview-tooltip
 | 
			
		||||
    [tx]="selectedTx || hoverTx"
 | 
			
		||||
    [cursorPosition]="tooltipPosition"
 | 
			
		||||
    [clickable]="!!selectedTx"
 | 
			
		||||
    [auditEnabled]="auditHighlighting"
 | 
			
		||||
    [blockConversion]="blockConversion"
 | 
			
		||||
    [filterFlags]="activeFilterFlags"
 | 
			
		||||
    [filterMode]="filterMode"
 | 
			
		||||
    [relativeTime]="relativeTime"
 | 
			
		||||
  ></app-block-overview-tooltip>
 | 
			
		||||
  <app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
 | 
			
		||||
  <div *ngIf="!webGlEnabled" class="placeholder">
 | 
			
		||||
    <span i18n="webgl-disabled">Your browser does not support this feature.</span>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,67 @@
 | 
			
		||||
.block-overview-graph {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  background: var(--stat-box-bg);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  grid-column: 1/-1;
 | 
			
		||||
 | 
			
		||||
  .placeholder {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.graph-alignment {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-align {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: repeat(auto-fit, 75px);
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-overview-canvas {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
 | 
			
		||||
  &.clickable {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loader-wrapper {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  background: #181b2d7f;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  transition: opacity 500ms 500ms;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
 | 
			
		||||
  &.hidden {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,647 @@
 | 
			
		||||
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
 | 
			
		||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { FastVertexArray } from '../block-overview-graph/fast-vertex-array';
 | 
			
		||||
import BlockScene from '../block-overview-graph/block-scene';
 | 
			
		||||
import TxSprite from '../block-overview-graph/tx-sprite';
 | 
			
		||||
import TxView from '../block-overview-graph/tx-view';
 | 
			
		||||
import { Color, Position } from '../block-overview-graph/sprite-types';
 | 
			
		||||
import { Price } from '../../services/price.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { ThemeService } from '../../services/theme.service';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '../block-overview-graph/utils';
 | 
			
		||||
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
 | 
			
		||||
const unmatchedOpacity = 0.2;
 | 
			
		||||
const unmatchedAuditColors = {
 | 
			
		||||
  censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
 | 
			
		||||
  missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
 | 
			
		||||
  added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
 | 
			
		||||
  added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
 | 
			
		||||
  prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
 | 
			
		||||
  accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
 | 
			
		||||
};
 | 
			
		||||
const unmatchedContrastAuditColors = {
 | 
			
		||||
  censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
 | 
			
		||||
  missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
 | 
			
		||||
  added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
 | 
			
		||||
  added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
 | 
			
		||||
  prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
 | 
			
		||||
  accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-overview-multi',
 | 
			
		||||
  templateUrl: './block-overview-multi.component.html',
 | 
			
		||||
  styleUrls: ['./block-overview-multi.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, OnChanges {
 | 
			
		||||
  @Input() isLoading: boolean;
 | 
			
		||||
  @Input() resolution: number;
 | 
			
		||||
  @Input() numBlocks: number;
 | 
			
		||||
  @Input() padding: number = 0;
 | 
			
		||||
  @Input() blockWidth: number = 360;
 | 
			
		||||
  @Input() autofit: boolean = false;
 | 
			
		||||
  @Input() blockLimit: number;
 | 
			
		||||
  @Input() orientation = 'left';
 | 
			
		||||
  @Input() flip = true;
 | 
			
		||||
  @Input() animationDuration: number = 1000;
 | 
			
		||||
  @Input() animationOffset: number | null = null;
 | 
			
		||||
  @Input() disableSpinner = false;
 | 
			
		||||
  @Input() mirrorTxid: string | void;
 | 
			
		||||
  @Input() unavailable: boolean = false;
 | 
			
		||||
  @Input() auditHighlighting: boolean = false;
 | 
			
		||||
  @Input() showFilters: boolean = false;
 | 
			
		||||
  @Input() excludeFilters: string[] = [];
 | 
			
		||||
  @Input() filterFlags: bigint | null = null;
 | 
			
		||||
  @Input() filterMode: FilterMode = 'and';
 | 
			
		||||
  @Input() gradientMode: 'fee' | 'age' = 'fee';
 | 
			
		||||
  @Input() relativeTime: number | null;
 | 
			
		||||
  @Input() blockConversion: Price;
 | 
			
		||||
  @Input() overrideColors: ((tx: TxView) => Color) | null = null;
 | 
			
		||||
  @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
 | 
			
		||||
  @Output() txHoverEvent = new EventEmitter<string>();
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockCanvas')
 | 
			
		||||
  canvas: ElementRef<HTMLCanvasElement>;
 | 
			
		||||
  themeChangedSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  gl: WebGLRenderingContext;
 | 
			
		||||
  animationFrameRequest: number;
 | 
			
		||||
  animationHeartBeat: number;
 | 
			
		||||
  displayWidth: number;
 | 
			
		||||
  displayHeight: number;
 | 
			
		||||
  displayBlockWidth: number;
 | 
			
		||||
  displayPadding: number;
 | 
			
		||||
  cssWidth: number;
 | 
			
		||||
  cssHeight: number;
 | 
			
		||||
  shaderProgram: WebGLProgram;
 | 
			
		||||
  vertexArray: FastVertexArray;
 | 
			
		||||
  running: boolean;
 | 
			
		||||
  scenes: BlockScene[] = [];
 | 
			
		||||
  hoverTx: TxView | void;
 | 
			
		||||
  selectedTx: TxView | void;
 | 
			
		||||
  highlightTx: TxView | void;
 | 
			
		||||
  mirrorTx: TxView | void;
 | 
			
		||||
  tooltipPosition: Position;
 | 
			
		||||
 | 
			
		||||
  readyNextFrame = false;
 | 
			
		||||
  lastUpdate: number = 0;
 | 
			
		||||
  pendingUpdates: {
 | 
			
		||||
    count: number,
 | 
			
		||||
    add: { [txid: string]: TransactionStripped },
 | 
			
		||||
    remove: { [txid: string]: string },
 | 
			
		||||
    change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
 | 
			
		||||
    direction?: string,
 | 
			
		||||
  }[] = [];
 | 
			
		||||
 | 
			
		||||
  searchText: string;
 | 
			
		||||
  searchSubscription: Subscription;
 | 
			
		||||
  filtersAvailable: boolean = true;
 | 
			
		||||
  activeFilterFlags: bigint | null = null;
 | 
			
		||||
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    readonly ngZone: NgZone,
 | 
			
		||||
    readonly elRef: ElementRef,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private themeService: ThemeService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
 | 
			
		||||
    this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    if (this.canvas) {
 | 
			
		||||
      this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
 | 
			
		||||
      this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
 | 
			
		||||
      this.gl = this.canvas.nativeElement.getContext('webgl');
 | 
			
		||||
      this.initScenes();
 | 
			
		||||
 | 
			
		||||
      if (this.gl) {
 | 
			
		||||
        this.initCanvas();
 | 
			
		||||
        this.resizeCanvas();
 | 
			
		||||
        this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => {
 | 
			
		||||
          for (const scene of this.scenes) {
 | 
			
		||||
            scene.setColorFunction(this.getColorFunction());
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initScenes(): void {
 | 
			
		||||
    for (const scene of this.scenes) {
 | 
			
		||||
      if (scene) {
 | 
			
		||||
        scene.destroy();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.scenes = [];
 | 
			
		||||
    this.pendingUpdates = [];
 | 
			
		||||
    for (let i = 0; i < this.numBlocks; i++) {
 | 
			
		||||
      this.scenes.push(null);
 | 
			
		||||
      this.pendingUpdates.push({
 | 
			
		||||
        count: 0,
 | 
			
		||||
        add: {},
 | 
			
		||||
        remove: {},
 | 
			
		||||
        change: {},
 | 
			
		||||
        direction: 'left',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    this.resizeCanvas();
 | 
			
		||||
    this.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes): void {
 | 
			
		||||
    if (changes.numBlocks) {
 | 
			
		||||
      this.initScenes();
 | 
			
		||||
    }
 | 
			
		||||
    if (changes.orientation || changes.flip) {
 | 
			
		||||
      for (const scene of this.scenes) {
 | 
			
		||||
        scene?.setOrientation(this.orientation, this.flip);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (changes.auditHighlighting) {
 | 
			
		||||
      this.setHighlightingEnabled(this.auditHighlighting);
 | 
			
		||||
    }
 | 
			
		||||
    if (changes.overrideColor) {
 | 
			
		||||
      for (const scene of this.scenes) {
 | 
			
		||||
        scene?.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) {
 | 
			
		||||
      this.setFilterFlags();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFilterFlags(goggle?: ActiveFilter): void {
 | 
			
		||||
    this.filterMode = goggle?.mode || this.filterMode;
 | 
			
		||||
    this.gradientMode = goggle?.gradient || this.gradientMode;
 | 
			
		||||
    this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
 | 
			
		||||
    for (const scene of this.scenes) {
 | 
			
		||||
      if (this.activeFilterFlags != null && this.filtersAvailable) {
 | 
			
		||||
        scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
 | 
			
		||||
      } else {
 | 
			
		||||
        scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    if (this.animationFrameRequest) {
 | 
			
		||||
      cancelAnimationFrame(this.animationFrameRequest);
 | 
			
		||||
      clearTimeout(this.animationHeartBeat);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.canvas) {
 | 
			
		||||
      this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
 | 
			
		||||
      this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
 | 
			
		||||
      this.themeChangedSubscription?.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clear(block: number, direction): void {
 | 
			
		||||
    this.exit(block, direction);
 | 
			
		||||
    this.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy(block: number): void {
 | 
			
		||||
    if (this.scenes[block]) {
 | 
			
		||||
      this.scenes[block].destroy();
 | 
			
		||||
      this.clearUpdateQueue(block);
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // initialize the scene without any entry transition
 | 
			
		||||
  setup(block: number, transactions: TransactionStripped[], sort: boolean = false): void {
 | 
			
		||||
    const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
 | 
			
		||||
    if (filtersAvailable !== this.filtersAvailable) {
 | 
			
		||||
      this.setFilterFlags();
 | 
			
		||||
    }
 | 
			
		||||
    this.filtersAvailable = filtersAvailable;
 | 
			
		||||
    if (this.scenes[block]) {
 | 
			
		||||
      this.clearUpdateQueue(block);
 | 
			
		||||
      this.scenes[block].setup(transactions, sort);
 | 
			
		||||
      this.readyNextFrame = true;
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  enter(block: number, transactions: TransactionStripped[], direction: string): void {
 | 
			
		||||
    if (this.scenes[block]) {
 | 
			
		||||
      this.clearUpdateQueue(block);
 | 
			
		||||
      this.scenes[block].enter(transactions, direction);
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exit(block: number, direction: string): void {
 | 
			
		||||
    if (this.scenes[block]) {
 | 
			
		||||
      this.clearUpdateQueue(block);
 | 
			
		||||
      this.scenes[block].exit(direction);
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  replace(block: number, transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
 | 
			
		||||
    if (this.scenes[block]) {
 | 
			
		||||
      this.clearUpdateQueue(block);
 | 
			
		||||
      this.scenes[block].replace(transactions || [], direction, sort, startTime);
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // collates deferred updates into a set of consistent pending changes
 | 
			
		||||
  queueUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
 | 
			
		||||
    for (const tx of add) {
 | 
			
		||||
      this.pendingUpdates[block].add[tx.txid] = tx;
 | 
			
		||||
      delete this.pendingUpdates[block].remove[tx.txid];
 | 
			
		||||
      delete this.pendingUpdates[block].change[tx.txid];
 | 
			
		||||
    }
 | 
			
		||||
    for (const txid of remove) {
 | 
			
		||||
      delete this.pendingUpdates[block].add[txid];
 | 
			
		||||
      this.pendingUpdates[block].remove[txid] = txid;
 | 
			
		||||
      delete this.pendingUpdates[block].change[txid];
 | 
			
		||||
    }
 | 
			
		||||
    for (const tx of change) {
 | 
			
		||||
      if (this.pendingUpdates[block].add[tx.txid]) {
 | 
			
		||||
        this.pendingUpdates[block].add[tx.txid].rate = tx.rate;
 | 
			
		||||
        this.pendingUpdates[block].add[tx.txid].acc = tx.acc;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.pendingUpdates[block].change[tx.txid] = tx;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.pendingUpdates[block].direction = direction;
 | 
			
		||||
    this.pendingUpdates[block].count++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deferredUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
 | 
			
		||||
    this.queueUpdate(block, add, remove, change, direction);
 | 
			
		||||
    this.applyQueuedUpdates();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  applyQueuedUpdates(): void {
 | 
			
		||||
    for (const [index, pendingUpdate] of this.pendingUpdates.entries()) {
 | 
			
		||||
      if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
 | 
			
		||||
        this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction);
 | 
			
		||||
        this.clearUpdateQueue(index);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clearUpdateQueue(block: number): void {
 | 
			
		||||
    this.pendingUpdates[block] = {
 | 
			
		||||
      count: 0,
 | 
			
		||||
      add: {},
 | 
			
		||||
      remove: {},
 | 
			
		||||
      change: {},
 | 
			
		||||
    };
 | 
			
		||||
    this.lastUpdate = performance.now();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  update(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
 | 
			
		||||
    // merge any pending changes into this update
 | 
			
		||||
    this.queueUpdate(block, add, remove, change, direction);
 | 
			
		||||
    this.applyUpdate(block,Object.values(this.pendingUpdates[block].add), Object.values(this.pendingUpdates[block].remove), Object.values(this.pendingUpdates[block].change), direction, resetLayout);
 | 
			
		||||
    this.clearUpdateQueue(block);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  applyUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
 | 
			
		||||
    if (this.scenes[block]) {
 | 
			
		||||
      add = add.filter(tx => !this.scenes[block].txs[tx.txid]);
 | 
			
		||||
      remove = remove.filter(txid => this.scenes[block].txs[txid]);
 | 
			
		||||
      change = change.filter(tx => this.scenes[block].txs[tx.txid]);
 | 
			
		||||
 | 
			
		||||
      if (this.gradientMode === 'age') {
 | 
			
		||||
        this.scenes[block].updateAllColors();
 | 
			
		||||
      }
 | 
			
		||||
      this.scenes[block].update(add, remove, change, direction, resetLayout);
 | 
			
		||||
      this.start();
 | 
			
		||||
      this.lastUpdate = performance.now();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initCanvas(): void {
 | 
			
		||||
    if (!this.canvas || !this.gl) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
 | 
			
		||||
    this.gl.clear(this.gl.COLOR_BUFFER_BIT);
 | 
			
		||||
 | 
			
		||||
    const shaderSet = [
 | 
			
		||||
      {
 | 
			
		||||
        type: this.gl.VERTEX_SHADER,
 | 
			
		||||
        src: vertShaderSrc
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: this.gl.FRAGMENT_SHADER,
 | 
			
		||||
        src: fragShaderSrc
 | 
			
		||||
      }
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    this.shaderProgram = this.buildShaderProgram(shaderSet);
 | 
			
		||||
 | 
			
		||||
    this.gl.useProgram(this.shaderProgram);
 | 
			
		||||
 | 
			
		||||
    // Set up alpha blending
 | 
			
		||||
    this.gl.enable(this.gl.BLEND);
 | 
			
		||||
    this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
 | 
			
		||||
 | 
			
		||||
    const glBuffer = this.gl.createBuffer();
 | 
			
		||||
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
 | 
			
		||||
 | 
			
		||||
    /* SET UP SHADER ATTRIBUTES */
 | 
			
		||||
    Object.keys(attribs).forEach((key, i) => {
 | 
			
		||||
      attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
 | 
			
		||||
      this.gl.enableVertexAttribArray(attribs[key].pointer);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleContextLost(event): void {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    cancelAnimationFrame(this.animationFrameRequest);
 | 
			
		||||
    this.animationFrameRequest = null;
 | 
			
		||||
    this.running = false;
 | 
			
		||||
    this.gl = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleContextRestored(event): void {
 | 
			
		||||
    if (this.canvas?.nativeElement) {
 | 
			
		||||
      this.gl = this.canvas.nativeElement.getContext('webgl');
 | 
			
		||||
      if (this.gl) {
 | 
			
		||||
        this.initCanvas();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
  resizeCanvas(): void {
 | 
			
		||||
    if (this.canvas) {
 | 
			
		||||
      this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
 | 
			
		||||
      this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
 | 
			
		||||
      this.displayWidth = window.devicePixelRatio * this.cssWidth;
 | 
			
		||||
      this.displayHeight = window.devicePixelRatio * this.cssHeight;
 | 
			
		||||
      this.displayBlockWidth = window.devicePixelRatio * this.blockWidth;
 | 
			
		||||
      this.displayPadding = window.devicePixelRatio * this.padding;
 | 
			
		||||
      this.canvas.nativeElement.width = this.displayWidth;
 | 
			
		||||
      this.canvas.nativeElement.height = this.displayHeight;
 | 
			
		||||
      if (this.gl) {
 | 
			
		||||
        this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
 | 
			
		||||
      }
 | 
			
		||||
      for (let i = 0; i < this.scenes.length; i++) {
 | 
			
		||||
        const blocksPerRow = Math.floor(this.displayWidth / (this.displayBlockWidth + (this.displayPadding * 2)));
 | 
			
		||||
        const x = this.displayPadding + ((i % blocksPerRow) * (this.displayBlockWidth + (this.displayPadding * 2)));
 | 
			
		||||
        const row = Math.floor(i / blocksPerRow);
 | 
			
		||||
        const y = this.displayPadding + this.displayHeight - ((row + 1) * (this.displayBlockWidth + (this.displayPadding * 2)));
 | 
			
		||||
        if (this.scenes[i]) {
 | 
			
		||||
          this.scenes[i].resize({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, animate: false });
 | 
			
		||||
          this.start();
 | 
			
		||||
        } else {
 | 
			
		||||
          this.scenes[i] = new BlockScene({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, resolution: this.resolution,
 | 
			
		||||
            blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService,
 | 
			
		||||
            highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
 | 
			
		||||
          colorFunction: this.getColorFunction() });
 | 
			
		||||
          this.start();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  compileShader(src, type): WebGLShader {
 | 
			
		||||
    if (!this.gl) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const shader = this.gl.createShader(type);
 | 
			
		||||
 | 
			
		||||
    this.gl.shaderSource(shader, src);
 | 
			
		||||
    this.gl.compileShader(shader);
 | 
			
		||||
 | 
			
		||||
    if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
 | 
			
		||||
      console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
 | 
			
		||||
      console.log(this.gl.getShaderInfoLog(shader));
 | 
			
		||||
    }
 | 
			
		||||
    return shader;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buildShaderProgram(shaderInfo): WebGLProgram {
 | 
			
		||||
    if (!this.gl) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const program = this.gl.createProgram();
 | 
			
		||||
 | 
			
		||||
    shaderInfo.forEach((desc) => {
 | 
			
		||||
      const shader = this.compileShader(desc.src, desc.type);
 | 
			
		||||
      if (shader) {
 | 
			
		||||
        this.gl.attachShader(program, shader);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.gl.linkProgram(program);
 | 
			
		||||
 | 
			
		||||
    if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
 | 
			
		||||
      console.log('Error linking shader program:');
 | 
			
		||||
      console.log(this.gl.getProgramInfoLog(program));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return program;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  start(): void {
 | 
			
		||||
    this.running = true;
 | 
			
		||||
    this.ngZone.runOutsideAngular(() => this.doRun());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  doRun(): void {
 | 
			
		||||
    if (this.animationFrameRequest) {
 | 
			
		||||
      cancelAnimationFrame(this.animationFrameRequest);
 | 
			
		||||
    }
 | 
			
		||||
    this.animationFrameRequest = requestAnimationFrame(() => this.run());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  run(now?: DOMHighResTimeStamp): void {
 | 
			
		||||
    if (!now) {
 | 
			
		||||
      now = performance.now();
 | 
			
		||||
    }
 | 
			
		||||
    this.applyQueuedUpdates();
 | 
			
		||||
    // skip re-render if there's no change to the scene
 | 
			
		||||
    if (this.scenes.length && this.gl) {
 | 
			
		||||
      /* SET UP SHADER UNIFORMS */
 | 
			
		||||
      // screen dimensions
 | 
			
		||||
      this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
 | 
			
		||||
      // frame timestamp
 | 
			
		||||
      this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
 | 
			
		||||
 | 
			
		||||
      if (this.vertexArray.dirty) {
 | 
			
		||||
        /* SET UP SHADER ATTRIBUTES */
 | 
			
		||||
        Object.keys(attribs).forEach((key, i) => {
 | 
			
		||||
          this.gl.vertexAttribPointer(attribs[key].pointer,
 | 
			
		||||
          attribs[key].count,  // number of primitives in this attribute
 | 
			
		||||
          this.gl[attribs[key].type],  // type of primitive in this attribute (e.g. gl.FLOAT)
 | 
			
		||||
          false, // never normalised
 | 
			
		||||
          stride,   // distance between values of the same attribute
 | 
			
		||||
          attribs[key].offset);  // offset of the first value
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const pointArray = this.vertexArray.getVertexData();
 | 
			
		||||
 | 
			
		||||
        if (pointArray.length) {
 | 
			
		||||
          this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
 | 
			
		||||
          this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
 | 
			
		||||
        }
 | 
			
		||||
        this.vertexArray.dirty = false;
 | 
			
		||||
      } else {
 | 
			
		||||
        const pointArray = this.vertexArray.getVertexData();
 | 
			
		||||
        if (pointArray.length) {
 | 
			
		||||
          this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.readyNextFrame) {
 | 
			
		||||
        this.readyNextFrame = false;
 | 
			
		||||
        this.readyEvent.emit();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* LOOP */
 | 
			
		||||
    if (this.running && this.scenes.length && now <= (this.scenes.reduce((max, scene) => scene.animateUntil > max ? scene.animateUntil : max, 0) + 500)) {
 | 
			
		||||
      this.doRun();
 | 
			
		||||
    } else {
 | 
			
		||||
      if (this.animationHeartBeat) {
 | 
			
		||||
        clearTimeout(this.animationHeartBeat);
 | 
			
		||||
      }
 | 
			
		||||
      this.animationHeartBeat = window.setTimeout(() => {
 | 
			
		||||
        this.start();
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setHighlightingEnabled(enabled: boolean): void {
 | 
			
		||||
    for (const scene of this.scenes) {
 | 
			
		||||
      scene.setHighlighting(enabled);
 | 
			
		||||
    }
 | 
			
		||||
    this.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getColorFunction(): ((tx: TxView) => Color) {
 | 
			
		||||
    if (this.overrideColors) {
 | 
			
		||||
      return this.overrideColors;
 | 
			
		||||
    } else if (this.filterFlags) {
 | 
			
		||||
      return this.getFilterColorFunction(this.filterFlags, this.gradientMode);
 | 
			
		||||
    } else if (this.activeFilterFlags) {
 | 
			
		||||
      return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode);
 | 
			
		||||
    } else {
 | 
			
		||||
      return this.getFilterColorFunction(0n, this.gradientMode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
 | 
			
		||||
    return (tx: TxView) => {
 | 
			
		||||
      if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
 | 
			
		||||
        if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
 | 
			
		||||
          return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
 | 
			
		||||
        } else {
 | 
			
		||||
          return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
 | 
			
		||||
          return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
 | 
			
		||||
            tx,
 | 
			
		||||
            defaultColors.unmatchedfee,
 | 
			
		||||
            unmatchedAuditColors,
 | 
			
		||||
            this.relativeTime || (Date.now() / 1000)
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : contrastColorFunction(
 | 
			
		||||
            tx,
 | 
			
		||||
            contrastColors.unmatchedfee,
 | 
			
		||||
            unmatchedContrastAuditColors,
 | 
			
		||||
            this.relativeTime || (Date.now() / 1000)
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebGL shader attributes
 | 
			
		||||
const attribs = {
 | 
			
		||||
  bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
 | 
			
		||||
  colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
 | 
			
		||||
};
 | 
			
		||||
// Calculate the number of bytes per vertex based on specified attributes
 | 
			
		||||
const stride = Object.values(attribs).reduce((total, attrib) => {
 | 
			
		||||
  return total + (attrib.count * 4);
 | 
			
		||||
}, 0);
 | 
			
		||||
// Calculate vertex attribute offsets
 | 
			
		||||
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
 | 
			
		||||
  const attrib = Object.values(attribs)[i];
 | 
			
		||||
  attrib.offset = offset;
 | 
			
		||||
  offset += (attrib.count * 4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const vertShaderSrc = `
 | 
			
		||||
varying lowp vec4 vColor;
 | 
			
		||||
 | 
			
		||||
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
 | 
			
		||||
// shader interpolates between start and end values at the given rate, from the given time
 | 
			
		||||
 | 
			
		||||
attribute vec4 bounds;
 | 
			
		||||
attribute vec4 posX;
 | 
			
		||||
attribute vec4 posY;
 | 
			
		||||
attribute vec4 colR;
 | 
			
		||||
attribute vec4 colG;
 | 
			
		||||
attribute vec4 colB;
 | 
			
		||||
attribute vec4 colA;
 | 
			
		||||
 | 
			
		||||
uniform vec2 screenSize;
 | 
			
		||||
uniform float now;
 | 
			
		||||
 | 
			
		||||
float smootherstep(float x) {
 | 
			
		||||
  x = clamp(x, 0.0, 1.0);
 | 
			
		||||
  float ix = 1.0 - x;
 | 
			
		||||
  x = x * x;
 | 
			
		||||
  return x / (x + ix * ix);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float interpolateAttribute(vec4 attr) {
 | 
			
		||||
  float d = (now - attr.z) * attr.w;
 | 
			
		||||
  float delta = smootherstep(d);
 | 
			
		||||
  return mix(attr.x, attr.y, delta);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
 | 
			
		||||
  // vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
 | 
			
		||||
  vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
 | 
			
		||||
  gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
 | 
			
		||||
 | 
			
		||||
  float red = interpolateAttribute(colR);
 | 
			
		||||
  float green = interpolateAttribute(colG);
 | 
			
		||||
  float blue = interpolateAttribute(colB);
 | 
			
		||||
  float alpha = interpolateAttribute(colA);
 | 
			
		||||
 | 
			
		||||
  vColor = vec4(red, green, blue, alpha);
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const fragShaderSrc = `
 | 
			
		||||
varying lowp vec4 vColor;
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  gl_FragColor = vColor;
 | 
			
		||||
  // premultiply alpha
 | 
			
		||||
  gl_FragColor.rgb *= gl_FragColor.a;
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
@ -1,23 +1,25 @@
 | 
			
		||||
 | 
			
		||||
<app-block-overview-multi
 | 
			
		||||
  #blockGraph
 | 
			
		||||
  [isLoading]="false"
 | 
			
		||||
  [numBlocks]="numBlocks"
 | 
			
		||||
  [padding]="padding"
 | 
			
		||||
  [blockWidth]="blockWidth"
 | 
			
		||||
  [resolution]="resolution"
 | 
			
		||||
  [blockLimit]="stateService.blockVSize"
 | 
			
		||||
  [orientation]="'top'"
 | 
			
		||||
  [flip]="false"
 | 
			
		||||
  [animationDuration]="animationDuration"
 | 
			
		||||
  [animationOffset]="animationOffset"
 | 
			
		||||
  [disableSpinner]="true"
 | 
			
		||||
></app-block-overview-multi>
 | 
			
		||||
<div class="blocks" [class.wrap]="wrapBlocks">
 | 
			
		||||
  <ng-container *ngFor="let i of blockIndices">
 | 
			
		||||
    <div class="block-wrapper" [style]="wrapperStyle">
 | 
			
		||||
      <div class="block-container" [style]="containerStyle">
 | 
			
		||||
        <app-block-overview-graph
 | 
			
		||||
          #blockGraph
 | 
			
		||||
          [isLoading]="false"
 | 
			
		||||
          [resolution]="resolution"
 | 
			
		||||
          [blockLimit]="stateService.blockVSize"
 | 
			
		||||
          [orientation]="'top'"
 | 
			
		||||
          [flip]="false"
 | 
			
		||||
          [animationDuration]="animationDuration"
 | 
			
		||||
          [animationOffset]="animationOffset"
 | 
			
		||||
          [disableSpinner]="true"
 | 
			
		||||
          [relativeTime]="blockInfo[i]?.timestamp"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)"
 | 
			
		||||
        ></app-block-overview-graph>
 | 
			
		||||
        <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
 | 
			
		||||
          <h1 class="height">{{ blockInfo[i].height }}</h1>
 | 
			
		||||
          <h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
 | 
			
		||||
          <h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }} <img class="pool-logo" [src]="'/resources/mining-pools/' + blockInfo[i].extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'"> </h2>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,7 @@
 | 
			
		||||
.blocks {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  min-width: 100vw;
 | 
			
		||||
@ -66,4 +69,12 @@
 | 
			
		||||
  .block-container {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pool-logo {
 | 
			
		||||
  width: 1.2em;
 | 
			
		||||
  height: 1.2em;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: -1px;
 | 
			
		||||
  margin-right: 2px;
 | 
			
		||||
}
 | 
			
		||||
@ -1,16 +1,16 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router';
 | 
			
		||||
import { catchError, startWith } from 'rxjs/operators';
 | 
			
		||||
import { catchError } from 'rxjs/operators';
 | 
			
		||||
import { Subject, Subscription, of } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
import { animate, style, transition, trigger } from '@angular/animations';
 | 
			
		||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
 | 
			
		||||
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
 | 
			
		||||
import { CacheService } from '../../services/cache.service';
 | 
			
		||||
 | 
			
		||||
function bestFitResolution(min, max, n): number {
 | 
			
		||||
  const target = (min + max) / 2;
 | 
			
		||||
@ -48,24 +48,26 @@ interface BlockInfo extends BlockExtended {
 | 
			
		||||
})
 | 
			
		||||
export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  network = '';
 | 
			
		||||
  latestBlocks: BlockExtended[] = [];
 | 
			
		||||
  latestBlocks: (BlockExtended | null)[] = [];
 | 
			
		||||
  pendingBlocks: Record<number, ((b: BlockExtended) => void)[]> = {};
 | 
			
		||||
  isLoadingTransactions = true;
 | 
			
		||||
  strippedTransactions: { [height: number]: TransactionStripped[] } = {};
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
  hoverTx: string | null = null;
 | 
			
		||||
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  tipSubscription: Subscription;
 | 
			
		||||
  cacheBlocksSubscription: Subscription;
 | 
			
		||||
  networkChangedSubscription: Subscription;
 | 
			
		||||
  queryParamsSubscription: Subscription;
 | 
			
		||||
  graphChangeSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  height: number = 0;
 | 
			
		||||
  numBlocks: number = 8;
 | 
			
		||||
  blockIndices: number[] = [...Array(8).keys()];
 | 
			
		||||
  autofit: boolean = false;
 | 
			
		||||
  padding: number = 0;
 | 
			
		||||
  wrapBlocks: boolean = false;
 | 
			
		||||
  blockWidth: number = 1080;
 | 
			
		||||
  blockWidth: number = 360;
 | 
			
		||||
  animationDuration: number = 2000;
 | 
			
		||||
  animationOffset: number = 0;
 | 
			
		||||
  stagger: number = 0;
 | 
			
		||||
@ -79,13 +81,14 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  wrapperStyle = {
 | 
			
		||||
    '--block-width': '1080px',
 | 
			
		||||
    width: '1080px',
 | 
			
		||||
    height: '1080px',
 | 
			
		||||
    maxWidth: '1080px',
 | 
			
		||||
    padding: '',
 | 
			
		||||
    margin: '',
 | 
			
		||||
  };
 | 
			
		||||
  containerStyle = {};
 | 
			
		||||
  resolution: number = 86;
 | 
			
		||||
 | 
			
		||||
  @ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
@ -93,6 +96,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private cacheService: CacheService,
 | 
			
		||||
    private bytesPipe: BytesPipe,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
 | 
			
		||||
@ -111,7 +115,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.wrapBlocks = params.wrap !== 'false';
 | 
			
		||||
      this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
 | 
			
		||||
      this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
 | 
			
		||||
      this.animationOffset = this.padding * 2;
 | 
			
		||||
      this.animationOffset = 0;
 | 
			
		||||
 | 
			
		||||
      if (this.autofit) {
 | 
			
		||||
        this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
 | 
			
		||||
@ -122,22 +126,26 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.wrapperStyle = {
 | 
			
		||||
        '--block-width': this.blockWidth + 'px',
 | 
			
		||||
        width: this.blockWidth + 'px',
 | 
			
		||||
        height: this.blockWidth + 'px',
 | 
			
		||||
        maxWidth: this.blockWidth + 'px',
 | 
			
		||||
        padding: (this.padding || 0) +'px 0px',
 | 
			
		||||
        margin: (this.padding || 0) +'px ',
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      if (params.test === 'true') {
 | 
			
		||||
        if (this.blocksSubscription) {
 | 
			
		||||
          this.blocksSubscription.unsubscribe();
 | 
			
		||||
      this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block: BlockExtended) => {
 | 
			
		||||
        if (this.pendingBlocks[block.height]) {
 | 
			
		||||
          this.pendingBlocks[block.height].forEach(resolve => resolve(block));
 | 
			
		||||
          delete this.pendingBlocks[block.height];
 | 
			
		||||
        }
 | 
			
		||||
        this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
 | 
			
		||||
          this.handleNewBlock(blocks.slice(0, this.numBlocks));
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.tipSubscription?.unsubscribe();
 | 
			
		||||
      if (params.test === 'true') {
 | 
			
		||||
        this.shiftTestBlocks();
 | 
			
		||||
      } else if (!this.blocksSubscription) {
 | 
			
		||||
        this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
          .subscribe((blocks) => {
 | 
			
		||||
            this.handleNewBlock(blocks.slice(0, this.numBlocks));
 | 
			
		||||
      } else {
 | 
			
		||||
        this.tipSubscription = this.stateService.chainTip$
 | 
			
		||||
          .subscribe((height) => {
 | 
			
		||||
            this.height = height;
 | 
			
		||||
            this.handleNewBlock(height);
 | 
			
		||||
          });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
@ -149,15 +157,13 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
 | 
			
		||||
      this.setupBlockGraphs();
 | 
			
		||||
    });
 | 
			
		||||
    this.setupBlockGraphs();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.stateService.markBlock$.next({});
 | 
			
		||||
    if (this.blocksSubscription) {
 | 
			
		||||
      this.blocksSubscription?.unsubscribe();
 | 
			
		||||
    if (this.tipSubscription) {
 | 
			
		||||
      this.tipSubscription?.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    this.cacheBlocksSubscription?.unsubscribe();
 | 
			
		||||
    this.networkChangedSubscription?.unsubscribe();
 | 
			
		||||
@ -167,32 +173,27 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  shiftTestBlocks(): void {
 | 
			
		||||
    const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
 | 
			
		||||
      sub.unsubscribe();
 | 
			
		||||
      this.handleNewBlock(result.slice(0, this.numBlocks));
 | 
			
		||||
      this.handleNewBlock(this.testHeight);
 | 
			
		||||
      this.testHeight++;
 | 
			
		||||
      clearTimeout(this.testShiftTimeout);
 | 
			
		||||
      this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
 | 
			
		||||
  async handleNewBlock(height: number): Promise<void> {
 | 
			
		||||
    const readyPromises: Promise<TransactionStripped[]>[] = [];
 | 
			
		||||
    const previousBlocks = this.latestBlocks;
 | 
			
		||||
 | 
			
		||||
    const blocks = await this.loadBlocks(height, this.numBlocks);
 | 
			
		||||
    console.log('loaded ', blocks.length, ' blocks from height ', height);
 | 
			
		||||
    console.log(blocks);
 | 
			
		||||
 | 
			
		||||
    const newHeights = {};
 | 
			
		||||
    this.latestBlocks = blocks;
 | 
			
		||||
    for (const block of blocks) {
 | 
			
		||||
      newHeights[block.height] = true;
 | 
			
		||||
      if (!this.strippedTransactions[block.height]) {
 | 
			
		||||
        readyPromises.push(new Promise((resolve) => {
 | 
			
		||||
          const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
 | 
			
		||||
            catchError(() => {
 | 
			
		||||
              return of([]);
 | 
			
		||||
            }),
 | 
			
		||||
          ).subscribe((transactions) => {
 | 
			
		||||
            this.strippedTransactions[block.height] = transactions;
 | 
			
		||||
            subscription.unsubscribe();
 | 
			
		||||
            resolve(transactions);
 | 
			
		||||
          });
 | 
			
		||||
        }));
 | 
			
		||||
        readyPromises.push(this.loadBlockTransactions(block));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.allSettled(readyPromises);
 | 
			
		||||
@ -206,12 +207,45 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async loadBlocks(height: number, numBlocks: number): Promise<BlockExtended[]> {
 | 
			
		||||
    console.log('loading ', numBlocks, ' blocks from height ', height);
 | 
			
		||||
    const promises: Promise<BlockExtended>[] = [];
 | 
			
		||||
    for (let i = 0; i < numBlocks; i++) {
 | 
			
		||||
      this.cacheService.loadBlock(height - i);
 | 
			
		||||
      const cachedBlock = this.cacheService.getCachedBlock(height - i);
 | 
			
		||||
      if (cachedBlock) {
 | 
			
		||||
        promises.push(Promise.resolve(cachedBlock));
 | 
			
		||||
      } else {
 | 
			
		||||
        promises.push(new Promise((resolve) => {
 | 
			
		||||
          if (!this.pendingBlocks[height - i]) {
 | 
			
		||||
            this.pendingBlocks[height - i] = [];
 | 
			
		||||
          }
 | 
			
		||||
          this.pendingBlocks[height - i].push(resolve);
 | 
			
		||||
        }));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return Promise.all(promises);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async loadBlockTransactions(block: BlockExtended): Promise<TransactionStripped[]> {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
      this.apiService.getStrippedBlockTransactions$(block.id).pipe(
 | 
			
		||||
        catchError(() => {
 | 
			
		||||
          return of([]);
 | 
			
		||||
        }),
 | 
			
		||||
      ).subscribe((transactions) => {
 | 
			
		||||
        this.strippedTransactions[block.height] = transactions;
 | 
			
		||||
        resolve(transactions);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateBlockGraphs(blocks): void {
 | 
			
		||||
    const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
 | 
			
		||||
    if (this.blockGraphs) {
 | 
			
		||||
      this.blockGraphs.forEach((graph, index) => {
 | 
			
		||||
        graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
 | 
			
		||||
      });
 | 
			
		||||
    if (this.blockGraph) {
 | 
			
		||||
      for (let i = 0; i < this.numBlocks; i++) {
 | 
			
		||||
        this.blockGraph.replace(i, this.strippedTransactions[blocks?.[i]?.height] || [], 'right', false, startTime + (this.stagger * i));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.showInfo = false;
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
@ -226,28 +260,11 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setupBlockGraphs(): void {
 | 
			
		||||
    if (this.blockGraphs) {
 | 
			
		||||
      this.blockGraphs.forEach((graph, index) => {
 | 
			
		||||
        graph.destroy();
 | 
			
		||||
        graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
 | 
			
		||||
    const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
 | 
			
		||||
    if (!event.keyModifier) {
 | 
			
		||||
      this.router.navigate([url]);
 | 
			
		||||
    } else {
 | 
			
		||||
      window.open(url, '_blank');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxHover(txid: string): void {
 | 
			
		||||
    if (txid && txid.length) {
 | 
			
		||||
      this.hoverTx = txid;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hoverTx = null;
 | 
			
		||||
    if (this.blockGraph) {
 | 
			
		||||
      for (let i = 0; i < this.numBlocks; i++) {
 | 
			
		||||
        this.blockGraph.destroy(i);
 | 
			
		||||
        this.blockGraph.setup(i, this.strippedTransactions[this.latestBlocks?.[i]?.height] || []);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
<app-block-overview-multi
 | 
			
		||||
  #blockGraph
 | 
			
		||||
  [isLoading]="false"
 | 
			
		||||
  [numBlocks]="numBlocks"
 | 
			
		||||
  [padding]="padding"
 | 
			
		||||
  [blockWidth]="blockWidth"
 | 
			
		||||
  [resolution]="resolution"
 | 
			
		||||
  [blockLimit]="stateService.blockVSize"
 | 
			
		||||
  [orientation]="'left'"
 | 
			
		||||
  [flip]="true"
 | 
			
		||||
  [animationDuration]="animationDuration"
 | 
			
		||||
  [animationOffset]="animationOffset"
 | 
			
		||||
  [disableSpinner]="true"
 | 
			
		||||
></app-block-overview-multi>
 | 
			
		||||
<div class="blocks" [class.wrap]="wrapBlocks">
 | 
			
		||||
  <ng-container *ngFor="let i of blockIndices">
 | 
			
		||||
    <div class="block-wrapper" [style]="wrapperStyle">
 | 
			
		||||
      <div class="block-container" [style]="containerStyle">
 | 
			
		||||
        <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
 | 
			
		||||
          <h1 class="height">{{ blockInfo[i].label }}</h1>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,72 @@
 | 
			
		||||
.blocks {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  min-width: 100vw;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: nowrap;
 | 
			
		||||
  justify-content: flex-start;
 | 
			
		||||
  align-items: flex-start;
 | 
			
		||||
  align-content: flex-start;
 | 
			
		||||
 | 
			
		||||
  &.wrap {
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .block-wrapper {
 | 
			
		||||
    flex-grow: 0;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    --block-width: 1080px;
 | 
			
		||||
 | 
			
		||||
    .info {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      left: 8%;
 | 
			
		||||
      top: 8%;
 | 
			
		||||
      right: 8%;
 | 
			
		||||
      bottom: 8%;
 | 
			
		||||
      height: 84%;
 | 
			
		||||
      width: 84%;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      font-weight: 700;
 | 
			
		||||
      font-size: calc(var(--block-width) * 0.03);
 | 
			
		||||
      text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
 | 
			
		||||
 | 
			
		||||
      h1 {
 | 
			
		||||
        font-size: 6em;
 | 
			
		||||
        line-height: 1;
 | 
			
		||||
        margin-bottom: calc(var(--block-width) * 0.03);
 | 
			
		||||
      }
 | 
			
		||||
      h2 {
 | 
			
		||||
        font-size: 1.8em;
 | 
			
		||||
        line-height: 1;
 | 
			
		||||
        margin-bottom: calc(var(--block-width) * 0.03);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .hash {
 | 
			
		||||
        font-family: monospace;
 | 
			
		||||
        word-wrap: break-word;
 | 
			
		||||
        font-size: 1.4em;
 | 
			
		||||
        line-height: 1;
 | 
			
		||||
        margin-bottom: calc(var(--block-width) * 0.03);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .mined-by {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
        margin: auto;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .block-container {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,208 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router';
 | 
			
		||||
import { catchError } from 'rxjs/operators';
 | 
			
		||||
import { Subject, Subscription, of } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
import { animate, style, transition, trigger } from '@angular/animations';
 | 
			
		||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
 | 
			
		||||
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
 | 
			
		||||
import { CacheService } from '../../services/cache.service';
 | 
			
		||||
import { isMempoolDelta, MempoolBlockDelta } from '../../interfaces/websocket.interface';
 | 
			
		||||
 | 
			
		||||
function bestFitResolution(min, max, n): number {
 | 
			
		||||
  const target = (min + max) / 2;
 | 
			
		||||
  let bestScore = Infinity;
 | 
			
		||||
  let best = null;
 | 
			
		||||
  for (let i = min; i <= max; i++) {
 | 
			
		||||
    const remainder = (n % i);
 | 
			
		||||
    if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
 | 
			
		||||
      bestScore = remainder;
 | 
			
		||||
      best = i;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return best;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-eight-mempool',
 | 
			
		||||
  templateUrl: './eight-mempool.component.html',
 | 
			
		||||
  styleUrls: ['./eight-mempool.component.scss'],
 | 
			
		||||
  animations: [
 | 
			
		||||
    trigger('infoChange', [
 | 
			
		||||
      transition(':enter', [
 | 
			
		||||
        style({ opacity: 0 }),
 | 
			
		||||
        animate('1000ms', style({ opacity: 1 })),
 | 
			
		||||
      ]),
 | 
			
		||||
      transition(':leave', [
 | 
			
		||||
        animate('1000ms 500ms', style({ opacity: 0 }))
 | 
			
		||||
      ])
 | 
			
		||||
    ]),
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class EightMempoolComponent implements OnInit, OnDestroy {
 | 
			
		||||
  network = '';
 | 
			
		||||
  strippedTransactions: { [height: number]: TransactionStripped[] } = {};
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
  hoverTx: string | null = null;
 | 
			
		||||
 | 
			
		||||
  networkChangedSubscription: Subscription;
 | 
			
		||||
  queryParamsSubscription: Subscription;
 | 
			
		||||
  graphChangeSubscription: Subscription;
 | 
			
		||||
  blockSub: Subscription;
 | 
			
		||||
  mempoolBlockSub: Subscription;
 | 
			
		||||
 | 
			
		||||
  chainDirection: string = 'right';
 | 
			
		||||
  poolDirection: string = 'left';
 | 
			
		||||
 | 
			
		||||
  lastBlockHeight: number = 0;
 | 
			
		||||
  lastBlockHeightUpdate: number[] = [];
 | 
			
		||||
  numBlocks: number = 8;
 | 
			
		||||
  blockIndices: number[] = [];
 | 
			
		||||
  autofit: boolean = false;
 | 
			
		||||
  padding: number = 0;
 | 
			
		||||
  wrapBlocks: boolean = false;
 | 
			
		||||
  blockWidth: number = 360;
 | 
			
		||||
  animationDuration: number = 2000;
 | 
			
		||||
  animationOffset: number = 0;
 | 
			
		||||
  stagger: number = 0;
 | 
			
		||||
  testing: boolean = true;
 | 
			
		||||
  testHeight: number = 800000;
 | 
			
		||||
  testShiftTimeout: number;
 | 
			
		||||
 | 
			
		||||
  showInfo: boolean = true;
 | 
			
		||||
  blockInfo: { label: string}[] = [
 | 
			
		||||
    { label: '' },
 | 
			
		||||
    { label: 'mempool' },
 | 
			
		||||
    { label: 'blocks' },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  wrapperStyle = {
 | 
			
		||||
    '--block-width': '1080px',
 | 
			
		||||
    width: '1080px',
 | 
			
		||||
    height: '1080px',
 | 
			
		||||
    maxWidth: '1080px',
 | 
			
		||||
    margin: '',
 | 
			
		||||
  };
 | 
			
		||||
  containerStyle = {};
 | 
			
		||||
  resolution: number = 86;
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private cacheService: CacheService,
 | 
			
		||||
    private bytesPipe: BytesPipe,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.websocketService.want(['blocks', 'mempool-blocks']);
 | 
			
		||||
    this.network = this.stateService.network;
 | 
			
		||||
 | 
			
		||||
    this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
 | 
			
		||||
      // process update
 | 
			
		||||
      if (isMempoolDelta(update)) {
 | 
			
		||||
        // delta
 | 
			
		||||
        this.updateBlock(update);
 | 
			
		||||
      } else {
 | 
			
		||||
        const transactionsStripped = update.transactions;
 | 
			
		||||
        const inOldBlock = {};
 | 
			
		||||
        const inNewBlock = {};
 | 
			
		||||
        const added: TransactionStripped[] = [];
 | 
			
		||||
        const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = [];
 | 
			
		||||
        const removed: string[] = [];
 | 
			
		||||
        for (const tx of transactionsStripped) {
 | 
			
		||||
          inNewBlock[tx.txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
        for (const txid of Object.keys(this.blockGraph?.scenes[this.numBlocks - update.block - 1]?.txs || {})) {
 | 
			
		||||
          inOldBlock[txid] = true;
 | 
			
		||||
          if (!inNewBlock[txid]) {
 | 
			
		||||
            removed.push(txid);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        for (const tx of transactionsStripped) {
 | 
			
		||||
          if (!inOldBlock[tx.txid]) {
 | 
			
		||||
            added.push(tx);
 | 
			
		||||
          } else {
 | 
			
		||||
            changed.push({
 | 
			
		||||
              txid: tx.txid,
 | 
			
		||||
              rate: tx.rate,
 | 
			
		||||
              flags: tx.flags,
 | 
			
		||||
              acc: tx.acc
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        this.updateBlock({
 | 
			
		||||
          block: update.block,
 | 
			
		||||
          removed,
 | 
			
		||||
          changed,
 | 
			
		||||
          added
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.mempoolBlockSub = this.stateService.mempoolBlocks$.subscribe((blocks) => {
 | 
			
		||||
      this.blockInfo[0].label = `+${blocks.length - this.numBlocks}`;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
 | 
			
		||||
      this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
 | 
			
		||||
      this.blockIndices = [...Array(this.numBlocks).keys()];
 | 
			
		||||
      this.lastBlockHeightUpdate = this.blockIndices.map(() => 0);
 | 
			
		||||
      this.autofit = params.autofit !== 'false';
 | 
			
		||||
      this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
 | 
			
		||||
      this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 0;
 | 
			
		||||
      this.wrapBlocks = params.wrap !== 'false';
 | 
			
		||||
      this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
 | 
			
		||||
      this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
 | 
			
		||||
      this.animationOffset = 0;
 | 
			
		||||
 | 
			
		||||
      if (this.autofit) {
 | 
			
		||||
        this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.resolution = 86;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.wrapperStyle = {
 | 
			
		||||
        '--block-width': this.blockWidth + 'px',
 | 
			
		||||
        width: this.blockWidth + 'px',
 | 
			
		||||
        height: this.blockWidth + 'px',
 | 
			
		||||
        maxWidth: this.blockWidth + 'px',
 | 
			
		||||
        margin: (this.padding || 0) +'px ',
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.websocketService.startTrackMempoolBlocks(this.blockIndices);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.networkChangedSubscription = this.stateService.networkChanged$
 | 
			
		||||
      .subscribe((network) => this.network = network);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.stateService.markBlock$.next({});
 | 
			
		||||
    this.blockSub.unsubscribe();
 | 
			
		||||
    this.mempoolBlockSub.unsubscribe();
 | 
			
		||||
    this.networkChangedSubscription?.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription?.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateBlock(delta: MempoolBlockDelta): void {
 | 
			
		||||
    const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeightUpdate[delta.block]);
 | 
			
		||||
    if (blockMined) {
 | 
			
		||||
      this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], this.poolDirection);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.lastBlockHeightUpdate[delta.block] = this.stateService.latestBlockHeight;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -33,6 +33,7 @@ export interface WebsocketResponse {
 | 
			
		||||
  'track-scriptpubkeys'?: string[];
 | 
			
		||||
  'track-asset'?: string;
 | 
			
		||||
  'track-mempool-block'?: number;
 | 
			
		||||
  'track-mempool-blocks'?: number[];
 | 
			
		||||
  'track-rbf'?: string;
 | 
			
		||||
  'track-rbf-summary'?: boolean;
 | 
			
		||||
  'track-accelerations'?: boolean;
 | 
			
		||||
 | 
			
		||||
@ -29,12 +29,14 @@ export class WebsocketService {
 | 
			
		||||
  private isTrackingTx = false;
 | 
			
		||||
  private trackingTxId: string;
 | 
			
		||||
  private isTrackingMempoolBlock = false;
 | 
			
		||||
  private isTrackingMempoolBlocks = false;
 | 
			
		||||
  private isTrackingRbf: 'all' | 'fullRbf' | false = false;
 | 
			
		||||
  private isTrackingRbfSummary = false;
 | 
			
		||||
  private isTrackingAddress: string | false = false;
 | 
			
		||||
  private isTrackingAddresses: string[] | false = false;
 | 
			
		||||
  private isTrackingAccelerations: boolean = false;
 | 
			
		||||
  private trackingMempoolBlock: number;
 | 
			
		||||
  private trackingMempoolBlocks: number[];
 | 
			
		||||
  private stoppingTrackMempoolBlock: any | null = null;
 | 
			
		||||
  private latestGitCommit = '';
 | 
			
		||||
  private onlineCheckTimeout: number;
 | 
			
		||||
@ -122,6 +124,9 @@ export class WebsocketService {
 | 
			
		||||
          if (this.isTrackingMempoolBlock) {
 | 
			
		||||
            this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
 | 
			
		||||
          }
 | 
			
		||||
          if (this.isTrackingMempoolBlocks) {
 | 
			
		||||
            this.startTrackMempoolBlocks(this.trackingMempoolBlocks);
 | 
			
		||||
          }
 | 
			
		||||
          if (this.isTrackingRbf) {
 | 
			
		||||
            this.startTrackRbf(this.isTrackingRbf);
 | 
			
		||||
          }
 | 
			
		||||
@ -218,6 +223,13 @@ export class WebsocketService {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startTrackMempoolBlocks(blocks: number[], force: boolean = false): boolean {
 | 
			
		||||
    this.websocketSubject.next({ 'track-mempool-blocks': blocks });
 | 
			
		||||
    this.isTrackingMempoolBlocks = true;
 | 
			
		||||
    this.trackingMempoolBlocks = blocks;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopTrackMempoolBlock(): void {
 | 
			
		||||
    if (this.stoppingTrackMempoolBlock) {
 | 
			
		||||
      clearTimeout(this.stoppingTrackMempoolBlock);
 | 
			
		||||
@ -231,6 +243,11 @@ export class WebsocketService {
 | 
			
		||||
    }, 2000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopTrackMempoolBlocks(): void {
 | 
			
		||||
    this.websocketSubject.next({ 'track-mempool-blocks': [] });
 | 
			
		||||
    this.isTrackingMempoolBlocks = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startTrackRbf(mode: 'all' | 'fullRbf') {
 | 
			
		||||
    this.websocketSubject.next({ 'track-rbf': mode });
 | 
			
		||||
    this.isTrackingRbf = mode;
 | 
			
		||||
@ -433,20 +450,25 @@ export class WebsocketService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response['projected-block-transactions']) {
 | 
			
		||||
      if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
 | 
			
		||||
        if (response['projected-block-transactions'].blockTransactions) {
 | 
			
		||||
          this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
 | 
			
		||||
      if (response['projected-block-transactions'].index != null) {
 | 
			
		||||
        const update = response['projected-block-transactions'];
 | 
			
		||||
        if (update.blockTransactions) {
 | 
			
		||||
          this.stateService.mempoolBlockUpdate$.next({
 | 
			
		||||
            block: this.trackingMempoolBlock,
 | 
			
		||||
            transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
 | 
			
		||||
            block: update.index,
 | 
			
		||||
            transactions: update.blockTransactions.map(uncompressTx),
 | 
			
		||||
          });
 | 
			
		||||
        } else if (response['projected-block-transactions'].delta) {
 | 
			
		||||
          if (this.stateService.mempoolSequence && response['projected-block-transactions'].sequence !== this.stateService.mempoolSequence + 1) {
 | 
			
		||||
            this.stateService.mempoolSequence = 0;
 | 
			
		||||
            this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
 | 
			
		||||
          } else {
 | 
			
		||||
            this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
 | 
			
		||||
            this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta));
 | 
			
		||||
        } else if (update.delta) {
 | 
			
		||||
          this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta));
 | 
			
		||||
        }
 | 
			
		||||
      } else if (response['projected-block-transactions'].length) {
 | 
			
		||||
        for (const update of response['projected-block-transactions']) {
 | 
			
		||||
          if (update.blockTransactions) {
 | 
			
		||||
            this.stateService.mempoolBlockUpdate$.next({
 | 
			
		||||
              block: update.index,
 | 
			
		||||
              transactions: update.blockTransactions.map(uncompressTx),
 | 
			
		||||
            });
 | 
			
		||||
          } else if (update.delta) {
 | 
			
		||||
            this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe';
 | 
			
		||||
import { StartComponent } from '../components/start/start.component';
 | 
			
		||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
 | 
			
		||||
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
 | 
			
		||||
import { BlockOverviewMultiComponent } from '../components/block-overview-multi/block-overview-multi.component';
 | 
			
		||||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
 | 
			
		||||
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
 | 
			
		||||
import { AddressGroupComponent } from '../components/address-group/address-group.component';
 | 
			
		||||
@ -105,6 +106,7 @@ import { AccelerationSparklesComponent } from '../components/acceleration/sparkl
 | 
			
		||||
 | 
			
		||||
import { BlockViewComponent } from '../components/block-view/block-view.component';
 | 
			
		||||
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
 | 
			
		||||
import { EightMempoolComponent } from '../components/eight-mempool/eight-mempool.component';
 | 
			
		||||
import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component';
 | 
			
		||||
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
 | 
			
		||||
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
 | 
			
		||||
@ -155,6 +157,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
			
		||||
    BlockchainComponent,
 | 
			
		||||
    BlockViewComponent,
 | 
			
		||||
    EightBlocksComponent,
 | 
			
		||||
    EightMempoolComponent,
 | 
			
		||||
    MempoolBlockViewComponent,
 | 
			
		||||
    MempoolBlocksComponent,
 | 
			
		||||
    BlockchainBlocksComponent,
 | 
			
		||||
@ -163,6 +166,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
			
		||||
    PreviewTitleComponent,
 | 
			
		||||
    StartComponent,
 | 
			
		||||
    BlockOverviewGraphComponent,
 | 
			
		||||
    BlockOverviewMultiComponent,
 | 
			
		||||
    BlockOverviewTooltipComponent,
 | 
			
		||||
    BlockFiltersComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
@ -217,6 +221,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
			
		||||
    BitcoinsatoshisPipe,
 | 
			
		||||
    BlockViewComponent,
 | 
			
		||||
    EightBlocksComponent,
 | 
			
		||||
    EightMempoolComponent,
 | 
			
		||||
    MempoolBlockViewComponent,
 | 
			
		||||
    MempoolBlockOverviewComponent,
 | 
			
		||||
    ClockchainComponent,
 | 
			
		||||
@ -306,6 +311,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
			
		||||
    AmountComponent,
 | 
			
		||||
    StartComponent,
 | 
			
		||||
    BlockOverviewGraphComponent,
 | 
			
		||||
    BlockOverviewMultiComponent,
 | 
			
		||||
    BlockOverviewTooltipComponent,
 | 
			
		||||
    BlockFiltersComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user