diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 58921fcfb..b9da7d4e8 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -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(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index d0e0b7fd8..3091a09cf 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -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}`, { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index ead0a84ad..71612f25f 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -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 diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index f60b04cdd..8c79cd438 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -1,4 +1,4 @@ -
+
beta @@ -14,6 +14,15 @@
+
Match
+
+ + +
{{ group.label }}
diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss index 6406a1d93..1009efd72 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.scss +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -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 { diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts index 9951984df..a16475c23 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.ts +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -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 = new EventEmitter(); + @Output() onFilterChanged: EventEmitter = 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; diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html index 9d27d8d90..34d192678 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html @@ -13,6 +13,9 @@ [auditEnabled]="auditHighlighting" [blockConversion]="blockConversion" > - + +
+ Your browser does not support this feature. +
diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss index d30dd3305..92964d948 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss @@ -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 { diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index ac1df2bf5..95305d72f 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -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( diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 7fb036718..29825491c 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -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(); @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 }); } diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index a2c46a198..6425e3b9a 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -26,7 +26,7 @@
@@ -34,8 +34,8 @@ diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 5dfd68419..1714d3d01 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -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; fullHistory$: Observable; 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) { diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 35bcbe9cc..ff5977332 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -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; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index f87a3dc31..dc1365baa 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -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 = new Subject(); menuOpen$: BehaviorSubject = new BehaviorSubject(false); - activeGoggles$: BehaviorSubject = new BehaviorSubject([]); + activeGoggles$: BehaviorSubject = new BehaviorSubject({ mode: 'and', filters: [] }); constructor( @Inject(PLATFORM_ID) private platformId: any, diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 3c72252db..11e24ef71 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -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)); } } } diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index a04fa1663..18a330fab 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -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], + })) + }; } \ No newline at end of file diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts index 0b652a192..3930dc8ca 100644 --- a/frontend/src/app/shared/filters.utils.ts +++ b/frontend/src/app/shared/filters.utils.ts @@ -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 },