Extract canvas/webgl code to separate component
This commit is contained in:
		
							parent
							
								
									c5bcf76353
								
							
						
					
					
						commit
						225decd286
					
				@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<div class="block-overview-graph">
 | 
				
			||||||
 | 
					  <canvas class="block-overview-canvas" #blockCanvas></canvas>
 | 
				
			||||||
 | 
					  <div class="loader-wrapper" [class.hidden]="!isLoading">
 | 
				
			||||||
 | 
					    <div class="spinner-border ml-3 loading" role="status"></div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
.mempool-block-overview {
 | 
					.block-overview-graph {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  padding-bottom: 100%;
 | 
					  padding-bottom: 100%;
 | 
				
			||||||
@ -8,12 +8,15 @@
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.block-overview {
 | 
					
 | 
				
			||||||
 | 
					.block-overview-canvas {
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  left: 0;
 | 
					  left: 0;
 | 
				
			||||||
  right: 0;
 | 
					  right: 0;
 | 
				
			||||||
  top: 0;
 | 
					  top: 0;
 | 
				
			||||||
  bottom: 0;
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,6 +30,7 @@
 | 
				
			|||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  transition: opacity 500ms 500ms;
 | 
					  transition: opacity 500ms 500ms;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.hidden {
 | 
					  &.hidden {
 | 
				
			||||||
    opacity: 0;
 | 
					    opacity: 0;
 | 
				
			||||||
@ -0,0 +1,360 @@
 | 
				
			|||||||
 | 
					import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit } from '@angular/core';
 | 
				
			||||||
 | 
					import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
 | 
				
			||||||
 | 
					import { WebsocketService } from 'src/app/services/websocket.service';
 | 
				
			||||||
 | 
					import { FastVertexArray } from './fast-vertex-array';
 | 
				
			||||||
 | 
					import BlockScene from './block-scene';
 | 
				
			||||||
 | 
					import TxSprite from './tx-sprite';
 | 
				
			||||||
 | 
					import TxView from './tx-view';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-block-overview-graph',
 | 
				
			||||||
 | 
					  templateUrl: './block-overview-graph.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./block-overview-graph.component.scss'],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class BlockOverviewGraphComponent implements AfterViewInit {
 | 
				
			||||||
 | 
					  @Input() isLoading: boolean;
 | 
				
			||||||
 | 
					  @Input() resolution: number;
 | 
				
			||||||
 | 
					  @Input() blockLimit: number;
 | 
				
			||||||
 | 
					  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ViewChild('blockCanvas')
 | 
				
			||||||
 | 
					  canvas: ElementRef<HTMLCanvasElement>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  gl: WebGLRenderingContext;
 | 
				
			||||||
 | 
					  animationFrameRequest: number;
 | 
				
			||||||
 | 
					  displayWidth: number;
 | 
				
			||||||
 | 
					  displayHeight: number;
 | 
				
			||||||
 | 
					  cssWidth: number;
 | 
				
			||||||
 | 
					  cssHeight: number;
 | 
				
			||||||
 | 
					  shaderProgram: WebGLProgram;
 | 
				
			||||||
 | 
					  vertexArray: FastVertexArray;
 | 
				
			||||||
 | 
					  running: boolean;
 | 
				
			||||||
 | 
					  scene: BlockScene;
 | 
				
			||||||
 | 
					  hoverTx: TxView | void;
 | 
				
			||||||
 | 
					  selectedTx: TxView | void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    readonly ngZone: NgZone,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngAfterViewInit(): void {
 | 
				
			||||||
 | 
					    this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
 | 
				
			||||||
 | 
					    this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
 | 
				
			||||||
 | 
					    this.gl = this.canvas.nativeElement.getContext('webgl');
 | 
				
			||||||
 | 
					    this.initCanvas();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.resizeCanvas();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  clear(direction): void {
 | 
				
			||||||
 | 
					    this.exit(direction);
 | 
				
			||||||
 | 
					    this.hoverTx = null;
 | 
				
			||||||
 | 
					    this.selectedTx = null;
 | 
				
			||||||
 | 
					    this.txPreviewEvent.emit(null);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  enter(transactions: TransactionStripped[], direction: string): void {
 | 
				
			||||||
 | 
					    if (this.scene) {
 | 
				
			||||||
 | 
					      this.scene.enter(transactions, direction);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  exit(direction: string): void {
 | 
				
			||||||
 | 
					    if (this.scene) {
 | 
				
			||||||
 | 
					      this.scene.exit(direction);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  replace(transactions: TransactionStripped[], direction: string): void {
 | 
				
			||||||
 | 
					    if (this.scene) {
 | 
				
			||||||
 | 
					      this.scene.replace(transactions, direction);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
 | 
				
			||||||
 | 
					    if (this.scene) {
 | 
				
			||||||
 | 
					      this.scene.update(add, remove, direction, resetLayout);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initCanvas(): void {
 | 
				
			||||||
 | 
					    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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleContextRestored(event): void {
 | 
				
			||||||
 | 
					    this.initCanvas();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @HostListener('window:resize', ['$event'])
 | 
				
			||||||
 | 
					  resizeCanvas(): void {
 | 
				
			||||||
 | 
					    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.canvas.nativeElement.width = this.displayWidth;
 | 
				
			||||||
 | 
					    this.canvas.nativeElement.height = this.displayHeight;
 | 
				
			||||||
 | 
					    if (this.gl) {
 | 
				
			||||||
 | 
					      this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.scene) {
 | 
				
			||||||
 | 
					      this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.scene = this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
 | 
				
			||||||
 | 
					        blockLimit: this.blockLimit, vertexArray: this.vertexArray });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  compileShader(src, type): WebGLShader {
 | 
				
			||||||
 | 
					    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 {
 | 
				
			||||||
 | 
					    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.run());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  run(now?: DOMHighResTimeStamp): void {
 | 
				
			||||||
 | 
					    if (!now) {
 | 
				
			||||||
 | 
					      now = performance.now();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* 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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* LOOP */
 | 
				
			||||||
 | 
					    if (this.running) {
 | 
				
			||||||
 | 
					      if (this.animationFrameRequest) {
 | 
				
			||||||
 | 
					        cancelAnimationFrame(this.animationFrameRequest);
 | 
				
			||||||
 | 
					        this.animationFrameRequest = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.animationFrameRequest = requestAnimationFrame(() => this.run());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @HostListener('click', ['$event'])
 | 
				
			||||||
 | 
					  onClick(event) {
 | 
				
			||||||
 | 
					    this.setPreviewTx(event.offsetX, event.offsetY, true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @HostListener('pointermove', ['$event'])
 | 
				
			||||||
 | 
					  onPointerMove(event) {
 | 
				
			||||||
 | 
					    this.setPreviewTx(event.offsetX, event.offsetY, false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @HostListener('pointerleave', ['$event'])
 | 
				
			||||||
 | 
					  onPointerLeave(event) {
 | 
				
			||||||
 | 
					    this.setPreviewTx(-1, -1, false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setPreviewTx(cssX: number, cssY: number, clicked: boolean = false) {
 | 
				
			||||||
 | 
					    const x = cssX * window.devicePixelRatio;
 | 
				
			||||||
 | 
					    const y = cssY * window.devicePixelRatio;
 | 
				
			||||||
 | 
					    if (this.scene && (!this.selectedTx || clicked)) {
 | 
				
			||||||
 | 
					      const selected = this.scene.getTxAt({ x, y });
 | 
				
			||||||
 | 
					      const currentPreview = this.selectedTx || this.hoverTx;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (selected !== currentPreview) {
 | 
				
			||||||
 | 
					        if (currentPreview) {
 | 
				
			||||||
 | 
					          currentPreview.setHover(false);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (selected) {
 | 
				
			||||||
 | 
					          selected.setHover(true);
 | 
				
			||||||
 | 
					          this.txPreviewEvent.emit({
 | 
				
			||||||
 | 
					            txid: selected.txid,
 | 
				
			||||||
 | 
					            fee: selected.fee,
 | 
				
			||||||
 | 
					            vsize: selected.vsize,
 | 
				
			||||||
 | 
					            value: selected.value
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          if (clicked) {
 | 
				
			||||||
 | 
					            this.selectedTx = selected;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.hoverTx = selected;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          if (clicked) {
 | 
				
			||||||
 | 
					            this.selectedTx = null;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          this.hoverTx = null;
 | 
				
			||||||
 | 
					          this.txPreviewEvent.emit(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (clicked) {
 | 
				
			||||||
 | 
					        if (selected === this.selectedTx) {
 | 
				
			||||||
 | 
					          this.hoverTx = this.selectedTx;
 | 
				
			||||||
 | 
					          this.selectedTx = null;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.selectedTx = selected;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// WebGL shader attributes
 | 
				
			||||||
 | 
					const attribs = {
 | 
				
			||||||
 | 
					  offset: { type: 'FLOAT', count: 2, 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 },
 | 
				
			||||||
 | 
					  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 vec2 offset;
 | 
				
			||||||
 | 
					attribute vec4 posX;
 | 
				
			||||||
 | 
					attribute vec4 posY;
 | 
				
			||||||
 | 
					attribute vec4 posR;
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  float radius = interpolateAttribute(posR);
 | 
				
			||||||
 | 
					  vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
							
								
								
									
										724
									
								
								frontend/src/app/components/block-overview-graph/block-scene.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										724
									
								
								frontend/src/app/components/block-overview-graph/block-scene.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,724 @@
 | 
				
			|||||||
 | 
					import { FastVertexArray } from './fast-vertex-array';
 | 
				
			||||||
 | 
					import TxView from './tx-view';
 | 
				
			||||||
 | 
					import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
 | 
				
			||||||
 | 
					import { Position, Square } from './sprite-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class BlockScene {
 | 
				
			||||||
 | 
					  scene: { count: number, offset: { x: number, y: number}};
 | 
				
			||||||
 | 
					  vertexArray: FastVertexArray;
 | 
				
			||||||
 | 
					  txs: { [key: string]: TxView };
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					  gridWidth: number;
 | 
				
			||||||
 | 
					  gridHeight: number;
 | 
				
			||||||
 | 
					  gridSize: number;
 | 
				
			||||||
 | 
					  vbytesPerUnit: number;
 | 
				
			||||||
 | 
					  unitPadding: number;
 | 
				
			||||||
 | 
					  unitWidth: number;
 | 
				
			||||||
 | 
					  initialised: boolean;
 | 
				
			||||||
 | 
					  layout: BlockLayout;
 | 
				
			||||||
 | 
					  dirty: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor({ width, height, resolution, blockLimit, vertexArray }:
 | 
				
			||||||
 | 
					      { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.init({ width, height, resolution, blockLimit, vertexArray });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  destroy(): void {
 | 
				
			||||||
 | 
					    Object.values(this.txs).forEach(tx => tx.destroy());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
 | 
				
			||||||
 | 
					    this.width = width;
 | 
				
			||||||
 | 
					    this.height = height;
 | 
				
			||||||
 | 
					    this.gridSize = this.width / this.gridWidth;
 | 
				
			||||||
 | 
					    this.unitPadding =  width / 500;
 | 
				
			||||||
 | 
					    this.unitWidth = this.gridSize - (this.unitPadding * 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.dirty = true;
 | 
				
			||||||
 | 
					    if (this.initialised && this.scene) {
 | 
				
			||||||
 | 
					      this.updateAll(performance.now());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Animate new block entering scene
 | 
				
			||||||
 | 
					  enter(txs: TransactionStripped[], direction) {
 | 
				
			||||||
 | 
					    this.replace(txs, direction);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Animate block leaving scene
 | 
				
			||||||
 | 
					  exit(direction: string): void {
 | 
				
			||||||
 | 
					    const startTime = performance.now();
 | 
				
			||||||
 | 
					    const removed = this.removeBatch(Object.keys(this.txs), startTime, direction);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // clean up sprites
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      removed.forEach(tx => {
 | 
				
			||||||
 | 
					        tx.destroy();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }, 2000);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Reset layout and replace with new set of transactions
 | 
				
			||||||
 | 
					  replace(txs: TransactionStripped[], direction: string = 'left'): void {
 | 
				
			||||||
 | 
					    const startTime = performance.now();
 | 
				
			||||||
 | 
					    const nextIds = {};
 | 
				
			||||||
 | 
					    const remove = [];
 | 
				
			||||||
 | 
					    txs.forEach(tx => {
 | 
				
			||||||
 | 
					      nextIds[tx.txid] = true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    Object.keys(this.txs).forEach(txid => {
 | 
				
			||||||
 | 
					      if (!nextIds[txid]) {
 | 
				
			||||||
 | 
					        remove.push(txid);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    txs.forEach(tx => {
 | 
				
			||||||
 | 
					      if (!this.txs[tx.txid]) {
 | 
				
			||||||
 | 
					        this.txs[tx.txid] = new TxView(tx, this.vertexArray);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const removed = this.removeBatch(remove, startTime, direction);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // clean up sprites
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      removed.forEach(tx => {
 | 
				
			||||||
 | 
					        tx.destroy();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }, 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.values(this.txs).sort(feeRateDescending).forEach(tx => {
 | 
				
			||||||
 | 
					      this.place(tx);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.updateAll(startTime, direction);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
 | 
				
			||||||
 | 
					    const startTime = performance.now();
 | 
				
			||||||
 | 
					    const removed = this.removeBatch(remove, startTime, direction);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // clean up sprites
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      removed.forEach(tx => {
 | 
				
			||||||
 | 
					        tx.destroy();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }, 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (resetLayout) {
 | 
				
			||||||
 | 
					      add.forEach(tx => {
 | 
				
			||||||
 | 
					        if (!this.txs[tx.txid]) {
 | 
				
			||||||
 | 
					          this.txs[tx.txid] = new TxView(tx, this.vertexArray);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
 | 
				
			||||||
 | 
					      Object.values(this.txs).sort(feeRateDescending).forEach(tx => {
 | 
				
			||||||
 | 
					        this.place(tx);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // try to insert new txs directly
 | 
				
			||||||
 | 
					      const remaining = [];
 | 
				
			||||||
 | 
					      add.map(tx => new TxView(tx, this.vertexArray)).sort(feeRateDescending).forEach(tx => {
 | 
				
			||||||
 | 
					        if (!this.tryInsertByFee(tx)) {
 | 
				
			||||||
 | 
					          remaining.push(tx);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      this.placeBatch(remaining);
 | 
				
			||||||
 | 
					      this.layout.applyGravity();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.updateAll(startTime, direction);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // return the tx at this screen position, if any
 | 
				
			||||||
 | 
					  getTxAt(position: Position): TxView | void {
 | 
				
			||||||
 | 
					    if (this.layout) {
 | 
				
			||||||
 | 
					      const gridPosition = this.screenToGrid(position);
 | 
				
			||||||
 | 
					      return this.layout.getTx(gridPosition);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private init({ width, height, resolution, blockLimit, vertexArray }:
 | 
				
			||||||
 | 
					      { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }
 | 
				
			||||||
 | 
					  ): void {
 | 
				
			||||||
 | 
					    this.vertexArray = vertexArray;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.scene = {
 | 
				
			||||||
 | 
					      count: 0,
 | 
				
			||||||
 | 
					      offset: {
 | 
				
			||||||
 | 
					        x: 0,
 | 
				
			||||||
 | 
					        y: 0
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Set the scale of the visualization (with a 5% margin)
 | 
				
			||||||
 | 
					    this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2);
 | 
				
			||||||
 | 
					    this.gridWidth = resolution;
 | 
				
			||||||
 | 
					    this.gridHeight = resolution;
 | 
				
			||||||
 | 
					    this.resize({ width, height });
 | 
				
			||||||
 | 
					    this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.txs = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.initialised = true;
 | 
				
			||||||
 | 
					    this.dirty = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private insert(tx: TxView, startTime: number, direction: string = 'left'): void {
 | 
				
			||||||
 | 
					    this.txs[tx.txid] = tx;
 | 
				
			||||||
 | 
					    this.place(tx);
 | 
				
			||||||
 | 
					    this.updateTx(tx, startTime, direction);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private updateTx(tx: TxView, startTime: number, direction: string = 'left'): void {
 | 
				
			||||||
 | 
					    if (tx.dirty || this.dirty) {
 | 
				
			||||||
 | 
					      this.saveGridToScreenPosition(tx);
 | 
				
			||||||
 | 
					      this.setTxOnScreen(tx, startTime, direction);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private setTxOnScreen(tx: TxView, startTime: number, direction: string = 'left'): void {
 | 
				
			||||||
 | 
					    if (!tx.initialised) {
 | 
				
			||||||
 | 
					      const txColor = tx.getColor();
 | 
				
			||||||
 | 
					      tx.update({
 | 
				
			||||||
 | 
					        display: {
 | 
				
			||||||
 | 
					          position: {
 | 
				
			||||||
 | 
					            x: tx.screenPosition.x + (direction === 'right' ? -this.width : this.width) * 1.4,
 | 
				
			||||||
 | 
					            y: tx.screenPosition.y,
 | 
				
			||||||
 | 
					            s: tx.screenPosition.s
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          color: txColor,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        start: startTime,
 | 
				
			||||||
 | 
					        delay: 0,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      tx.update({
 | 
				
			||||||
 | 
					        display: {
 | 
				
			||||||
 | 
					          position: tx.screenPosition,
 | 
				
			||||||
 | 
					          color: txColor
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        duration: 1000,
 | 
				
			||||||
 | 
					        start: startTime,
 | 
				
			||||||
 | 
					        delay: 50,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      tx.update({
 | 
				
			||||||
 | 
					        display: {
 | 
				
			||||||
 | 
					          position: tx.screenPosition
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        duration: 1000,
 | 
				
			||||||
 | 
					        minDuration: 500,
 | 
				
			||||||
 | 
					        start: startTime,
 | 
				
			||||||
 | 
					        delay: 50,
 | 
				
			||||||
 | 
					        adjust: true
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private updateAll(startTime: number, direction: string = 'left'): void {
 | 
				
			||||||
 | 
					    this.scene.count = 0;
 | 
				
			||||||
 | 
					    const ids = this.getTxList();
 | 
				
			||||||
 | 
					    startTime = startTime || performance.now();
 | 
				
			||||||
 | 
					    for (const id of ids) {
 | 
				
			||||||
 | 
					      this.updateTx(this.txs[id], startTime, direction);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.dirty = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private remove(id: string, startTime: number, direction: string = 'left'): TxView | void {
 | 
				
			||||||
 | 
					    const tx = this.txs[id];
 | 
				
			||||||
 | 
					    if (tx) {
 | 
				
			||||||
 | 
					      this.layout.remove(tx);
 | 
				
			||||||
 | 
					      tx.update({
 | 
				
			||||||
 | 
					        display: {
 | 
				
			||||||
 | 
					          position: {
 | 
				
			||||||
 | 
					            x: tx.screenPosition.x + (direction === 'right' ? this.width : -this.width) * 1.4,
 | 
				
			||||||
 | 
					            y: this.txs[id].screenPosition.y,
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        duration: 1000,
 | 
				
			||||||
 | 
					        start: startTime,
 | 
				
			||||||
 | 
					        delay: 50
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    delete this.txs[id];
 | 
				
			||||||
 | 
					    return tx;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getTxList(): string[] {
 | 
				
			||||||
 | 
					    return Object.keys(this.txs);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private saveGridToScreenPosition(tx: TxView): void {
 | 
				
			||||||
 | 
					    tx.screenPosition = this.gridToScreen(tx.gridPosition);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // convert grid coordinates to screen coordinates
 | 
				
			||||||
 | 
					  private gridToScreen(position: Square | void): Square {
 | 
				
			||||||
 | 
					    if (position) {
 | 
				
			||||||
 | 
					      const slotSize = (position.s * this.gridSize);
 | 
				
			||||||
 | 
					      const squareSize = slotSize - (this.unitPadding * 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // The grid is laid out notionally left-to-right, bottom-to-top
 | 
				
			||||||
 | 
					      // So we rotate 90deg counterclockwise then flip the y axis
 | 
				
			||||||
 | 
					      //
 | 
				
			||||||
 | 
					      //    grid                             screen
 | 
				
			||||||
 | 
					      //  ________          ________        ________
 | 
				
			||||||
 | 
					      // |        |        |       b|      |       a|
 | 
				
			||||||
 | 
					      // |        | rotate |        | flip |     c  |
 | 
				
			||||||
 | 
					      // |  c     |   -->  |      c |  --> |        |
 | 
				
			||||||
 | 
					      // |a______b|        |_______a|      |_______b|
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
 | 
				
			||||||
 | 
					        y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
 | 
				
			||||||
 | 
					        s: squareSize
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return { x: 0, y: 0, s: 0 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  screenToGrid(position: Position): Position {
 | 
				
			||||||
 | 
					    const grid = {
 | 
				
			||||||
 | 
					      x: Math.floor((position.y - this.unitPadding) / this.gridSize),
 | 
				
			||||||
 | 
					      y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    return grid;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // calculates and returns the size of the tx in multiples of the grid size
 | 
				
			||||||
 | 
					  private txSize(tx: TxView): number {
 | 
				
			||||||
 | 
					    const scale = Math.max(1, Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)));
 | 
				
			||||||
 | 
					    return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private place(tx: TxView): void {
 | 
				
			||||||
 | 
					    const size = this.txSize(tx);
 | 
				
			||||||
 | 
					    this.layout.insert(tx, size);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private tryInsertByFee(tx: TxView): boolean {
 | 
				
			||||||
 | 
					    const size = this.txSize(tx);
 | 
				
			||||||
 | 
					    const position = this.layout.tryInsertByFee(tx, size);
 | 
				
			||||||
 | 
					    if (position) {
 | 
				
			||||||
 | 
					      this.txs[tx.txid] = tx;
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Add a list of transactions to the layout,
 | 
				
			||||||
 | 
					  // keeping everything approximately sorted by feerate.
 | 
				
			||||||
 | 
					  private placeBatch(txs: TxView[]): void {
 | 
				
			||||||
 | 
					    if (txs.length) {
 | 
				
			||||||
 | 
					      // grab the new tx with the highest fee rate
 | 
				
			||||||
 | 
					      txs = txs.sort(feeRateDescending);
 | 
				
			||||||
 | 
					      const maxSize = 2 * txs.reduce((max, tx) => {
 | 
				
			||||||
 | 
					        return Math.max(this.txSize(tx), max);
 | 
				
			||||||
 | 
					      }, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // find a reasonable place for it in the layout
 | 
				
			||||||
 | 
					      const root = this.layout.getReplacementRoot(txs[0].feerate, maxSize);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // extract a sub tree of transactions from the layout, rooted at that point
 | 
				
			||||||
 | 
					      const popped = this.layout.popTree(root.x, root.y, maxSize);
 | 
				
			||||||
 | 
					      // combine those with the new transactions and sort
 | 
				
			||||||
 | 
					      txs = txs.concat(popped);
 | 
				
			||||||
 | 
					      txs = txs.sort(feeRateDescending);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // insert everything back into the layout
 | 
				
			||||||
 | 
					      txs.forEach(tx => {
 | 
				
			||||||
 | 
					        this.txs[tx.txid] = tx;
 | 
				
			||||||
 | 
					        this.place(tx);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private removeBatch(ids: string[], startTime: number, direction: string = 'left'): TxView[] {
 | 
				
			||||||
 | 
					    if (!startTime) {
 | 
				
			||||||
 | 
					      startTime = performance.now();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return ids.map(id => {
 | 
				
			||||||
 | 
					      return this.remove(id, startTime, direction);
 | 
				
			||||||
 | 
					    }).filter(tx => tx != null) as TxView[];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Slot {
 | 
				
			||||||
 | 
					  l: number;
 | 
				
			||||||
 | 
					  r: number;
 | 
				
			||||||
 | 
					  w: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(l: number, r: number) {
 | 
				
			||||||
 | 
					    this.l = l;
 | 
				
			||||||
 | 
					    this.r = r;
 | 
				
			||||||
 | 
					    this.w = r - l;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  intersects(slot: Slot): boolean {
 | 
				
			||||||
 | 
					    return !((slot.r <= this.l) || (slot.l >= this.r));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  subtract(slot: Slot): Slot[] | void {
 | 
				
			||||||
 | 
					    if (this.intersects(slot)) {
 | 
				
			||||||
 | 
					      // from middle
 | 
				
			||||||
 | 
					      if (slot.l > this.l && slot.r < this.r) {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					          new Slot(this.l, slot.l),
 | 
				
			||||||
 | 
					          new Slot(slot.r, this.r)
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					      } // totally covered
 | 
				
			||||||
 | 
					      else if (slot.l <= this.l && slot.r >= this.r) {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					      } // from left side
 | 
				
			||||||
 | 
					      else if (slot.l <= this.l) {
 | 
				
			||||||
 | 
					        if (slot.r === this.r) {
 | 
				
			||||||
 | 
					          return [];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return [new Slot(slot.r, this.r)];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } // from right side
 | 
				
			||||||
 | 
					      else if (slot.r >= this.r) {
 | 
				
			||||||
 | 
					        if (slot.l === this.l) {
 | 
				
			||||||
 | 
					          return [];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return [new Slot(this.l, slot.l)];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return [this];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TxSlot extends Slot {
 | 
				
			||||||
 | 
					  tx: TxView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(l: number, r: number, tx: TxView) {
 | 
				
			||||||
 | 
					    super(l, r);
 | 
				
			||||||
 | 
					    this.tx = tx;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Row {
 | 
				
			||||||
 | 
					  y: number;
 | 
				
			||||||
 | 
					  w: number;
 | 
				
			||||||
 | 
					  filled: TxSlot[];
 | 
				
			||||||
 | 
					  slots: Slot[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(y: number, width: number) {
 | 
				
			||||||
 | 
					    this.y = y;
 | 
				
			||||||
 | 
					    this.w = width;
 | 
				
			||||||
 | 
					    this.filled = [];
 | 
				
			||||||
 | 
					    this.slots = [new Slot(0, this.w)];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // insert a transaction w/ given width into row starting at position x
 | 
				
			||||||
 | 
					  insert(x: number, w: number, tx: TxView): void {
 | 
				
			||||||
 | 
					    const newSlot = new TxSlot(x, x + w, tx);
 | 
				
			||||||
 | 
					    // insert into filled list
 | 
				
			||||||
 | 
					    let index = this.filled.findIndex((slot) => (slot.l >= newSlot.r));
 | 
				
			||||||
 | 
					    if (index < 0) {
 | 
				
			||||||
 | 
					      index = this.filled.length;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.filled.splice(index || 0, 0, newSlot);
 | 
				
			||||||
 | 
					    // subtract from overlapping slots
 | 
				
			||||||
 | 
					    for (let i = 0; i < this.slots.length; i++) {
 | 
				
			||||||
 | 
					      if (newSlot.intersects(this.slots[i])) {
 | 
				
			||||||
 | 
					        const diff = this.slots[i].subtract(newSlot);
 | 
				
			||||||
 | 
					        if (diff) {
 | 
				
			||||||
 | 
					          this.slots.splice(i, 1, ...diff);
 | 
				
			||||||
 | 
					          i += diff.length - 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  remove(x: number, w: number): void {
 | 
				
			||||||
 | 
					    const txIndex = this.filled.findIndex((slot) => (slot.l === x) );
 | 
				
			||||||
 | 
					    this.filled.splice(txIndex, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const newSlot = new Slot(x, x + w);
 | 
				
			||||||
 | 
					    let slotIndex = this.slots.findIndex((slot) => (slot.l >= newSlot.r) );
 | 
				
			||||||
 | 
					    if (slotIndex < 0) {
 | 
				
			||||||
 | 
					      slotIndex = this.slots.length;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.slots.splice(slotIndex || 0, 0, newSlot);
 | 
				
			||||||
 | 
					    this.normalize();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // merge any contiguous empty slots
 | 
				
			||||||
 | 
					  private normalize(): void {
 | 
				
			||||||
 | 
					    for (let i = 0; i < this.slots.length - 1; i++) {
 | 
				
			||||||
 | 
					      if (this.slots[i].r === this.slots[i + 1].l) {
 | 
				
			||||||
 | 
					        this.slots[i].r = this.slots[i + 1].r;
 | 
				
			||||||
 | 
					        this.slots[i].w += this.slots[i + 1].w;
 | 
				
			||||||
 | 
					        this.slots.splice(i + 1, 1);
 | 
				
			||||||
 | 
					        i--;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  txAt(x: number): TxView | void {
 | 
				
			||||||
 | 
					    let i = 0;
 | 
				
			||||||
 | 
					    while (i < this.filled.length && this.filled[i].l <= x) {
 | 
				
			||||||
 | 
					      if (this.filled[i].l <= x && this.filled[i].r > x) {
 | 
				
			||||||
 | 
					        return this.filled[i].tx;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      i++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getSlotsBetween(left: number, right: number): TxSlot[] {
 | 
				
			||||||
 | 
					    const range = new Slot(left, right);
 | 
				
			||||||
 | 
					    return this.filled.filter(slot => {
 | 
				
			||||||
 | 
					      return slot.intersects(range);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  slotAt(x: number): Slot | void {
 | 
				
			||||||
 | 
					    let i = 0;
 | 
				
			||||||
 | 
					    while (i < this.slots.length && this.slots[i].l <= x) {
 | 
				
			||||||
 | 
					      if (this.slots[i].l <= x && this.slots[i].r > x) {
 | 
				
			||||||
 | 
					        return this.slots[i];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      i++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAvgFeerate(): number {
 | 
				
			||||||
 | 
					    let count = 0;
 | 
				
			||||||
 | 
					    let total = 0;
 | 
				
			||||||
 | 
					    this.filled.forEach(slot => {
 | 
				
			||||||
 | 
					      if (slot.tx) {
 | 
				
			||||||
 | 
					        count += slot.w;
 | 
				
			||||||
 | 
					        total += (slot.tx.feerate * slot.w);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return total / count;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BlockLayout {
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					  rows: Row[];
 | 
				
			||||||
 | 
					  txPositions: { [key: string]: Square };
 | 
				
			||||||
 | 
					  txs: { [key: string]: TxView };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor({ width, height }: { width: number, height: number }) {
 | 
				
			||||||
 | 
					    this.width = width;
 | 
				
			||||||
 | 
					    this.height = height;
 | 
				
			||||||
 | 
					    this.rows = [new Row(0, this.width)];
 | 
				
			||||||
 | 
					    this.txPositions = {};
 | 
				
			||||||
 | 
					    this.txs = {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getRow(position: Square): Row {
 | 
				
			||||||
 | 
					    return this.rows[position.y];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getTx(position: Square): TxView | void {
 | 
				
			||||||
 | 
					    if (this.getRow(position)) {
 | 
				
			||||||
 | 
					      return this.getRow(position).txAt(position.x);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addRow(): void {
 | 
				
			||||||
 | 
					    this.rows.push(new Row(this.rows.length, this.width));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  remove(tx: TxView) {
 | 
				
			||||||
 | 
					    const position = this.txPositions[tx.txid];
 | 
				
			||||||
 | 
					    if (position) {
 | 
				
			||||||
 | 
					      for (let y = position.y; y < position.y + position.s && y < this.rows.length; y++) {
 | 
				
			||||||
 | 
					        this.rows[y].remove(position.x, position.s);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    delete this.txPositions[tx.txid];
 | 
				
			||||||
 | 
					    delete this.txs[tx.txid];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  insert(tx: TxView, width: number): Square {
 | 
				
			||||||
 | 
					    const fit = this.fit(tx, width);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // insert the tx into rows at that position
 | 
				
			||||||
 | 
					    for (let y = fit.y; y < fit.y + width; y++) {
 | 
				
			||||||
 | 
					      if (y >= this.rows.length) {
 | 
				
			||||||
 | 
					        this.addRow();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.rows[y].insert(fit.x, width, tx);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const position = { x: fit.x, y: fit.y, s: width };
 | 
				
			||||||
 | 
					    this.txPositions[tx.txid] = position;
 | 
				
			||||||
 | 
					    this.txs[tx.txid] = tx;
 | 
				
			||||||
 | 
					    tx.applyGridPosition(position);
 | 
				
			||||||
 | 
					    return position;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Find the first slot large enough to hold a transaction of this size
 | 
				
			||||||
 | 
					  fit(tx: TxView, width: number): Square {
 | 
				
			||||||
 | 
					    let fit;
 | 
				
			||||||
 | 
					    for (let y = 0; y < this.rows.length && !fit; y++) {
 | 
				
			||||||
 | 
					      fit = this.findFit(0, this.width, y, y, width);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // fall back to placing tx in a new row at the top of the layout
 | 
				
			||||||
 | 
					    if (!fit) {
 | 
				
			||||||
 | 
					      fit = { x: 0, y: this.rows.length };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return fit;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // recursively check rows to see if there's space for a tx (depth-first)
 | 
				
			||||||
 | 
					  // left/right: initial column boundaries to check
 | 
				
			||||||
 | 
					  // row: current row to check
 | 
				
			||||||
 | 
					  // start: starting row
 | 
				
			||||||
 | 
					  // size: size of space needed
 | 
				
			||||||
 | 
					  findFit(left: number, right: number, row: number, start: number, size: number): Square {
 | 
				
			||||||
 | 
					    if ((row - start) >= size || row >= this.rows.length) {
 | 
				
			||||||
 | 
					      return { x: left, y: start };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const slot of this.rows[row].slots) {
 | 
				
			||||||
 | 
					      const l = Math.max(left, slot.l);
 | 
				
			||||||
 | 
					      const r = Math.min(right, slot.r);
 | 
				
			||||||
 | 
					      if (r - l >= size) {
 | 
				
			||||||
 | 
					        const fit = this.findFit(l, r, row + 1, start, size);
 | 
				
			||||||
 | 
					        if (fit) {
 | 
				
			||||||
 | 
					          return fit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // insert only if the tx fits into a fee-appropriate position
 | 
				
			||||||
 | 
					  tryInsertByFee(tx: TxView, size: number): Square | void {
 | 
				
			||||||
 | 
					    const fit = this.fit(tx, size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.checkRowFees(fit.y, tx.feerate)) {
 | 
				
			||||||
 | 
					      // insert the tx into rows at that position
 | 
				
			||||||
 | 
					      for (let y = fit.y; y < fit.y + size; y++) {
 | 
				
			||||||
 | 
					        if (y >= this.rows.length) {
 | 
				
			||||||
 | 
					          this.addRow();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.rows[y].insert(fit.x, size, tx);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const position = { x: fit.x, y: fit.y, s: size };
 | 
				
			||||||
 | 
					      this.txPositions[tx.txid] = position;
 | 
				
			||||||
 | 
					      this.txs[tx.txid] = tx;
 | 
				
			||||||
 | 
					      tx.applyGridPosition(position);
 | 
				
			||||||
 | 
					      return position;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Return the first slot with a lower feerate
 | 
				
			||||||
 | 
					  getReplacementRoot(feerate: number, width: number): Square {
 | 
				
			||||||
 | 
					    let slot;
 | 
				
			||||||
 | 
					    for (let row = 0; row <= this.rows.length; row++) {
 | 
				
			||||||
 | 
					      if (this.rows[row].slots.length > 0) {
 | 
				
			||||||
 | 
					        return { x: this.rows[row].slots[0].l, y: row };
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        slot = this.rows[row].filled.find(x => {
 | 
				
			||||||
 | 
					          return x.tx.feerate < feerate;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (slot) {
 | 
				
			||||||
 | 
					          return { x: Math.min(slot.l, this.width - width), y: row };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return { x: 0, y: this.rows.length };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // remove and return all transactions in a subtree of the layout
 | 
				
			||||||
 | 
					  popTree(x: number, y: number, width: number) {
 | 
				
			||||||
 | 
					    const selected: { [key: string]: TxView } = {};
 | 
				
			||||||
 | 
					    let left = x;
 | 
				
			||||||
 | 
					    let right = x + width;
 | 
				
			||||||
 | 
					    let prevWidth = right - left;
 | 
				
			||||||
 | 
					    let prevFee = Infinity;
 | 
				
			||||||
 | 
					    // scan rows upwards within a channel bounded by 'left' and 'right'
 | 
				
			||||||
 | 
					    for (let row = y; row < this.rows.length; row++) {
 | 
				
			||||||
 | 
					      let rowMax = 0;
 | 
				
			||||||
 | 
					      const slots = this.rows[row].getSlotsBetween(left, right);
 | 
				
			||||||
 | 
					      // check each slot in this row overlapping the search channel
 | 
				
			||||||
 | 
					      slots.forEach(slot => {
 | 
				
			||||||
 | 
					        // select the associated transaction
 | 
				
			||||||
 | 
					        selected[slot.tx.txid] = slot.tx;
 | 
				
			||||||
 | 
					        rowMax = Math.max(rowMax, slot.tx.feerate);
 | 
				
			||||||
 | 
					        // widen the search channel to accommodate this slot if necessary
 | 
				
			||||||
 | 
					        if (slot.w > prevWidth) {
 | 
				
			||||||
 | 
					          left = slot.l;
 | 
				
			||||||
 | 
					          right = slot.r;
 | 
				
			||||||
 | 
					          // if this slot's tx has a higher feerate than the max in the previous row
 | 
				
			||||||
 | 
					          // (i.e. it's out of position)
 | 
				
			||||||
 | 
					          // select all txs overlapping the slot's full width in some rows *below*
 | 
				
			||||||
 | 
					          // to free up space for this tx to sink down to its proper position
 | 
				
			||||||
 | 
					          if (slot.tx.feerate > prevFee) {
 | 
				
			||||||
 | 
					            let count = 0;
 | 
				
			||||||
 | 
					            // keep scanning back down until we find a full row of higher-feerate txs
 | 
				
			||||||
 | 
					            for (let echo = row - 1; echo >= 0 && count < slot.w; echo--) {
 | 
				
			||||||
 | 
					              const echoSlots = this.rows[echo].getSlotsBetween(slot.l, slot.r);
 | 
				
			||||||
 | 
					              count = 0;
 | 
				
			||||||
 | 
					              echoSlots.forEach(echoSlot => {
 | 
				
			||||||
 | 
					                selected[echoSlot.tx.txid] = echoSlot.tx;
 | 
				
			||||||
 | 
					                if (echoSlot.tx.feerate >= slot.tx.feerate) {
 | 
				
			||||||
 | 
					                  count += echoSlot.w;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      prevWidth = right - left;
 | 
				
			||||||
 | 
					      prevFee = rowMax;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const txList = Object.values(selected);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    txList.forEach(tx => {
 | 
				
			||||||
 | 
					      this.remove(tx);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return txList;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check if this row has high enough avg fees
 | 
				
			||||||
 | 
					  // for a tx with this feerate to make sense here
 | 
				
			||||||
 | 
					  checkRowFees(row: number, targetFee: number): boolean {
 | 
				
			||||||
 | 
					    // first row is always fine
 | 
				
			||||||
 | 
					    if (row === 0 || !this.rows[row]) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return (this.rows[row].getAvgFeerate() > (targetFee * 0.9));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // drop any free-floating transactions down into empty spaces
 | 
				
			||||||
 | 
					  applyGravity(): void {
 | 
				
			||||||
 | 
					    Object.entries(this.txPositions).sort(([keyA, posA], [keyB, posB]) => {
 | 
				
			||||||
 | 
					      return posA.y - posB.y || posA.x - posB.x;
 | 
				
			||||||
 | 
					    }).forEach(([txid, position]) => {
 | 
				
			||||||
 | 
					      // see how far this transaction can fall
 | 
				
			||||||
 | 
					      let dropTo = position.y;
 | 
				
			||||||
 | 
					      while (dropTo > 0 && !this.rows[dropTo - 1].getSlotsBetween(position.x, position.x + position.s).length) {
 | 
				
			||||||
 | 
					        dropTo--;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // if it can fall at all
 | 
				
			||||||
 | 
					      if (dropTo < position.y) {
 | 
				
			||||||
 | 
					        // remove and reinsert in the row we found
 | 
				
			||||||
 | 
					        const tx = this.txs[txid];
 | 
				
			||||||
 | 
					        this.remove(tx);
 | 
				
			||||||
 | 
					        this.insert(tx, position.s);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function feeRateDescending(a: TxView, b: TxView) {
 | 
				
			||||||
 | 
					  return b.feerate - a.feerate;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,684 +0,0 @@
 | 
				
			|||||||
import { FastVertexArray } from './fast-vertex-array'
 | 
					 | 
				
			||||||
import TxView from './tx-view'
 | 
					 | 
				
			||||||
import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
 | 
					 | 
				
			||||||
import { Position, Square } from './sprite-types'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class BlockScene {
 | 
					 | 
				
			||||||
  scene: { count: number, offset: { x: number, y: number}};
 | 
					 | 
				
			||||||
  vertexArray: FastVertexArray;
 | 
					 | 
				
			||||||
  txs: { [key: string]: TxView };
 | 
					 | 
				
			||||||
  width: number;
 | 
					 | 
				
			||||||
  height: number;
 | 
					 | 
				
			||||||
  gridWidth: number;
 | 
					 | 
				
			||||||
  gridHeight: number;
 | 
					 | 
				
			||||||
  gridSize: number;
 | 
					 | 
				
			||||||
  vbytesPerUnit: number;
 | 
					 | 
				
			||||||
  unitPadding: number;
 | 
					 | 
				
			||||||
  unitWidth: number;
 | 
					 | 
				
			||||||
  initialised: boolean;
 | 
					 | 
				
			||||||
  layout: BlockLayout;
 | 
					 | 
				
			||||||
  dirty: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }) {
 | 
					 | 
				
			||||||
    this.init({ width, height, resolution, blockLimit, vertexArray })
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  destroy (): void {
 | 
					 | 
				
			||||||
    Object.values(this.txs).forEach(tx => tx.destroy())
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
 | 
					 | 
				
			||||||
    this.width = width;
 | 
					 | 
				
			||||||
    this.height = height;
 | 
					 | 
				
			||||||
    this.gridSize = this.width / this.gridWidth;
 | 
					 | 
				
			||||||
    this.unitPadding =  width / 500;
 | 
					 | 
				
			||||||
    this.unitWidth = this.gridSize - (this.unitPadding * 2);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.dirty = true;
 | 
					 | 
				
			||||||
    if (this.initialised && this.scene) {
 | 
					 | 
				
			||||||
      this.updateAll(performance.now());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Animate new block entering scene
 | 
					 | 
				
			||||||
  enter (txs: TransactionStripped[], direction) {
 | 
					 | 
				
			||||||
    this.replace(txs, direction)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Animate block leaving scene
 | 
					 | 
				
			||||||
  exit (direction: string): void {
 | 
					 | 
				
			||||||
    const startTime = performance.now()
 | 
					 | 
				
			||||||
    const removed = this.removeBatch(Object.keys(this.txs), startTime, direction)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // clean up sprites
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
      removed.forEach(tx => {
 | 
					 | 
				
			||||||
        tx.destroy()
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }, 2000)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Reset layout and replace with new set of transactions
 | 
					 | 
				
			||||||
  replace (txs: TransactionStripped[], direction: string = 'left'): void {
 | 
					 | 
				
			||||||
    const startTime = performance.now()
 | 
					 | 
				
			||||||
    const nextIds = {}
 | 
					 | 
				
			||||||
    const remove = []
 | 
					 | 
				
			||||||
    txs.forEach(tx => {
 | 
					 | 
				
			||||||
      nextIds[tx.txid] = true
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    Object.keys(this.txs).forEach(txid => {
 | 
					 | 
				
			||||||
      if (!nextIds[txid]) remove.push(txid)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    txs.forEach(tx => {
 | 
					 | 
				
			||||||
      if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const removed = this.removeBatch(remove, startTime, direction)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // clean up sprites
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
      removed.forEach(tx => {
 | 
					 | 
				
			||||||
        tx.destroy()
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }, 1000)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
 | 
					 | 
				
			||||||
      this.place(tx)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.updateAll(startTime, direction)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  update (add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
 | 
					 | 
				
			||||||
    const startTime = performance.now()
 | 
					 | 
				
			||||||
    const removed = this.removeBatch(remove, startTime, direction)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // clean up sprites
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
      removed.forEach(tx => {
 | 
					 | 
				
			||||||
        tx.destroy()
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }, 1000)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (resetLayout) {
 | 
					 | 
				
			||||||
      add.forEach(tx => {
 | 
					 | 
				
			||||||
        if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
 | 
					 | 
				
			||||||
      Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
 | 
					 | 
				
			||||||
        this.place(tx)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      // try to insert new txs directly
 | 
					 | 
				
			||||||
      const remaining = []
 | 
					 | 
				
			||||||
      add.map(tx => new TxView(tx, this.vertexArray)).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
 | 
					 | 
				
			||||||
        if (!this.tryInsertByFee(tx)) {
 | 
					 | 
				
			||||||
          remaining.push(tx)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      this.placeBatch(remaining)
 | 
					 | 
				
			||||||
      this.layout.applyGravity()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.updateAll(startTime, direction)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  //return the tx at this screen position, if any
 | 
					 | 
				
			||||||
  getTxAt (position: Position): TxView | void {
 | 
					 | 
				
			||||||
    if (this.layout) {
 | 
					 | 
				
			||||||
      const gridPosition = this.screenToGrid(position)
 | 
					 | 
				
			||||||
      return this.layout.getTx(gridPosition)
 | 
					 | 
				
			||||||
    } else return null
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private init ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }): void {
 | 
					 | 
				
			||||||
    this.vertexArray = vertexArray
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.scene = {
 | 
					 | 
				
			||||||
      count: 0,
 | 
					 | 
				
			||||||
      offset: {
 | 
					 | 
				
			||||||
        x: 0,
 | 
					 | 
				
			||||||
        y: 0
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Set the scale of the visualization (with a 5% margin)
 | 
					 | 
				
			||||||
    this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2)
 | 
					 | 
				
			||||||
    this.gridWidth = resolution
 | 
					 | 
				
			||||||
    this.gridHeight = resolution
 | 
					 | 
				
			||||||
    this.resize({ width, height })
 | 
					 | 
				
			||||||
    this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.txs = {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.initialised = true
 | 
					 | 
				
			||||||
    this.dirty = true
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private insert (tx: TxView, startTime: number, direction: string = 'left'): void {
 | 
					 | 
				
			||||||
    this.txs[tx.txid] = tx
 | 
					 | 
				
			||||||
    this.place(tx)
 | 
					 | 
				
			||||||
    this.updateTx(tx, startTime, direction)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private updateTx (tx: TxView, startTime: number, direction: string = 'left'): void {
 | 
					 | 
				
			||||||
    if (tx.dirty || this.dirty) {
 | 
					 | 
				
			||||||
      this.saveGridToScreenPosition(tx)
 | 
					 | 
				
			||||||
      this.setTxOnScreen(tx, startTime, direction)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private setTxOnScreen (tx: TxView, startTime: number, direction: string = 'left'): void {
 | 
					 | 
				
			||||||
    if (!tx.initialised) {
 | 
					 | 
				
			||||||
      const txColor = tx.getColor()
 | 
					 | 
				
			||||||
      tx.update({
 | 
					 | 
				
			||||||
        display: {
 | 
					 | 
				
			||||||
          position: {
 | 
					 | 
				
			||||||
            x: tx.screenPosition.x + (direction == 'right' ? -this.width : this.width) * 1.4,
 | 
					 | 
				
			||||||
            y: tx.screenPosition.y,
 | 
					 | 
				
			||||||
            s: tx.screenPosition.s
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          color: txColor,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        start: startTime,
 | 
					 | 
				
			||||||
        delay: 0,
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      tx.update({
 | 
					 | 
				
			||||||
        display: {
 | 
					 | 
				
			||||||
          position: tx.screenPosition,
 | 
					 | 
				
			||||||
          color: txColor
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        duration: 1000,
 | 
					 | 
				
			||||||
        start: startTime,
 | 
					 | 
				
			||||||
        delay: 50,
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      tx.update({
 | 
					 | 
				
			||||||
        display: {
 | 
					 | 
				
			||||||
          position: tx.screenPosition
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        duration: 1000,
 | 
					 | 
				
			||||||
        minDuration: 500,
 | 
					 | 
				
			||||||
        start: startTime,
 | 
					 | 
				
			||||||
        delay: 50,
 | 
					 | 
				
			||||||
        adjust: true
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private updateAll (startTime: number, direction: string = 'left'): void {
 | 
					 | 
				
			||||||
    this.scene.count = 0
 | 
					 | 
				
			||||||
    const ids = this.getTxList()
 | 
					 | 
				
			||||||
    startTime = startTime || performance.now()
 | 
					 | 
				
			||||||
    for (let i = 0; i < ids.length; i++) {
 | 
					 | 
				
			||||||
      this.updateTx(this.txs[ids[i]], startTime, direction)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this.dirty = false
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private remove (id: string, startTime: number, direction: string = 'left'): TxView | void {
 | 
					 | 
				
			||||||
    const tx = this.txs[id]
 | 
					 | 
				
			||||||
    if (tx) {
 | 
					 | 
				
			||||||
      this.layout.remove(tx)
 | 
					 | 
				
			||||||
      tx.update({
 | 
					 | 
				
			||||||
        display: {
 | 
					 | 
				
			||||||
          position: {
 | 
					 | 
				
			||||||
            x: tx.screenPosition.x + (direction == 'right' ? this.width : -this.width) * 1.4,
 | 
					 | 
				
			||||||
            y: this.txs[id].screenPosition.y,
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        duration: 1000,
 | 
					 | 
				
			||||||
        start: startTime,
 | 
					 | 
				
			||||||
        delay: 50
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    delete this.txs[id]
 | 
					 | 
				
			||||||
    return tx
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private getTxList (): string[] {
 | 
					 | 
				
			||||||
    return Object.keys(this.txs)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private saveGridToScreenPosition (tx: TxView): void {
 | 
					 | 
				
			||||||
    tx.screenPosition = this.gridToScreen(tx.gridPosition)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // convert grid coordinates to screen coordinates
 | 
					 | 
				
			||||||
  private gridToScreen (position: Square | void): Square {
 | 
					 | 
				
			||||||
    if (position) {
 | 
					 | 
				
			||||||
      const slotSize = (position.s * this.gridSize)
 | 
					 | 
				
			||||||
      const squareSize = slotSize - (this.unitPadding * 2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // The grid is laid out notionally left-to-right, bottom-to-top
 | 
					 | 
				
			||||||
      // So we rotate 90deg counterclockwise then flip the y axis
 | 
					 | 
				
			||||||
      //
 | 
					 | 
				
			||||||
      //    grid                             screen
 | 
					 | 
				
			||||||
      //  ________          ________        ________
 | 
					 | 
				
			||||||
      // |        |        |       b|      |       a|
 | 
					 | 
				
			||||||
      // |        | rotate |        | flip |     c  |
 | 
					 | 
				
			||||||
      // |  c     |   -->  |      c |  --> |        |
 | 
					 | 
				
			||||||
      // |a______b|        |_______a|      |_______b|
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
 | 
					 | 
				
			||||||
        y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
 | 
					 | 
				
			||||||
        s: squareSize
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return { x: 0, y: 0, s: 0 }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  screenToGrid (position: Position): Position {
 | 
					 | 
				
			||||||
    const grid = {
 | 
					 | 
				
			||||||
      x: Math.floor((position.y - this.unitPadding) / this.gridSize),
 | 
					 | 
				
			||||||
      y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return grid
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // calculates and returns the size of the tx in multiples of the grid size
 | 
					 | 
				
			||||||
  private txSize (tx: TxView): number {
 | 
					 | 
				
			||||||
    let scale = Math.max(1,Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)))
 | 
					 | 
				
			||||||
    return Math.min(this.gridWidth, Math.max(1, scale)) // bound between 1 and the max displayable size (just in case!)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private place (tx: TxView): void {
 | 
					 | 
				
			||||||
    const size = this.txSize(tx)
 | 
					 | 
				
			||||||
    this.layout.insert(tx, size)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private tryInsertByFee (tx: TxView): boolean {
 | 
					 | 
				
			||||||
    const size = this.txSize(tx)
 | 
					 | 
				
			||||||
    const position = this.layout.tryInsertByFee(tx, size)
 | 
					 | 
				
			||||||
    if (position) {
 | 
					 | 
				
			||||||
      this.txs[tx.txid] = tx
 | 
					 | 
				
			||||||
      return true
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Add a list of transactions to the layout,
 | 
					 | 
				
			||||||
  // keeping everything approximately sorted by feerate.
 | 
					 | 
				
			||||||
  private placeBatch (txs: TxView[]): void {
 | 
					 | 
				
			||||||
    if (txs.length) {
 | 
					 | 
				
			||||||
      // grab the new tx with the highest fee rate
 | 
					 | 
				
			||||||
      txs = txs.sort((a,b) => { return b.feerate - a.feerate })
 | 
					 | 
				
			||||||
      let i = 0
 | 
					 | 
				
			||||||
      let maxSize = txs.reduce((max, tx) => {
 | 
					 | 
				
			||||||
        return Math.max(this.txSize(tx), max)
 | 
					 | 
				
			||||||
      }, 1) * 2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // find a reasonable place for it in the layout
 | 
					 | 
				
			||||||
      const root = this.layout.getReplacementRoot(txs[0].feerate, maxSize)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // extract a sub tree of transactions from the layout, rooted at that point
 | 
					 | 
				
			||||||
      const popped = this.layout.popTree(root.x, root.y, maxSize)
 | 
					 | 
				
			||||||
      // combine those with the new transactions and sort
 | 
					 | 
				
			||||||
      txs = txs.concat(popped)
 | 
					 | 
				
			||||||
      txs = txs.sort((a,b) => { return b.feerate - a.feerate })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // insert everything back into the layout
 | 
					 | 
				
			||||||
      txs.forEach(tx => {
 | 
					 | 
				
			||||||
        this.txs[tx.txid] = tx
 | 
					 | 
				
			||||||
        this.place(tx)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private removeBatch (ids: string[], startTime: number, direction: string = 'left'): TxView[] {
 | 
					 | 
				
			||||||
    if (!startTime) startTime = performance.now()
 | 
					 | 
				
			||||||
    return ids.map(id => {
 | 
					 | 
				
			||||||
      return this.remove(id, startTime, direction)
 | 
					 | 
				
			||||||
    }).filter(tx => tx != null) as TxView[]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Slot {
 | 
					 | 
				
			||||||
  l: number
 | 
					 | 
				
			||||||
  r: number
 | 
					 | 
				
			||||||
  w: number
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor (l: number, r: number) {
 | 
					 | 
				
			||||||
    this.l = l
 | 
					 | 
				
			||||||
    this.r = r
 | 
					 | 
				
			||||||
    this.w = r - l
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  intersects (slot: Slot): boolean {
 | 
					 | 
				
			||||||
    return !((slot.r <= this.l) || (slot.l >= this.r))
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  subtract (slot: Slot): Slot[] | void {
 | 
					 | 
				
			||||||
    if (this.intersects(slot)) {
 | 
					 | 
				
			||||||
      // from middle
 | 
					 | 
				
			||||||
      if (slot.l > this.l && slot.r < this.r) {
 | 
					 | 
				
			||||||
        return [
 | 
					 | 
				
			||||||
          new Slot(this.l, slot.l),
 | 
					 | 
				
			||||||
          new Slot(slot.r, this.r)
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      } // totally covered
 | 
					 | 
				
			||||||
      else if (slot.l <= this.l && slot.r >= this.r) {
 | 
					 | 
				
			||||||
        return []
 | 
					 | 
				
			||||||
      } // from left side
 | 
					 | 
				
			||||||
      else if (slot.l <= this.l) {
 | 
					 | 
				
			||||||
        if (slot.r == this.r) return []
 | 
					 | 
				
			||||||
        else return [new Slot(slot.r, this.r)]
 | 
					 | 
				
			||||||
      } // from right side
 | 
					 | 
				
			||||||
      else if (slot.r >= this.r) {
 | 
					 | 
				
			||||||
        if (slot.l == this.l) return []
 | 
					 | 
				
			||||||
        else return [new Slot(this.l, slot.l)]
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else return [this]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TxSlot extends Slot {
 | 
					 | 
				
			||||||
  tx: TxView
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor (l: number, r: number, tx: TxView) {
 | 
					 | 
				
			||||||
    super(l, r)
 | 
					 | 
				
			||||||
    this.tx = tx
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Row {
 | 
					 | 
				
			||||||
  y: number
 | 
					 | 
				
			||||||
  w: number
 | 
					 | 
				
			||||||
  filled: TxSlot[]
 | 
					 | 
				
			||||||
  slots: Slot[]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor (y: number, width: number) {
 | 
					 | 
				
			||||||
    this.y = y
 | 
					 | 
				
			||||||
    this.w = width
 | 
					 | 
				
			||||||
    this.filled = []
 | 
					 | 
				
			||||||
    this.slots = [new Slot(0, this.w)]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // insert a transaction w/ given width into row starting at position x
 | 
					 | 
				
			||||||
  insert (x: number, w: number, tx: TxView): void {
 | 
					 | 
				
			||||||
    const newSlot = new TxSlot(x, x + w, tx)
 | 
					 | 
				
			||||||
    // insert into filled list
 | 
					 | 
				
			||||||
    let index = this.filled.findIndex((slot) => { return slot.l >= newSlot.r })
 | 
					 | 
				
			||||||
    if (index < 0) index = this.filled.length
 | 
					 | 
				
			||||||
    this.filled.splice(index || 0, 0, newSlot)
 | 
					 | 
				
			||||||
    // subtract from overlapping slots
 | 
					 | 
				
			||||||
    for (let i = 0; i < this.slots.length; i++) {
 | 
					 | 
				
			||||||
      if (newSlot.intersects(this.slots[i])) {
 | 
					 | 
				
			||||||
        const diff = this.slots[i].subtract(newSlot)
 | 
					 | 
				
			||||||
        if (diff) {
 | 
					 | 
				
			||||||
          this.slots.splice(i, 1, ...diff)
 | 
					 | 
				
			||||||
          i += diff.length - 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  remove (x: number, w: number): void {
 | 
					 | 
				
			||||||
    const txIndex = this.filled.findIndex((slot) => { return slot.l == x })
 | 
					 | 
				
			||||||
    this.filled.splice(txIndex, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const newSlot = new Slot(x, x + w)
 | 
					 | 
				
			||||||
    let slotIndex = this.slots.findIndex((slot) => { return slot.l >= newSlot.r })
 | 
					 | 
				
			||||||
    if (slotIndex < 0) slotIndex = this.slots.length
 | 
					 | 
				
			||||||
    this.slots.splice(slotIndex || 0, 0, newSlot)
 | 
					 | 
				
			||||||
    this.normalize()
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // merge any contiguous empty slots
 | 
					 | 
				
			||||||
  private normalize (): void {
 | 
					 | 
				
			||||||
    for (let i = 0; i < this.slots.length - 1; i++) {
 | 
					 | 
				
			||||||
      if (this.slots[i].r == this.slots[i+1].l) {
 | 
					 | 
				
			||||||
        this.slots[i].r = this.slots[i+1].r
 | 
					 | 
				
			||||||
        this.slots[i].w += this.slots[i+1].w
 | 
					 | 
				
			||||||
        this.slots.splice(i+1, 1)
 | 
					 | 
				
			||||||
        i--
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  txAt (x: number): TxView | void {
 | 
					 | 
				
			||||||
    let i = 0
 | 
					 | 
				
			||||||
    while (i < this.filled.length && this.filled[i].l <= x) {
 | 
					 | 
				
			||||||
      if (this.filled[i].l <= x && this.filled[i].r > x) return this.filled[i].tx
 | 
					 | 
				
			||||||
      i++
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getSlotsBetween (left: number, right: number): TxSlot[] {
 | 
					 | 
				
			||||||
    const range = new Slot(left, right)
 | 
					 | 
				
			||||||
    return this.filled.filter(slot => {
 | 
					 | 
				
			||||||
      return slot.intersects(range)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  slotAt (x: number): Slot | void {
 | 
					 | 
				
			||||||
    let i = 0
 | 
					 | 
				
			||||||
    while (i < this.slots.length && this.slots[i].l <= x) {
 | 
					 | 
				
			||||||
      if (this.slots[i].l <= x && this.slots[i].r > x) return this.slots[i]
 | 
					 | 
				
			||||||
      i++
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getAvgFeerate (): number {
 | 
					 | 
				
			||||||
    let count = 0
 | 
					 | 
				
			||||||
    let total = 0
 | 
					 | 
				
			||||||
    this.filled.forEach(slot => {
 | 
					 | 
				
			||||||
      if (slot.tx) {
 | 
					 | 
				
			||||||
        count += slot.w
 | 
					 | 
				
			||||||
        total += (slot.tx.feerate * slot.w)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    return total / count
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BlockLayout {
 | 
					 | 
				
			||||||
  width: number;
 | 
					 | 
				
			||||||
  height: number;
 | 
					 | 
				
			||||||
  rows: Row[];
 | 
					 | 
				
			||||||
  txPositions: { [key: string]: Square }
 | 
					 | 
				
			||||||
  txs: { [key: string]: TxView }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor ({ width, height } : { width: number, height: number }) {
 | 
					 | 
				
			||||||
    this.width = width
 | 
					 | 
				
			||||||
    this.height = height
 | 
					 | 
				
			||||||
    this.rows = [new Row(0, this.width)]
 | 
					 | 
				
			||||||
    this.txPositions = {}
 | 
					 | 
				
			||||||
    this.txs = {}
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getRow (position: Square): Row {
 | 
					 | 
				
			||||||
    return this.rows[position.y]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getTx (position: Square): TxView | void {
 | 
					 | 
				
			||||||
    if (this.getRow(position)) {
 | 
					 | 
				
			||||||
      return this.getRow(position).txAt(position.x)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  addRow (): void {
 | 
					 | 
				
			||||||
    this.rows.push(new Row(this.rows.length, this.width))
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  remove (tx: TxView) {
 | 
					 | 
				
			||||||
    const position = this.txPositions[tx.txid]
 | 
					 | 
				
			||||||
    if (position) {
 | 
					 | 
				
			||||||
      for (let y = position.y; y < position.y + position.s && y < this.rows.length; y++) {
 | 
					 | 
				
			||||||
        this.rows[y].remove(position.x, position.s)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    delete this.txPositions[tx.txid]
 | 
					 | 
				
			||||||
    delete this.txs[tx.txid]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  insert (tx: TxView, width: number): Square {
 | 
					 | 
				
			||||||
    const fit = this.fit(tx, width)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // insert the tx into rows at that position
 | 
					 | 
				
			||||||
    for (let y = fit.y; y < fit.y + width; y++) {
 | 
					 | 
				
			||||||
      if (y >= this.rows.length) this.addRow()
 | 
					 | 
				
			||||||
      this.rows[y].insert(fit.x, width, tx)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const position = { x: fit.x, y: fit.y, s: width }
 | 
					 | 
				
			||||||
    this.txPositions[tx.txid] = position
 | 
					 | 
				
			||||||
    this.txs[tx.txid] = tx
 | 
					 | 
				
			||||||
    tx.applyGridPosition(position)
 | 
					 | 
				
			||||||
    return position
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Find the first slot large enough to hold a transaction of this size
 | 
					 | 
				
			||||||
  fit (tx: TxView, width: number): Square {
 | 
					 | 
				
			||||||
    let fit
 | 
					 | 
				
			||||||
    for (let y = 0; y < this.rows.length && !fit; y++) {
 | 
					 | 
				
			||||||
      fit = this.findFit(0, this.width, y, y, width)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // fall back to placing tx in a new row at the top of the layout
 | 
					 | 
				
			||||||
    if (!fit) {
 | 
					 | 
				
			||||||
      fit = { x: 0, y: this.rows.length }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return fit
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // recursively check rows to see if there's space for a tx (depth-first)
 | 
					 | 
				
			||||||
  // left/right: initial column boundaries to check
 | 
					 | 
				
			||||||
  // row: current row to check
 | 
					 | 
				
			||||||
  // start: starting row
 | 
					 | 
				
			||||||
  // size: size of space needed
 | 
					 | 
				
			||||||
  findFit (left: number, right: number, row: number, start: number, size: number) : Square {
 | 
					 | 
				
			||||||
    if ((row - start) >= size || row >= this.rows.length) {
 | 
					 | 
				
			||||||
      return { x: left, y: start }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    for (let i = 0; i < this.rows[row].slots.length; i++) {
 | 
					 | 
				
			||||||
      const slot = this.rows[row].slots[i]
 | 
					 | 
				
			||||||
      const l = Math.max(left, slot.l)
 | 
					 | 
				
			||||||
      const r = Math.min(right, slot.r)
 | 
					 | 
				
			||||||
      if (r - l >= size) {
 | 
					 | 
				
			||||||
        const fit = this.findFit(l, r, row + 1, start, size)
 | 
					 | 
				
			||||||
        if (fit) return fit
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // insert only if the tx fits into a fee-appropriate position
 | 
					 | 
				
			||||||
  tryInsertByFee (tx: TxView, size: number): Square | void {
 | 
					 | 
				
			||||||
    const fit = this.fit(tx, size)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.checkRowFees(fit.y, tx.feerate)) {
 | 
					 | 
				
			||||||
      // insert the tx into rows at that position
 | 
					 | 
				
			||||||
      for (let y = fit.y; y < fit.y + size; y++) {
 | 
					 | 
				
			||||||
        if (y >= this.rows.length) this.addRow()
 | 
					 | 
				
			||||||
        this.rows[y].insert(fit.x, size, tx)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const position = { x: fit.x, y: fit.y, s: size }
 | 
					 | 
				
			||||||
      this.txPositions[tx.txid] = position
 | 
					 | 
				
			||||||
      this.txs[tx.txid] = tx
 | 
					 | 
				
			||||||
      tx.applyGridPosition(position)
 | 
					 | 
				
			||||||
      return position
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Return the first slot with a lower feerate
 | 
					 | 
				
			||||||
  getReplacementRoot (feerate: number, width: number): Square {
 | 
					 | 
				
			||||||
    let slot
 | 
					 | 
				
			||||||
    for (let row = 0; row <= this.rows.length; row++) {
 | 
					 | 
				
			||||||
      if (this.rows[row].slots.length > 0) {
 | 
					 | 
				
			||||||
        return { x: this.rows[row].slots[0].l, y: row }
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        slot = this.rows[row].filled.find(x => {
 | 
					 | 
				
			||||||
          return x.tx.feerate < feerate
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        if (slot) {
 | 
					 | 
				
			||||||
          return { x: Math.min(slot.l, this.width - width), y: row }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return { x: 0, y: this.rows.length }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // remove and return all transactions in a subtree of the layout
 | 
					 | 
				
			||||||
  popTree (x: number, y: number, width: number) {
 | 
					 | 
				
			||||||
    const selected: { [key: string]: TxView } = {}
 | 
					 | 
				
			||||||
    let left = x
 | 
					 | 
				
			||||||
    let right = x + width
 | 
					 | 
				
			||||||
    let prevWidth = right - left
 | 
					 | 
				
			||||||
    let prevFee = Infinity
 | 
					 | 
				
			||||||
    // scan rows upwards within a channel bounded by 'left' and 'right'
 | 
					 | 
				
			||||||
    for (let row = y; row < this.rows.length; row++) {
 | 
					 | 
				
			||||||
      let rowMax = 0
 | 
					 | 
				
			||||||
      let slots = this.rows[row].getSlotsBetween(left, right)
 | 
					 | 
				
			||||||
      // check each slot in this row overlapping the search channel
 | 
					 | 
				
			||||||
      slots.forEach(slot => {
 | 
					 | 
				
			||||||
        // select the associated transaction
 | 
					 | 
				
			||||||
        selected[slot.tx.txid] = slot.tx
 | 
					 | 
				
			||||||
        rowMax = Math.max(rowMax, slot.tx.feerate)
 | 
					 | 
				
			||||||
        // widen the search channel to accommodate this slot if necessary
 | 
					 | 
				
			||||||
        if (slot.w > prevWidth) {
 | 
					 | 
				
			||||||
          left = slot.l
 | 
					 | 
				
			||||||
          right = slot.r
 | 
					 | 
				
			||||||
          // if this slot's tx has a higher feerate than the max in the previous row
 | 
					 | 
				
			||||||
          // (i.e. it's out of position)
 | 
					 | 
				
			||||||
          // select all txs overlapping the slot's full width in some rows *below*
 | 
					 | 
				
			||||||
          // to free up space for this tx to sink down to its proper position
 | 
					 | 
				
			||||||
          if (slot.tx.feerate > prevFee) {
 | 
					 | 
				
			||||||
            let count = 0
 | 
					 | 
				
			||||||
            // keep scanning back down until we find a full row of higher-feerate txs
 | 
					 | 
				
			||||||
            for (let echo = row - 1; echo >= 0 && count < slot.w; echo--) {
 | 
					 | 
				
			||||||
              let echoSlots = this.rows[echo].getSlotsBetween(slot.l, slot.r)
 | 
					 | 
				
			||||||
              count = 0
 | 
					 | 
				
			||||||
              echoSlots.forEach(echoSlot => {
 | 
					 | 
				
			||||||
                selected[echoSlot.tx.txid] = echoSlot.tx
 | 
					 | 
				
			||||||
                if (echoSlot.tx.feerate >= slot.tx.feerate) {
 | 
					 | 
				
			||||||
                  count += echoSlot.w
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              })
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      prevWidth = right - left
 | 
					 | 
				
			||||||
      prevFee = rowMax
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const txList = Object.values(selected)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    txList.forEach(tx => {
 | 
					 | 
				
			||||||
      this.remove(tx)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    return txList
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Check if this row has high enough avg fees
 | 
					 | 
				
			||||||
  // for a tx with this feerate to make sense here
 | 
					 | 
				
			||||||
  checkRowFees (row: number, targetFee: number): boolean {
 | 
					 | 
				
			||||||
    // first row is always fine
 | 
					 | 
				
			||||||
    if (row == 0 || !this.rows[row]) return true
 | 
					 | 
				
			||||||
    return (this.rows[row].getAvgFeerate() > (targetFee * 0.9))
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // drop any free-floating transactions down into empty spaces
 | 
					 | 
				
			||||||
  applyGravity (): void {
 | 
					 | 
				
			||||||
    Object.entries(this.txPositions).sort(([keyA, posA], [keyB, posB]) => {
 | 
					 | 
				
			||||||
      return posA.y - posB.y || posA.x - posB.x
 | 
					 | 
				
			||||||
    }).forEach(([txid, position]) => {
 | 
					 | 
				
			||||||
      // see how far this transaction can fall
 | 
					 | 
				
			||||||
      let dropTo = position.y
 | 
					 | 
				
			||||||
      while (dropTo > 0 && !this.rows[dropTo - 1].getSlotsBetween(position.x, position.x + position.s).length) {
 | 
					 | 
				
			||||||
        dropTo--;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      // if it can fall at all
 | 
					 | 
				
			||||||
      if (dropTo < position.y) {
 | 
					 | 
				
			||||||
        // remove and reinsert in the row we found
 | 
					 | 
				
			||||||
        const tx = this.txs[txid]
 | 
					 | 
				
			||||||
        this.remove(tx)
 | 
					 | 
				
			||||||
        this.insert(tx, position.s)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
<div class="mempool-block-overview">
 | 
					<app-block-overview-graph
 | 
				
			||||||
  <canvas class="block-overview" [style.width]="cssWidth + 'px'" [style.height]="cssHeight + 'px'" #blockCanvas></canvas>
 | 
					  #blockGraph
 | 
				
			||||||
  <div class="loader-wrapper" [class.hidden]="!(isLoading$ | async)">
 | 
					  [isLoading]="isLoading$ | async"
 | 
				
			||||||
    <div class="spinner-border ml-3 loading" role="status"></div>
 | 
					  [resolution]="75"
 | 
				
			||||||
  </div>
 | 
					  [blockLimit]="stateService.blockVSize"
 | 
				
			||||||
</div>
 | 
					  (txPreviewEvent)="onTxPreview($event)">
 | 
				
			||||||
 | 
					</app-block-overview-graph>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,40 +1,23 @@
 | 
				
			|||||||
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit,
 | 
					import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
 | 
				
			||||||
  OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone, AfterViewInit } from '@angular/core';
 | 
					  OnDestroy, OnChanges, ChangeDetectionStrategy, AfterViewInit } from '@angular/core';
 | 
				
			||||||
import { StateService } from 'src/app/services/state.service';
 | 
					import { StateService } from 'src/app/services/state.service';
 | 
				
			||||||
import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
 | 
					import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
 | 
				
			||||||
 | 
					import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component';
 | 
				
			||||||
import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
 | 
					import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
 | 
				
			||||||
import { switchMap, filter } from 'rxjs/operators';
 | 
					import { switchMap, filter } from 'rxjs/operators';
 | 
				
			||||||
import { WebsocketService } from 'src/app/services/websocket.service';
 | 
					import { WebsocketService } from 'src/app/services/websocket.service';
 | 
				
			||||||
import { FastVertexArray } from './fast-vertex-array';
 | 
					 | 
				
			||||||
import BlockScene from './block-scene';
 | 
					 | 
				
			||||||
import TxSprite from './tx-sprite';
 | 
					 | 
				
			||||||
import TxView from './tx-view';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-mempool-block-overview',
 | 
					  selector: 'app-mempool-block-overview',
 | 
				
			||||||
  templateUrl: './mempool-block-overview.component.html',
 | 
					  templateUrl: './mempool-block-overview.component.html',
 | 
				
			||||||
  styleUrls: ['./mempool-block-overview.component.scss'],
 | 
					 | 
				
			||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
 | 
					export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, AfterViewInit {
 | 
				
			||||||
  @Input() index: number;
 | 
					  @Input() index: number;
 | 
				
			||||||
  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
					  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ViewChild('blockCanvas')
 | 
					  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
				
			||||||
  canvas: ElementRef<HTMLCanvasElement>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  gl: WebGLRenderingContext;
 | 
					 | 
				
			||||||
  animationFrameRequest: number;
 | 
					 | 
				
			||||||
  displayWidth: number;
 | 
					 | 
				
			||||||
  displayHeight: number;
 | 
					 | 
				
			||||||
  cssWidth: number;
 | 
					 | 
				
			||||||
  cssHeight: number;
 | 
					 | 
				
			||||||
  shaderProgram: WebGLProgram;
 | 
					 | 
				
			||||||
  vertexArray: FastVertexArray;
 | 
					 | 
				
			||||||
  running: boolean;
 | 
					 | 
				
			||||||
  scene: BlockScene;
 | 
					 | 
				
			||||||
  hoverTx: TxView | void;
 | 
					 | 
				
			||||||
  selectedTx: TxView | void;
 | 
					 | 
				
			||||||
  lastBlockHeight: number;
 | 
					  lastBlockHeight: number;
 | 
				
			||||||
  blockIndex: number;
 | 
					  blockIndex: number;
 | 
				
			||||||
  isLoading$ = new BehaviorSubject<boolean>(true);
 | 
					  isLoading$ = new BehaviorSubject<boolean>(true);
 | 
				
			||||||
@ -44,13 +27,10 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public stateService: StateService,
 | 
					    public stateService: StateService,
 | 
				
			||||||
    private websocketService: WebsocketService,
 | 
					    private websocketService: WebsocketService
 | 
				
			||||||
    readonly ngZone: NgZone,
 | 
					  ) { }
 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit(): void {
 | 
					  ngAfterViewInit(): void {
 | 
				
			||||||
    this.blockSub = merge(
 | 
					    this.blockSub = merge(
 | 
				
			||||||
        of(true),
 | 
					        of(true),
 | 
				
			||||||
        this.stateService.connectionState$.pipe(filter((state) => state === 2))
 | 
					        this.stateService.connectionState$.pipe(filter((state) => state === 2))
 | 
				
			||||||
@ -64,18 +44,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngAfterViewInit(): void {
 | 
					 | 
				
			||||||
    this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
 | 
					 | 
				
			||||||
    this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
 | 
					 | 
				
			||||||
    this.gl = this.canvas.nativeElement.getContext('webgl');
 | 
					 | 
				
			||||||
    this.initCanvas();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.resizeCanvas();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ngOnChanges(changes): void {
 | 
					  ngOnChanges(changes): void {
 | 
				
			||||||
    if (changes.index) {
 | 
					    if (changes.index) {
 | 
				
			||||||
      this.clearBlock(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left');
 | 
					      if (this.blockGraph) {
 | 
				
			||||||
 | 
					        this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      this.isLoading$.next(true);
 | 
					      this.isLoading$.next(true);
 | 
				
			||||||
      this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
 | 
					      this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -87,26 +60,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
				
			|||||||
    this.websocketService.stopTrackMempoolBlock();
 | 
					    this.websocketService.stopTrackMempoolBlock();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  clearBlock(direction): void {
 | 
					 | 
				
			||||||
    if (this.scene) {
 | 
					 | 
				
			||||||
      this.scene.exit(direction);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this.hoverTx = null;
 | 
					 | 
				
			||||||
    this.selectedTx = null;
 | 
					 | 
				
			||||||
    this.txPreviewEvent.emit(null);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  replaceBlock(transactionsStripped: TransactionStripped[]): void {
 | 
					  replaceBlock(transactionsStripped: TransactionStripped[]): void {
 | 
				
			||||||
    if (!this.scene) {
 | 
					 | 
				
			||||||
      this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
 | 
					 | 
				
			||||||
        blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
 | 
					    const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
 | 
				
			||||||
    if (this.blockIndex !== this.index) {
 | 
					    if (this.blockIndex !== this.index) {
 | 
				
			||||||
      const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
 | 
					      const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
 | 
				
			||||||
      this.scene.enter(transactionsStripped, direction);
 | 
					      this.blockGraph.enter(transactionsStripped, direction);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.scene.replace(transactionsStripped, blockMined ? 'right' : 'left');
 | 
					      this.blockGraph.replace(transactionsStripped, blockMined ? 'right' : 'left');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.lastBlockHeight = this.stateService.latestBlockHeight;
 | 
					    this.lastBlockHeight = this.stateService.latestBlockHeight;
 | 
				
			||||||
@ -115,20 +75,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateBlock(delta: MempoolBlockDelta): void {
 | 
					  updateBlock(delta: MempoolBlockDelta): void {
 | 
				
			||||||
    if (!this.scene) {
 | 
					 | 
				
			||||||
      this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
 | 
					 | 
				
			||||||
        blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
 | 
					    const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.blockIndex !== this.index) {
 | 
					    if (this.blockIndex !== this.index) {
 | 
				
			||||||
      const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
 | 
					      const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
 | 
				
			||||||
      this.scene.exit(direction);
 | 
					      this.blockGraph.replace(delta.added, direction);
 | 
				
			||||||
      this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
 | 
					 | 
				
			||||||
        blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
 | 
					 | 
				
			||||||
      this.scene.enter(delta.added, direction);
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.scene.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
 | 
					      this.blockGraph.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.lastBlockHeight = this.stateService.latestBlockHeight;
 | 
					    this.lastBlockHeight = this.stateService.latestBlockHeight;
 | 
				
			||||||
@ -136,279 +89,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
				
			|||||||
    this.isLoading$.next(false);
 | 
					    this.isLoading$.next(false);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  initCanvas(): void {
 | 
					  onTxPreview(event: TransactionStripped | void): void {
 | 
				
			||||||
    this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
 | 
					    this.txPreviewEvent.emit(event);
 | 
				
			||||||
    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;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleContextRestored(event): void {
 | 
					 | 
				
			||||||
    this.initCanvas();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @HostListener('window:resize', ['$event'])
 | 
					 | 
				
			||||||
  resizeCanvas(): void {
 | 
					 | 
				
			||||||
    this.cssWidth = this.canvas.nativeElement.parentElement.clientWidth;
 | 
					 | 
				
			||||||
    this.cssHeight = this.canvas.nativeElement.parentElement.clientHeight;
 | 
					 | 
				
			||||||
    this.displayWidth = window.devicePixelRatio * this.cssWidth;
 | 
					 | 
				
			||||||
    this.displayHeight = window.devicePixelRatio * this.cssHeight;
 | 
					 | 
				
			||||||
    this.canvas.nativeElement.width = this.displayWidth;
 | 
					 | 
				
			||||||
    this.canvas.nativeElement.height = this.displayHeight;
 | 
					 | 
				
			||||||
    if (this.gl) {
 | 
					 | 
				
			||||||
      this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (this.scene) {
 | 
					 | 
				
			||||||
      this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  compileShader(src, type): WebGLShader {
 | 
					 | 
				
			||||||
    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 {
 | 
					 | 
				
			||||||
    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.run());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  run(now?: DOMHighResTimeStamp): void {
 | 
					 | 
				
			||||||
    if (!now) {
 | 
					 | 
				
			||||||
      now = performance.now();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /* 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);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /* 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);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /* LOOP */
 | 
					 | 
				
			||||||
    if (this.running) {
 | 
					 | 
				
			||||||
      if (this.animationFrameRequest) {
 | 
					 | 
				
			||||||
        cancelAnimationFrame(this.animationFrameRequest);
 | 
					 | 
				
			||||||
        this.animationFrameRequest = null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.animationFrameRequest = requestAnimationFrame(() => this.run());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @HostListener('click', ['$event'])
 | 
					 | 
				
			||||||
  onClick(event) {
 | 
					 | 
				
			||||||
    this.setPreviewTx(event.offsetX, event.offsetY, true);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @HostListener('pointermove', ['$event'])
 | 
					 | 
				
			||||||
  onPointerMove(event) {
 | 
					 | 
				
			||||||
    this.setPreviewTx(event.offsetX, event.offsetY, false);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @HostListener('pointerleave', ['$event'])
 | 
					 | 
				
			||||||
  onPointerLeave(event) {
 | 
					 | 
				
			||||||
    this.setPreviewTx(-1, -1, false);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setPreviewTx(cssX: number, cssY: number, clicked: boolean = false) {
 | 
					 | 
				
			||||||
    const x = cssX * window.devicePixelRatio;
 | 
					 | 
				
			||||||
    const y = cssY * window.devicePixelRatio;
 | 
					 | 
				
			||||||
    if (this.scene && (!this.selectedTx || clicked)) {
 | 
					 | 
				
			||||||
      const selected = this.scene.getTxAt({ x, y });
 | 
					 | 
				
			||||||
      const currentPreview = this.selectedTx || this.hoverTx;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (selected !== currentPreview) {
 | 
					 | 
				
			||||||
        if (currentPreview) {
 | 
					 | 
				
			||||||
          currentPreview.setHover(false);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (selected) {
 | 
					 | 
				
			||||||
          selected.setHover(true);
 | 
					 | 
				
			||||||
          this.txPreviewEvent.emit({
 | 
					 | 
				
			||||||
            txid: selected.txid,
 | 
					 | 
				
			||||||
            fee: selected.fee,
 | 
					 | 
				
			||||||
            vsize: selected.vsize,
 | 
					 | 
				
			||||||
            value: selected.value
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
          if (clicked) {
 | 
					 | 
				
			||||||
            this.selectedTx = selected;
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            this.hoverTx = selected;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          if (clicked) {
 | 
					 | 
				
			||||||
            this.selectedTx = null;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          this.hoverTx = null;
 | 
					 | 
				
			||||||
          this.txPreviewEvent.emit(null);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else if (clicked) {
 | 
					 | 
				
			||||||
        if (selected === this.selectedTx) {
 | 
					 | 
				
			||||||
          this.hoverTx = this.selectedTx;
 | 
					 | 
				
			||||||
          this.selectedTx = null;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this.selectedTx = selected;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// WebGL shader attributes
 | 
					 | 
				
			||||||
const attribs = {
 | 
					 | 
				
			||||||
  offset: { type: 'FLOAT', count: 2, 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 },
 | 
					 | 
				
			||||||
  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 vec2 offset;
 | 
					 | 
				
			||||||
attribute vec4 posX;
 | 
					 | 
				
			||||||
attribute vec4 posY;
 | 
					 | 
				
			||||||
attribute vec4 posR;
 | 
					 | 
				
			||||||
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);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  float radius = interpolateAttribute(posR);
 | 
					 | 
				
			||||||
  vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -81,12 +81,12 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setTxPreview(event: TransactionStripped | void): void {
 | 
					  setTxPreview(event: TransactionStripped | void): void {
 | 
				
			||||||
    this.previewTx = event
 | 
					    this.previewTx = event;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function detectWebGL () {
 | 
					function detectWebGL() {
 | 
				
			||||||
  const canvas = document.createElement("canvas");
 | 
					  const canvas = document.createElement('canvas');
 | 
				
			||||||
  const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
 | 
					  const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
 | 
				
			||||||
  return (gl && gl instanceof WebGLRenderingContext)
 | 
					  return (gl && gl instanceof WebGLRenderingContext);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -45,6 +45,7 @@ import { StartComponent } from '../components/start/start.component';
 | 
				
			|||||||
import { TransactionComponent } from '../components/transaction/transaction.component';
 | 
					import { TransactionComponent } from '../components/transaction/transaction.component';
 | 
				
			||||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
 | 
					import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
 | 
				
			||||||
import { BlockComponent } from '../components/block/block.component';
 | 
					import { BlockComponent } from '../components/block/block.component';
 | 
				
			||||||
 | 
					import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
 | 
				
			||||||
import { AddressComponent } from '../components/address/address.component';
 | 
					import { AddressComponent } from '../components/address/address.component';
 | 
				
			||||||
import { SearchFormComponent } from '../components/search-form/search-form.component';
 | 
					import { SearchFormComponent } from '../components/search-form/search-form.component';
 | 
				
			||||||
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
 | 
					import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
 | 
				
			||||||
@ -110,6 +111,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
 | 
				
			|||||||
    StartComponent,
 | 
					    StartComponent,
 | 
				
			||||||
    TransactionComponent,
 | 
					    TransactionComponent,
 | 
				
			||||||
    BlockComponent,
 | 
					    BlockComponent,
 | 
				
			||||||
 | 
					    BlockOverviewGraphComponent,
 | 
				
			||||||
    TransactionsListComponent,
 | 
					    TransactionsListComponent,
 | 
				
			||||||
    AddressComponent,
 | 
					    AddressComponent,
 | 
				
			||||||
    SearchFormComponent,
 | 
					    SearchFormComponent,
 | 
				
			||||||
@ -203,6 +205,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
 | 
				
			|||||||
    StartComponent,
 | 
					    StartComponent,
 | 
				
			||||||
    TransactionComponent,
 | 
					    TransactionComponent,
 | 
				
			||||||
    BlockComponent,
 | 
					    BlockComponent,
 | 
				
			||||||
 | 
					    BlockOverviewGraphComponent,
 | 
				
			||||||
    TransactionsListComponent,
 | 
					    TransactionsListComponent,
 | 
				
			||||||
    AddressComponent,
 | 
					    AddressComponent,
 | 
				
			||||||
    SearchFormComponent,
 | 
					    SearchFormComponent,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user