diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts
index 58921fcfb..b9da7d4e8 100644
--- a/backend/src/api/mempool-blocks.ts
+++ b/backend/src/api/mempool-blocks.ts
@@ -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();
diff --git a/backend/src/api/statistics/statistics-api.ts b/backend/src/api/statistics/statistics-api.ts
index 5c6896619..c7c3f37b0 100644
--- a/backend/src/api/statistics/statistics-api.ts
+++ b/backend/src/api/statistics/statistics-api.ts
@@ -285,7 +285,7 @@ class StatisticsApi {
public async $list2H(): Promise {
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 {
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) {
diff --git a/backend/src/api/statistics/statistics.ts b/backend/src/api/statistics/statistics.ts
index 494777aad..2926a4b17 100644
--- a/backend/src/api/statistics/statistics.ts
+++ b/backend/src/api/statistics/statistics.ts
@@ -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 {
+ public async runStatistics(skipIfRecent = false): Promise {
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();
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index d0e0b7fd8..b78389b64 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -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
diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts
index ead0a84ad..71612f25f 100644
--- a/backend/src/mempool.interfaces.ts
+++ b/backend/src/mempool.interfaces.ts
@@ -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
diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html
index f60b04cdd..8c79cd438 100644
--- a/frontend/src/app/components/block-filters/block-filters.component.html
+++ b/frontend/src/app/components/block-filters/block-filters.component.html
@@ -1,4 +1,4 @@
- 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
+
0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
beta
@@ -14,6 +14,15 @@
diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss
index d30dd3305..92964d948 100644
--- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss
+++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss
@@ -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 {
diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
index ac1df2bf5..95305d72f 100644
--- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
+++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
@@ -9,6 +9,8 @@ import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils';
+import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
+import { detectWebGL } from '../../shared/graphs.utils';
const unmatchedOpacity = 0.2;
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
@@ -42,7 +44,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
- @Input() filterMode: 'and' | 'or' = 'and';
+ @Input() filterMode: FilterMode = 'and';
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@@ -76,11 +78,14 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null;
+ webGlEnabled = true;
+
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
private stateService: StateService,
) {
+ this.webGlEnabled = detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text;
@@ -119,10 +124,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
}
- setFilterFlags(flags?: bigint | null): void {
- this.activeFilterFlags = this.filterFlags || flags || null;
+ setFilterFlags(goggle?: ActiveFilter): void {
+ this.filterMode = goggle?.mode || this.filterMode;
+ this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
if (this.scene) {
- if (this.activeFilterFlags != null) {
+ if (this.activeFilterFlags != null && this.filtersAvailable) {
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
} else {
this.scene.setColorFunction(this.overrideColors);
@@ -157,7 +163,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
// initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void {
- this.filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
+ const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
+ if (filtersAvailable !== this.filtersAvailable) {
+ this.setFilterFlags();
+ }
+ this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.scene.setup(transactions);
this.readyNextFrame = true;
@@ -500,11 +510,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
- const x = cssX * window.devicePixelRatio;
- const y = cssY * window.devicePixelRatio;
- const selected = this.scene.getTxAt({ x, y });
- if (selected && selected.txid) {
- this.txClickEvent.emit({ tx: selected, keyModifier });
+ if (this.scene) {
+ const x = cssX * window.devicePixelRatio;
+ const y = cssY * window.devicePixelRatio;
+ const selected = this.scene.getTxAt({ x, y });
+ if (selected && selected.txid) {
+ this.txClickEvent.emit({ tx: selected, keyModifier });
+ }
}
}
@@ -524,7 +536,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
return (tx: TxView) => {
- if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (tx.bigintFlags & flags) > 0n)) {
+ if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
return defaultColorFunction(tx);
} else {
return defaultColorFunction(
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
index 7fb036718..29825491c 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
+++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
@@ -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();
@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
});
}
diff --git a/frontend/src/app/components/menu/menu.component.scss b/frontend/src/app/components/menu/menu.component.scss
index 8a2006a4e..99377a564 100644
--- a/frontend/src/app/components/menu/menu.component.scss
+++ b/frontend/src/app/components/menu/menu.component.scss
@@ -85,6 +85,6 @@
background-color: #f1c40f;
}
-.badge-platinium {
+.badge-platinum {
background-color: #653b9c;
}
\ No newline at end of file
diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html
index 05cb9d5c5..fda59f542 100644
--- a/frontend/src/app/dashboard/dashboard.component.html
+++ b/frontend/src/app/dashboard/dashboard.component.html
@@ -25,7 +25,7 @@
@@ -33,8 +33,8 @@
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts
index 4f041145a..1f380a99f 100644
--- a/frontend/src/app/dashboard/dashboard.component.ts
+++ b/frontend/src/app/dashboard/dashboard.component.ts
@@ -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;
fullHistory$: Observable;
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$
@@ -392,6 +421,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
getArrayFromNumber(num: number): number[] {
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 {
diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts
index 86a63e513..b28f45195 100644
--- a/frontend/src/app/docs/api-docs/api-docs-data.ts
+++ b/frontend/src/app/docs/api-docs/api-docs-data.ts
@@ -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: "Returns a list of deposits the user has made as prepayment for the accelerator service.
"
+ },
+ 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: "Returns the user's currently available balance, currently locked funds, and total fees paid so far.
"
+ },
+ 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: "Returns estimated costs to accelerate a transaction.
"
+ },
+ 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: "Sends a request to accelerate a transaction.
"
+ },
+ 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: "Returns the user's past acceleration requests.
Pass one of the following for :status
: all
, requested
, accelerating
, mined
, completed
, failed
. Pass true
in :details
to get a detailed history
of the acceleration request.
"
+ },
+ 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: "Returns all transactions currently being accelerated.
"
+ },
+ 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: `Returns all past accelerated transactions.
+ Filters can be applied:
+ status
: all
, requested
, accelerating
, mined
, completed
, failed
+ timeframe
: 24h
, 3d
, 1w
, 1m
, 3m
, 6m
, 1y
, 2y
, 3y
, all
+ poolUniqueId
: any id from https://github.com/mempool/mining-pools/blob/master/pools-v2.json
+ blockHash
: a block hash
+ blockHeight
: a block height
+ page
: the requested page number if using pagination
+ pageLength
: the page lenght if using pagination
+
`
+ },
+ 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 = [
diff --git a/frontend/src/app/docs/api-docs/api-docs-nav.component.html b/frontend/src/app/docs/api-docs/api-docs-nav.component.html
index ec1cde38f..96622c424 100644
--- a/frontend/src/app/docs/api-docs/api-docs-nav.component.html
+++ b/frontend/src/app/docs/api-docs/api-docs-nav.component.html
@@ -1,4 +1,4 @@
diff --git a/frontend/src/app/docs/api-docs/api-docs.component.html b/frontend/src/app/docs/api-docs/api-docs.component.html
index c3a260995..ef7782199 100644
--- a/frontend/src/app/docs/api-docs/api-docs.component.html
+++ b/frontend/src/app/docs/api-docs/api-docs.component.html
@@ -43,54 +43,56 @@
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 enterprise sponsorship if you need higher API limits.
-
-1 )">{{ item.title }}
-
-1 )" class="endpoint-container" id="{{ item.fragment }}">
-
-
-
diff --git a/frontend/src/app/docs/code-template/code-template.component.ts b/frontend/src/app/docs/code-template/code-template.component.ts
index 5ef8a64ba..6b91f5a9d 100644
--- a/frontend/src/app/docs/code-template/code-template.component.ts
+++ b/frontend/src/app/docs/code-template/code-template.component.ts
@@ -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}"`;
}
}
diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts
index 35bcbe9cc..ff5977332 100644
--- a/frontend/src/app/interfaces/websocket.interface.ts
+++ b/frontend/src/app/interfaces/websocket.interface.ts
@@ -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;
diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts
index 758829852..f11b3460c 100644
--- a/frontend/src/app/services/services-api.service.ts
+++ b/frontend/src/app/services/services-api.service.ts
@@ -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
(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();
}
/**
diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts
index f87a3dc31..dc1365baa 100644
--- a/frontend/src/app/services/state.service.ts
+++ b/frontend/src/app/services/state.service.ts
@@ -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 = new Subject();
menuOpen$: BehaviorSubject = new BehaviorSubject(false);
- activeGoggles$: BehaviorSubject = new BehaviorSubject([]);
+ activeGoggles$: BehaviorSubject = new BehaviorSubject({ mode: 'and', filters: [] });
constructor(
@Inject(PLATFORM_ID) private platformId: any,
diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts
index 3c72252db..11e24ef71 100644
--- a/frontend/src/app/services/websocket.service.ts
+++ b/frontend/src/app/services/websocket.service.ts
@@ -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));
}
}
}
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index a04fa1663..18a330fab 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -1,3 +1,5 @@
+import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed, TransactionStripped } from "../interfaces/websocket.interface";
+
export function isMobile(): boolean {
return (window.innerWidth <= 767.98);
}
@@ -152,4 +154,29 @@ export function seoDescriptionNetwork(network: string): string {
return ' ' + network.charAt(0).toUpperCase() + network.slice(1);
}
return '';
+}
+
+export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
+ return {
+ txid: tx[0],
+ fee: tx[1],
+ vsize: tx[2],
+ value: tx[3],
+ rate: tx[4],
+ flags: tx[5],
+ acc: !!tx[6],
+ };
+}
+
+export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
+ return {
+ added: delta.added.map(uncompressTx),
+ removed: delta.removed,
+ changed: delta.changed.map(tx => ({
+ txid: tx[0],
+ rate: tx[1],
+ flags: tx[2],
+ acc: !!tx[3],
+ }))
+ };
}
\ No newline at end of file
diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts
index 0b652a192..3930dc8ca 100644
--- a/frontend/src/app/shared/filters.utils.ts
+++ b/frontend/src/app/shared/filters.utils.ts
@@ -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 },