Resolve conflicts in dashboard component

This commit is contained in:
natsoni 2024-02-13 09:47:45 +01:00
commit d55d5db01d
No known key found for this signature in database
GPG Key ID: C65917583181743B
25 changed files with 750 additions and 109 deletions

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, 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 { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config'; import config from '../config';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
@ -171,7 +171,7 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionClassified[] = []; let added: TransactionClassified[] = [];
let removed: string[] = []; let removed: string[] = [];
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = []; const changed: TransactionClassified[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) { if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions; added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) { } else if (!mempoolBlocks[i] && prevBlocks[i]) {
@ -194,14 +194,14 @@ class MempoolBlocks {
if (!prevIds[tx.txid]) { if (!prevIds[tx.txid]) {
added.push(tx); added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) { } 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({ mempoolBlockDeltas.push({
added, added: added.map(this.compressTx),
removed, removed,
changed, changed: changed.map(this.compressDeltaChange),
}); });
} }
return mempoolBlockDeltas; return mempoolBlockDeltas;
@ -691,6 +691,38 @@ class MempoolBlocks {
}); });
return { blocks: convertedBlocks, blockWeights, rates: convertedRates, clusters: convertedClusters, overflow: convertedOverflow }; 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(); export default new MempoolBlocks();

View File

@ -285,7 +285,7 @@ class StatisticsApi {
public async $list2H(): Promise<OptimizedStatistic[]> { public async $list2H(): Promise<OptimizedStatistic[]> {
try { try {
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 120`; const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL 2 HOUR) AND NOW() ORDER BY statistics.added DESC`;
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) { } catch (e) {
@ -296,7 +296,7 @@ class StatisticsApi {
public async $list24H(): Promise<OptimizedStatistic[]> { public async $list24H(): Promise<OptimizedStatistic[]> {
try { try {
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 1440`; const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL 24 HOUR) AND NOW() ORDER BY statistics.added DESC`;
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) { } catch (e) {

View File

@ -6,6 +6,7 @@ import statisticsApi from './statistics-api';
class Statistics { class Statistics {
protected intervalTimer: NodeJS.Timer | undefined; protected intervalTimer: NodeJS.Timer | undefined;
protected lastRun: number = 0;
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined; protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) { public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
@ -23,15 +24,21 @@ class Statistics {
setTimeout(() => { setTimeout(() => {
this.runStatistics(); this.runStatistics();
this.intervalTimer = setInterval(() => { this.intervalTimer = setInterval(() => {
this.runStatistics(); this.runStatistics(true);
}, 1 * 60 * 1000); }, 1 * 60 * 1000);
}, difference); }, difference);
} }
private async runStatistics(): Promise<void> { public async runStatistics(skipIfRecent = false): Promise<void> {
if (!memPool.isInSync()) { if (!memPool.isInSync()) {
return; return;
} }
if (skipIfRecent && new Date().getTime() / 1000 - this.lastRun < 30) {
return;
}
this.lastRun = new Date().getTime() / 1000;
const currentMempool = memPool.getMempool(); const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond(); const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();

View File

@ -23,6 +23,7 @@ import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository'; import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration'; import accelerationApi from './services/acceleration';
import mempool from './mempool'; import mempool from './mempool';
import statistics from './statistics/statistics';
interface AddressTransactions { interface AddressTransactions {
mempool: MempoolTransactionExtended[], mempool: MempoolTransactionExtended[],
@ -259,7 +260,7 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = JSON.stringify({ response['projected-block-transactions'] = JSON.stringify({
index: index, index: index,
blockTransactions: mBlocksWithTransactions[index]?.transactions || [], blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
}); });
} else { } else {
client['track-mempool-block'] = null; client['track-mempool-block'] = null;
@ -723,6 +724,7 @@ class WebsocketHandler {
} }
this.printLogs(); this.printLogs();
await statistics.runStatistics();
const _memPool = memPool.getMempool(); const _memPool = memPool.getMempool();
@ -999,7 +1001,7 @@ class WebsocketHandler {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) { if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index, index: index,
blockTransactions: mBlocksWithTransactions[index].transactions, blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
}); });
} else { } else {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
@ -1014,6 +1016,8 @@ class WebsocketHandler {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }
}); });
await statistics.runStatistics();
} }
// takes a dictionary of JSON serialized values // takes a dictionary of JSON serialized values

View File

@ -65,9 +65,9 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
} }
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionClassified[]; added: TransactionCompressed[];
removed: string[]; removed: string[];
changed: { txid: string, rate: number | undefined, flags?: number }[]; changed: MempoolDeltaChange[];
} }
interface VinStrippedToScriptsig { interface VinStrippedToScriptsig {
@ -196,6 +196,11 @@ export interface TransactionClassified extends TransactionStripped {
flags: number; 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 // binary flags for transaction classification
export const TransactionFlags = { export const TransactionFlags = {
// features // features

View File

@ -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"> <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> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon> <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
@ -14,6 +14,15 @@
</div> </div>
</div> </div>
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280"> <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;"> <ng-container *ngFor="let group of filterGroups;">
<h5>{{ group.label }}</h5> <h5>{{ group.label }}</h5>
<div class="filter-group"> <div class="filter-group">

View File

@ -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 { :host-context(.block-overview-graph:hover) &, &:hover, &:active {
.menu-toggle { .menu-toggle {
opacity: 0.5; opacity: 0.5;
@ -132,6 +175,11 @@
.filter-tag { .filter-tag {
font-size: 0.7em; font-size: 0.7em;
} }
.mode-toggle {
font-size: 0.7em;
margin-bottom: 5px;
margin-top: 2px;
}
} }
&.tiny { &.tiny {

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } 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 { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -12,7 +12,7 @@ import { Subscription } from 'rxjs';
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@Input() cssWidth: number = 800; @Input() cssWidth: number = 800;
@Input() excludeFilters: string[] = []; @Input() excludeFilters: string[] = [];
@Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter(); @Output() onFilterChanged: EventEmitter<ActiveFilter | null> = new EventEmitter();
filterSubscription: Subscription; filterSubscription: Subscription;
@ -21,6 +21,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
disabledFilters: { [key: string]: boolean } = {}; disabledFilters: { [key: string]: boolean } = {};
activeFilters: string[] = []; activeFilters: string[] = [];
filterFlags: { [key: string]: boolean } = {}; filterFlags: { [key: string]: boolean } = {};
filterMode: FilterMode = 'and';
menuOpen: boolean = false; menuOpen: boolean = false;
constructor( constructor(
@ -29,15 +30,16 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
) {} ) {}
ngOnInit(): void { 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)) { for (const key of Object.keys(this.filterFlags)) {
this.filterFlags[key] = false; this.filterFlags[key] = false;
} }
for (const key of activeFilters) { for (const key of active.filters) {
this.filterFlags[key] = !this.disabledFilters[key]; this.filterFlags[key] = !this.disabledFilters[key];
} }
this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])]; this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit(this.getBooleanFlags()); 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 { toggleFilter(key): void {
const filter = this.filters[key]; const filter = this.filters[key];
this.filterFlags[key] = !this.filterFlags[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); this.activeFilters = this.activeFilters.filter(f => f != key);
} }
const booleanFlags = this.getBooleanFlags(); const booleanFlags = this.getBooleanFlags();
this.onFilterChanged.emit(booleanFlags); this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next([...this.activeFilters]); this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
} }
getBooleanFlags(): bigint | null { getBooleanFlags(): bigint | null {
@ -90,7 +98,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
onClick(event): boolean { onClick(event): boolean {
// click away from menu // click away from menu
if (!event.target.closest('button')) { if (!event.target.closest('button') && !event.target.closest('label')) {
this.menuOpen = false; this.menuOpen = false;
} }
return true; return true;

View File

@ -13,6 +13,9 @@
[auditEnabled]="auditHighlighting" [auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion" [blockConversion]="blockConversion"
></app-block-overview-tooltip> ></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>
</div> </div>

View File

@ -7,6 +7,19 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
grid-column: 1/-1; 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 { .grid-align {

View File

@ -9,6 +9,8 @@ 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'; 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 unmatchedOpacity = 0.2;
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
@ -42,7 +44,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() showFilters: boolean = false; @Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = []; @Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null; @Input() filterFlags: bigint | null = null;
@Input() filterMode: 'and' | 'or' = 'and'; @Input() filterMode: FilterMode = 'and';
@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}>();
@ -76,11 +78,14 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
filtersAvailable: boolean = true; filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null; activeFilterFlags: bigint | null = null;
webGlEnabled = true;
constructor( constructor(
readonly ngZone: NgZone, readonly ngZone: NgZone,
readonly elRef: ElementRef, readonly elRef: ElementRef,
private stateService: StateService, private stateService: StateService,
) { ) {
this.webGlEnabled = detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize); this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => { this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text; this.searchText = text;
@ -119,10 +124,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
setFilterFlags(flags?: bigint | null): void { setFilterFlags(goggle?: ActiveFilter): void {
this.activeFilterFlags = this.filterFlags || flags || null; this.filterMode = goggle?.mode || this.filterMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
if (this.scene) { if (this.scene) {
if (this.activeFilterFlags != null) { if (this.activeFilterFlags != null && this.filtersAvailable) {
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags)); this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
} else { } else {
this.scene.setColorFunction(this.overrideColors); this.scene.setColorFunction(this.overrideColors);
@ -157,7 +163,11 @@ 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); const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
}
this.filtersAvailable = filtersAvailable;
if (this.scene) { if (this.scene) {
this.scene.setup(transactions); this.scene.setup(transactions);
this.readyNextFrame = true; this.readyNextFrame = true;
@ -500,6 +510,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) { onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
if (this.scene) {
const x = cssX * window.devicePixelRatio; const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio; const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y }); const selected = this.scene.getTxAt({ x, y });
@ -507,6 +518,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.txClickEvent.emit({ tx: selected, keyModifier }); this.txClickEvent.emit({ tx: selected, keyModifier });
} }
} }
}
onTxHover(hoverId: string) { onTxHover(hoverId: string) {
this.txHoverEvent.emit(hoverId); this.txHoverEvent.emit(hoverId);
@ -524,7 +536,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
return (tx: TxView) => { 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); return defaultColorFunction(tx);
} else { } else {
return defaultColorFunction( return defaultColorFunction(

View File

@ -10,6 +10,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Color } from '../block-overview-graph/sprite-types'; import { Color } from '../block-overview-graph/sprite-types';
import TxView from '../block-overview-graph/tx-view'; import TxView from '../block-overview-graph/tx-view';
import { FilterMode } from '../../shared/filters.utils';
@Component({ @Component({
selector: 'app-mempool-block-overview', selector: 'app-mempool-block-overview',
@ -22,7 +23,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
@Input() showFilters: boolean = false; @Input() showFilters: boolean = false;
@Input() overrideColors: ((tx: TxView) => Color) | null = null; @Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Input() filterFlags: bigint | undefined = undefined; @Input() filterFlags: bigint | undefined = undefined;
@Input() filterMode: 'and' | 'or' = 'and'; @Input() filterMode: FilterMode = 'and';
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
@ -99,7 +100,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
const inOldBlock = {}; const inOldBlock = {};
const inNewBlock = {}; const inNewBlock = {};
const added: TransactionStripped[] = []; 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[] = []; const removed: string[] = [];
for (const tx of transactionsStripped) { for (const tx of transactionsStripped) {
inNewBlock[tx.txid] = true; inNewBlock[tx.txid] = true;
@ -117,6 +118,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
changed.push({ changed.push({
txid: tx.txid, txid: tx.txid,
rate: tx.rate, rate: tx.rate,
flags: tx.flags,
acc: tx.acc acc: tx.acc
}); });
} }

View File

@ -85,6 +85,6 @@
background-color: #f1c40f; background-color: #f1c40f;
} }
.badge-platinium { .badge-platinum {
background-color: #653b9c; background-color: #653b9c;
} }

View File

@ -25,7 +25,7 @@
<div class="quick-filter"> <div class="quick-filter">
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle"> <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> </label>
</div> </div>
</div> </div>
@ -33,8 +33,8 @@
<app-mempool-block-overview <app-mempool-block-overview
[index]="0" [index]="0"
[resolution]="goggleResolution" [resolution]="goggleResolution"
[filterFlags]="goggleCycle[goggleIndex].flag" [filterFlags]="goggleFlags"
filterMode="or" [filterMode]="goggleMode"
></app-mempool-block-overview> ></app-mempool-block-overview>
</div> </div>
</div> </div>

View File

@ -7,6 +7,7 @@ import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service'; import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';
import { SeoService } from '../services/seo.service'; import { SeoService } from '../services/seo.service';
import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils';
interface MempoolBlocksData { interface MempoolBlocksData {
blocks: number; blocks: number;
@ -58,6 +59,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
federationUtxosNumber$: Observable<number>; federationUtxosNumber$: Observable<number>;
fullHistory$: Observable<any>; fullHistory$: Observable<any>;
isLoad: boolean = true; isLoad: boolean = true;
filterSubscription: Subscription;
mempoolInfoSubscription: Subscription; mempoolInfoSubscription: Subscription;
currencySubscription: Subscription; currencySubscription: Subscription;
currency: string; currency: string;
@ -68,13 +70,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
private lastReservesBlockUpdate: number = 0; private lastReservesBlockUpdate: number = 0;
goggleResolution = 82; goggleResolution = 82;
goggleCycle = [ goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [
{ index: 0, name: 'All' }, { index: 0, name: 'All', mode: 'and', filters: [] },
{ index: 1, name: 'Consolidations', flag: 0b00000010_00000000_00000000_00000000_00000000n }, { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] },
{ index: 2, name: 'Coinjoin', flag: 0b00000001_00000000_00000000_00000000_00000000n }, { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] },
{ index: 3, name: '💩', flag: 0b00000100_00000000_00000000_00000000n | 0b00000010_00000000_00000000_00000000n | 0b00000001_00000000_00000000_00000000n }, { 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(); private destroy$ = new Subject();
@ -90,6 +94,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.filterSubscription.unsubscribe();
this.mempoolInfoSubscription.unsubscribe(); this.mempoolInfoSubscription.unsubscribe();
this.currencySubscription.unsubscribe(); this.currencySubscription.unsubscribe();
this.websocketService.stopTrackRbfSummary(); this.websocketService.stopTrackRbfSummary();
@ -110,6 +115,30 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100) 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.mempoolInfoData$ = combineLatest([
this.stateService.mempoolInfo$, this.stateService.mempoolInfo$,
this.stateService.vbytesPerSecond$ this.stateService.vbytesPerSecond$
@ -393,6 +422,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
return Array.from({ length: num }, (_, i) => i + 1); 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']) @HostListener('window:resize', ['$event'])
onResize(): void { onResize(): void {
if (window.innerWidth >= 992) { if (window.innerWidth >= 992) {

View File

@ -9871,7 +9871,403 @@ export const restApiDocsData = [
codeSampleBisq: emptyCodeSample, codeSampleBisq: emptyCodeSample,
} }
} }
},
{
type: "category",
category: "accelerator",
fragment: "accelerator",
title: "Accelerator",
showConditions: [""],
options: { officialOnly: true },
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator",
httpRequestMethod: "GET",
fragment: "accelerator-deposit-history",
title: "GET Deposit History",
description: {
default: "<p>Returns a list of deposits the user has made as prepayment for the accelerator service.</p>"
},
urlString: "/v1/services/accelerator/deposit-history",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `/api/v1/services/accelerator/deposit-history`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: [],
headers: "api_key: stacksats",
response: `[
{
"type": "Bitcoin",
"invoiceId": "CCunucVyNw7jUiUz64mmHz",
"amount": 10311031,
"status": "pending",
"date": 1706372653000,
"link": "/payment/bitcoin/CCunucVyNw7jUiUz64mmHz"
},
{
"type": "Bitcoin",
"invoiceId": "SG1U27R9PdWi3gH3jB9tm9",
"amount": 21000000,
"status": "paid",
"date": 1706372582000,
"link": null
},
...
]`,
},
} }
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator",
httpRequestMethod: "GET",
fragment: "accelerator-balance",
title: "GET Available Balance",
description: {
default: "<p>Returns the user's currently available balance, currently locked funds, and total fees paid so far.</p>"
},
urlString: "/v1/services/accelerator/balance",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `/api/v1/services/accelerator/balance`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: [],
headers: "api_key: stacksats",
response: `{
"balance": 99900000,
"hold": 101829,
"feesPaid": 133721
}`,
},
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator",
httpRequestMethod: "POST",
fragment: "accelerator-estimate",
title: "POST Calculate Estimated Costs",
description: {
default: "<p>Returns estimated costs to accelerate a transaction.</p>"
},
urlString: "/v1/services/accelerator/estimate",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/accelerator/estimate`, //custom interpolation technique handled in replaceCurlPlaceholder()
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29"],
headers: "api_key: stacksats",
response: `{
"txSummary": {
"txid": "ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29",
"effectiveVsize": 154,
"effectiveFee": 154,
"ancestorCount": 1
},
"cost": 3850,
"targetFeeRate": 26,
"nextBlockFee": 4004,
"userBalance": 99900000,
"mempoolBaseFee": 40000,
"vsizeFee": 50000,
"hasAccess": true
}`,
},
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator",
httpRequestMethod: "POST",
fragment: "accelerator-accelerate",
title: "POST Accelerate A Transaction",
description: {
default: "<p>Sends a request to accelerate a transaction.</p>"
},
urlString: "/v1/services/accelerator/accelerate",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/accelerator/accelerate`, //custom interpolation technique handled in replaceCurlPlaceholder()
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&userBid=21000000"],
headers: "api_key: stacksats",
response: `HTTP/1.1 200 OK`,
},
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator",
httpRequestMethod: "GET",
fragment: "accelerator-history",
title: "GET Private Acceleration History",
description: {
default: "<p>Returns the user's past acceleration requests.</p><p>Pass one of the following for <code>:status</code>: <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code>. Pass <code>true</code> in <code>:details</code> to get a detailed <code>history</code> of the acceleration request.</p>"
},
urlString: "/v1/services/accelerator/history?status=:status&details=:details",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `/api/v1/services/accelerator/history?status=all&details=true`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: [],
headers: "api_key: stacksats",
response: `[
{
"id": 89,
"user_id": 1,
"txid": "ae2639469ec000ed1d14e2550cbb01794e1cd288a00cdc7cce18398ba3cc2ffe",
"status": "failed",
"estimated_fee": 247,
"fee_paid": 0,
"added": 1706378712,
"last_updated": 1706378712,
"confirmations": 4,
"base_fee": 0,
"vsize_fee": 0,
"max_bid": 7000,
"effective_vsize": 135,
"effective_fee": 3128,
"history": [
{
"event": "user-requested-acceleration",
"timestamp": 1706378712
},
{
"event": "accepted_test-api-key",
"timestamp": 1706378712
},
{
"event": "failed-at-block-827672",
"timestamp": 1706380261
}
]
},
{
"id": 88,
"user_id": 1,
"txid": "c5840e89173331760e959a190b24e2a289121277ed7f8a095fe289b37cee9fde",
"status": "completed",
"estimated_fee": 223,
"fee_paid": 140019,
"added": 1706378704,
"last_updated": 1706380231,
"confirmations": 6,
"base_fee": 40000,
"vsize_fee": 100000,
"max_bid": 14000,
"effective_vsize": 135,
"effective_fee": 3152,
"history": [
{
"event": "user-requested-acceleration",
"timestamp": 1706378704
},
{
"event": "accepted_test-api-key",
"timestamp": 1706378704
},
{
"event": "complete-at-block-827670",
"timestamp": 1706380231
}
]
},
{
"id": 87,
"user_id": 1,
"txid": "178b5b9b310f0d667d7ea563a2cdcc17bc8cd15261b58b1653860a724ca83458",
"status": "completed",
"estimated_fee": 115,
"fee_paid": 90062,
"added": 1706378684,
"last_updated": 1706380231,
"confirmations": 6,
"base_fee": 40000,
"vsize_fee": 50000,
"max_bid": 14000,
"effective_vsize": 135,
"effective_fee": 3260,
"history": [
{
"event": "user-requested-acceleration",
"timestamp": 1706378684
},
{
"event": "accepted_test-api-key",
"timestamp": 1706378684
},
{
"event": "complete-at-block-827670",
"timestamp": 1706380231
}
]
}
]`,
},
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator",
httpRequestMethod: "GET",
fragment: "accelerator-pending",
title: "GET Pending Accelerations",
description: {
default: "<p>Returns all transactions currently being accelerated.</p>"
},
urlString: "/v1/services/accelerator/accelerations",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `/api/v1/services/accelerator/accelerations`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: [],
headers: '',
response: `[
{
"txid": "8a183c8ae929a2afb857e7f2acd440aaefdf2797f8f7eab1c5f95ff8602abc81",
"added": 1707558316,
"feeDelta": 3500,
"effectiveVsize": 111,
"effectiveFee": 1671,
"pools": [
111
]
},
{
"txid": "6097f295e21bdd8d725bd8d9ad4dd72b05bd795dc648bfef52150a9b2b7f7a45",
"added": 1707560464,
"feeDelta": 60000,
"effectiveVsize": 812,
"effectiveFee": 7790,
"pools": [
111
]
}
]`,
},
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator",
httpRequestMethod: "GET",
fragment: "accelerator-public-history",
title: "GET Public Acceleration History",
description: {
default: `<p>Returns all past accelerated transactions.
Filters can be applied:<ul>
<li><code>status</code>: <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code></li>
<li><code>timeframe</code>: <code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>, <code>all</code></li>
<li><code>poolUniqueId</code>: any id from <a target="_blank" href="https://github.com/mempool/mining-pools/blob/master/pools-v2.json">https://github.com/mempool/mining-pools/blob/master/pools-v2.json</a>
<li><code>blockHash</code>: a block hash</a>
<li><code>blockHeight</code>: a block height</a>
<li><code>page</code>: the requested page number if using pagination</a>
<li><code>pageLength</code>: the page lenght if using pagination</a>
</ul></p>`
},
urlString: "/v1/services/accelerator/accelerations/history",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `/api/v1/services/accelerator/accelerations/history?blockHash=00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: [],
headers: '',
response: `[
{
"txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d",
"status": "completed",
"feePaid": 53239,
"added": 1707421053,
"lastUpdated": 1707422952,
"baseFee": 50000,
"vsizeFee": 0,
"effectiveFee": 146,
"effectiveVsize": 141,
"feeDelta": 14000,
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
"blockHeight": 829559,
"pools": [
{
"pool_unique_id": 111,
"username": "foundryusa"
}
]
}
]`,
},
}
}
},
]; ];
export const faqData = [ export const faqData = [

View File

@ -1,4 +1,4 @@
<div *ngFor="let item of tabData"> <div *ngFor="let item of tabData">
<p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</p> <p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ))">{{ item.title }}</p>
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a> <a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>
</div> </div>

View File

@ -43,6 +43,7 @@
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits.</p> <p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>
<div class="doc-item-container" *ngFor="let item of restDocs"> <div class="doc-item-container" *ngFor="let item of restDocs">
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance )">
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3> <h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
<div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}"> <div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}">
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}">{{ item.title }} <span>{{ item.category }}</span></a> <a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}">{{ item.title }} <span>{{ item.category }}</span></a>
@ -94,6 +95,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>

View File

@ -311,27 +311,29 @@ yarn add @mempool/liquid.js`;
text = text.replace('%{' + indexNumber + '}', textReplace); text = text.replace('%{' + indexNumber + '}', textReplace);
} }
const headersString = code.headers ? ` -H "${code.headers}"` : ``;
if (this.env.BASE_MODULE === 'mempool') { if (this.env.BASE_MODULE === 'mempool') {
if (this.network === 'main' || this.network === '') { if (this.network === 'main' || this.network === '') {
if (this.method === 'POST') { if (this.method === 'POST') {
return `curl -X POST -sSLd "${text}"`; return `curl${headersString} -X POST -sSLd "${text}"`;
} }
return `curl -sSL "${this.hostname}${text}"`; return `curl${headersString} -sSL "${this.hostname}${text}"`;
} }
if (this.method === 'POST') { if (this.method === 'POST') {
return `curl -X POST -sSLd "${text}"`; return `curl${headersString} -X POST -sSLd "${text}"`;
} }
return `curl -sSL "${this.hostname}/${this.network}${text}"`; return `curl${headersString} -sSL "${this.hostname}/${this.network}${text}"`;
} else if (this.env.BASE_MODULE === 'liquid') { } else if (this.env.BASE_MODULE === 'liquid') {
if (this.method === 'POST') { if (this.method === 'POST') {
if (this.network !== 'liquid') { if (this.network !== 'liquid') {
text = text.replace('/api', `/${this.network}/api`); text = text.replace('/api', `/${this.network}/api`);
} }
return `curl -X POST -sSLd "${text}"`; return `curl${headersString} -X POST -sSLd "${text}"`;
} }
return ( this.network === 'liquid' ? `curl -sSL "${this.hostname}${text}"` : `curl -sSL "${this.hostname}/${this.network}${text}"` ); return ( this.network === 'liquid' ? `curl${headersString} -sSL "${this.hostname}${text}"` : `curl${headersString} -sSL "${this.hostname}/${this.network}${text}"` );
} else { } else {
return `curl -sSL "${this.hostname}${text}"`; return `curl${headersString} -sSL "${this.hostname}${text}"`;
} }
} }

View File

@ -70,9 +70,15 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
} }
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionStripped[], added: TransactionStripped[];
removed: string[], removed: string[];
changed?: { txid: string, rate: number | undefined, acc: boolean | undefined }[]; changed: { txid: string, rate: number, flags: number, acc: boolean }[];
}
export interface MempoolBlockDeltaCompressed {
added: TransactionCompressed[];
removed: string[];
changed: MempoolDeltaChange[];
} }
export interface MempoolInfo { export interface MempoolInfo {
@ -97,6 +103,11 @@ export interface TransactionStripped {
context?: 'projected' | 'actual'; 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 { export interface IBackendInfo {
hostname?: string; hostname?: string;
gitCommit: string; gitCommit: string;

View File

@ -1,9 +1,10 @@
import { Router, NavigationStart } from '@angular/router';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { MenuGroup } from '../interfaces/services.interface'; import { MenuGroup } from '../interfaces/services.interface';
import { Observable, of, ReplaySubject, tap, catchError, share } from 'rxjs'; import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs';
import { IBackendInfo } from '../interfaces/websocket.interface'; import { IBackendInfo } from '../interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
@ -30,16 +31,20 @@ const SERVICES_API_PREFIX = `/api/v1/services`;
providedIn: 'root' providedIn: 'root'
}) })
export class ServicesApiServices { export class ServicesApiServices {
private apiBaseUrl: string; // base URL is protocol, hostname, and port apiBaseUrl: string; // base URL is protocol, hostname, and port
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
userSubject$ = new ReplaySubject<IUser | null>(1); userSubject$ = new ReplaySubject<IUser | null>(1);
currentAuth = null;
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private stateService: StateService, private stateService: StateService,
private storageService: StorageService private storageService: StorageService,
private router: Router,
) { ) {
this.currentAuth = localStorage.getItem('auth');
this.apiBaseUrl = ''; // use relative URL by default this.apiBaseUrl = ''; // use relative URL by default
if (!stateService.isBrowser) { // except when inside AU SSR process if (!stateService.isBrowser) { // except when inside AU SSR process
this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT; this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
@ -59,6 +64,10 @@ export class ServicesApiServices {
} }
this.getUserInfo$().subscribe(); this.getUserInfo$().subscribe();
this.router.events.pipe(
filter((event) => event instanceof NavigationStart && this.currentAuth !== localStorage.getItem('auth')),
switchMap(() => this.getUserInfo$()),
).subscribe();
} }
/** /**

View File

@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; 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 { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
@ -9,6 +9,7 @@ import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { ActiveFilter } from '../shared/filters.utils';
export interface MarkBlockState { export interface MarkBlockState {
blockHeight?: number; blockHeight?: number;
@ -150,7 +151,7 @@ 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([]); activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [] });
constructor( constructor(
@Inject(PLATFORM_ID) private platformId: any, @Inject(PLATFORM_ID) private platformId: any,

View File

@ -8,6 +8,7 @@ import { ApiService } from './api.service';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser'; import { TransferState, makeStateKey } from '@angular/platform-browser';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';
import { uncompressDeltaChange, uncompressTx } from '../shared/common.utils';
const OFFLINE_RETRY_AFTER_MS = 2000; const OFFLINE_RETRY_AFTER_MS = 2000;
const OFFLINE_PING_CHECK_AFTER_MS = 30000; const OFFLINE_PING_CHECK_AFTER_MS = 30000;
@ -382,9 +383,9 @@ export class WebsocketService {
if (response['projected-block-transactions']) { if (response['projected-block-transactions']) {
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) { if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
if (response['projected-block-transactions'].blockTransactions) { 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) { } 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));
} }
} }
} }

View File

@ -1,3 +1,5 @@
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed, TransactionStripped } from "../interfaces/websocket.interface";
export function isMobile(): boolean { export function isMobile(): boolean {
return (window.innerWidth <= 767.98); return (window.innerWidth <= 767.98);
} }
@ -153,3 +155,28 @@ export function seoDescriptionNetwork(network: string): string {
} }
return ''; 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],
}))
};
}

View File

@ -7,6 +7,13 @@ export interface Filter {
important?: boolean, important?: boolean,
} }
export type FilterMode = 'and' | 'or';
export interface ActiveFilter {
mode: FilterMode,
filters: string[],
}
// binary flags for transaction classification // binary flags for transaction classification
export const TransactionFlags = { export const TransactionFlags = {
// features // features
@ -43,6 +50,14 @@ export const TransactionFlags = {
sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, 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 } = { export const TransactionFilters: { [key: string]: Filter } = {
/* features */ /* features */
rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true }, rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true },