Merge pull request #4526 from mempool/mononaut/mined-goggles

Mined Goggles
This commit is contained in:
softsimon 2024-01-24 00:13:43 +07:00 committed by GitHub
commit 2faca121d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 92 additions and 31 deletions

View file

@ -1,3 +1,4 @@
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {

View file

@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger'; import logger from '../logger';
import memPool from './mempool'; import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended } from '../mempool.interfaces'; import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
import diskCache from './disk-cache'; import diskCache from './disk-cache';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
@ -201,7 +201,8 @@ class Blocks {
txid: tx.txid, txid: tx.txid,
vsize: tx.weight / 4, vsize: tx.weight / 4,
fee: tx.fee ? Math.round(tx.fee * 100000000) : 0, fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000) value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000),
flags: 0,
}; };
}); });
@ -214,7 +215,7 @@ class Blocks {
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
return { return {
id: hash, id: hash,
transactions: Common.stripTransactions(transactions), transactions: Common.classifyTransactions(transactions),
}; };
} }
@ -945,7 +946,7 @@ class Blocks {
} }
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false, public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false,
skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise<TransactionStripped[]> skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise<TransactionClassified[]>
{ {
if (skipMemoryCache === false) { if (skipMemoryCache === false) {
// Check the memory cache // Check the memory cache
@ -974,7 +975,8 @@ class Blocks {
fee: tx.fee || 0, fee: tx.fee || 0,
vsize: tx.vsize, vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)), value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
rate: tx.effectiveFeePerVsize rate: tx.effectiveFeePerVsize,
flags: tx.flags || Common.getTransactionFlags(tx),
}; };
}), }),
}; };

View file

@ -1,10 +1,9 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express'; import { Request } from 'express';
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import config from '../config'; import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net'; import { isIP } from 'net';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
import { isPoint } from '../utils/secp256k1'; import { isPoint } from '../utils/secp256k1';
export class Common { export class Common {
@ -349,14 +348,18 @@ export class Common {
} }
static classifyTransaction(tx: TransactionExtended): TransactionClassified { static classifyTransaction(tx: TransactionExtended): TransactionClassified {
const flags = this.getTransactionFlags(tx); const flags = Common.getTransactionFlags(tx);
tx.flags = flags; tx.flags = flags;
return { return {
...this.stripTransaction(tx), ...Common.stripTransaction(tx),
flags, flags,
}; };
} }
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
return txs.map(Common.classifyTransaction);
}
static stripTransaction(tx: TransactionExtended): TransactionStripped { static stripTransaction(tx: TransactionExtended): TransactionStripped {
return { return {
txid: tx.txid, txid: tx.txid,
@ -369,7 +372,7 @@ export class Common {
} }
static stripTransactions(txs: TransactionExtended[]): TransactionStripped[] { static stripTransactions(txs: TransactionExtended[]): TransactionStripped[] {
return txs.map(this.stripTransaction); return txs.map(Common.stripTransaction);
} }
static sleep$(ms: number): Promise<void> { static sleep$(ms: number): Promise<void> {

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, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } from '../mempool.interfaces'; import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } 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';

View file

@ -280,7 +280,7 @@ export interface BlockExtended extends IEsploraApi.Block {
export interface BlockSummary { export interface BlockSummary {
id: string; id: string;
transactions: TransactionStripped[]; transactions: TransactionClassified[];
} }
export interface AuditSummary extends BlockAudit { export interface AuditSummary extends BlockAudit {
@ -288,8 +288,8 @@ export interface AuditSummary extends BlockAudit {
size?: number, size?: number,
weight?: number, weight?: number,
tx_count?: number, tx_count?: number,
transactions: TransactionStripped[]; transactions: TransactionClassified[];
template?: TransactionStripped[]; template?: TransactionClassified[];
} }
export interface BlockPrice { export interface BlockPrice {

View file

@ -1,6 +1,6 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { BlockSummary, TransactionStripped } from '../mempool.interfaces'; import { BlockSummary, TransactionClassified } from '../mempool.interfaces';
class BlocksSummariesRepository { class BlocksSummariesRepository {
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> { public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
@ -17,7 +17,7 @@ class BlocksSummariesRepository {
return undefined; return undefined;
} }
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> { public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionClassified[]): Promise<void> {
try { try {
const transactionsStr = JSON.stringify(transactions); const transactionsStr = JSON.stringify(transactions);
await DB.query(` await DB.query(`

View file

@ -18,7 +18,7 @@
<h5>{{ group.label }}</h5> <h5>{{ group.label }}</h5>
<div class="filter-group"> <div class="filter-group">
<ng-container *ngFor="let filter of group.filters;"> <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> <button *ngIf="!disabledFilters[filter.key]" class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>

View file

@ -1,5 +1,7 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges } 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 { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
@ -7,24 +9,48 @@ import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
templateUrl: './block-filters.component.html', templateUrl: './block-filters.component.html',
styleUrls: ['./block-filters.component.scss'], styleUrls: ['./block-filters.component.scss'],
}) })
export class BlockFiltersComponent implements OnChanges { export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@Input() cssWidth: number = 800; @Input() cssWidth: number = 800;
@Input() excludeFilters: string[] = [];
@Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter(); @Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter();
filterSubscription: Subscription;
filters = TransactionFilters; filters = TransactionFilters;
filterGroups = FilterGroups; filterGroups = FilterGroups;
disabledFilters: { [key: string]: boolean } = {};
activeFilters: string[] = []; activeFilters: string[] = [];
filterFlags: { [key: string]: boolean } = {}; filterFlags: { [key: string]: boolean } = {};
menuOpen: boolean = false; menuOpen: boolean = false;
constructor( constructor(
private stateService: StateService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
) {} ) {}
ngOnInit(): void {
this.filterSubscription = this.stateService.activeGoggles$.subscribe((activeFilters: string[]) => {
for (const key of Object.keys(this.filterFlags)) {
this.filterFlags[key] = false;
}
for (const key of activeFilters) {
this.filterFlags[key] = !this.disabledFilters[key];
}
this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit(this.getBooleanFlags());
});
}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes.cssWidth) { if (changes.cssWidth) {
this.cd.markForCheck(); this.cd.markForCheck();
} }
if (changes.excludeFilters) {
this.disabledFilters = {};
this.excludeFilters.forEach(filter => {
this.disabledFilters[filter] = true;
});
}
} }
toggleFilter(key): void { toggleFilter(key): void {
@ -46,7 +72,9 @@ export class BlockFiltersComponent implements OnChanges {
// remove active filter // remove active filter
this.activeFilters = this.activeFilters.filter(f => f != key); this.activeFilters = this.activeFilters.filter(f => f != key);
} }
this.onFilterChanged.emit(this.getBooleanFlags()); const booleanFlags = this.getBooleanFlags();
this.onFilterChanged.emit(booleanFlags);
this.stateService.activeGoggles$.next([...this.activeFilters]);
} }
getBooleanFlags(): bigint | null { getBooleanFlags(): bigint | null {
@ -67,4 +95,8 @@ export class BlockFiltersComponent implements OnChanges {
} }
return true; return true;
} }
ngOnDestroy(): void {
this.filterSubscription.unsubscribe();
}
} }

View file

@ -13,6 +13,6 @@
[auditEnabled]="auditHighlighting" [auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion" [blockConversion]="blockConversion"
></app-block-overview-tooltip> ></app-block-overview-tooltip>
<app-block-filters *ngIf="showFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters> <app-block-filters *ngIf="showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
</div> </div>
</div> </div>

View file

@ -40,6 +40,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() unavailable: boolean = false; @Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false; @Input() auditHighlighting: boolean = false;
@Input() showFilters: boolean = false; @Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null; @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;
@ -71,6 +72,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
searchText: string; searchText: string;
searchSubscription: Subscription; searchSubscription: Subscription;
filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null;
constructor( constructor(
readonly ngZone: NgZone, readonly ngZone: NgZone,
@ -110,16 +113,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (changes.overrideColor && this.scene) { if (changes.overrideColor && this.scene) {
this.scene.setColorFunction(this.overrideColors); this.scene.setColorFunction(this.overrideColors);
} }
if ((changes.filterFlags || changes.showFilters) && this.scene) { if ((changes.filterFlags || changes.showFilters)) {
this.setFilterFlags(this.filterFlags); this.setFilterFlags();
} }
} }
setFilterFlags(flags: bigint | null): void { setFilterFlags(flags?: bigint | null): void {
if (flags != null) { this.activeFilterFlags = this.filterFlags || flags || null;
this.scene.setColorFunction(this.getFilterColorFunction(flags)); if (this.scene) {
} else { if (flags != null) {
this.scene.setColorFunction(this.overrideColors); this.scene.setColorFunction(this.getFilterColorFunction(flags));
} else {
this.scene.setColorFunction(this.overrideColors);
}
} }
this.start(); this.start();
} }
@ -150,6 +156,7 @@ 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);
if (this.scene) { if (this.scene) {
this.scene.setup(transactions); this.scene.setup(transactions);
this.readyNextFrame = true; this.readyNextFrame = true;
@ -260,7 +267,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset, highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
colorFunction: this.overrideColors }); colorFunction: this.getColorFunction() });
this.start(); this.start();
} }
} }
@ -504,6 +511,16 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.txHoverEvent.emit(hoverId); this.txHoverEvent.emit(hoverId);
} }
getColorFunction(): ((tx: TxView) => Color) {
if (this.filterFlags) {
return this.getFilterColorFunction(this.filterFlags);
} else if (this.activeFilterFlags) {
return this.getFilterColorFunction(this.activeFilterFlags);
} else {
return this.overrideColors;
}
}
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
return (tx: TxView) => { return (tx: TxView) => {
if ((tx.bigintFlags & flags) === flags) { if ((tx.bigintFlags & flags) === flags) {

View file

@ -115,6 +115,8 @@
[orientation]="'top'" [orientation]="'top'"
[flip]="false" [flip]="false"
[blockConversion]="blockConversion" [blockConversion]="blockConversion"
[showFilters]="true"
[excludeFilters]="['replacement']"
(txClickEvent)="onTxClick($event)" (txClickEvent)="onTxClick($event)"
></app-block-overview-graph> ></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
@ -229,7 +231,8 @@
<div class="block-graph-wrapper"> <div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86" <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit" [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph> (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container> <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
</div> </div>
<ng-container *ngIf="network !== 'liquid'"> <ng-container *ngIf="network !== 'liquid'">
@ -243,7 +246,8 @@
<div class="block-graph-wrapper"> <div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86" <app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit" [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph> (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
</div> </div>
<ng-container *ngIf="network !== 'liquid'"> <ng-container *ngIf="network !== 'liquid'">

View file

@ -150,6 +150,8 @@ 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([]);
constructor( constructor(
@Inject(PLATFORM_ID) private platformId: any, @Inject(PLATFORM_ID) private platformId: any,
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,