Merge branch 'master' into nymkappa/fix-username-not-showing
This commit is contained in:
		
						commit
						6933d3c395
					
				| @ -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(); | ||||
|  | ||||
| @ -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}`, { | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200"> | ||||
| <div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200"> | ||||
|   <a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions"> | ||||
|     <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span> | ||||
|     <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon> | ||||
| @ -14,6 +14,15 @@ | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="filter-menu" *ngIf="menuOpen && cssWidth > 280"> | ||||
|     <h5>Match</h5> | ||||
|     <div class="btn-group btn-group-toggle"> | ||||
|       <label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'"> | ||||
|         <input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All | ||||
|       </label> | ||||
|       <label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'"> | ||||
|         <input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any | ||||
|       </label> | ||||
|     </div> | ||||
|     <ng-container *ngFor="let group of filterGroups;"> | ||||
|       <h5>{{ group.label }}</h5> | ||||
|       <div class="filter-group"> | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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<bigint | null> = new EventEmitter(); | ||||
|   @Output() onFilterChanged: EventEmitter<ActiveFilter | null> = 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; | ||||
|  | ||||
| @ -13,6 +13,9 @@ | ||||
|       [auditEnabled]="auditHighlighting" | ||||
|       [blockConversion]="blockConversion" | ||||
|     ></app-block-overview-tooltip> | ||||
|     <app-block-filters *ngIf="showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters> | ||||
|     <app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters> | ||||
|     <div *ngIf="!webGlEnabled" class="placeholder"> | ||||
|       <span i18n="webgl-disabled">Your browser does not support this feature.</span> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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( | ||||
|  | ||||
| @ -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<TransactionStripped | void>(); | ||||
| 
 | ||||
|   @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 | ||||
|               }); | ||||
|             } | ||||
|  | ||||
| @ -26,7 +26,7 @@ | ||||
|             <div class="quick-filter"> | ||||
|               <div class="btn-group btn-group-toggle"> | ||||
|                 <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex"  *ngFor="let filter of goggleCycle"> | ||||
|                   <input type="radio" [value]="'3m'" fragment="3m" (click)="goggleIndex = filter.index" [attr.data-cy]="'3m'"> {{ filter.name }} | ||||
|                   <input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }} | ||||
|                 </label> | ||||
|               </div> | ||||
|             </div> | ||||
| @ -34,8 +34,8 @@ | ||||
|               <app-mempool-block-overview | ||||
|                 [index]="0" | ||||
|                 [resolution]="goggleResolution" | ||||
|                 [filterFlags]="goggleCycle[goggleIndex].flag" | ||||
|                 filterMode="or" | ||||
|                 [filterFlags]="goggleFlags" | ||||
|                 [filterMode]="goggleMode" | ||||
|               ></app-mempool-block-overview> | ||||
|             </div> | ||||
|           </ng-template> | ||||
|  | ||||
| @ -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<CurrentPegs>; | ||||
|   fullHistory$: Observable<any>; | ||||
|   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) { | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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<boolean> = new Subject<boolean>(); | ||||
|   menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false); | ||||
| 
 | ||||
|   activeGoggles$: BehaviorSubject<string[]> = new BehaviorSubject([]); | ||||
|   activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [] }); | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(PLATFORM_ID) private platformId: any, | ||||
|  | ||||
| @ -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)); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -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], | ||||
|     })) | ||||
|   }; | ||||
| } | ||||
| @ -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 }, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user