Merge branch 'master' into simon/load-more-mempool-txs
This commit is contained in:
		
						commit
						e3ddde9c90
					
				
							
								
								
									
										4
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-backend",
 | 
			
		||||
  "version": "2.6.0-dev",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "lockfileVersion": 2,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "mempool-backend",
 | 
			
		||||
      "version": "2.6.0-dev",
 | 
			
		||||
      "version": "3.0.0-dev",
 | 
			
		||||
      "license": "GNU Affero General Public License v3.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/core": "^7.21.3",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-backend",
 | 
			
		||||
  "version": "2.6.0-dev",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "description": "Bitcoin mempool visualizer and blockchain explorer backend",
 | 
			
		||||
  "license": "GNU Affero General Public License v3.0",
 | 
			
		||||
  "homepage": "https://mempool.space",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								backend/rust-gbt/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								backend/rust-gbt/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "gbt",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "gbt",
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "version": "3.0.0-dev",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@napi-rs/cli": "^2.16.1"
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "gbt",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "description": "An inefficient re-implementation of the getBlockTemplate algorithm in Rust",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "types": "index.d.ts",
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import { Common } from "./common";
 | 
			
		||||
interface RbfTransaction extends TransactionStripped {
 | 
			
		||||
  rbf?: boolean;
 | 
			
		||||
  mined?: boolean;
 | 
			
		||||
  fullRbf?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface RbfTree {
 | 
			
		||||
@ -17,6 +18,16 @@ interface RbfTree {
 | 
			
		||||
  replaces: RbfTree[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReplacementInfo {
 | 
			
		||||
  mined: boolean;
 | 
			
		||||
  fullRbf: boolean;
 | 
			
		||||
  txid: string;
 | 
			
		||||
  oldFee: number;
 | 
			
		||||
  oldVsize: number;
 | 
			
		||||
  newFee: number;
 | 
			
		||||
  newVsize: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class RbfCache {
 | 
			
		||||
  private replacedBy: Map<string, string> = new Map();
 | 
			
		||||
  private replaces: Map<string, string[]> = new Map();
 | 
			
		||||
@ -41,11 +52,15 @@ class RbfCache {
 | 
			
		||||
    this.txs.set(newTx.txid, newTxExtended);
 | 
			
		||||
 | 
			
		||||
    // maintain rbf trees
 | 
			
		||||
    let fullRbf = false;
 | 
			
		||||
    let txFullRbf = false;
 | 
			
		||||
    let treeFullRbf = false;
 | 
			
		||||
    const replacedTrees: RbfTree[] = [];
 | 
			
		||||
    for (const replacedTxExtended of replaced) {
 | 
			
		||||
      const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
 | 
			
		||||
      replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
			
		||||
      if (!replacedTx.rbf) {
 | 
			
		||||
        txFullRbf = true;
 | 
			
		||||
      }
 | 
			
		||||
      this.replacedBy.set(replacedTx.txid, newTx.txid);
 | 
			
		||||
      if (this.treeMap.has(replacedTx.txid)) {
 | 
			
		||||
        const treeId = this.treeMap.get(replacedTx.txid);
 | 
			
		||||
@ -55,7 +70,7 @@ class RbfCache {
 | 
			
		||||
          if (tree) {
 | 
			
		||||
            tree.interval = newTime - tree?.time;
 | 
			
		||||
            replacedTrees.push(tree);
 | 
			
		||||
            fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf;
 | 
			
		||||
            treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
@ -67,15 +82,16 @@ class RbfCache {
 | 
			
		||||
          fullRbf: !replacedTx.rbf,
 | 
			
		||||
          replaces: [],
 | 
			
		||||
        });
 | 
			
		||||
        fullRbf = fullRbf || !replacedTx.rbf;
 | 
			
		||||
        treeFullRbf = treeFullRbf || !replacedTx.rbf;
 | 
			
		||||
        this.txs.set(replacedTx.txid, replacedTxExtended);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    newTx.fullRbf = txFullRbf;
 | 
			
		||||
    const treeId = replacedTrees[0].tx.txid;
 | 
			
		||||
    const newTree = {
 | 
			
		||||
      tx: newTx,
 | 
			
		||||
      time: newTime,
 | 
			
		||||
      fullRbf,
 | 
			
		||||
      fullRbf: treeFullRbf,
 | 
			
		||||
      replaces: replacedTrees
 | 
			
		||||
    };
 | 
			
		||||
    this.rbfTrees.set(treeId, newTree);
 | 
			
		||||
@ -349,6 +365,27 @@ class RbfCache {
 | 
			
		||||
    }
 | 
			
		||||
    return tree;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getLatestRbfSummary(): ReplacementInfo[] {
 | 
			
		||||
    const rbfList = this.getRbfTrees(false);
 | 
			
		||||
    return rbfList.slice(0, 6).map(rbfTree => {
 | 
			
		||||
      let oldFee = 0;
 | 
			
		||||
      let oldVsize = 0;
 | 
			
		||||
      for (const replaced of rbfTree.replaces) {
 | 
			
		||||
        oldFee += replaced.tx.fee;
 | 
			
		||||
        oldVsize += replaced.tx.vsize;
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        txid: rbfTree.tx.txid,
 | 
			
		||||
        mined: !!rbfTree.tx.mined,
 | 
			
		||||
        fullRbf: !!rbfTree.tx.fullRbf,
 | 
			
		||||
        oldFee,
 | 
			
		||||
        oldVsize,
 | 
			
		||||
        newFee: rbfTree.tx.fee,
 | 
			
		||||
        newVsize: rbfTree.tx.vsize,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new RbfCache();
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ import { Common } from './common';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
import rbfCache from './rbf-cache';
 | 
			
		||||
import rbfCache, { ReplacementInfo } from './rbf-cache';
 | 
			
		||||
import difficultyAdjustment from './difficulty-adjustment';
 | 
			
		||||
import feeApi from './fee-api';
 | 
			
		||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
 | 
			
		||||
@ -40,6 +40,7 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
  private socketData: { [key: string]: string } = {};
 | 
			
		||||
  private serializedInitData: string = '{}';
 | 
			
		||||
  private lastRbfSummary: ReplacementInfo | null = null;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
@ -225,6 +226,15 @@ class WebsocketHandler {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-rbf-summary'] != null) {
 | 
			
		||||
            if (parsedMessage['track-rbf-summary']) {
 | 
			
		||||
              client['track-rbf-summary'] = true;
 | 
			
		||||
              response['rbfLatestSummary'] = this.socketData['rbfSummary'];
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-rbf-summary'] = false;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage.action === 'init') {
 | 
			
		||||
            if (!this.socketData['blocks']?.length || !this.socketData['da']) {
 | 
			
		||||
              this.updateSocketData();
 | 
			
		||||
@ -395,10 +405,13 @@ class WebsocketHandler {
 | 
			
		||||
    const rbfChanges = rbfCache.getRbfChanges();
 | 
			
		||||
    let rbfReplacements;
 | 
			
		||||
    let fullRbfReplacements;
 | 
			
		||||
    let rbfSummary;
 | 
			
		||||
    if (Object.keys(rbfChanges.trees).length) {
 | 
			
		||||
      rbfReplacements = rbfCache.getRbfTrees(false);
 | 
			
		||||
      fullRbfReplacements = rbfCache.getRbfTrees(true);
 | 
			
		||||
      rbfSummary = rbfCache.getLatestRbfSummary();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const deletedTx of deletedTransactions) {
 | 
			
		||||
      rbfCache.evict(deletedTx.txid);
 | 
			
		||||
    }
 | 
			
		||||
@ -409,7 +422,7 @@ class WebsocketHandler {
 | 
			
		||||
    const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
 | 
			
		||||
 | 
			
		||||
    // update init data
 | 
			
		||||
    this.updateSocketDataFields({
 | 
			
		||||
    const socketDataFields = {
 | 
			
		||||
      'mempoolInfo': mempoolInfo,
 | 
			
		||||
      'vBytesPerSecond': vBytesPerSecond,
 | 
			
		||||
      'mempool-blocks': mBlocks,
 | 
			
		||||
@ -417,7 +430,11 @@ class WebsocketHandler {
 | 
			
		||||
      'loadingIndicators': loadingIndicators.getLoadingIndicators(),
 | 
			
		||||
      'da': da?.previousTime ? da : undefined,
 | 
			
		||||
      'fees': recommendedFees,
 | 
			
		||||
    });
 | 
			
		||||
    };
 | 
			
		||||
    if (rbfSummary) {
 | 
			
		||||
      socketDataFields['rbfSummary'] = rbfSummary;
 | 
			
		||||
    }
 | 
			
		||||
    this.updateSocketDataFields(socketDataFields);
 | 
			
		||||
 | 
			
		||||
    // cache serialized objects to avoid stringify-ing the same thing for every client
 | 
			
		||||
    const responseCache = { ...this.socketData };
 | 
			
		||||
@ -601,6 +618,10 @@ class WebsocketHandler {
 | 
			
		||||
        response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-rbf-summary'] && rbfSummary) {
 | 
			
		||||
        response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Object.keys(response).length) {
 | 
			
		||||
        const serializedResponse = this.serializeResponse(response);
 | 
			
		||||
        client.send(serializedResponse);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/bennyhodl.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/bennyhodl.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 28, 2023.
 | 
			
		||||
 | 
			
		||||
Signed: bennyhodl
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-frontend",
 | 
			
		||||
  "version": "2.6.0-dev",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "lockfileVersion": 2,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "mempool-frontend",
 | 
			
		||||
      "version": "2.6.0-dev",
 | 
			
		||||
      "version": "3.0.0-dev",
 | 
			
		||||
      "license": "GNU Affero General Public License v3.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@angular-devkit/build-angular": "^14.2.10",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-frontend",
 | 
			
		||||
  "version": "2.6.0-dev",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "description": "Bitcoin mempool visualizer and blockchain explorer backend",
 | 
			
		||||
  "license": "GNU Affero General Public License v3.0",
 | 
			
		||||
  "homepage": "https://mempool.space",
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,17 @@
 | 
			
		||||
<div class="block-overview-graph">
 | 
			
		||||
  <canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
 | 
			
		||||
  <div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
 | 
			
		||||
    <div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
 | 
			
		||||
    <div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <app-block-overview-tooltip
 | 
			
		||||
    [tx]="selectedTx || hoverTx"
 | 
			
		||||
    [cursorPosition]="tooltipPosition"
 | 
			
		||||
    [clickable]="!!selectedTx"
 | 
			
		||||
    [auditEnabled]="auditHighlighting"
 | 
			
		||||
    [blockConversion]="blockConversion"
 | 
			
		||||
  ></app-block-overview-tooltip>
 | 
			
		||||
<div class="grid-align" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
 | 
			
		||||
  <div class="block-overview-graph">
 | 
			
		||||
    <canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
 | 
			
		||||
    <div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
 | 
			
		||||
      <div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
 | 
			
		||||
      <div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <app-block-overview-tooltip
 | 
			
		||||
      [tx]="selectedTx || hoverTx"
 | 
			
		||||
      [cursorPosition]="tooltipPosition"
 | 
			
		||||
      [clickable]="!!selectedTx"
 | 
			
		||||
      [auditEnabled]="auditHighlighting"
 | 
			
		||||
      [blockConversion]="blockConversion"
 | 
			
		||||
    ></app-block-overview-tooltip>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,16 @@
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  grid-column: 1/-1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-align {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: repeat(auto-fit, 75px);
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-overview-canvas {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,8 @@ import TxSprite from './tx-sprite';
 | 
			
		||||
import TxView from './tx-view';
 | 
			
		||||
import { Position } from './sprite-types';
 | 
			
		||||
import { Price } from '../../services/price.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-overview-graph',
 | 
			
		||||
@ -23,7 +25,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  @Input() unavailable: boolean = false;
 | 
			
		||||
  @Input() auditHighlighting: boolean = false;
 | 
			
		||||
  @Input() blockConversion: Price;
 | 
			
		||||
  @Input() pixelAlign: boolean = false;
 | 
			
		||||
  @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
 | 
			
		||||
  @Output() txHoverEvent = new EventEmitter<string>();
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
@ -44,16 +45,25 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  scene: BlockScene;
 | 
			
		||||
  hoverTx: TxView | void;
 | 
			
		||||
  selectedTx: TxView | void;
 | 
			
		||||
  highlightTx: TxView | void;
 | 
			
		||||
  mirrorTx: TxView | void;
 | 
			
		||||
  tooltipPosition: Position;
 | 
			
		||||
 | 
			
		||||
  readyNextFrame = false;
 | 
			
		||||
 | 
			
		||||
  searchText: string;
 | 
			
		||||
  searchSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    readonly ngZone: NgZone,
 | 
			
		||||
    readonly elRef: ElementRef,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
 | 
			
		||||
    this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
 | 
			
		||||
      this.searchText = text;
 | 
			
		||||
      this.updateSearchHighlight();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
@ -109,6 +119,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
      this.scene.setup(transactions);
 | 
			
		||||
      this.readyNextFrame = true;
 | 
			
		||||
      this.start();
 | 
			
		||||
      this.updateSearchHighlight();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -116,6 +127,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.enter(transactions, direction);
 | 
			
		||||
      this.start();
 | 
			
		||||
      this.updateSearchHighlight();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -123,6 +135,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.exit(direction);
 | 
			
		||||
      this.start();
 | 
			
		||||
      this.updateSearchHighlight();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -130,6 +143,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.replace(transactions || [], direction, sort);
 | 
			
		||||
      this.start();
 | 
			
		||||
      this.updateSearchHighlight();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -137,6 +151,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.update(add, remove, change, direction, resetLayout);
 | 
			
		||||
      this.start();
 | 
			
		||||
      this.updateSearchHighlight();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -203,7 +218,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    } else {
 | 
			
		||||
      this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
 | 
			
		||||
        blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
 | 
			
		||||
        highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign });
 | 
			
		||||
        highlighting: this.auditHighlighting });
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -406,6 +421,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateSearchHighlight(): void {
 | 
			
		||||
    if (this.highlightTx && this.highlightTx.txid !== this.searchText && this.scene) {
 | 
			
		||||
      this.scene.setHighlight(this.highlightTx, false);
 | 
			
		||||
      this.start();
 | 
			
		||||
    } else if (this.scene?.txs && this.searchText && this.searchText.length === 64) {
 | 
			
		||||
      this.highlightTx = this.scene.txs[this.searchText];
 | 
			
		||||
      if (this.highlightTx) {
 | 
			
		||||
        this.scene.setHighlight(this.highlightTx, true);
 | 
			
		||||
        this.start();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setHighlightingEnabled(enabled: boolean): void {
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.setHighlighting(enabled);
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@ export default class BlockScene {
 | 
			
		||||
  gridWidth: number;
 | 
			
		||||
  gridHeight: number;
 | 
			
		||||
  gridSize: number;
 | 
			
		||||
  pixelAlign: boolean;
 | 
			
		||||
  vbytesPerUnit: number;
 | 
			
		||||
  unitPadding: number;
 | 
			
		||||
  unitWidth: number;
 | 
			
		||||
@ -24,24 +23,19 @@ export default class BlockScene {
 | 
			
		||||
  animateUntil = 0;
 | 
			
		||||
  dirty: boolean;
 | 
			
		||||
 | 
			
		||||
  constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
 | 
			
		||||
  constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
 | 
			
		||||
      { width: number, height: number, resolution: number, blockLimit: number,
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
 | 
			
		||||
  ) {
 | 
			
		||||
    this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign });
 | 
			
		||||
    this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
    this.gridSize = this.width / this.gridWidth;
 | 
			
		||||
    if (this.pixelAlign) {
 | 
			
		||||
      this.unitPadding =  Math.max(1, Math.floor(this.gridSize / 2.5));
 | 
			
		||||
      this.unitWidth = this.gridSize - (this.unitPadding);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.unitPadding =  width / 500;
 | 
			
		||||
      this.unitWidth = this.gridSize - (this.unitPadding * 2);
 | 
			
		||||
    }
 | 
			
		||||
    this.unitPadding =  Math.max(1, Math.floor(this.gridSize / 5));
 | 
			
		||||
    this.unitWidth = this.gridSize - (this.unitPadding * 2);
 | 
			
		||||
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
    if (this.initialised && this.scene) {
 | 
			
		||||
@ -215,15 +209,18 @@ export default class BlockScene {
 | 
			
		||||
    this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
 | 
			
		||||
  setHighlight(tx: TxView, value: boolean): void {
 | 
			
		||||
    this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
 | 
			
		||||
      { width: number, height: number, resolution: number, blockLimit: number,
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
 | 
			
		||||
  ): void {
 | 
			
		||||
    this.orientation = orientation;
 | 
			
		||||
    this.flip = flip;
 | 
			
		||||
    this.vertexArray = vertexArray;
 | 
			
		||||
    this.highlightingEnabled = highlighting;
 | 
			
		||||
    this.pixelAlign = pixelAlign;
 | 
			
		||||
 | 
			
		||||
    this.scene = {
 | 
			
		||||
      count: 0,
 | 
			
		||||
@ -349,12 +346,7 @@ export default class BlockScene {
 | 
			
		||||
  private gridToScreen(position: Square | void): Square {
 | 
			
		||||
    if (position) {
 | 
			
		||||
      const slotSize = (position.s * this.gridSize);
 | 
			
		||||
      let squareSize;
 | 
			
		||||
      if (this.pixelAlign) {
 | 
			
		||||
        squareSize = slotSize - (this.unitPadding);
 | 
			
		||||
      } else {
 | 
			
		||||
        squareSize = slotSize - (this.unitPadding * 2);
 | 
			
		||||
      }
 | 
			
		||||
      const squareSize = slotSize - (this.unitPadding * 2);
 | 
			
		||||
 | 
			
		||||
      // The grid is laid out notionally left-to-right, bottom-to-top,
 | 
			
		||||
      // so we rotate and/or flip the y axis to match the target configuration.
 | 
			
		||||
@ -430,7 +422,7 @@ export default class BlockScene {
 | 
			
		||||
 | 
			
		||||
  // 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)));
 | 
			
		||||
    const scale = Math.max(1, Math.round(Math.sqrt(1.1 * tx.vsize / this.vbytesPerUnit)));
 | 
			
		||||
    return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import BlockScene from './block-scene';
 | 
			
		||||
 | 
			
		||||
const hoverTransitionTime = 300;
 | 
			
		||||
const defaultHoverColor = hexToColor('1bd8f4');
 | 
			
		||||
const defaultHighlightColor = hexToColor('800080');
 | 
			
		||||
 | 
			
		||||
const feeColors = mempoolFeeColors.map(hexToColor);
 | 
			
		||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
 | 
			
		||||
@ -44,8 +45,10 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
  initialised: boolean;
 | 
			
		||||
  vertexArray: FastVertexArray;
 | 
			
		||||
  hover: boolean;
 | 
			
		||||
  highlight: boolean;
 | 
			
		||||
  sprite: TxSprite;
 | 
			
		||||
  hoverColor: Color | void;
 | 
			
		||||
  highlightColor: Color | void;
 | 
			
		||||
 | 
			
		||||
  screenPosition: Square;
 | 
			
		||||
  gridPosition: Square | void;
 | 
			
		||||
@ -150,8 +153,40 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hover = false;
 | 
			
		||||
      this.hoverColor = null;
 | 
			
		||||
      if (this.sprite) {
 | 
			
		||||
        this.sprite.resume(hoverTransitionTime);
 | 
			
		||||
      if (this.highlight) {
 | 
			
		||||
        this.setHighlight(true, this.highlightColor);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (this.sprite) {
 | 
			
		||||
          this.sprite.resume(hoverTransitionTime);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.dirty = false;
 | 
			
		||||
    return performance.now() + hoverTransitionTime;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Temporarily override the tx color
 | 
			
		||||
  // returns minimum transition end time
 | 
			
		||||
  setHighlight(highlightOn: boolean, color: Color | void = defaultHighlightColor): number {
 | 
			
		||||
    if (highlightOn) {
 | 
			
		||||
      this.highlight = true;
 | 
			
		||||
      this.highlightColor = color;
 | 
			
		||||
 | 
			
		||||
      this.sprite.update({
 | 
			
		||||
        ...this.highlightColor,
 | 
			
		||||
        duration: hoverTransitionTime,
 | 
			
		||||
        adjust: false,
 | 
			
		||||
        temp: true
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      this.highlight = false;
 | 
			
		||||
      this.highlightColor = null;
 | 
			
		||||
      if (this.hover) {
 | 
			
		||||
        this.setHover(true, this.hoverColor);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (this.sprite) {
 | 
			
		||||
          this.sprite.resume(hoverTransitionTime);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.dirty = false;
 | 
			
		||||
 | 
			
		||||
@ -71,7 +71,7 @@
 | 
			
		||||
      <app-block-overview-graph
 | 
			
		||||
        #blockGraph
 | 
			
		||||
        [isLoading]="false"
 | 
			
		||||
        [resolution]="75"
 | 
			
		||||
        [resolution]="80"
 | 
			
		||||
        [blockLimit]="stateService.blockVSize"
 | 
			
		||||
        [orientation]="'top'"
 | 
			
		||||
        [flip]="false"
 | 
			
		||||
 | 
			
		||||
@ -52,8 +52,8 @@
 | 
			
		||||
.chart-container {
 | 
			
		||||
  flex-grow: 0;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  width: 470px;
 | 
			
		||||
  min-width: 470px;
 | 
			
		||||
  width: 480px;
 | 
			
		||||
  min-width: 480px;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin-right: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -100,7 +100,7 @@
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-sm">
 | 
			
		||||
      <div class="col-sm" [class.graph-col]="webGlEnabled && !showAudit">
 | 
			
		||||
        <table class="table table-borderless table-striped" *ngIf="!isMobile && !(webGlEnabled && !showAudit)">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <ng-container *ngTemplateOutlet="restOfTable"></ng-container>
 | 
			
		||||
@ -110,7 +110,7 @@
 | 
			
		||||
          <app-block-overview-graph
 | 
			
		||||
            #blockGraphActual
 | 
			
		||||
            [isLoading]="isLoadingOverview"
 | 
			
		||||
            [resolution]="75"
 | 
			
		||||
            [resolution]="86"
 | 
			
		||||
            [blockLimit]="stateService.blockVSize"
 | 
			
		||||
            [orientation]="'top'"
 | 
			
		||||
            [flip]="false"
 | 
			
		||||
@ -227,7 +227,7 @@
 | 
			
		||||
      <div class="col-sm">
 | 
			
		||||
        <h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
 | 
			
		||||
        <div class="block-graph-wrapper">
 | 
			
		||||
          <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
 | 
			
		||||
          <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
 | 
			
		||||
            [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
 | 
			
		||||
            (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
 | 
			
		||||
          <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
 | 
			
		||||
@ -239,7 +239,7 @@
 | 
			
		||||
      <div class="col-sm" *ngIf="!isMobile">
 | 
			
		||||
        <h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
 | 
			
		||||
        <div class="block-graph-wrapper">
 | 
			
		||||
          <app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
 | 
			
		||||
          <app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
 | 
			
		||||
            [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"  [auditHighlighting]="showAudit"
 | 
			
		||||
            (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
 | 
			
		||||
          <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
 | 
			
		||||
 | 
			
		||||
@ -239,6 +239,7 @@ h1 {
 | 
			
		||||
.nav-tabs {
 | 
			
		||||
  border-color: white;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-tabs .nav-link {
 | 
			
		||||
@ -293,3 +294,7 @@ h1 {
 | 
			
		||||
    margin-top: 0.75rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.graph-col {
 | 
			
		||||
  flex-grow: 1.11;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-template #mempoolMode>
 | 
			
		||||
            <div class="block-sizer" [style]="blockSizerStyle">
 | 
			
		||||
              <app-mempool-block-overview [index]="blockIndex" [pixelAlign]="true"></app-mempool-block-overview>
 | 
			
		||||
              <app-mempool-block-overview [index]="blockIndex"></app-mempool-block-overview>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <div class="fader"></div>
 | 
			
		||||
 | 
			
		||||
@ -130,6 +130,7 @@ export class HashrateChartComponent implements OnInit {
 | 
			
		||||
                });
 | 
			
		||||
                ++hashIndex;
 | 
			
		||||
              }
 | 
			
		||||
              diffIndex++;
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,9 @@
 | 
			
		||||
<app-block-overview-graph
 | 
			
		||||
  #blockGraph
 | 
			
		||||
  [isLoading]="isLoading$ | async"
 | 
			
		||||
  [resolution]="75"
 | 
			
		||||
  [resolution]="86"
 | 
			
		||||
  [blockLimit]="stateService.blockVSize"
 | 
			
		||||
  [orientation]="timeLtr ? 'right' : 'left'"
 | 
			
		||||
  [flip]="true"
 | 
			
		||||
  [pixelAlign]="pixelAlign"
 | 
			
		||||
  (txClickEvent)="onTxClick($event)"
 | 
			
		||||
></app-block-overview-graph>
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Router } from '@angular/router';
 | 
			
		||||
})
 | 
			
		||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
 | 
			
		||||
  @Input() index: number;
 | 
			
		||||
  @Input() pixelAlign: boolean = false;
 | 
			
		||||
  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
			
		||||
 | 
			
		||||
@ -80,6 +80,9 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
        }
 | 
			
		||||
        return text.trim();
 | 
			
		||||
      }),
 | 
			
		||||
      tap((text) => {
 | 
			
		||||
        this.stateService.searchText$.next(text);
 | 
			
		||||
      }),
 | 
			
		||||
      distinctUntilChanged(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -75,36 +75,31 @@
 | 
			
		||||
    <div class="col" style="max-height: 410px">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
 | 
			
		||||
          <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
 | 
			
		||||
            <span> </span>
 | 
			
		||||
            <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
 | 
			
		||||
          </a>
 | 
			
		||||
          <table class="table lastest-blocks-table">
 | 
			
		||||
          <table class="table lastest-replacements-table">
 | 
			
		||||
            <thead>
 | 
			
		||||
              <th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
 | 
			
		||||
              <th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
 | 
			
		||||
              <th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
 | 
			
		||||
              <th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
 | 
			
		||||
              <th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
 | 
			
		||||
              <th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
 | 
			
		||||
              <th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
 | 
			
		||||
              <th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
 | 
			
		||||
              <th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
 | 
			
		||||
                <td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
 | 
			
		||||
                <td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
 | 
			
		||||
                <td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
 | 
			
		||||
                  <a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
 | 
			
		||||
                    <img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
 | 
			
		||||
                      onError="this.src = '/resources/mining-pools/default.svg'">
 | 
			
		||||
                    <span class="pool-name">{{ block.extras.pool.name }}</span>
 | 
			
		||||
              <tr *ngFor="let replacement of replacements$ | async;">
 | 
			
		||||
                <td class="table-cell-txid">
 | 
			
		||||
                  <a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
 | 
			
		||||
                    <app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
 | 
			
		||||
                <td class="table-cell-size">
 | 
			
		||||
                  <div class="progress">
 | 
			
		||||
                    <div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
 | 
			
		||||
                    <div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                <td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
 | 
			
		||||
                <td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
 | 
			
		||||
                <td class="table-cell-badges">
 | 
			
		||||
                  <span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
 | 
			
		||||
                  <span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
 | 
			
		||||
                  <span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="transaction.rbf">RBF</span>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
 | 
			
		||||
@ -175,39 +175,43 @@
 | 
			
		||||
  height: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.lastest-blocks-table {
 | 
			
		||||
.lastest-replacements-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  table-layout:fixed;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.65rem !important;
 | 
			
		||||
    padding-bottom: 0.7rem !important;
 | 
			
		||||
    padding-top: 0.71rem !important;
 | 
			
		||||
    padding-bottom: 0.75rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-height {
 | 
			
		||||
    width: 15%;
 | 
			
		||||
  td {
 | 
			
		||||
    overflow:hidden;
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-mined {
 | 
			
		||||
    width: 35%;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
  .table-cell-txid {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
    text-align: start;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-transaction-count {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    display: table-cell;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-size {
 | 
			
		||||
    display: none;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    width: 30%;
 | 
			
		||||
    @media (min-width: 485px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 768px) {
 | 
			
		||||
  .table-cell-old-fee {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
    text-align: end;
 | 
			
		||||
 | 
			
		||||
    @media(max-width: 1080px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    @media (min-width: 992px) {
 | 
			
		||||
      display: table-cell;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-new-fee {
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    text-align: end;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-badges {
 | 
			
		||||
    width: 23%;
 | 
			
		||||
    padding-right: 0;
 | 
			
		||||
    padding-left: 5px;
 | 
			
		||||
    text-align: end;
 | 
			
		||||
 | 
			
		||||
    .badge {
 | 
			
		||||
      margin-left: 5px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
 | 
			
		||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { filter, map, scan, share, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
 | 
			
		||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
 | 
			
		||||
import { ApiService } from '../services/api.service';
 | 
			
		||||
import { StateService } from '../services/state.service';
 | 
			
		||||
import { WebsocketService } from '../services/websocket.service';
 | 
			
		||||
@ -38,8 +38,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
 | 
			
		||||
  mempoolInfoData$: Observable<MempoolInfoData>;
 | 
			
		||||
  mempoolLoadingStatus$: Observable<number>;
 | 
			
		||||
  vBytesPerSecondLimit = 1667;
 | 
			
		||||
  blocks$: Observable<BlockExtended[]>;
 | 
			
		||||
  transactions$: Observable<TransactionStripped[]>;
 | 
			
		||||
  replacements$: Observable<ReplacementInfo[]>;
 | 
			
		||||
  latestBlockHeight: number;
 | 
			
		||||
  mempoolTransactionsWeightPerSecondData: any;
 | 
			
		||||
  mempoolStats$: Observable<MempoolStatsData>;
 | 
			
		||||
@ -58,12 +58,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.currencySubscription.unsubscribe();
 | 
			
		||||
    this.websocketService.stopTrackRbfSummary();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
 | 
			
		||||
    this.seoService.resetTitle();
 | 
			
		||||
    this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
 | 
			
		||||
    this.websocketService.startTrackRbfSummary();
 | 
			
		||||
    this.network$ = merge(of(''), this.stateService.networkChanged$);
 | 
			
		||||
    this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
 | 
			
		||||
      .pipe(
 | 
			
		||||
@ -130,23 +132,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = this.stateService.blocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        tap((blocks) => {
 | 
			
		||||
          this.latestBlockHeight = blocks[0].height;
 | 
			
		||||
        }),
 | 
			
		||||
        switchMap((blocks) => {
 | 
			
		||||
          if (this.stateService.env.MINING_DASHBOARD === true) {
 | 
			
		||||
            for (const block of blocks) {
 | 
			
		||||
              // @ts-ignore: Need to add an extra field for the template
 | 
			
		||||
              block.extras.pool.logo = `/resources/mining-pools/` +
 | 
			
		||||
                block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return of(blocks.slice(0, 6));
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.transactions$ = this.stateService.transactions$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        scan((acc, tx) => {
 | 
			
		||||
@ -159,6 +144,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }, []),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.replacements$ = this.stateService.rbfLatestSummary$;
 | 
			
		||||
 | 
			
		||||
    this.mempoolStats$ = this.stateService.connectionState$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        filter((state) => state === 2),
 | 
			
		||||
@ -219,4 +206,16 @@ export class DashboardComponent implements OnInit, OnDestroy {
 | 
			
		||||
  trackByBlock(index: number, block: BlockExtended) {
 | 
			
		||||
    return block.height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  checkFullRbf(tree: RbfTree): void {
 | 
			
		||||
    let fullRbf = false;
 | 
			
		||||
    for (const replaced of tree.replaces) {
 | 
			
		||||
      if (!replaced.tx.rbf) {
 | 
			
		||||
        fullRbf = true;
 | 
			
		||||
      }
 | 
			
		||||
      replaced.replacedBy = tree.tx;
 | 
			
		||||
      this.checkFullRbf(replaced);
 | 
			
		||||
    }
 | 
			
		||||
    tree.tx.fullRbf = fullRbf;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ export interface WebsocketResponse {
 | 
			
		||||
  txReplaced?: ReplacedTransaction;
 | 
			
		||||
  rbfInfo?: RbfTree;
 | 
			
		||||
  rbfLatest?: RbfTree[];
 | 
			
		||||
  rbfLatestSummary?: ReplacementInfo[];
 | 
			
		||||
  utxoSpent?: object;
 | 
			
		||||
  transactions?: TransactionStripped[];
 | 
			
		||||
  loadingIndicators?: ILoadingIndicators;
 | 
			
		||||
@ -29,6 +30,7 @@ export interface WebsocketResponse {
 | 
			
		||||
  'track-asset'?: string;
 | 
			
		||||
  'track-mempool-block'?: number;
 | 
			
		||||
  'track-rbf'?: string;
 | 
			
		||||
  'track-rbf-summary'?: boolean;
 | 
			
		||||
  'watch-mempool'?: boolean;
 | 
			
		||||
  'track-bisq-market'?: string;
 | 
			
		||||
  'refresh-blocks'?: boolean;
 | 
			
		||||
@ -37,6 +39,16 @@ export interface WebsocketResponse {
 | 
			
		||||
export interface ReplacedTransaction extends Transaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReplacementInfo {
 | 
			
		||||
  mined: boolean;
 | 
			
		||||
  fullRbf: boolean;
 | 
			
		||||
  txid: string;
 | 
			
		||||
  oldFee: number;
 | 
			
		||||
  oldVsize: number;
 | 
			
		||||
  newFee: number;
 | 
			
		||||
  newVsize: number;
 | 
			
		||||
}
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
  blink?: boolean;
 | 
			
		||||
  height?: number;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
 | 
			
		||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
 | 
			
		||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Router, NavigationStart } from '@angular/router';
 | 
			
		||||
import { isPlatformBrowser } from '@angular/common';
 | 
			
		||||
@ -108,6 +108,7 @@ export class StateService {
 | 
			
		||||
  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
			
		||||
  txRbfInfo$ = new Subject<RbfTree>();
 | 
			
		||||
  rbfLatest$ = new Subject<RbfTree[]>();
 | 
			
		||||
  rbfLatestSummary$ = new Subject<ReplacementInfo[]>();
 | 
			
		||||
  utxoSpent$ = new Subject<object>();
 | 
			
		||||
  difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
 | 
			
		||||
  mempoolTransactions$ = new Subject<Transaction>();
 | 
			
		||||
@ -129,6 +130,7 @@ export class StateService {
 | 
			
		||||
 | 
			
		||||
  markBlock$ = new BehaviorSubject<MarkBlockState>({});
 | 
			
		||||
  keyNavigation$ = new Subject<KeyboardEvent>();
 | 
			
		||||
  searchText$ = new BehaviorSubject<string>('');
 | 
			
		||||
 | 
			
		||||
  blockScrolling$: Subject<boolean> = new Subject<boolean>();
 | 
			
		||||
  resetScroll$: Subject<boolean> = new Subject<boolean>();
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ export class WebsocketService {
 | 
			
		||||
  private trackingTxId: string;
 | 
			
		||||
  private isTrackingMempoolBlock = false;
 | 
			
		||||
  private isTrackingRbf = false;
 | 
			
		||||
  private isTrackingRbfSummary = false;
 | 
			
		||||
  private trackingMempoolBlock: number;
 | 
			
		||||
  private latestGitCommit = '';
 | 
			
		||||
  private onlineCheckTimeout: number;
 | 
			
		||||
@ -185,6 +186,16 @@ export class WebsocketService {
 | 
			
		||||
    this.isTrackingRbf = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startTrackRbfSummary() {
 | 
			
		||||
    this.websocketSubject.next({ 'track-rbf-summary': true });
 | 
			
		||||
    this.isTrackingRbfSummary = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopTrackRbfSummary() {
 | 
			
		||||
    this.websocketSubject.next({ 'track-rbf-summary': false });
 | 
			
		||||
    this.isTrackingRbfSummary = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startTrackBisqMarket(market: string) {
 | 
			
		||||
    this.websocketSubject.next({ 'track-bisq-market': market });
 | 
			
		||||
  }
 | 
			
		||||
@ -283,6 +294,10 @@ export class WebsocketService {
 | 
			
		||||
      this.stateService.rbfLatest$.next(response.rbfLatest);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.rbfLatestSummary) {
 | 
			
		||||
      this.stateService.rbfLatestSummary$.next(response.rbfLatestSummary);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.txReplaced) {
 | 
			
		||||
      this.stateService.txReplaced$.next(response.txReplaced);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								unfurler/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								unfurler/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-unfurl",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "lockfileVersion": 2,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "mempool-unfurl",
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "version": "3.0.0-dev",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/node": "^16.11.41",
 | 
			
		||||
        "express": "^4.18.0",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-unfurl",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "version": "3.0.0-dev",
 | 
			
		||||
  "description": "Renderer for mempool open graph link preview images",
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user