Add block viz filter UI
This commit is contained in:
parent
e12f43e741
commit
24dbe5d4ee
@ -0,0 +1,22 @@
|
|||||||
|
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<button class="menu-toggle" (click)="menuOpen = !menuOpen">
|
||||||
|
<fa-icon [icon]="['fas', 'filter']"></fa-icon>
|
||||||
|
</button>
|
||||||
|
<div class="active-tags">
|
||||||
|
<ng-container *ngFor="let filter of activeFilters;">
|
||||||
|
<button class="btn filter-tag selected" (click)="toggleFilter(filter)">{{ filters[filter].label }}</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-menu" *ngIf="menuOpen">
|
||||||
|
<ng-container *ngFor="let group of filterGroups;">
|
||||||
|
<h5>{{ group.label }}</h5>
|
||||||
|
<div class="filter-group">
|
||||||
|
<ng-container *ngFor="let filter of group.filters;">
|
||||||
|
<button class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<bigint | null> = 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;
|
||||||
|
}
|
||||||
|
}
|
@ -13,5 +13,6 @@
|
|||||||
[auditEnabled]="auditHighlighting"
|
[auditEnabled]="auditHighlighting"
|
||||||
[blockConversion]="blockConversion"
|
[blockConversion]="blockConversion"
|
||||||
></app-block-overview-tooltip>
|
></app-block-overview-tooltip>
|
||||||
|
<app-block-filters (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,19 @@ import { Color, Position } from './sprite-types';
|
|||||||
import { Price } from '../../services/price.service';
|
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';
|
||||||
|
|
||||||
|
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({
|
@Component({
|
||||||
selector: 'app-block-overview-graph',
|
selector: 'app-block-overview-graph',
|
||||||
@ -26,7 +39,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
@Input() mirrorTxid: string | void;
|
@Input() mirrorTxid: string | void;
|
||||||
@Input() unavailable: boolean = false;
|
@Input() unavailable: boolean = false;
|
||||||
@Input() auditHighlighting: 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() 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}>();
|
||||||
@ -93,7 +107,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
if (changes.auditHighlighting) {
|
if (changes.auditHighlighting) {
|
||||||
this.setHighlightingEnabled(this.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);
|
this.scene.setColorFunction(this.overrideColors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -375,6 +400,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
onPointerMove(event) {
|
onPointerMove(event) {
|
||||||
if (event.target === this.canvas.nativeElement) {
|
if (event.target === this.canvas.nativeElement) {
|
||||||
this.setPreviewTx(event.offsetX, event.offsetY, false);
|
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) {
|
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
|
||||||
const x = cssX * window.devicePixelRatio;
|
const x = cssX * window.devicePixelRatio;
|
||||||
const y = cssY * window.devicePixelRatio;
|
const y = cssY * window.devicePixelRatio;
|
||||||
@ -483,6 +502,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
onTxHover(hoverId: string) {
|
onTxHover(hoverId: string) {
|
||||||
this.txHoverEvent.emit(hoverId);
|
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
|
// WebGL shader attributes
|
||||||
|
@ -2,19 +2,7 @@ import { FastVertexArray } from './fast-vertex-array';
|
|||||||
import TxView from './tx-view';
|
import TxView from './tx-view';
|
||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||||
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
||||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
import { defaultColorFunction } from './utils';
|
||||||
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'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class BlockScene {
|
export default class BlockScene {
|
||||||
scene: { count: number, offset: { x: number, y: number}};
|
scene: { count: number, offset: { x: number, y: number}};
|
||||||
@ -79,7 +67,7 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
|
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
|
||||||
this.getColor = colorFunction;
|
this.getColor = colorFunction || defaultColorFunction;
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
if (this.initialised && this.scene) {
|
if (this.initialised && this.scene) {
|
||||||
this.updateColors(performance.now(), 50);
|
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 {
|
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
|
||||||
if (tx.dirty || this.dirty) {
|
if (tx.dirty || this.dirty) {
|
||||||
const txColor = tx.getColor();
|
const txColor = this.getColor(tx);
|
||||||
this.applyTxUpdate(tx, {
|
this.applyTxUpdate(tx, {
|
||||||
display: {
|
display: {
|
||||||
color: txColor
|
color: txColor
|
||||||
@ -918,49 +906,4 @@ class BlockLayout {
|
|||||||
|
|
||||||
function feeRateDescending(a: TxView, b: TxView) {
|
function feeRateDescending(a: TxView, b: TxView) {
|
||||||
return b.feerate - a.feerate;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,4 +1,6 @@
|
|||||||
|
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||||
import { Color } from './sprite-types';
|
import { Color } from './sprite-types';
|
||||||
|
import TxView from './tx-view';
|
||||||
|
|
||||||
export function hexToColor(hex: string): Color {
|
export function hexToColor(hex: string): Color {
|
||||||
return {
|
return {
|
||||||
@ -25,5 +27,75 @@ export function darken(color: Color, amount: number): Color {
|
|||||||
g: color.g * amount,
|
g: color.g * amount,
|
||||||
b: color.b * amount,
|
b: color.b * amount,
|
||||||
a: color.a,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,7 @@
|
|||||||
[blockLimit]="stateService.blockVSize"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[orientation]="timeLtr ? 'right' : 'left'"
|
[orientation]="timeLtr ? 'right' : 'left'"
|
||||||
[flip]="true"
|
[flip]="true"
|
||||||
|
[showFilters]="showFilters"
|
||||||
[overrideColors]="overrideColors"
|
[overrideColors]="overrideColors"
|
||||||
(txClickEvent)="onTxClick($event)"
|
(txClickEvent)="onTxClick($event)"
|
||||||
></app-block-overview-graph>
|
></app-block-overview-graph>
|
||||||
|
@ -18,6 +18,7 @@ import TxView from '../block-overview-graph/tx-view';
|
|||||||
})
|
})
|
||||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
||||||
@Input() index: number;
|
@Input() index: number;
|
||||||
|
@Input() showFilters: boolean = false;
|
||||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="block-wrapper">
|
<div class="block-wrapper">
|
||||||
<div class="block-container">
|
<div class="block-container">
|
||||||
<app-mempool-block-overview [index]="index" [filterFlags]="filterFlags"></app-mempool-block-overview>
|
<app-mempool-block-overview [index]="index" [showFilters]="true"></app-mempool-block-overview>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -46,7 +46,9 @@
|
|||||||
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md chart-container">
|
<div class="col-md chart-container">
|
||||||
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" [filterFlags]="filterFlags" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
|
<div class="block-with-filters" *ngIf="webGlEnabled">
|
||||||
|
<app-mempool-block-overview [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)" [showFilters]="true"></app-mempool-block-overview>
|
||||||
|
</div>
|
||||||
<app-fee-distribution-graph *ngIf="!webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
<app-fee-distribution-graph *ngIf="!webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
mempoolBlockTransactions$: Observable<TransactionStripped[]>;
|
mempoolBlockTransactions$: Observable<TransactionStripped[]>;
|
||||||
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
|
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
|
||||||
previewTx: TransactionStripped | void;
|
previewTx: TransactionStripped | void;
|
||||||
filterFlags: bigint | null = 0n;
|
|
||||||
webGlEnabled: boolean;
|
webGlEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -35,7 +34,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
window['setFlags'] = this.setFilterFlags.bind(this);
|
|
||||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
|
|
||||||
this.mempoolBlock$ = this.route.paramMap
|
this.mempoolBlock$ = this.route.paramMap
|
||||||
@ -92,11 +90,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
setTxPreview(event: TransactionStripped | void): void {
|
setTxPreview(event: TransactionStripped | void): void {
|
||||||
this.previewTx = event;
|
this.previewTx = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilterFlags(flags: bigint | null) {
|
|
||||||
this.filterFlags = flags;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectWebGL() {
|
function detectWebGL() {
|
||||||
|
@ -185,41 +185,6 @@ export interface TransactionStripped {
|
|||||||
context?: 'projected' | 'actual';
|
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 {
|
export interface RbfTransaction extends TransactionStripped {
|
||||||
rbf?: boolean;
|
rbf?: boolean;
|
||||||
mined?: boolean,
|
mined?: boolean,
|
||||||
|
87
frontend/src/app/shared/filters.utils.ts
Normal file
87
frontend/src/app/shared/filters.utils.ts
Normal file
@ -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) }));
|
@ -44,6 +44,7 @@ import { StartComponent } from '../components/start/start.component';
|
|||||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
|
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
|
||||||
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
|
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
|
||||||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.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 { AddressComponent } from '../components/address/address.component';
|
||||||
import { SearchFormComponent } from '../components/search-form/search-form.component';
|
import { SearchFormComponent } from '../components/search-form/search-form.component';
|
||||||
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
|
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
|
||||||
@ -141,6 +142,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
StartComponent,
|
StartComponent,
|
||||||
BlockOverviewGraphComponent,
|
BlockOverviewGraphComponent,
|
||||||
BlockOverviewTooltipComponent,
|
BlockOverviewTooltipComponent,
|
||||||
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
AddressComponent,
|
AddressComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
@ -266,6 +268,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
StartComponent,
|
StartComponent,
|
||||||
BlockOverviewGraphComponent,
|
BlockOverviewGraphComponent,
|
||||||
BlockOverviewTooltipComponent,
|
BlockOverviewTooltipComponent,
|
||||||
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
AddressComponent,
|
AddressComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user