Resolve conflicts in dashboard component
This commit is contained in:
commit
d55d5db01d
@ -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();
|
||||
|
@ -285,7 +285,7 @@ class StatisticsApi {
|
||||
|
||||
public async $list2H(): Promise<OptimizedStatistic[]> {
|
||||
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 });
|
||||
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
||||
} catch (e) {
|
||||
@ -296,7 +296,7 @@ class StatisticsApi {
|
||||
|
||||
public async $list24H(): Promise<OptimizedStatistic[]> {
|
||||
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 });
|
||||
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
||||
} catch (e) {
|
||||
|
@ -6,6 +6,7 @@ import statisticsApi from './statistics-api';
|
||||
|
||||
class Statistics {
|
||||
protected intervalTimer: NodeJS.Timer | undefined;
|
||||
protected lastRun: number = 0;
|
||||
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
|
||||
|
||||
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
|
||||
@ -23,15 +24,21 @@ class Statistics {
|
||||
setTimeout(() => {
|
||||
this.runStatistics();
|
||||
this.intervalTimer = setInterval(() => {
|
||||
this.runStatistics();
|
||||
this.runStatistics(true);
|
||||
}, 1 * 60 * 1000);
|
||||
}, difference);
|
||||
}
|
||||
|
||||
private async runStatistics(): Promise<void> {
|
||||
public async runStatistics(skipIfRecent = false): Promise<void> {
|
||||
if (!memPool.isInSync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipIfRecent && new Date().getTime() / 1000 - this.lastRun < 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastRun = new Date().getTime() / 1000;
|
||||
const currentMempool = memPool.getMempool();
|
||||
const txPerSecond = memPool.getTxPerSecond();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
|
@ -23,6 +23,7 @@ import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
|
||||
interface AddressTransactions {
|
||||
mempool: MempoolTransactionExtended[],
|
||||
@ -259,7 +260,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;
|
||||
@ -723,6 +724,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
await statistics.runStatistics();
|
||||
|
||||
const _memPool = memPool.getMempool();
|
||||
|
||||
@ -999,7 +1001,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}`, {
|
||||
@ -1014,6 +1016,8 @@ class WebsocketHandler {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
});
|
||||
|
||||
await statistics.runStatistics();
|
||||
}
|
||||
|
||||
// takes a dictionary of JSON serialized values
|
||||
|
@ -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,6 +510,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
|
||||
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
|
||||
if (this.scene) {
|
||||
const x = cssX * window.devicePixelRatio;
|
||||
const y = cssY * window.devicePixelRatio;
|
||||
const selected = this.scene.getTxAt({ x, y });
|
||||
@ -507,6 +518,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.txClickEvent.emit({ tx: selected, keyModifier });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTxHover(hoverId: string) {
|
||||
this.txHoverEvent.emit(hoverId);
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -85,6 +85,6 @@
|
||||
background-color: #f1c40f;
|
||||
}
|
||||
|
||||
.badge-platinium {
|
||||
.badge-platinum {
|
||||
background-color: #653b9c;
|
||||
}
|
@ -25,7 +25,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>
|
||||
@ -33,8 +33,8 @@
|
||||
<app-mempool-block-overview
|
||||
[index]="0"
|
||||
[resolution]="goggleResolution"
|
||||
[filterFlags]="goggleCycle[goggleIndex].flag"
|
||||
filterMode="or"
|
||||
[filterFlags]="goggleFlags"
|
||||
[filterMode]="goggleMode"
|
||||
></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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;
|
||||
@ -58,6 +59,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
federationUtxosNumber$: Observable<number>;
|
||||
fullHistory$: Observable<any>;
|
||||
isLoad: boolean = true;
|
||||
filterSubscription: Subscription;
|
||||
mempoolInfoSubscription: Subscription;
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
@ -68,13 +70,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();
|
||||
|
||||
@ -90,6 +94,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.filterSubscription.unsubscribe();
|
||||
this.mempoolInfoSubscription.unsubscribe();
|
||||
this.currencySubscription.unsubscribe();
|
||||
this.websocketService.stopTrackRbfSummary();
|
||||
@ -110,6 +115,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$
|
||||
@ -393,6 +422,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) {
|
||||
|
@ -9871,7 +9871,403 @@ export const restApiDocsData = [
|
||||
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 = [
|
||||
|
@ -1,4 +1,4 @@
|
||||
<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>
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
@ -94,6 +95,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -311,27 +311,29 @@ yarn add @mempool/liquid.js`;
|
||||
text = text.replace('%{' + indexNumber + '}', textReplace);
|
||||
}
|
||||
|
||||
const headersString = code.headers ? ` -H "${code.headers}"` : ``;
|
||||
|
||||
if (this.env.BASE_MODULE === 'mempool') {
|
||||
if (this.network === 'main' || this.network === '') {
|
||||
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') {
|
||||
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') {
|
||||
if (this.method === 'POST') {
|
||||
if (this.network !== 'liquid') {
|
||||
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 {
|
||||
return `curl -sSL "${this.hostname}${text}"`;
|
||||
return `curl${headersString} -sSL "${this.hostname}${text}"`;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,9 +1,10 @@
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { StateService } from './state.service';
|
||||
import { StorageService } from './storage.service';
|
||||
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 { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
|
||||
|
||||
@ -30,16 +31,20 @@ const SERVICES_API_PREFIX = `/api/v1/services`;
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ServicesApiServices {
|
||||
private apiBaseUrl: string; // base URL is protocol, hostname, and port
|
||||
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
|
||||
apiBaseUrl: string; // base URL is protocol, hostname, and port
|
||||
apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
userSubject$ = new ReplaySubject<IUser | null>(1);
|
||||
currentAuth = null;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService
|
||||
private storageService: StorageService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.currentAuth = localStorage.getItem('auth');
|
||||
|
||||
this.apiBaseUrl = ''; // use relative URL by default
|
||||
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;
|
||||
@ -59,6 +64,10 @@ export class ServicesApiServices {
|
||||
}
|
||||
|
||||
this.getUserInfo$().subscribe();
|
||||
this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationStart && this.currentAuth !== localStorage.getItem('auth')),
|
||||
switchMap(() => this.getUserInfo$()),
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
@ -153,3 +155,28 @@ export function seoDescriptionNetwork(network: string): string {
|
||||
}
|
||||
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