Merge pull request #1779 from mempool/simon/projected-block-transactions-fixes

Minor refactor for projected block transactions
This commit is contained in:
wiz 2022-06-02 21:30:57 +09:00 committed by GitHub
commit a6517ebdc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 379 additions and 348 deletions

View File

@ -27,7 +27,7 @@ class MempoolBlocks {
} }
public getMempoolBlockDeltas(): MempoolBlockDelta[] { public getMempoolBlockDeltas(): MempoolBlockDelta[] {
return this.mempoolBlockDeltas return this.mempoolBlockDeltas;
} }
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void { public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void {
@ -72,11 +72,12 @@ class MempoolBlocks {
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds'); logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks); const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
this.mempoolBlocks = blocks this.mempoolBlocks = blocks;
this.mempoolBlockDeltas = deltas this.mempoolBlockDeltas = deltas;
} }
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): { blocks: MempoolBlockWithTransactions[], deltas: MempoolBlockDelta[] } { private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]):
{ blocks: MempoolBlockWithTransactions[], deltas: MempoolBlockDelta[] } {
const mempoolBlocks: MempoolBlockWithTransactions[] = []; const mempoolBlocks: MempoolBlockWithTransactions[] = [];
const mempoolBlockDeltas: MempoolBlockDelta[] = []; const mempoolBlockDeltas: MempoolBlockDelta[] = [];
let blockWeight = 0; let blockWeight = 0;
@ -100,37 +101,41 @@ class MempoolBlocks {
} }
// Calculate change from previous block states // Calculate change from previous block states
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = [] let added: TransactionStripped[] = [];
let removed: string[] = [] let removed: string[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) { if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) { } else if (!mempoolBlocks[i] && prevBlocks[i]) {
removed = prevBlocks[i].transactions.map(tx => tx.txid) removed = prevBlocks[i].transactions.map(tx => tx.txid);
} else if (mempoolBlocks[i] && prevBlocks[i]) { } else if (mempoolBlocks[i] && prevBlocks[i]) {
const prevIds = {} const prevIds = {};
const newIds = {} const newIds = {};
prevBlocks[i].transactions.forEach(tx => { prevBlocks[i].transactions.forEach(tx => {
prevIds[tx.txid] = true prevIds[tx.txid] = true;
}) });
mempoolBlocks[i].transactions.forEach(tx => { mempoolBlocks[i].transactions.forEach(tx => {
newIds[tx.txid] = true newIds[tx.txid] = true;
}) });
prevBlocks[i].transactions.forEach(tx => { prevBlocks[i].transactions.forEach(tx => {
if (!newIds[tx.txid]) removed.push(tx.txid) if (!newIds[tx.txid]) {
}) removed.push(tx.txid);
}
});
mempoolBlocks[i].transactions.forEach(tx => { mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) added.push(tx) if (!prevIds[tx.txid]) {
}) added.push(tx);
}
});
} }
mempoolBlockDeltas.push({ mempoolBlockDeltas.push({
added, added,
removed removed
}) });
} }
return { return {
blocks: mempoolBlocks, blocks: mempoolBlocks,
deltas: mempoolBlockDeltas deltas: mempoolBlockDeltas
} };
} }
private dataToMempoolBlocks(transactions: TransactionExtended[], private dataToMempoolBlocks(transactions: TransactionExtended[],

View File

@ -1,6 +1,7 @@
import logger from '../logger'; import logger from '../logger';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta, OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces'; import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
import blocks from './blocks'; import blocks from './blocks';
import memPool from './mempool'; import memPool from './mempool';
import backendInfo from './backend-info'; import backendInfo from './backend-info';
@ -110,15 +111,15 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-mempool-block'] != null) { if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) { if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
const index = parsedMessage['track-mempool-block']; const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index; client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
if (mBlocksWithTransactions[index]) { if (mBlocksWithTransactions[index]) {
response['projected-mempool-block'] = { response['projected-block-transactions'] = {
index: index, index: index,
block: mBlocksWithTransactions[index], blockTransactions: mBlocksWithTransactions[index].transactions
}; };
} }
} else { } else {
@ -389,7 +390,7 @@ class WebsocketHandler {
if (client['track-mempool-block'] >= 0) { if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block']; const index = client['track-mempool-block'];
if (mBlockDeltas[index]) { if (mBlockDeltas[index]) {
response['projected-mempool-block'] = { response['projected-block-transactions'] = {
index: index, index: index,
delta: mBlockDeltas[index], delta: mBlockDeltas[index],
}; };
@ -526,7 +527,7 @@ class WebsocketHandler {
if (client['track-mempool-block'] >= 0) { if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block']; const index = client['track-mempool-block'];
if (mBlockDeltas && mBlockDeltas[index]) { if (mBlockDeltas && mBlockDeltas[index]) {
response['projected-mempool-block'] = { response['projected-block-transactions'] = {
index: index, index: index,
delta: mBlockDeltas[index], delta: mBlockDeltas[index],
}; };

View File

@ -1,5 +1,4 @@
import { FastVertexArray } from './fast-vertex-array' import { FastVertexArray } from './fast-vertex-array'
import TxSprite from './tx-sprite'
import TxView from './tx-view' import TxView from './tx-view'
import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Position, Square } from './sprite-types' import { Position, Square } from './sprite-types'

View File

@ -8,7 +8,7 @@
or compacting into a smaller Float32Array when there's space to do so. or compacting into a smaller Float32Array when there's space to do so.
*/ */
import TxSprite from './tx-sprite' import TxSprite from './tx-sprite';
export class FastVertexArray { export class FastVertexArray {
length: number; length: number;
@ -19,85 +19,87 @@ export class FastVertexArray {
freeSlots: number[]; freeSlots: number[];
lastSlot: number; lastSlot: number;
constructor (length, stride) { constructor(length, stride) {
this.length = length this.length = length;
this.count = 0 this.count = 0;
this.stride = stride this.stride = stride;
this.sprites = [] this.sprites = [];
this.data = new Float32Array(this.length * this.stride) this.data = new Float32Array(this.length * this.stride);
this.freeSlots = [] this.freeSlots = [];
this.lastSlot = 0 this.lastSlot = 0;
} }
insert (sprite: TxSprite): number { insert(sprite: TxSprite): number {
this.count++ this.count++;
let position let position;
if (this.freeSlots.length) { if (this.freeSlots.length) {
position = this.freeSlots.shift() position = this.freeSlots.shift();
} else { } else {
position = this.lastSlot position = this.lastSlot;
this.lastSlot++ this.lastSlot++;
if (this.lastSlot > this.length) { if (this.lastSlot > this.length) {
this.expand() this.expand();
} }
} }
this.sprites[position] = sprite this.sprites[position] = sprite;
return position return position;
} }
remove (index: number): void { remove(index: number): void {
this.count-- this.count--;
this.clearData(index) this.clearData(index);
this.freeSlots.push(index) this.freeSlots.push(index);
this.sprites[index] = null this.sprites[index] = null;
if (this.length > 2048 && this.count < (this.length * 0.4)) this.compact() if (this.length > 2048 && this.count < (this.length * 0.4)) {
this.compact();
}
} }
setData (index: number, dataChunk: number[]): void { setData(index: number, dataChunk: number[]): void {
this.data.set(dataChunk, (index * this.stride)) this.data.set(dataChunk, (index * this.stride));
} }
clearData (index: number): void { clearData(index: number): void {
this.data.fill(0, (index * this.stride), ((index+1) * this.stride)) this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
} }
getData (index: number): Float32Array { getData(index: number): Float32Array {
return this.data.subarray(index, this.stride) return this.data.subarray(index, this.stride);
} }
expand (): void { expand(): void {
this.length *= 2 this.length *= 2;
const newData = new Float32Array(this.length * this.stride) const newData = new Float32Array(this.length * this.stride);
newData.set(this.data) newData.set(this.data);
this.data = newData this.data = newData;
} }
compact (): void { compact(): void {
// New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512) // New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512)
const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count)))) const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
if (newLength != this.length) { if (newLength !== this.length) {
this.length = newLength this.length = newLength;
this.data = new Float32Array(this.length * this.stride) this.data = new Float32Array(this.length * this.stride);
let sprite let sprite;
const newSprites = [] const newSprites = [];
let i = 0 let i = 0;
for (var index in this.sprites) { for (const index in this.sprites) {
sprite = this.sprites[index] sprite = this.sprites[index];
if (sprite) { if (sprite) {
newSprites.push(sprite) newSprites.push(sprite);
sprite.moveVertexPointer(i) sprite.moveVertexPointer(i);
sprite.compile() sprite.compile();
i++ i++;
} }
} }
this.sprites = newSprites this.sprites = newSprites;
this.freeSlots = [] this.freeSlots = [];
this.lastSlot = i this.lastSlot = i;
} }
} }
getVertexData (): Float32Array { getVertexData(): Float32Array {
return this.data return this.data;
} }
} }

View File

@ -1,7 +1,8 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone } from '@angular/core'; import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit,
OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone, AfterViewInit } from '@angular/core';
import { StateService } from 'src/app/services/state.service'; import { StateService } from 'src/app/services/state.service';
import { MempoolBlockWithTransactions, MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { MempoolBlockWithTransactions, MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Observable, Subscription, BehaviorSubject } from 'rxjs'; import { Subscription, BehaviorSubject } from 'rxjs';
import { WebsocketService } from 'src/app/services/websocket.service'; import { WebsocketService } from 'src/app/services/websocket.service';
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene'; import BlockScene from './block-scene';
@ -14,9 +15,9 @@ import TxView from './tx-view';
styleUrls: ['./mempool-block-overview.component.scss'], styleUrls: ['./mempool-block-overview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges { export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() index: number; @Input() index: number;
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>() @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockCanvas') @ViewChild('blockCanvas')
canvas: ElementRef<HTMLCanvasElement>; canvas: ElementRef<HTMLCanvasElement>;
@ -41,33 +42,33 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
constructor( constructor(
public stateService: StateService, public stateService: StateService,
private websocketService: WebsocketService, private websocketService: WebsocketService,
readonly _ngZone: NgZone, readonly ngZone: NgZone,
) { ) {
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize) this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
} }
ngOnInit(): void { ngOnInit(): void {
this.blockSub = this.stateService.mempoolBlock$.subscribe((block) => { this.blockSub = this.stateService.mempoolBlockTransactions$.subscribe((transactionsStripped) => {
this.replaceBlock(block) this.replaceBlock(transactionsStripped);
}) });
this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => { this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => {
this.updateBlock(delta) this.updateBlock(delta);
}) });
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.canvas.nativeElement.addEventListener("webglcontextlost", this.handleContextLost, false) this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener("webglcontextrestored", this.handleContextRestored, false) this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl') this.gl = this.canvas.nativeElement.getContext('webgl');
this.initCanvas() this.initCanvas();
this.resizeCanvas() this.resizeCanvas();
} }
ngOnChanges(changes): void { ngOnChanges(changes): void {
if (changes.index) { if (changes.index) {
this.clearBlock(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left') this.clearBlock(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);
} }
} }
@ -80,53 +81,56 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
clearBlock(direction): void { clearBlock(direction): void {
if (this.scene) { if (this.scene) {
this.scene.exit(direction) this.scene.exit(direction);
} }
this.hoverTx = null this.hoverTx = null;
this.selectedTx = null this.selectedTx = null;
this.txPreviewEvent.emit(null) this.txPreviewEvent.emit(null);
} }
replaceBlock(block: MempoolBlockWithTransactions): void { replaceBlock(transactionsStripped: TransactionStripped[]): void {
if (!this.scene) { if (!this.scene) {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75, blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray }) 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(block.transactions, direction) this.scene.enter(transactionsStripped, direction);
} else { } else {
this.scene.replace(block.transactions, blockMined ? 'right' : 'left') this.scene.replace(transactionsStripped, blockMined ? 'right' : 'left');
} }
this.lastBlockHeight = this.stateService.latestBlockHeight this.lastBlockHeight = this.stateService.latestBlockHeight;
this.blockIndex = this.index this.blockIndex = this.index;
this.isLoading$.next(false) this.isLoading$.next(false);
} }
updateBlock(delta: MempoolBlockDelta): void { updateBlock(delta: MempoolBlockDelta): void {
if (!this.scene) { if (!this.scene) {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75, blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray }) 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.scene.exit(direction);
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75, blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray }) this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
this.scene.enter(delta.added, direction) 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.scene.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
} }
this.lastBlockHeight = this.stateService.latestBlockHeight this.lastBlockHeight = this.stateService.latestBlockHeight;
this.blockIndex = this.index this.blockIndex = this.index;
this.isLoading$.next(false) this.isLoading$.next(false);
} }
initCanvas (): void { initCanvas(): void {
this.gl.clearColor(0.0, 0.0, 0.0, 0.0) this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT) this.gl.clear(this.gl.COLOR_BUFFER_BIT);
const shaderSet = [ const shaderSet = [
{ {
@ -137,97 +141,101 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
type: this.gl.FRAGMENT_SHADER, type: this.gl.FRAGMENT_SHADER,
src: fragShaderSrc src: fragShaderSrc
} }
] ];
this.shaderProgram = this.buildShaderProgram(shaderSet) this.shaderProgram = this.buildShaderProgram(shaderSet);
this.gl.useProgram(this.shaderProgram) this.gl.useProgram(this.shaderProgram);
// Set up alpha blending // Set up alpha blending
this.gl.enable(this.gl.BLEND); this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA); this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
const glBuffer = this.gl.createBuffer() const glBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer) this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
/* SET UP SHADER ATTRIBUTES */ /* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => { Object.keys(attribs).forEach((key, i) => {
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key) attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
this.gl.enableVertexAttribArray(attribs[key].pointer); this.gl.enableVertexAttribArray(attribs[key].pointer);
}) });
this.start() this.start();
} }
handleContextLost(event): void { handleContextLost(event): void {
event.preventDefault() event.preventDefault();
cancelAnimationFrame(this.animationFrameRequest) cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null this.animationFrameRequest = null;
this.running = false this.running = false;
} }
handleContextRestored(event): void { handleContextRestored(event): void {
this.initCanvas() this.initCanvas();
} }
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
resizeCanvas(): void { resizeCanvas(): void {
this.displayWidth = this.canvas.nativeElement.parentElement.clientWidth this.displayWidth = this.canvas.nativeElement.parentElement.clientWidth;
this.displayHeight = this.canvas.nativeElement.parentElement.clientHeight this.displayHeight = this.canvas.nativeElement.parentElement.clientHeight;
this.canvas.nativeElement.width = this.displayWidth this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) this.gl.viewport(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height) if (this.gl) {
if (this.scene) this.scene.resize({ width: this.displayWidth, height: this.displayHeight }) this.gl.viewport(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
}
if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
}
} }
compileShader(src, type): WebGLShader { compileShader(src, type): WebGLShader {
let shader = this.gl.createShader(type) const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src) this.gl.shaderSource(shader, src);
this.gl.compileShader(shader) this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? "vertex" : "fragment"} shader:`) console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
console.log(this.gl.getShaderInfoLog(shader)) console.log(this.gl.getShaderInfoLog(shader));
} }
return shader return shader;
} }
buildShaderProgram(shaderInfo): WebGLProgram { buildShaderProgram(shaderInfo): WebGLProgram {
let program = this.gl.createProgram() const program = this.gl.createProgram();
shaderInfo.forEach((desc) => { shaderInfo.forEach((desc) => {
let shader = this.compileShader(desc.src, desc.type) const shader = this.compileShader(desc.src, desc.type);
if (shader) { if (shader) {
this.gl.attachShader(program, shader) this.gl.attachShader(program, shader);
} }
}) });
this.gl.linkProgram(program) this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.log("Error linking shader program:") console.log('Error linking shader program:');
console.log(this.gl.getProgramInfoLog(program)) console.log(this.gl.getProgramInfoLog(program));
} }
return program return program;
} }
start(): void { start(): void {
this.running = true this.running = true;
this._ngZone.runOutsideAngular(() => this.run()) this.ngZone.runOutsideAngular(() => this.run());
} }
run (now?: DOMHighResTimeStamp): void { run(now?: DOMHighResTimeStamp): void {
if (!now) { if (!now) {
now = performance.now() now = performance.now();
} }
/* SET UP SHADER UNIFORMS */ /* SET UP SHADER UNIFORMS */
// screen dimensions // screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight) this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
// frame timestamp // frame timestamp
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now) this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
/* SET UP SHADER ATTRIBUTES */ /* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => { Object.keys(attribs).forEach((key, i) => {
@ -237,20 +245,20 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
false, // never normalised false, // never normalised
stride, // distance between values of the same attribute stride, // distance between values of the same attribute
attribs[key].offset); // offset of the first value attribs[key].offset); // offset of the first value
}) });
const pointArray = this.vertexArray.getVertexData() const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) { if (pointArray.length) {
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW) this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize) this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
} }
/* LOOP */ /* LOOP */
if (this.running) { if (this.running) {
if (this.animationFrameRequest) { if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest) cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null this.animationFrameRequest = null;
} }
this.animationFrameRequest = requestAnimationFrame(() => this.run()); this.animationFrameRequest = requestAnimationFrame(() => this.run());
} }
@ -258,49 +266,54 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
@HostListener('click', ['$event']) @HostListener('click', ['$event'])
onClick(event) { onClick(event) {
this.setPreviewTx(event.offsetX, event.offsetY, true) this.setPreviewTx(event.offsetX, event.offsetY, true);
} }
@HostListener('pointermove', ['$event']) @HostListener('pointermove', ['$event'])
onPointerMove(event) { onPointerMove(event) {
this.setPreviewTx(event.offsetX, event.offsetY, false) this.setPreviewTx(event.offsetX, event.offsetY, false);
} }
@HostListener('pointerleave', ['$event']) @HostListener('pointerleave', ['$event'])
onPointerLeave(event) { onPointerLeave(event) {
this.setPreviewTx(-1, -1, false) this.setPreviewTx(-1, -1, false);
} }
setPreviewTx(x: number, y: number, clicked: boolean = false) { setPreviewTx(x: number, y: number, clicked: boolean = false) {
if (this.scene && (!this.selectedTx || clicked)) { if (this.scene && (!this.selectedTx || clicked)) {
const selected = this.scene.getTxAt({ x, y }) const selected = this.scene.getTxAt({ x, y });
const currentPreview = this.selectedTx || this.hoverTx const currentPreview = this.selectedTx || this.hoverTx;
if (selected !== currentPreview) { if (selected !== currentPreview) {
if (currentPreview) currentPreview.setHover(false) if (currentPreview) {
currentPreview.setHover(false);
}
if (selected) { if (selected) {
selected.setHover(true) selected.setHover(true);
this.txPreviewEvent.emit({ this.txPreviewEvent.emit({
txid: selected.txid, txid: selected.txid,
fee: selected.fee, fee: selected.fee,
vsize: selected.vsize, vsize: selected.vsize,
value: selected.value value: selected.value
}) });
if (clicked) this.selectedTx = selected if (clicked) {
else this.hoverTx = selected this.selectedTx = selected;
} else {
this.hoverTx = selected;
}
} else { } else {
if (clicked) { if (clicked) {
this.selectedTx = null this.selectedTx = null;
} }
this.hoverTx = null this.hoverTx = null;
this.txPreviewEvent.emit(null) this.txPreviewEvent.emit(null);
} }
} else if (clicked) { } else if (clicked) {
if (selected === this.selectedTx) { if (selected === this.selectedTx) {
this.hoverTx = this.selectedTx this.hoverTx = this.selectedTx;
this.selectedTx = null this.selectedTx = null;
} else { } else {
this.selectedTx = selected this.selectedTx = selected;
} }
} }
} }
@ -317,16 +330,16 @@ const attribs = {
colG: { 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 }, colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colA: { 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 // Calculate the number of bytes per vertex based on specified attributes
const stride = Object.values(attribs).reduce((total, attrib) => { const stride = Object.values(attribs).reduce((total, attrib) => {
return total + (attrib.count * 4) return total + (attrib.count * 4);
}, 0) }, 0);
// Calculate vertex attribute offsets // Calculate vertex attribute offsets
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) { for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
let attrib = Object.values(attribs)[i] const attrib = Object.values(attribs)[i];
attrib.offset = offset attrib.offset = offset;
offset += (attrib.count * 4) offset += (attrib.count * 4);
} }
const vertShaderSrc = ` const vertShaderSrc = `
@ -376,7 +389,7 @@ void main() {
vColor = vec4(red, green, blue, alpha); vColor = vec4(red, green, blue, alpha);
} }
` `;
const fragShaderSrc = ` const fragShaderSrc = `
varying lowp vec4 vColor; varying lowp vec4 vColor;
@ -386,4 +399,4 @@ void main() {
// premultiply alpha // premultiply alpha
gl_FragColor.rgb *= gl_FragColor.a; gl_FragColor.rgb *= gl_FragColor.a;
} }
` `;

View File

@ -1,18 +1,18 @@
export type Position = { export type Position = {
x: number, x: number,
y: number, y: number,
} };
export type Square = Position & { export type Square = Position & {
s?: number s?: number
} };
export type Color = { export type Color = {
r: number, r: number,
g: number, g: number,
b: number, b: number,
a: number, a: number,
} };
export type InterpolatedAttribute = { export type InterpolatedAttribute = {
a: number, a: number,
@ -20,9 +20,9 @@ export type InterpolatedAttribute = {
t: number, t: number,
v: number, v: number,
d: number d: number
} };
export type Update = Position & { s: number } & Color export type Update = Position & { s: number } & Color;
export type Attributes = { export type Attributes = {
x: InterpolatedAttribute, x: InterpolatedAttribute,
@ -32,7 +32,7 @@ export type Attributes = {
g: InterpolatedAttribute, g: InterpolatedAttribute,
b: InterpolatedAttribute, b: InterpolatedAttribute,
a: InterpolatedAttribute a: InterpolatedAttribute
} };
export type OptionalAttributes = { export type OptionalAttributes = {
x?: InterpolatedAttribute, x?: InterpolatedAttribute,
@ -42,7 +42,8 @@ export type OptionalAttributes = {
g?: InterpolatedAttribute, g?: InterpolatedAttribute,
b?: InterpolatedAttribute, b?: InterpolatedAttribute,
a?: InterpolatedAttribute a?: InterpolatedAttribute
} };
export type SpriteUpdateParams = { export type SpriteUpdateParams = {
x?: number, x?: number,
y?: number, y?: number,
@ -56,7 +57,7 @@ export type SpriteUpdateParams = {
minDuration?: number, minDuration?: number,
adjust?: boolean, adjust?: boolean,
temp?: boolean temp?: boolean
} };
export type ViewUpdateParams = { export type ViewUpdateParams = {
display: { display: {
@ -70,4 +71,4 @@ export type ViewUpdateParams = {
jitter?: number, jitter?: number,
state?: string, state?: string,
adjust?: boolean adjust?: boolean
} };

View File

@ -1,10 +1,14 @@
import { FastVertexArray } from './fast-vertex-array' import { FastVertexArray } from './fast-vertex-array';
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types' import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
const attribKeys = ['a', 'b', 't', 'v'] const attribKeys = ['a', 'b', 't', 'v'];
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'] const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
export default class TxSprite { export default class TxSprite {
static vertexSize = 30;
static vertexCount = 6;
static dataSize: number = (30 * 6);
vertexArray: FastVertexArray; vertexArray: FastVertexArray;
vertexPointer: number; vertexPointer: number;
vertexData: number[]; vertexData: number[];
@ -12,17 +16,14 @@ export default class TxSprite {
attributes: Attributes; attributes: Attributes;
tempAttributes: OptionalAttributes; tempAttributes: OptionalAttributes;
static vertexSize: number = 30;
static vertexCount: number = 6;
static dataSize: number = (30*6);
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) { constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
const offsetTime = params.start const offsetTime = params.start;
this.vertexArray = vertexArray this.vertexArray = vertexArray;
this.vertexData = Array(VI.length).fill(0) this.vertexData = Array(VI.length).fill(0);
this.updateMap = { this.updateMap = {
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0 x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
} };
this.attributes = { this.attributes = {
x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 }, x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
@ -32,35 +33,36 @@ export default class TxSprite {
g: { a: params.g, b: params.g, t: offsetTime, v: 0, d: 0 }, g: { a: params.g, b: params.g, t: offsetTime, v: 0, d: 0 },
b: { a: params.b, b: params.b, t: offsetTime, v: 0, d: 0 }, b: { a: params.b, b: params.b, t: offsetTime, v: 0, d: 0 },
a: { a: params.a, b: params.a, t: offsetTime, v: 0, d: 0 }, a: { a: params.a, b: params.a, t: offsetTime, v: 0, d: 0 },
} };
// Used to temporarily modify the sprite, so that the base view can be resumed later // Used to temporarily modify the sprite, so that the base view can be resumed later
this.tempAttributes = null this.tempAttributes = null;
this.vertexPointer = this.vertexArray.insert(this) this.vertexPointer = this.vertexArray.insert(this);
this.compile() this.compile();
} }
private interpolateAttributes (updateMap: Update, attributes: OptionalAttributes, offsetTime: DOMHighResTimeStamp, v: number, duration: number, minDuration: number, adjust: boolean): void { private interpolateAttributes(updateMap: Update, attributes: OptionalAttributes, offsetTime: DOMHighResTimeStamp, v: number,
duration: number, minDuration: number, adjust: boolean): void {
for (const key of Object.keys(updateMap)) { for (const key of Object.keys(updateMap)) {
// for each non-null attribute: // for each non-null attribute:
if (updateMap[key] != null) { if (updateMap[key] != null) {
// calculate current interpolated value, and set as 'from' // calculate current interpolated value, and set as 'from'
interpolateAttributeStart(attributes[key], offsetTime) interpolateAttributeStart(attributes[key], offsetTime);
// update start time // update start time
attributes[key].t = offsetTime attributes[key].t = offsetTime;
if (!adjust || (duration && attributes[key].d == 0)) { if (!adjust || (duration && attributes[key].d === 0)) {
attributes[key].v = v attributes[key].v = v;
attributes[key].d = duration attributes[key].d = duration;
} else if (minDuration > attributes[key].d) { } else if (minDuration > attributes[key].d) {
// enforce minimum transition duration // enforce minimum transition duration
attributes[key].v = 1 / minDuration attributes[key].v = 1 / minDuration;
attributes[key].d = minDuration attributes[key].d = minDuration;
} }
// set 'to' to target value // set 'to' to target value
attributes[key].b = updateMap[key] attributes[key].b = updateMap[key];
} }
} }
} }
@ -75,137 +77,139 @@ export default class TxSprite {
minDuration: minimum remaining transition duration when adjust = true minDuration: minimum remaining transition duration when adjust = true
temp: if true, this update is only temporary (can be reversed with 'resume') temp: if true, this update is only temporary (can be reversed with 'resume')
*/ */
update (params: SpriteUpdateParams): void { update(params: SpriteUpdateParams): void {
const offsetTime = params.start || performance.now() const offsetTime = params.start || performance.now();
const v = params.duration > 0 ? (1 / params.duration) : 0 const v = params.duration > 0 ? (1 / params.duration) : 0;
updateKeys.forEach(key => { updateKeys.forEach(key => {
this.updateMap[key] = params[key] this.updateMap[key] = params[key];
}) });
const isModified = !!this.tempAttributes const isModified = !!this.tempAttributes;
if (!params.temp) { if (!params.temp) {
this.interpolateAttributes(this.updateMap, this.attributes, offsetTime, v, params.duration, params.minDuration, params.adjust) this.interpolateAttributes(this.updateMap, this.attributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
} else { } else {
if (!isModified) { // set up tempAttributes if (!isModified) { // set up tempAttributes
this.tempAttributes = {} this.tempAttributes = {};
for (const key of Object.keys(this.updateMap)) { for (const key of Object.keys(this.updateMap)) {
if (this.updateMap[key] != null) { if (this.updateMap[key] != null) {
this.tempAttributes[key] = { ...this.attributes[key] } this.tempAttributes[key] = { ...this.attributes[key] };
} }
} }
} }
this.interpolateAttributes(this.updateMap, this.tempAttributes, offsetTime, v, params.duration, params.minDuration, params.adjust) this.interpolateAttributes(this.updateMap, this.tempAttributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
} }
this.compile() this.compile();
} }
// Transition back from modified state back to base attributes // Transition back from modified state back to base attributes
resume (duration: number, start : DOMHighResTimeStamp = performance.now()): void { resume(duration: number, start: DOMHighResTimeStamp = performance.now()): void {
// If not in modified state, there's nothing to do // If not in modified state, there's nothing to do
if (!this.tempAttributes) return if (!this.tempAttributes) {
return;
}
const offsetTime = start const offsetTime = start;
const v = duration > 0 ? (1 / duration) : 0 const v = duration > 0 ? (1 / duration) : 0;
for (const key of Object.keys(this.tempAttributes)) { for (const key of Object.keys(this.tempAttributes)) {
// If this base attribute is static (fixed or post-transition), transition smoothly back // If this base attribute is static (fixed or post-transition), transition smoothly back
if (this.attributes[key].v == 0 || (this.attributes[key].t + this.attributes[key].d) <= start) { if (this.attributes[key].v === 0 || (this.attributes[key].t + this.attributes[key].d) <= start) {
// calculate current interpolated value, and set as 'from' // calculate current interpolated value, and set as 'from'
interpolateAttributeStart(this.tempAttributes[key], offsetTime) interpolateAttributeStart(this.tempAttributes[key], offsetTime);
this.attributes[key].a = this.tempAttributes[key].a this.attributes[key].a = this.tempAttributes[key].a;
this.attributes[key].t = offsetTime this.attributes[key].t = offsetTime;
this.attributes[key].v = v this.attributes[key].v = v;
this.attributes[key].d = duration this.attributes[key].d = duration;
} }
} }
this.tempAttributes = null this.tempAttributes = null;
this.compile() this.compile();
} }
// Write current state into the graphics vertex array for rendering // Write current state into the graphics vertex array for rendering
compile (): void { compile(): void {
let attributes = this.attributes let attributes = this.attributes;
if (this.tempAttributes) { if (this.tempAttributes) {
attributes = { attributes = {
...this.attributes, ...this.attributes,
...this.tempAttributes ...this.tempAttributes
} };
} }
const size = attributes.s const size = attributes.s;
// update vertex data in place // update vertex data in place
// ugly, but avoids overhead of allocating large temporary arrays // ugly, but avoids overhead of allocating large temporary arrays
const vertexStride = VI.length + 2 const vertexStride = VI.length + 2;
for (let vertex = 0; vertex < 6; vertex++) { for (let vertex = 0; vertex < 6; vertex++) {
this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0] this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1] this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
for (let step = 0; step < VI.length; step++) { for (let step = 0; step < VI.length; step++) {
// components of each field in the vertex array are defined by an entry in VI: // components of each field in the vertex array are defined by an entry in VI:
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors // VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f] this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
} }
} }
this.vertexArray.setData(this.vertexPointer, this.vertexData) this.vertexArray.setData(this.vertexPointer, this.vertexData);
} }
moveVertexPointer (index: number): void { moveVertexPointer(index: number): void {
this.vertexPointer = index this.vertexPointer = index;
} }
destroy (): void { destroy(): void {
this.vertexArray.remove(this.vertexPointer) this.vertexArray.remove(this.vertexPointer);
this.vertexPointer = null this.vertexPointer = null;
} }
} }
// expects 0 <= x <= 1 // expects 0 <= x <= 1
function smootherstep(x: number): number { function smootherstep(x: number): number {
let ix = 1 - x; const ix = 1 - x;
x = x * x x = x * x;
return x / (x + ix * ix) return x / (x + ix * ix);
} }
function interpolateAttributeStart(attribute: InterpolatedAttribute, start: DOMHighResTimeStamp): void { function interpolateAttributeStart(attribute: InterpolatedAttribute, start: DOMHighResTimeStamp): void {
if (attribute.v == 0 || (attribute.t + attribute.d) <= start) { if (attribute.v === 0 || (attribute.t + attribute.d) <= start) {
// transition finished, next transition starts from current end state // transition finished, next transition starts from current end state
// (clamp to 1) // (clamp to 1)
attribute.a = attribute.b attribute.a = attribute.b;
attribute.v = 0 attribute.v = 0;
attribute.d = 0 attribute.d = 0;
} else if (attribute.t > start) { } else if (attribute.t > start) {
// transition not started // transition not started
// (clamp to 0) // (clamp to 0)
} else { } else {
// transition in progress // transition in progress
// (interpolate) // (interpolate)
let progress = (start - attribute.t) const progress = (start - attribute.t);
let delta = smootherstep(progress / attribute.d) const delta = smootherstep(progress / attribute.d);
attribute.a = attribute.a + (delta * (attribute.b - attribute.a)) attribute.a = attribute.a + (delta * (attribute.b - attribute.a));
attribute.d = attribute.d - progress attribute.d = attribute.d - progress;
attribute.v = 1 / attribute.d attribute.v = 1 / attribute.d;
} }
} }
const vertexOffsetFactors = [ const vertexOffsetFactors = [
[0,0], [0, 0],
[1,1], [1, 1],
[1,0], [1, 0],
[0,0], [0, 0],
[1,1], [1, 1],
[0,1] [0, 1]
] ];
const VI = [] const VI = [];
updateKeys.forEach((attribute, aIndex) => { updateKeys.forEach((attribute, aIndex) => {
attribKeys.forEach(field => { attribKeys.forEach(field => {
VI.push({ VI.push({
a: attribute, a: attribute,
f: field f: field
}) });
}) });
}) });

View File

@ -1,14 +1,14 @@
import TxSprite from './tx-sprite' import TxSprite from './tx-sprite';
import { FastVertexArray } from './fast-vertex-array' import { FastVertexArray } from './fast-vertex-array';
import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types' import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
const hoverTransitionTime = 300 const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4') const defaultHoverColor = hexToColor('1bd8f4');
// convert from this class's update format to TxSprite's update format // convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params : ViewUpdateParams): SpriteUpdateParams { function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
return { return {
start: (params.start || performance.now()) + (params.delay || 0), start: (params.start || performance.now()) + (params.delay || 0),
duration: params.duration, duration: params.duration,
@ -16,7 +16,7 @@ function toSpriteUpdate(params : ViewUpdateParams): SpriteUpdateParams {
...params.display.position, ...params.display.position,
...params.display.color, ...params.display.color,
adjust: params.adjust adjust: params.adjust
} };
} }
export default class TxView implements TransactionStripped { export default class TxView implements TransactionStripped {
@ -33,41 +33,43 @@ export default class TxView implements TransactionStripped {
hoverColor: Color | void; hoverColor: Color | void;
screenPosition: Square; screenPosition: Square;
gridPosition : Square | void; gridPosition: Square | void;
dirty: boolean; dirty: boolean;
constructor (tx: TransactionStripped, vertexArray: FastVertexArray) { constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
this.txid = tx.txid this.txid = tx.txid;
this.fee = tx.fee this.fee = tx.fee;
this.vsize = tx.vsize this.vsize = tx.vsize;
this.value = tx.value this.value = tx.value;
this.feerate = tx.fee / tx.vsize this.feerate = tx.fee / tx.vsize;
this.initialised = false this.initialised = false;
this.vertexArray = vertexArray this.vertexArray = vertexArray;
this.hover = false this.hover = false;
this.screenPosition = { x: 0, y: 0, s: 0 } this.screenPosition = { x: 0, y: 0, s: 0 };
this.dirty = true this.dirty = true;
} }
destroy (): void { destroy(): void {
if (this.sprite) { if (this.sprite) {
this.sprite.destroy() this.sprite.destroy();
this.sprite = null this.sprite = null;
this.initialised = false this.initialised = false;
} }
} }
applyGridPosition (position: Square): void { applyGridPosition(position: Square): void {
if (!this.gridPosition) this.gridPosition = { x: 0, y: 0, s: 0 } if (!this.gridPosition) {
if (this.gridPosition.x != position.x || this.gridPosition.y != position.y || this.gridPosition.s != position.s) { this.gridPosition = { x: 0, y: 0, s: 0 };
this.gridPosition.x = position.x }
this.gridPosition.y = position.y if (this.gridPosition.x !== position.x || this.gridPosition.y !== position.y || this.gridPosition.s !== position.s) {
this.gridPosition.s = position.s this.gridPosition.x = position.x;
this.dirty = true this.gridPosition.y = position.y;
this.gridPosition.s = position.s;
this.dirty = true;
} }
} }
@ -81,15 +83,17 @@ export default class TxView implements TransactionStripped {
jitter: if set, adds a random amount to the delay, jitter: if set, adds a random amount to the delay,
adjust: if true, modify an in-progress transition instead of replacing it adjust: if true, modify an in-progress transition instead of replacing it
*/ */
update (params: ViewUpdateParams): void { update(params: ViewUpdateParams): void {
if (params.jitter) params.delay += (Math.random() * params.jitter) if (params.jitter) {
params.delay += (Math.random() * params.jitter);
}
if (!this.initialised || !this.sprite) { if (!this.initialised || !this.sprite) {
this.initialised = true this.initialised = true;
this.sprite = new TxSprite( this.sprite = new TxSprite(
toSpriteUpdate(params), toSpriteUpdate(params),
this.vertexArray this.vertexArray
) );
// apply any pending hover event // apply any pending hover event
if (this.hover) { if (this.hover) {
this.sprite.update({ this.sprite.update({
@ -97,48 +101,50 @@ export default class TxView implements TransactionStripped {
duration: hoverTransitionTime, duration: hoverTransitionTime,
adjust: false, adjust: false,
temp: true temp: true
}) });
} }
} else { } else {
this.sprite.update( this.sprite.update(
toSpriteUpdate(params) toSpriteUpdate(params)
) );
} }
this.dirty = false this.dirty = false;
} }
// Temporarily override the tx color // Temporarily override the tx color
setHover (hoverOn: boolean, color: Color | void = defaultHoverColor): void { setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): void {
if (hoverOn) { if (hoverOn) {
this.hover = true this.hover = true;
this.hoverColor = color this.hoverColor = color;
this.sprite.update({ this.sprite.update({
...this.hoverColor, ...this.hoverColor,
duration: hoverTransitionTime, duration: hoverTransitionTime,
adjust: false, adjust: false,
temp: true temp: true
}) });
} else { } else {
this.hover = false this.hover = false;
this.hoverColor = null this.hoverColor = null;
if (this.sprite) this.sprite.resume(hoverTransitionTime) if (this.sprite) {
this.sprite.resume(hoverTransitionTime);
}
} }
this.dirty = false this.dirty = false;
} }
getColor (): Color { getColor(): Color {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => (this.feerate || 1) >= feeLvl); let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => (this.feerate || 1) >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex; feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
return hexToColor(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]) return hexToColor(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
} }
} }
function hexToColor (hex: string): Color { function hexToColor(hex: string): Color {
return { return {
r: parseInt(hex.slice(0,2), 16) / 255, r: parseInt(hex.slice(0, 2), 16) / 255,
g: parseInt(hex.slice(2,4), 16) / 255, g: parseInt(hex.slice(2, 4), 16) / 255,
b: parseInt(hex.slice(4,6), 16) / 255, b: parseInt(hex.slice(4, 6), 16) / 255,
a: 1 a: 1
} };
} }

View File

@ -80,7 +80,7 @@ export class StateService {
bsqPrice$ = new ReplaySubject<number>(1); bsqPrice$ = new ReplaySubject<number>(1);
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1); mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlock$ = new Subject<MempoolBlockWithTransactions>(); mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>(); mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
txReplaced$ = new Subject<ReplacedTransaction>(); txReplaced$ = new Subject<ReplacedTransaction>();
utxoSpent$ = new Subject<object>(); utxoSpent$ = new Subject<object>();

View File

@ -309,12 +309,12 @@ export class WebsocketService {
}); });
} }
if (response['projected-mempool-block']) { if (response['projected-block-transactions']) {
if (response['projected-mempool-block'].index == this.trackingMempoolBlock) { if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
if (response['projected-mempool-block'].block) { if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolBlock$.next(response['projected-mempool-block'].block); this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions);
} else if (response['projected-mempool-block'].delta) { } else if (response['projected-block-transactions'].delta) {
this.stateService.mempoolBlockDelta$.next(response['projected-mempool-block'].delta); this.stateService.mempoolBlockDelta$.next(response['projected-block-transactions'].delta);
} }
} }
} }