Merge branch 'master' into nymkappa/fix-username-not-showing
This commit is contained in:
		
						commit
						6933d3c395
					
				@ -1,6 +1,6 @@
 | 
			
		||||
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } from '../mempool.interfaces';
 | 
			
		||||
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified, TransactionCompressed, MempoolDeltaChange } from '../mempool.interfaces';
 | 
			
		||||
import { Common, OnlineFeeStatsCalculator } from './common';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { Worker } from 'worker_threads';
 | 
			
		||||
@ -171,7 +171,7 @@ class MempoolBlocks {
 | 
			
		||||
    for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
 | 
			
		||||
      let added: TransactionClassified[] = [];
 | 
			
		||||
      let removed: string[] = [];
 | 
			
		||||
      const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
 | 
			
		||||
      const changed: TransactionClassified[] = [];
 | 
			
		||||
      if (mempoolBlocks[i] && !prevBlocks[i]) {
 | 
			
		||||
        added = mempoolBlocks[i].transactions;
 | 
			
		||||
      } else if (!mempoolBlocks[i] && prevBlocks[i]) {
 | 
			
		||||
@ -194,14 +194,14 @@ class MempoolBlocks {
 | 
			
		||||
          if (!prevIds[tx.txid]) {
 | 
			
		||||
            added.push(tx);
 | 
			
		||||
          } else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
 | 
			
		||||
            changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
 | 
			
		||||
            changed.push(tx);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      mempoolBlockDeltas.push({
 | 
			
		||||
        added,
 | 
			
		||||
        added: added.map(this.compressTx),
 | 
			
		||||
        removed,
 | 
			
		||||
        changed,
 | 
			
		||||
        changed: changed.map(this.compressDeltaChange),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return mempoolBlockDeltas;
 | 
			
		||||
@ -691,6 +691,38 @@ class MempoolBlocks {
 | 
			
		||||
    });
 | 
			
		||||
    return { blocks: convertedBlocks, blockWeights, rates: convertedRates, clusters: convertedClusters, overflow: convertedOverflow };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public compressTx(tx: TransactionClassified): TransactionCompressed {
 | 
			
		||||
    if (tx.acc) {
 | 
			
		||||
      return [
 | 
			
		||||
        tx.txid,
 | 
			
		||||
        tx.fee,
 | 
			
		||||
        tx.vsize,
 | 
			
		||||
        tx.value,
 | 
			
		||||
        Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
 | 
			
		||||
        tx.flags,
 | 
			
		||||
        1
 | 
			
		||||
      ];
 | 
			
		||||
    } else {
 | 
			
		||||
      return [
 | 
			
		||||
        tx.txid,
 | 
			
		||||
        tx.fee,
 | 
			
		||||
        tx.vsize,
 | 
			
		||||
        tx.value,
 | 
			
		||||
        Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
 | 
			
		||||
        tx.flags,
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public compressDeltaChange(tx: TransactionClassified): MempoolDeltaChange {
 | 
			
		||||
    return [
 | 
			
		||||
      tx.txid,
 | 
			
		||||
      Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
 | 
			
		||||
      tx.flags,
 | 
			
		||||
      tx.acc ? 1 : 0,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new MempoolBlocks();
 | 
			
		||||
 | 
			
		||||
@ -259,7 +259,7 @@ class WebsocketHandler {
 | 
			
		||||
              const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
 | 
			
		||||
              response['projected-block-transactions'] = JSON.stringify({
 | 
			
		||||
                index: index,
 | 
			
		||||
                blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
 | 
			
		||||
                blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-mempool-block'] = null;
 | 
			
		||||
@ -999,7 +999,7 @@ class WebsocketHandler {
 | 
			
		||||
          if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
 | 
			
		||||
            response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
 | 
			
		||||
              index: index,
 | 
			
		||||
              blockTransactions: mBlocksWithTransactions[index].transactions,
 | 
			
		||||
              blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
 | 
			
		||||
            });
 | 
			
		||||
          } else {
 | 
			
		||||
            response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
 | 
			
		||||
 | 
			
		||||
@ -65,9 +65,9 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlockDelta {
 | 
			
		||||
  added: TransactionClassified[];
 | 
			
		||||
  added: TransactionCompressed[];
 | 
			
		||||
  removed: string[];
 | 
			
		||||
  changed: { txid: string, rate: number | undefined, flags?: number }[];
 | 
			
		||||
  changed: MempoolDeltaChange[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VinStrippedToScriptsig {
 | 
			
		||||
@ -196,6 +196,11 @@ export interface TransactionClassified extends TransactionStripped {
 | 
			
		||||
  flags: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// [txid, fee, vsize, value, rate, flags, acceleration?]
 | 
			
		||||
export type TransactionCompressed = [string, number, number, number, number, number, 1?];
 | 
			
		||||
// [txid, rate, flags, acceleration?]
 | 
			
		||||
export type MempoolDeltaChange = [string, number, number, (1|0)];
 | 
			
		||||
 | 
			
		||||
// binary flags for transaction classification
 | 
			
		||||
export const TransactionFlags = {
 | 
			
		||||
  // features
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
 | 
			
		||||
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
 | 
			
		||||
  <a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions">
 | 
			
		||||
    <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
 | 
			
		||||
    <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
 | 
			
		||||
@ -14,6 +14,15 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
 | 
			
		||||
    <h5>Match</h5>
 | 
			
		||||
    <div class="btn-group btn-group-toggle">
 | 
			
		||||
      <label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'">
 | 
			
		||||
        <input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All
 | 
			
		||||
      </label>
 | 
			
		||||
      <label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
 | 
			
		||||
        <input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ng-container *ngFor="let group of filterGroups;">
 | 
			
		||||
      <h5>{{ group.label }}</h5>
 | 
			
		||||
      <div class="filter-group">
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,49 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.any-mode {
 | 
			
		||||
    .filter-tag {
 | 
			
		||||
      border: solid 1px #1a9436;
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: #1a9436;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn-group {
 | 
			
		||||
    font-size: 0.9em;
 | 
			
		||||
    margin-right: 0.25em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .mode-toggle {
 | 
			
		||||
    padding: 0.2em 0.5em;
 | 
			
		||||
    pointer-events: all;
 | 
			
		||||
    line-height: 1.5;
 | 
			
		||||
    background: #181b2daf;
 | 
			
		||||
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      border-top-left-radius: 0.2rem;
 | 
			
		||||
      border-bottom-left-radius: 0.2rem;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      border-top-right-radius: 0.2rem;
 | 
			
		||||
      border-bottom-right-radius: 0.2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.blue {
 | 
			
		||||
      border: solid 1px #105fb0;
 | 
			
		||||
      &.active {
 | 
			
		||||
        background: #105fb0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.green {
 | 
			
		||||
      border: solid 1px #1a9436;
 | 
			
		||||
      &.active {
 | 
			
		||||
        background: #1a9436;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :host-context(.block-overview-graph:hover) &, &:hover, &:active {
 | 
			
		||||
    .menu-toggle {
 | 
			
		||||
      opacity: 0.5;
 | 
			
		||||
@ -132,6 +175,11 @@
 | 
			
		||||
    .filter-tag {
 | 
			
		||||
      font-size: 0.7em;
 | 
			
		||||
    }
 | 
			
		||||
    .mode-toggle {
 | 
			
		||||
      font-size: 0.7em;
 | 
			
		||||
      margin-bottom: 5px;
 | 
			
		||||
      margin-top: 2px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.tiny {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
 | 
			
		||||
import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ import { Subscription } from 'rxjs';
 | 
			
		||||
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  @Input() cssWidth: number = 800;
 | 
			
		||||
  @Input() excludeFilters: string[] = [];
 | 
			
		||||
  @Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter();
 | 
			
		||||
  @Output() onFilterChanged: EventEmitter<ActiveFilter | null> = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  filterSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  disabledFilters: { [key: string]: boolean } = {};
 | 
			
		||||
  activeFilters: string[] = [];
 | 
			
		||||
  filterFlags: { [key: string]: boolean } = {};
 | 
			
		||||
  filterMode: FilterMode = 'and';
 | 
			
		||||
  menuOpen: boolean = false;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -29,15 +30,16 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.filterSubscription = this.stateService.activeGoggles$.subscribe((activeFilters: string[]) => {
 | 
			
		||||
    this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
 | 
			
		||||
      this.filterMode = active.mode;
 | 
			
		||||
      for (const key of Object.keys(this.filterFlags)) {
 | 
			
		||||
        this.filterFlags[key] = false;
 | 
			
		||||
      }
 | 
			
		||||
      for (const key of activeFilters) {
 | 
			
		||||
      for (const key of active.filters) {
 | 
			
		||||
        this.filterFlags[key] = !this.disabledFilters[key];
 | 
			
		||||
      }
 | 
			
		||||
      this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])];
 | 
			
		||||
      this.onFilterChanged.emit(this.getBooleanFlags());
 | 
			
		||||
      this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
 | 
			
		||||
      this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -53,6 +55,12 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFilterMode(mode): void {
 | 
			
		||||
    this.filterMode = mode;
 | 
			
		||||
    this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
 | 
			
		||||
    this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleFilter(key): void {
 | 
			
		||||
    const filter = this.filters[key];
 | 
			
		||||
    this.filterFlags[key] = !this.filterFlags[key];
 | 
			
		||||
@ -73,8 +81,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
      this.activeFilters = this.activeFilters.filter(f => f != key);
 | 
			
		||||
    }
 | 
			
		||||
    const booleanFlags = this.getBooleanFlags();
 | 
			
		||||
    this.onFilterChanged.emit(booleanFlags);
 | 
			
		||||
    this.stateService.activeGoggles$.next([...this.activeFilters]);
 | 
			
		||||
    this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
 | 
			
		||||
    this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  getBooleanFlags(): bigint | null {
 | 
			
		||||
@ -90,7 +98,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  @HostListener('document:click', ['$event'])
 | 
			
		||||
  onClick(event): boolean {
 | 
			
		||||
    // click away from menu
 | 
			
		||||
    if (!event.target.closest('button')) {
 | 
			
		||||
    if (!event.target.closest('button') && !event.target.closest('label')) {
 | 
			
		||||
      this.menuOpen = false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,9 @@
 | 
			
		||||
      [auditEnabled]="auditHighlighting"
 | 
			
		||||
      [blockConversion]="blockConversion"
 | 
			
		||||
    ></app-block-overview-tooltip>
 | 
			
		||||
    <app-block-filters *ngIf="showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
 | 
			
		||||
    <app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
 | 
			
		||||
    <div *ngIf="!webGlEnabled" class="placeholder">
 | 
			
		||||
      <span i18n="webgl-disabled">Your browser does not support this feature.</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,19 @@
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  grid-column: 1/-1;
 | 
			
		||||
 | 
			
		||||
  .placeholder {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-align {
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,8 @@ import { Price } from '../../services/price.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils';
 | 
			
		||||
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
 | 
			
		||||
const unmatchedOpacity = 0.2;
 | 
			
		||||
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
 | 
			
		||||
@ -42,7 +44,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  @Input() showFilters: boolean = false;
 | 
			
		||||
  @Input() excludeFilters: string[] = [];
 | 
			
		||||
  @Input() filterFlags: bigint | null = null;
 | 
			
		||||
  @Input() filterMode: 'and' | 'or' = 'and';
 | 
			
		||||
  @Input() filterMode: FilterMode = 'and';
 | 
			
		||||
  @Input() blockConversion: Price;
 | 
			
		||||
  @Input() overrideColors: ((tx: TxView) => Color) | null = null;
 | 
			
		||||
  @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
 | 
			
		||||
@ -76,11 +78,14 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  filtersAvailable: boolean = true;
 | 
			
		||||
  activeFilterFlags: bigint | null = null;
 | 
			
		||||
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    readonly ngZone: NgZone,
 | 
			
		||||
    readonly elRef: ElementRef,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = detectWebGL();
 | 
			
		||||
    this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
 | 
			
		||||
    this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
 | 
			
		||||
      this.searchText = text;
 | 
			
		||||
@ -119,10 +124,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFilterFlags(flags?: bigint | null): void {
 | 
			
		||||
    this.activeFilterFlags = this.filterFlags || flags || null;
 | 
			
		||||
  setFilterFlags(goggle?: ActiveFilter): void {
 | 
			
		||||
    this.filterMode = goggle?.mode || this.filterMode;
 | 
			
		||||
    this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      if (this.activeFilterFlags != null) {
 | 
			
		||||
      if (this.activeFilterFlags != null && this.filtersAvailable) {
 | 
			
		||||
        this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
 | 
			
		||||
      } else {
 | 
			
		||||
        this.scene.setColorFunction(this.overrideColors);
 | 
			
		||||
@ -157,7 +163,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
 | 
			
		||||
  // initialize the scene without any entry transition
 | 
			
		||||
  setup(transactions: TransactionStripped[]): void {
 | 
			
		||||
    this.filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
 | 
			
		||||
    const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
 | 
			
		||||
    if (filtersAvailable !== this.filtersAvailable) {
 | 
			
		||||
      this.setFilterFlags();
 | 
			
		||||
    }
 | 
			
		||||
    this.filtersAvailable = filtersAvailable;
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.setup(transactions);
 | 
			
		||||
      this.readyNextFrame = true;
 | 
			
		||||
@ -500,11 +510,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
 | 
			
		||||
    const x = cssX * window.devicePixelRatio;
 | 
			
		||||
    const y = cssY * window.devicePixelRatio;
 | 
			
		||||
    const selected = this.scene.getTxAt({ x, y });
 | 
			
		||||
    if (selected && selected.txid) {
 | 
			
		||||
      this.txClickEvent.emit({ tx: selected, keyModifier });
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      const x = cssX * window.devicePixelRatio;
 | 
			
		||||
      const y = cssY * window.devicePixelRatio;
 | 
			
		||||
      const selected = this.scene.getTxAt({ x, y });
 | 
			
		||||
      if (selected && selected.txid) {
 | 
			
		||||
        this.txClickEvent.emit({ tx: selected, keyModifier });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -524,7 +536,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
 | 
			
		||||
  getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
 | 
			
		||||
    return (tx: TxView) => {
 | 
			
		||||
      if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (tx.bigintFlags & flags) > 0n)) {
 | 
			
		||||
      if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
 | 
			
		||||
        return defaultColorFunction(tx);
 | 
			
		||||
      } else {
 | 
			
		||||
        return defaultColorFunction(
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { Color } from '../block-overview-graph/sprite-types';
 | 
			
		||||
import TxView from '../block-overview-graph/tx-view';
 | 
			
		||||
import { FilterMode } from '../../shared/filters.utils';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mempool-block-overview',
 | 
			
		||||
@ -22,7 +23,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
			
		||||
  @Input() showFilters: boolean = false;
 | 
			
		||||
  @Input() overrideColors: ((tx: TxView) => Color) | null = null;
 | 
			
		||||
  @Input() filterFlags: bigint | undefined = undefined;
 | 
			
		||||
  @Input() filterMode: 'and' | 'or' = 'and';
 | 
			
		||||
  @Input() filterMode: FilterMode = 'and';
 | 
			
		||||
  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
			
		||||
@ -99,7 +100,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
			
		||||
          const inOldBlock = {};
 | 
			
		||||
          const inNewBlock = {};
 | 
			
		||||
          const added: TransactionStripped[] = [];
 | 
			
		||||
          const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
 | 
			
		||||
          const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = [];
 | 
			
		||||
          const removed: string[] = [];
 | 
			
		||||
          for (const tx of transactionsStripped) {
 | 
			
		||||
            inNewBlock[tx.txid] = true;
 | 
			
		||||
@ -117,6 +118,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
			
		||||
              changed.push({
 | 
			
		||||
                txid: tx.txid,
 | 
			
		||||
                rate: tx.rate,
 | 
			
		||||
                flags: tx.flags,
 | 
			
		||||
                acc: tx.acc
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@
 | 
			
		||||
            <div class="quick-filter">
 | 
			
		||||
              <div class="btn-group btn-group-toggle">
 | 
			
		||||
                <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex"  *ngFor="let filter of goggleCycle">
 | 
			
		||||
                  <input type="radio" [value]="'3m'" fragment="3m" (click)="goggleIndex = filter.index" [attr.data-cy]="'3m'"> {{ filter.name }}
 | 
			
		||||
                  <input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
 | 
			
		||||
                </label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -34,8 +34,8 @@
 | 
			
		||||
              <app-mempool-block-overview
 | 
			
		||||
                [index]="0"
 | 
			
		||||
                [resolution]="goggleResolution"
 | 
			
		||||
                [filterFlags]="goggleCycle[goggleIndex].flag"
 | 
			
		||||
                filterMode="or"
 | 
			
		||||
                [filterFlags]="goggleFlags"
 | 
			
		||||
                [filterMode]="goggleMode"
 | 
			
		||||
              ></app-mempool-block-overview>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import { ApiService } from '../services/api.service';
 | 
			
		||||
import { StateService } from '../services/state.service';
 | 
			
		||||
import { WebsocketService } from '../services/websocket.service';
 | 
			
		||||
import { SeoService } from '../services/seo.service';
 | 
			
		||||
import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils';
 | 
			
		||||
 | 
			
		||||
interface MempoolBlocksData {
 | 
			
		||||
  blocks: number;
 | 
			
		||||
@ -55,6 +56,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
  currentReserves$: Observable<CurrentPegs>;
 | 
			
		||||
  fullHistory$: Observable<any>;
 | 
			
		||||
  isLoad: boolean = true;
 | 
			
		||||
  filterSubscription: Subscription;
 | 
			
		||||
  mempoolInfoSubscription: Subscription;
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
@ -65,13 +67,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
  private lastReservesBlockUpdate: number = 0;
 | 
			
		||||
 | 
			
		||||
  goggleResolution = 82;
 | 
			
		||||
  goggleCycle = [
 | 
			
		||||
    { index: 0, name: 'All' },
 | 
			
		||||
    { index: 1, name: 'Consolidations', flag: 0b00000010_00000000_00000000_00000000_00000000n },
 | 
			
		||||
    { index: 2, name: 'Coinjoin', flag: 0b00000001_00000000_00000000_00000000_00000000n },
 | 
			
		||||
    { index: 3, name: '💩', flag: 0b00000100_00000000_00000000_00000000n | 0b00000010_00000000_00000000_00000000n | 0b00000001_00000000_00000000_00000000n },
 | 
			
		||||
  goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [
 | 
			
		||||
    { index: 0, name: 'All', mode: 'and', filters: [] },
 | 
			
		||||
    { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] },
 | 
			
		||||
    { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] },
 | 
			
		||||
    { index: 3, name: 'Data', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'] },
 | 
			
		||||
  ];
 | 
			
		||||
  goggleIndex = 0; // Math.floor(Math.random() * this.goggleCycle.length);
 | 
			
		||||
  goggleFlags = 0n;
 | 
			
		||||
  goggleMode: FilterMode = 'and';
 | 
			
		||||
  goggleIndex = 0;
 | 
			
		||||
 | 
			
		||||
  private destroy$ = new Subject();
 | 
			
		||||
 | 
			
		||||
@ -87,6 +91,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.filterSubscription.unsubscribe();
 | 
			
		||||
    this.mempoolInfoSubscription.unsubscribe();
 | 
			
		||||
    this.currencySubscription.unsubscribe();
 | 
			
		||||
    this.websocketService.stopTrackRbfSummary();
 | 
			
		||||
@ -107,6 +112,30 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
        map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
 | 
			
		||||
      const activeFilters = active.filters.sort().join(',');
 | 
			
		||||
      for (const goggle of this.goggleCycle) {
 | 
			
		||||
        if (goggle.mode === active.mode) {
 | 
			
		||||
          const goggleFilters = goggle.filters.sort().join(',');
 | 
			
		||||
          if (goggleFilters === activeFilters) {
 | 
			
		||||
            this.goggleIndex = goggle.index;
 | 
			
		||||
            this.goggleFlags = toFlags(goggle.filters);
 | 
			
		||||
            this.goggleMode = goggle.mode;
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this.goggleCycle.push({
 | 
			
		||||
        index: this.goggleCycle.length,
 | 
			
		||||
        name: 'Custom',
 | 
			
		||||
        mode: active.mode,
 | 
			
		||||
        filters: active.filters,
 | 
			
		||||
      });
 | 
			
		||||
      this.goggleIndex = this.goggleCycle.length - 1;
 | 
			
		||||
      this.goggleFlags = toFlags(active.filters);
 | 
			
		||||
      this.goggleMode = active.mode;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.mempoolInfoData$ = combineLatest([
 | 
			
		||||
      this.stateService.mempoolInfo$,
 | 
			
		||||
      this.stateService.vbytesPerSecond$
 | 
			
		||||
@ -375,6 +404,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
    return Array.from({ length: num }, (_, i) => i + 1);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  setFilter(index): void {
 | 
			
		||||
    const selected = this.goggleCycle[index];
 | 
			
		||||
    this.stateService.activeGoggles$.next(selected);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
  onResize(): void {
 | 
			
		||||
    if (window.innerWidth >= 992) {
 | 
			
		||||
 | 
			
		||||
@ -70,9 +70,15 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlockDelta {
 | 
			
		||||
  added: TransactionStripped[],
 | 
			
		||||
  removed: string[],
 | 
			
		||||
  changed?: { txid: string, rate: number | undefined, acc: boolean | undefined }[];
 | 
			
		||||
  added: TransactionStripped[];
 | 
			
		||||
  removed: string[];
 | 
			
		||||
  changed: { txid: string, rate: number, flags: number, acc: boolean }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlockDeltaCompressed {
 | 
			
		||||
  added: TransactionCompressed[];
 | 
			
		||||
  removed: string[];
 | 
			
		||||
  changed: MempoolDeltaChange[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolInfo {
 | 
			
		||||
@ -97,6 +103,11 @@ export interface TransactionStripped {
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// [txid, fee, vsize, value, rate, flags, acceleration?]
 | 
			
		||||
export type TransactionCompressed = [string, number, number, number, number, number, 1?];
 | 
			
		||||
// [txid, rate, flags, acceleration?]
 | 
			
		||||
export type MempoolDeltaChange = [string, number, number, (1|0)];
 | 
			
		||||
 | 
			
		||||
export interface IBackendInfo {
 | 
			
		||||
  hostname?: string;
 | 
			
		||||
  gitCommit: string;
 | 
			
		||||
 | 
			
		||||
@ -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, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionCompressed, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Router, NavigationStart } from '@angular/router';
 | 
			
		||||
import { isPlatformBrowser } from '@angular/common';
 | 
			
		||||
@ -9,6 +9,7 @@ import { filter, map, scan, shareReplay } from 'rxjs/operators';
 | 
			
		||||
import { StorageService } from './storage.service';
 | 
			
		||||
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
 | 
			
		||||
import { ApiService } from './api.service';
 | 
			
		||||
import { ActiveFilter } from '../shared/filters.utils';
 | 
			
		||||
 | 
			
		||||
export interface MarkBlockState {
 | 
			
		||||
  blockHeight?: number;
 | 
			
		||||
@ -150,7 +151,7 @@ export class StateService {
 | 
			
		||||
  searchFocus$: Subject<boolean> = new Subject<boolean>();
 | 
			
		||||
  menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
 | 
			
		||||
 | 
			
		||||
  activeGoggles$: BehaviorSubject<string[]> = new BehaviorSubject([]);
 | 
			
		||||
  activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [] });
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(PLATFORM_ID) private platformId: any,
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { ApiService } from './api.service';
 | 
			
		||||
import { take } from 'rxjs/operators';
 | 
			
		||||
import { TransferState, makeStateKey } from '@angular/platform-browser';
 | 
			
		||||
import { CacheService } from './cache.service';
 | 
			
		||||
import { uncompressDeltaChange, uncompressTx } from '../shared/common.utils';
 | 
			
		||||
 | 
			
		||||
const OFFLINE_RETRY_AFTER_MS = 2000;
 | 
			
		||||
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
 | 
			
		||||
@ -382,9 +383,9 @@ export class WebsocketService {
 | 
			
		||||
    if (response['projected-block-transactions']) {
 | 
			
		||||
      if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
 | 
			
		||||
        if (response['projected-block-transactions'].blockTransactions) {
 | 
			
		||||
          this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions);
 | 
			
		||||
          this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions.map(uncompressTx));
 | 
			
		||||
        } else if (response['projected-block-transactions'].delta) {
 | 
			
		||||
          this.stateService.mempoolBlockDelta$.next(response['projected-block-transactions'].delta);
 | 
			
		||||
          this.stateService.mempoolBlockDelta$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed, TransactionStripped } from "../interfaces/websocket.interface";
 | 
			
		||||
 | 
			
		||||
export function isMobile(): boolean {
 | 
			
		||||
  return (window.innerWidth <= 767.98);
 | 
			
		||||
}
 | 
			
		||||
@ -152,4 +154,29 @@ export function seoDescriptionNetwork(network: string): string {
 | 
			
		||||
    return ' ' + network.charAt(0).toUpperCase() + network.slice(1);
 | 
			
		||||
  }
 | 
			
		||||
  return '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
 | 
			
		||||
  return {
 | 
			
		||||
    txid: tx[0],
 | 
			
		||||
    fee: tx[1],
 | 
			
		||||
    vsize: tx[2],
 | 
			
		||||
    value: tx[3],
 | 
			
		||||
    rate: tx[4],
 | 
			
		||||
    flags: tx[5],
 | 
			
		||||
    acc: !!tx[6],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
 | 
			
		||||
  return {
 | 
			
		||||
    added: delta.added.map(uncompressTx),
 | 
			
		||||
    removed: delta.removed,
 | 
			
		||||
    changed: delta.changed.map(tx => ({
 | 
			
		||||
      txid: tx[0],
 | 
			
		||||
      rate: tx[1],
 | 
			
		||||
      flags: tx[2],
 | 
			
		||||
      acc: !!tx[3],
 | 
			
		||||
    }))
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -7,6 +7,13 @@ export interface Filter {
 | 
			
		||||
  important?: boolean,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type FilterMode = 'and' | 'or';
 | 
			
		||||
 | 
			
		||||
export interface ActiveFilter {
 | 
			
		||||
  mode: FilterMode,
 | 
			
		||||
  filters: string[],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// binary flags for transaction classification
 | 
			
		||||
export const TransactionFlags = {
 | 
			
		||||
  // features
 | 
			
		||||
@ -43,6 +50,14 @@ export const TransactionFlags = {
 | 
			
		||||
  sighash_acp:    0b00010000_00000000_00000000_00000000_00000000_00000000n,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function toFlags(filters: string[]): bigint {
 | 
			
		||||
  let flag = 0n;
 | 
			
		||||
  for (const filter of filters) {
 | 
			
		||||
    flag |= TransactionFlags[filter];
 | 
			
		||||
  }
 | 
			
		||||
  return flag;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TransactionFilters: { [key: string]: Filter } = {
 | 
			
		||||
    /* features */
 | 
			
		||||
    rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true },
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user