Merge branch 'master' into nymkappa/fix-username-not-showing

This commit is contained in:
nymkappa 2024-02-11 12:03:04 +01:00 committed by GitHub
commit 6933d3c395
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 270 additions and 49 deletions

View File

@ -1,6 +1,6 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt'; import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import logger from '../logger'; 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 { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config'; import config from '../config';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
@ -171,7 +171,7 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionClassified[] = []; let added: TransactionClassified[] = [];
let removed: string[] = []; let removed: string[] = [];
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = []; const changed: TransactionClassified[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) { if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions; added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) { } else if (!mempoolBlocks[i] && prevBlocks[i]) {
@ -194,14 +194,14 @@ class MempoolBlocks {
if (!prevIds[tx.txid]) { if (!prevIds[tx.txid]) {
added.push(tx); added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) { } 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({ mempoolBlockDeltas.push({
added, added: added.map(this.compressTx),
removed, removed,
changed, changed: changed.map(this.compressDeltaChange),
}); });
} }
return mempoolBlockDeltas; return mempoolBlockDeltas;
@ -691,6 +691,38 @@ class MempoolBlocks {
}); });
return { blocks: convertedBlocks, blockWeights, rates: convertedRates, clusters: convertedClusters, overflow: convertedOverflow }; 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(); export default new MempoolBlocks();

View File

@ -259,7 +259,7 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = JSON.stringify({ response['projected-block-transactions'] = JSON.stringify({
index: index, index: index,
blockTransactions: mBlocksWithTransactions[index]?.transactions || [], blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
}); });
} else { } else {
client['track-mempool-block'] = null; client['track-mempool-block'] = null;
@ -999,7 +999,7 @@ class WebsocketHandler {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) { if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index, index: index,
blockTransactions: mBlocksWithTransactions[index].transactions, blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
}); });
} else { } else {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {

View File

@ -65,9 +65,9 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
} }
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionClassified[]; added: TransactionCompressed[];
removed: string[]; removed: string[];
changed: { txid: string, rate: number | undefined, flags?: number }[]; changed: MempoolDeltaChange[];
} }
interface VinStrippedToScriptsig { interface VinStrippedToScriptsig {
@ -196,6 +196,11 @@ export interface TransactionClassified extends TransactionStripped {
flags: number; 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 // binary flags for transaction classification
export const TransactionFlags = { export const TransactionFlags = {
// features // features

View File

@ -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"> <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> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon> <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
@ -14,6 +14,15 @@
</div> </div>
</div> </div>
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280"> <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;"> <ng-container *ngFor="let group of filterGroups;">
<h5>{{ group.label }}</h5> <h5>{{ group.label }}</h5>
<div class="filter-group"> <div class="filter-group">

View File

@ -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 { :host-context(.block-overview-graph:hover) &, &:hover, &:active {
.menu-toggle { .menu-toggle {
opacity: 0.5; opacity: 0.5;
@ -132,6 +175,11 @@
.filter-tag { .filter-tag {
font-size: 0.7em; font-size: 0.7em;
} }
.mode-toggle {
font-size: 0.7em;
margin-bottom: 5px;
margin-top: 2px;
}
} }
&.tiny { &.tiny {

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; 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 { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -12,7 +12,7 @@ import { Subscription } from 'rxjs';
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@Input() cssWidth: number = 800; @Input() cssWidth: number = 800;
@Input() excludeFilters: string[] = []; @Input() excludeFilters: string[] = [];
@Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter(); @Output() onFilterChanged: EventEmitter<ActiveFilter | null> = new EventEmitter();
filterSubscription: Subscription; filterSubscription: Subscription;
@ -21,6 +21,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
disabledFilters: { [key: string]: boolean } = {}; disabledFilters: { [key: string]: boolean } = {};
activeFilters: string[] = []; activeFilters: string[] = [];
filterFlags: { [key: string]: boolean } = {}; filterFlags: { [key: string]: boolean } = {};
filterMode: FilterMode = 'and';
menuOpen: boolean = false; menuOpen: boolean = false;
constructor( constructor(
@ -29,15 +30,16 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
) {} ) {}
ngOnInit(): void { 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)) { for (const key of Object.keys(this.filterFlags)) {
this.filterFlags[key] = false; this.filterFlags[key] = false;
} }
for (const key of activeFilters) { for (const key of active.filters) {
this.filterFlags[key] = !this.disabledFilters[key]; this.filterFlags[key] = !this.disabledFilters[key];
} }
this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])]; this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit(this.getBooleanFlags()); 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 { toggleFilter(key): void {
const filter = this.filters[key]; const filter = this.filters[key];
this.filterFlags[key] = !this.filterFlags[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); this.activeFilters = this.activeFilters.filter(f => f != key);
} }
const booleanFlags = this.getBooleanFlags(); const booleanFlags = this.getBooleanFlags();
this.onFilterChanged.emit(booleanFlags); this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next([...this.activeFilters]); this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
} }
getBooleanFlags(): bigint | null { getBooleanFlags(): bigint | null {
@ -90,7 +98,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
onClick(event): boolean { onClick(event): boolean {
// click away from menu // click away from menu
if (!event.target.closest('button')) { if (!event.target.closest('button') && !event.target.closest('label')) {
this.menuOpen = false; this.menuOpen = false;
} }
return true; return true;

View File

@ -13,6 +13,9 @@
[auditEnabled]="auditHighlighting" [auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion" [blockConversion]="blockConversion"
></app-block-overview-tooltip> ></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>
</div> </div>

View File

@ -7,6 +7,19 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
grid-column: 1/-1; 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 { .grid-align {

View File

@ -9,6 +9,8 @@ import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; 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 unmatchedOpacity = 0.2;
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
@ -42,7 +44,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() showFilters: boolean = false; @Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = []; @Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null; @Input() filterFlags: bigint | null = null;
@Input() filterMode: 'and' | 'or' = 'and'; @Input() filterMode: FilterMode = 'and';
@Input() blockConversion: Price; @Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null; @Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@ -76,11 +78,14 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
filtersAvailable: boolean = true; filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null; activeFilterFlags: bigint | null = null;
webGlEnabled = true;
constructor( constructor(
readonly ngZone: NgZone, readonly ngZone: NgZone,
readonly elRef: ElementRef, readonly elRef: ElementRef,
private stateService: StateService, private stateService: StateService,
) { ) {
this.webGlEnabled = detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize); this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => { this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text; this.searchText = text;
@ -119,10 +124,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
setFilterFlags(flags?: bigint | null): void { setFilterFlags(goggle?: ActiveFilter): void {
this.activeFilterFlags = this.filterFlags || flags || null; this.filterMode = goggle?.mode || this.filterMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
if (this.scene) { if (this.scene) {
if (this.activeFilterFlags != null) { if (this.activeFilterFlags != null && this.filtersAvailable) {
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags)); this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
} else { } else {
this.scene.setColorFunction(this.overrideColors); this.scene.setColorFunction(this.overrideColors);
@ -157,7 +163,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
// initialize the scene without any entry transition // initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void { 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) { if (this.scene) {
this.scene.setup(transactions); this.scene.setup(transactions);
this.readyNextFrame = true; this.readyNextFrame = true;
@ -500,6 +510,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) { onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
if (this.scene) {
const x = cssX * window.devicePixelRatio; const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio; const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y }); const selected = this.scene.getTxAt({ x, y });
@ -507,6 +518,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.txClickEvent.emit({ tx: selected, keyModifier }); this.txClickEvent.emit({ tx: selected, keyModifier });
} }
} }
}
onTxHover(hoverId: string) { onTxHover(hoverId: string) {
this.txHoverEvent.emit(hoverId); this.txHoverEvent.emit(hoverId);
@ -524,7 +536,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
return (tx: TxView) => { 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); return defaultColorFunction(tx);
} else { } else {
return defaultColorFunction( return defaultColorFunction(

View File

@ -10,6 +10,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Color } from '../block-overview-graph/sprite-types'; import { Color } from '../block-overview-graph/sprite-types';
import TxView from '../block-overview-graph/tx-view'; import TxView from '../block-overview-graph/tx-view';
import { FilterMode } from '../../shared/filters.utils';
@Component({ @Component({
selector: 'app-mempool-block-overview', selector: 'app-mempool-block-overview',
@ -22,7 +23,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
@Input() showFilters: boolean = false; @Input() showFilters: boolean = false;
@Input() overrideColors: ((tx: TxView) => Color) | null = null; @Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Input() filterFlags: bigint | undefined = undefined; @Input() filterFlags: bigint | undefined = undefined;
@Input() filterMode: 'and' | 'or' = 'and'; @Input() filterMode: FilterMode = 'and';
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
@ -99,7 +100,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
const inOldBlock = {}; const inOldBlock = {};
const inNewBlock = {}; const inNewBlock = {};
const added: TransactionStripped[] = []; 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[] = []; const removed: string[] = [];
for (const tx of transactionsStripped) { for (const tx of transactionsStripped) {
inNewBlock[tx.txid] = true; inNewBlock[tx.txid] = true;
@ -117,6 +118,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
changed.push({ changed.push({
txid: tx.txid, txid: tx.txid,
rate: tx.rate, rate: tx.rate,
flags: tx.flags,
acc: tx.acc acc: tx.acc
}); });
} }

View File

@ -26,7 +26,7 @@
<div class="quick-filter"> <div class="quick-filter">
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle"> <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> </label>
</div> </div>
</div> </div>
@ -34,8 +34,8 @@
<app-mempool-block-overview <app-mempool-block-overview
[index]="0" [index]="0"
[resolution]="goggleResolution" [resolution]="goggleResolution"
[filterFlags]="goggleCycle[goggleIndex].flag" [filterFlags]="goggleFlags"
filterMode="or" [filterMode]="goggleMode"
></app-mempool-block-overview> ></app-mempool-block-overview>
</div> </div>
</ng-template> </ng-template>

View File

@ -7,6 +7,7 @@ import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service'; import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';
import { SeoService } from '../services/seo.service'; import { SeoService } from '../services/seo.service';
import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils';
interface MempoolBlocksData { interface MempoolBlocksData {
blocks: number; blocks: number;
@ -55,6 +56,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
currentReserves$: Observable<CurrentPegs>; currentReserves$: Observable<CurrentPegs>;
fullHistory$: Observable<any>; fullHistory$: Observable<any>;
isLoad: boolean = true; isLoad: boolean = true;
filterSubscription: Subscription;
mempoolInfoSubscription: Subscription; mempoolInfoSubscription: Subscription;
currencySubscription: Subscription; currencySubscription: Subscription;
currency: string; currency: string;
@ -65,13 +67,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
private lastReservesBlockUpdate: number = 0; private lastReservesBlockUpdate: number = 0;
goggleResolution = 82; goggleResolution = 82;
goggleCycle = [ goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [
{ index: 0, name: 'All' }, { index: 0, name: 'All', mode: 'and', filters: [] },
{ index: 1, name: 'Consolidations', flag: 0b00000010_00000000_00000000_00000000_00000000n }, { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] },
{ index: 2, name: 'Coinjoin', flag: 0b00000001_00000000_00000000_00000000_00000000n }, { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] },
{ index: 3, name: '💩', flag: 0b00000100_00000000_00000000_00000000n | 0b00000010_00000000_00000000_00000000n | 0b00000001_00000000_00000000_00000000n }, { 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(); private destroy$ = new Subject();
@ -87,6 +91,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.filterSubscription.unsubscribe();
this.mempoolInfoSubscription.unsubscribe(); this.mempoolInfoSubscription.unsubscribe();
this.currencySubscription.unsubscribe(); this.currencySubscription.unsubscribe();
this.websocketService.stopTrackRbfSummary(); this.websocketService.stopTrackRbfSummary();
@ -107,6 +112,30 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100) 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.mempoolInfoData$ = combineLatest([
this.stateService.mempoolInfo$, this.stateService.mempoolInfo$,
this.stateService.vbytesPerSecond$ this.stateService.vbytesPerSecond$
@ -375,6 +404,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
return Array.from({ length: num }, (_, i) => i + 1); 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']) @HostListener('window:resize', ['$event'])
onResize(): void { onResize(): void {
if (window.innerWidth >= 992) { if (window.innerWidth >= 992) {

View File

@ -70,9 +70,15 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
} }
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionStripped[], added: TransactionStripped[];
removed: string[], removed: string[];
changed?: { txid: string, rate: number | undefined, acc: boolean | undefined }[]; changed: { txid: string, rate: number, flags: number, acc: boolean }[];
}
export interface MempoolBlockDeltaCompressed {
added: TransactionCompressed[];
removed: string[];
changed: MempoolDeltaChange[];
} }
export interface MempoolInfo { export interface MempoolInfo {
@ -97,6 +103,11 @@ export interface TransactionStripped {
context?: 'projected' | 'actual'; 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 { export interface IBackendInfo {
hostname?: string; hostname?: string;
gitCommit: string; gitCommit: string;

View File

@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; 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 { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
@ -9,6 +9,7 @@ import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { ActiveFilter } from '../shared/filters.utils';
export interface MarkBlockState { export interface MarkBlockState {
blockHeight?: number; blockHeight?: number;
@ -150,7 +151,7 @@ export class StateService {
searchFocus$: Subject<boolean> = new Subject<boolean>(); searchFocus$: Subject<boolean> = new Subject<boolean>();
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false); menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
activeGoggles$: BehaviorSubject<string[]> = new BehaviorSubject([]); activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [] });
constructor( constructor(
@Inject(PLATFORM_ID) private platformId: any, @Inject(PLATFORM_ID) private platformId: any,

View File

@ -8,6 +8,7 @@ import { ApiService } from './api.service';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser'; import { TransferState, makeStateKey } from '@angular/platform-browser';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';
import { uncompressDeltaChange, uncompressTx } from '../shared/common.utils';
const OFFLINE_RETRY_AFTER_MS = 2000; const OFFLINE_RETRY_AFTER_MS = 2000;
const OFFLINE_PING_CHECK_AFTER_MS = 30000; const OFFLINE_PING_CHECK_AFTER_MS = 30000;
@ -382,9 +383,9 @@ export class WebsocketService {
if (response['projected-block-transactions']) { if (response['projected-block-transactions']) {
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) { if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
if (response['projected-block-transactions'].blockTransactions) { 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) { } 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));
} }
} }
} }

View File

@ -1,3 +1,5 @@
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed, TransactionStripped } from "../interfaces/websocket.interface";
export function isMobile(): boolean { export function isMobile(): boolean {
return (window.innerWidth <= 767.98); return (window.innerWidth <= 767.98);
} }
@ -153,3 +155,28 @@ export function seoDescriptionNetwork(network: string): string {
} }
return ''; 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],
}))
};
}

View File

@ -7,6 +7,13 @@ export interface Filter {
important?: boolean, important?: boolean,
} }
export type FilterMode = 'and' | 'or';
export interface ActiveFilter {
mode: FilterMode,
filters: string[],
}
// binary flags for transaction classification // binary flags for transaction classification
export const TransactionFlags = { export const TransactionFlags = {
// features // features
@ -43,6 +50,14 @@ export const TransactionFlags = {
sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, 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 } = { export const TransactionFilters: { [key: string]: Filter } = {
/* features */ /* features */
rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true }, rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true },