From 24dbe5d4ee45bf6fe7701d848c317392606660e3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 13 Dec 2023 10:59:28 +0000 Subject: [PATCH] Add block viz filter UI --- .../block-filters.component.html | 22 ++++ .../block-filters.component.scss | 104 ++++++++++++++++++ .../block-filters/block-filters.component.ts | 65 +++++++++++ .../block-overview-graph.component.html | 1 + .../block-overview-graph.component.ts | 55 +++++++-- .../block-overview-graph/block-scene.ts | 63 +---------- .../components/block-overview-graph/utils.ts | 72 ++++++++++++ .../mempool-block-overview.component.html | 1 + .../mempool-block-overview.component.ts | 1 + .../mempool-block-view.component.html | 2 +- .../mempool-block.component.html | 4 +- .../mempool-block/mempool-block.component.ts | 7 -- .../src/app/interfaces/node-api.interface.ts | 35 ------ frontend/src/app/shared/filters.utils.ts | 87 +++++++++++++++ frontend/src/app/shared/shared.module.ts | 3 + 15 files changed, 408 insertions(+), 114 deletions(-) create mode 100644 frontend/src/app/components/block-filters/block-filters.component.html create mode 100644 frontend/src/app/components/block-filters/block-filters.component.scss create mode 100644 frontend/src/app/components/block-filters/block-filters.component.ts create mode 100644 frontend/src/app/shared/filters.utils.ts diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html new file mode 100644 index 000000000..ff86e6b3b --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -0,0 +1,22 @@ +
+
+ +
+ + + +
+
+
+ +
{{ group.label }}
+
+ + + +
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss new file mode 100644 index 000000000..ee9e7f4d3 --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -0,0 +1,104 @@ +.block-filters { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding: 1em; + z-index: 10; + pointer-events: none; + + .filter-bar, .active-tags { + display: flex; + flex-direction: row; + align-items: center; + } + + .active-tags { + flex-wrap: wrap; + row-gap: 0.25em; + margin-left: 0.5em; + } + + .menu-toggle { + opacity: 0; + cursor: pointer; + color: white; + background: none; + border: solid 2px white; + border-radius: 0.35em; + pointer-events: all; + } + + .filter-menu { + h5 { + font-size: 0.8rem; + color: white; + margin: 0; + margin-top: 0.5em; + } + } + + .filter-group { + display: flex; + flex-direction: row; + flex-wrap: wrap; + row-gap: 0.25em; + margin-bottom: 0.5em; + } + + .filter-tag { + font-size: 0.9em; + background: #181b2daf; + border: solid 1px #105fb0; + color: white; + border-radius: 0.2rem; + padding: 0.2em 0.5em; + transition: background-color 300ms; + margin-right: 0.25em; + pointer-events: all; + + &.selected { + background-color: #105fb0; + } + } + + :host-context(.block-overview-graph:hover) &, &:hover, &:active { + .menu-toggle { + opacity: 0.5; + background: #181b2d; + + &:hover { + opacity: 1; + background: #181b2d7f; + } + } + + &.menu-open, &.filters-active { + .menu-toggle { + opacity: 1; + background: none; + + &:hover { + background: #181b2d7f; + } + } + } + } + + &.menu-open, &.filters-active { + .menu-toggle { + opacity: 1; + background: none; + + &:hover { + background: #181b2d7f; + } + } + } + + &.menu-open { + pointer-events: all; + background: #181b2d7f; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts new file mode 100644 index 000000000..fc154bb69 --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -0,0 +1,65 @@ +import { Component, OnChanges, EventEmitter, Output, SimpleChanges, HostListener } from '@angular/core'; +import { FilterGroups, TransactionFilters, Filter, TransactionFlags } from '../../shared/filters.utils'; + + +@Component({ + selector: 'app-block-filters', + templateUrl: './block-filters.component.html', + styleUrls: ['./block-filters.component.scss'], +}) +export class BlockFiltersComponent implements OnChanges { + @Output() onFilterChanged: EventEmitter = new EventEmitter(); + + filters = TransactionFilters; + filterGroups = FilterGroups; + activeFilters: string[] = []; + filterFlags: { [key: string]: boolean } = {}; + menuOpen: boolean = false; + + constructor() {} + + ngOnChanges(changes: SimpleChanges): void { + + } + + toggleFilter(key): void { + const filter = this.filters[key]; + this.filterFlags[key] = !this.filterFlags[key]; + if (this.filterFlags[key]) { + // remove any other flags in the same toggle group + if (filter.toggle) { + this.activeFilters.forEach(f => { + if (this.filters[f].toggle === filter.toggle) { + this.filterFlags[f] = false; + } + }); + this.activeFilters = this.activeFilters.filter(f => this.filters[f].toggle !== filter.toggle); + } + // add new active filter + this.activeFilters.push(key); + } else { + // remove active filter + this.activeFilters = this.activeFilters.filter(f => f != key); + } + this.onFilterChanged.emit(this.getBooleanFlags()); + } + + getBooleanFlags(): bigint | null { + let flags = 0n; + for (const key of Object.keys(this.filterFlags)) { + if (this.filterFlags[key]) { + flags |= this.filters[key].flag; + } + } + return flags || null; + } + + @HostListener('document:click', ['$event']) + onClick(event): boolean { + // click away from menu + if (!event.target.closest('button')) { + this.menuOpen = false; + } + return true; + } +} \ No newline at end of file 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 a625a0385..9f7408323 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,5 +13,6 @@ [auditEnabled]="auditHighlighting" [blockConversion]="blockConversion" > + 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 5eaee25a1..716ba540e 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 @@ -8,6 +8,19 @@ import { Color, Position } from './sprite-types'; 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'; + +const unmatchedOpacity = 0.2; +const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedAuditColors = { + censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), + missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), + added: setOpacity(defaultAuditColors.added, unmatchedOpacity), + selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity), + accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity), +}; @Component({ selector: 'app-block-overview-graph', @@ -26,7 +39,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() mirrorTxid: string | void; @Input() unavailable: boolean = false; @Input() auditHighlighting: boolean = false; - @Input() filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n; + @Input() showFilters: boolean = false; + @Input() filterFlags: bigint | null = null; @Input() blockConversion: Price; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @@ -93,7 +107,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (changes.auditHighlighting) { this.setHighlightingEnabled(this.auditHighlighting); } - if (changes.overrideColor) { + if (changes.overrideColor && this.scene) { + this.scene.setColorFunction(this.overrideColors); + } + if ((changes.filterFlags || changes.showFilters) && this.scene) { + this.setFilterFlags(this.filterFlags); + } + } + + setFilterFlags(flags: bigint | null): void { + if (flags != null) { + this.scene.setColorFunction(this.getFilterColorFunction(flags)); + } else { this.scene.setColorFunction(this.overrideColors); } } @@ -375,6 +400,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On onPointerMove(event) { if (event.target === this.canvas.nativeElement) { this.setPreviewTx(event.offsetX, event.offsetY, false); + } else { + this.onPointerLeave(event); } } @@ -463,14 +490,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } } - setFilterFlags(flags: bigint | null): void { - if (this.scene) { - console.log('setting filter flags to ', this.filterFlags.toString(2)); - this.scene.setFilterFlags(flags); - this.start(); - } - } - onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) { const x = cssX * window.devicePixelRatio; const y = cssY * window.devicePixelRatio; @@ -483,6 +502,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On onTxHover(hoverId: string) { this.txHoverEvent.emit(hoverId); } + + getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { + return (tx: TxView) => { + if ((tx.bigintFlags & flags) === flags) { + return defaultColorFunction(tx); + } else { + return defaultColorFunction( + tx, + unmatchedFeeColors, + unmatchedAuditFeeColors, + unmatchedMarginalFeeColors, + unmatchedAuditColors + ); + } + }; + } } // WebGL shader attributes diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index b6cf0ce59..cb589527d 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -2,19 +2,7 @@ import { FastVertexArray } from './fast-vertex-array'; import TxView from './tx-view'; import { TransactionStripped } from '../../interfaces/websocket.interface'; import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; -import { feeLevels, mempoolFeeColors } from '../../app.constants'; -import { darken, desaturate, hexToColor } from './utils'; - -const feeColors = mempoolFeeColors.map(hexToColor); -const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); -const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); -const auditColors = { - censored: hexToColor('f344df'), - missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), - added: hexToColor('0099ff'), - selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), - accelerated: hexToColor('8F5FF6'), -}; +import { defaultColorFunction } from './utils'; export default class BlockScene { scene: { count: number, offset: { x: number, y: number}}; @@ -79,7 +67,7 @@ export default class BlockScene { } setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { - this.getColor = colorFunction; + this.getColor = colorFunction || defaultColorFunction; this.dirty = true; if (this.initialised && this.scene) { this.updateColors(performance.now(), 50); @@ -280,7 +268,7 @@ export default class BlockScene { private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void { if (tx.dirty || this.dirty) { - const txColor = tx.getColor(); + const txColor = this.getColor(tx); this.applyTxUpdate(tx, { display: { color: txColor @@ -918,49 +906,4 @@ class BlockLayout { function feeRateDescending(a: TxView, b: TxView) { return b.feerate - a.feerate; -} - -function defaultColorFunction(tx: TxView): Color { - const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate - const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; - const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; - // Normal mode - if (!tx.scene?.highlightingEnabled) { - if (tx.acc) { - return auditColors.accelerated; - } else { - return feeLevelColor; - } - return feeLevelColor; - } - // Block audit - switch(tx.status) { - case 'censored': - return auditColors.censored; - case 'missing': - case 'sigop': - case 'rbf': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; - case 'fresh': - case 'freshcpfp': - return auditColors.missing; - case 'added': - return auditColors.added; - case 'selected': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; - case 'accelerated': - return auditColors.accelerated; - case 'found': - if (tx.context === 'projected') { - return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; - } else { - return feeLevelColor; - } - default: - if (tx.acc) { - return auditColors.accelerated; - } else { - return feeLevelColor; - } - } } \ No newline at end of file diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index a0bb8e868..9c800ad85 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -1,4 +1,6 @@ +import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { Color } from './sprite-types'; +import TxView from './tx-view'; export function hexToColor(hex: string): Color { return { @@ -25,5 +27,75 @@ export function darken(color: Color, amount: number): Color { g: color.g * amount, b: color.b * amount, a: color.a, + }; +} + +export function setOpacity(color: Color, opacity: number): Color { + return { + ...color, + a: opacity + }; +} + +// precomputed colors +export const defaultFeeColors = mempoolFeeColors.map(hexToColor); +export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); +export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); +export const defaultAuditColors = { + censored: hexToColor('f344df'), + missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), + added: hexToColor('0099ff'), + selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), + accelerated: hexToColor('8F5FF6'), +}; + +export function defaultColorFunction( + tx: TxView, + feeColors: Color[] = defaultFeeColors, + auditFeeColors: Color[] = defaultAuditFeeColors, + marginalFeeColors: Color[] = defaultMarginalFeeColors, + auditColors: { [status: string]: Color } = defaultAuditColors +): Color { + const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate + const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; + const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; + // Normal mode + if (!tx.scene?.highlightingEnabled) { + if (tx.acc) { + return auditColors.accelerated; + } else { + return feeLevelColor; + } + return feeLevelColor; + } + // Block audit + switch(tx.status) { + case 'censored': + return auditColors.censored; + case 'missing': + case 'sigop': + case 'rbf': + return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + case 'fresh': + case 'freshcpfp': + return auditColors.missing; + case 'added': + return auditColors.added; + case 'selected': + return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + case 'accelerated': + return auditColors.accelerated; + case 'found': + if (tx.context === 'projected') { + return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; + } else { + return feeLevelColor; + } + default: + if (tx.acc) { + return auditColors.accelerated; + } else { + return feeLevelColor; + } } } \ No newline at end of file diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html index 1e0cba48c..85e7eebb1 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html @@ -5,6 +5,7 @@ [blockLimit]="stateService.blockVSize" [orientation]="timeLtr ? 'right' : 'left'" [flip]="true" + [showFilters]="showFilters" [overrideColors]="overrideColors" (txClickEvent)="onTxClick($event)" > 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 09eac989e..4beda043a 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 @@ -18,6 +18,7 @@ import TxView from '../block-overview-graph/tx-view'; }) export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { @Input() index: number; + @Input() showFilters: boolean = false; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txPreviewEvent = new EventEmitter(); diff --git a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html index 33ccf439b..2fafb31cd 100644 --- a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html +++ b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html @@ -1,5 +1,5 @@
- +
\ No newline at end of file diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.html b/frontend/src/app/components/mempool-block/mempool-block.component.html index af9225fe6..d2aa1aed2 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.html +++ b/frontend/src/app/components/mempool-block/mempool-block.component.html @@ -46,7 +46,9 @@
- +
+ +
diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index 4b8d4de66..bb6e7791f 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -21,7 +21,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { mempoolBlockTransactions$: Observable; ordinal$: BehaviorSubject = new BehaviorSubject(''); previewTx: TransactionStripped | void; - filterFlags: bigint | null = 0n; webGlEnabled: boolean; constructor( @@ -35,7 +34,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { } ngOnInit(): void { - window['setFlags'] = this.setFilterFlags.bind(this); this.websocketService.want(['blocks', 'mempool-blocks']); this.mempoolBlock$ = this.route.paramMap @@ -92,11 +90,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { setTxPreview(event: TransactionStripped | void): void { this.previewTx = event; } - - setFilterFlags(flags: bigint | null) { - this.filterFlags = flags; - this.cd.markForCheck(); - } } function detectWebGL() { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 3075491a8..e225eb758 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -185,41 +185,6 @@ export interface TransactionStripped { context?: 'projected' | 'actual'; } -// binary flags for transaction classification -export const TransactionFlags = { - // features - rbf: 0b00000001n, - no_rbf: 0b00000010n, - v1: 0b00000100n, - v2: 0b00001000n, - // address types - p2pk: 0b00000001_00000000n, - p2ms: 0b00000010_00000000n, - p2pkh: 0b00000100_00000000n, - p2sh: 0b00001000_00000000n, - p2wpkh: 0b00010000_00000000n, - p2wsh: 0b00100000_00000000n, - p2tr: 0b01000000_00000000n, - // behavior - cpfp_parent: 0b00000001_00000000_00000000n, - cpfp_child: 0b00000010_00000000_00000000n, - replacement: 0b00000100_00000000_00000000n, - // data - op_return: 0b00000001_00000000_00000000_00000000n, - fake_multisig: 0b00000010_00000000_00000000_00000000n, - inscription: 0b00000100_00000000_00000000_00000000n, - // heuristics - coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, - consolidation: 0b00000010_00000000_00000000_00000000_00000000n, - batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, - // sighash - sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, - sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, - sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, - sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, - sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, -}; - export interface RbfTransaction extends TransactionStripped { rbf?: boolean; mined?: boolean, diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts new file mode 100644 index 000000000..f7e2f91c1 --- /dev/null +++ b/frontend/src/app/shared/filters.utils.ts @@ -0,0 +1,87 @@ +export interface Filter { + key: string, + label: string, + flag: bigint, + toggle?: string, + group?: string, +} + +// binary flags for transaction classification +export const TransactionFlags = { + // features + rbf: 0b00000001n, + no_rbf: 0b00000010n, + v1: 0b00000100n, + v2: 0b00001000n, + multisig: 0b00010000n, + // address types + p2pk: 0b00000001_00000000n, + p2ms: 0b00000010_00000000n, + p2pkh: 0b00000100_00000000n, + p2sh: 0b00001000_00000000n, + p2wpkh: 0b00010000_00000000n, + p2wsh: 0b00100000_00000000n, + p2tr: 0b01000000_00000000n, + // behavior + cpfp_parent: 0b00000001_00000000_00000000n, + cpfp_child: 0b00000010_00000000_00000000n, + replacement: 0b00000100_00000000_00000000n, + // data + op_return: 0b00000001_00000000_00000000_00000000n, + fake_multisig: 0b00000010_00000000_00000000_00000000n, + inscription: 0b00000100_00000000_00000000_00000000n, + // heuristics + coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, + consolidation: 0b00000010_00000000_00000000_00000000_00000000n, + batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, + // sighash + sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, + sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, + sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, + sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, + sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, +}; + +export const TransactionFilters: { [key: string]: Filter } = { + // features + rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf' }, + no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf' }, + v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' }, + v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' }, + multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig }, + // address types + p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk }, + p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms }, + p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh }, + p2sh: { key: 'p2sh', label: 'P2SH', flag: TransactionFlags.p2sh }, + p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh }, + p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh }, + p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr }, + // behavior + cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent }, + cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child }, + replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement }, + // data + op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return }, + // fake_multisig: { key: 'fake_multisig', label: 'Fake multisig', flag: TransactionFlags.fake_multisig }, + inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription }, + // heuristics + coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin }, + consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation }, + batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout }, + // sighash + sighash_all: { key: 'sighash_all', label: 'sighash_all', flag: TransactionFlags.sighash_all }, + sighash_none: { key: 'sighash_none', label: 'sighash_none', flag: TransactionFlags.sighash_none }, + sighash_single: { key: 'sighash_single', label: 'sighash_single', flag: TransactionFlags.sighash_single }, + sighash_default: { key: 'sighash_default', label: 'sighash_default', flag: TransactionFlags.sighash_default }, + sighash_acp: { key: 'sighash_acp', label: 'sighash_anyonecanpay', flag: TransactionFlags.sighash_acp }, +}; + +export const FilterGroups: { label: string, filters: Filter[]}[] = [ + { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] }, + { label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] }, + { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] }, + { label: 'Data', filters: ['op_return', 'fake_multisig', 'inscription'] }, + { label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] }, + { label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] }, +].map(group => ({ label: group.label, filters: group.filters.map(filter => TransactionFilters[filter] || null).filter(f => f != null) })); \ No newline at end of file diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 52123f995..9bcfb932c 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -44,6 +44,7 @@ import { StartComponent } from '../components/start/start.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; +import { BlockFiltersComponent } from '../components/block-filters/block-filters.component'; import { AddressComponent } from '../components/address/address.component'; import { SearchFormComponent } from '../components/search-form/search-form.component'; import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; @@ -141,6 +142,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir StartComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, + BlockFiltersComponent, TransactionsListComponent, AddressComponent, SearchFormComponent, @@ -266,6 +268,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir StartComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, + BlockFiltersComponent, TransactionsListComponent, AddressComponent, SearchFormComponent,