Compare commits

...

41 Commits

Author SHA1 Message Date
Mononaut
5e8e1f3dee [ops] slack alerts from syslog errs 2024-11-12 20:33:02 +00:00
softsimon
3b4eda432f fix cpfp list title position 2024-11-12 03:22:16 +07:00
wiz
5b2f613856 Merge pull request #5633 from mempool/mononaut/fix-acc-ping-pong
Fix acceleration websocket ping/pong
2024-11-12 02:25:15 +09:00
Mononaut
f1e2c893cc Fix acceleration websocket ping/pong 2024-11-11 17:23:30 +00:00
wiz
7d3d59c348 Merge pull request #5632 from mempool/mononaut/acc-ws-ping
regularly ping acceleration websocket server
2024-11-12 02:08:48 +09:00
wiz
8719b424e5 Increase websocket from 5s to 10s 2024-11-12 02:03:56 +09:00
Mononaut
ef498b55ed reset acceleration websocket if unresponsive 2024-11-11 16:59:08 +00:00
Mononaut
9718610104 regularly ping acceleration websocket server 2024-11-11 16:26:30 +00:00
wiz
937e82bb89 Merge pull request #5631 from mempool/mononaut/acc-ws-debug
Better debug logs for accelerator websocket
2024-11-12 00:32:54 +09:00
Mononaut
91bf35bb65 Better debug logs for accelerator websocket 2024-11-11 14:40:25 +00:00
wiz
f0f6ee1919 Merge pull request #5630 from mempool/mononaut/fix-acc-websocket
fix acceleration websocket protocol
2024-11-11 20:53:27 +09:00
Mononaut
7b837b96da fix acceleration websocket protocol 2024-11-11 11:46:17 +00:00
softsimon
c417470be2 Merge pull request #5621 from mempool/natsoni/tx-first-seen-fix
Show tx first seen time with audit disabled
2024-11-09 00:28:47 +07:00
softsimon
dfd7877f82 Merge pull request #5584 from mempool/simon/cpfp-polish
Polish CPFP button
2024-11-09 00:25:05 +07:00
softsimon
136e80e5cf Merge pull request #5625 from mempool/natsoni/fix-url-nav
Fix navigation to use relative paths
2024-11-09 00:08:52 +07:00
natsoni
b3aed2f58b Fix navigation to use relative paths 2024-11-02 10:41:37 +01:00
wiz
e75f913af3 ops: Cache testnet electrs endpoints for minimum of 1s 2024-11-01 21:20:54 +09:00
wiz
0a9703f164 ops: Cache all electrs endpoints for minimum of 1s 2024-11-01 21:15:07 +09:00
natsoni
db321c3fa5 Get tx first seen from block summary if not available in audit 2024-10-30 15:43:28 +01:00
natsoni
b6aeb5661f Add block/:hash/tx/:txid/summary endpoint 2024-10-30 15:31:01 +01:00
natsoni
f08fa034cc Add missing frontend audit flag for testnet4 2024-10-30 15:19:46 +01:00
wiz
c0ef01d4da Merge pull request #5617 from vostrnad/fix-missing-fakescripthash
Add missing fake_scripthash to the data filter
2024-10-30 19:11:08 +09:00
wiz
c6711d8191 Merge pull request #5619 from mempool/nymkappa/fix-zindex-loading-indicator
[ui] fix loading indicator zindex
2024-10-30 19:10:51 +09:00
wiz
216bd5ad23 Merge pull request #5618 from mempool/nymkappa/fix-pool-blocks-page-length
[mining] return 100 blocks per page instead of 10 for pool block list
2024-10-30 19:10:37 +09:00
wiz
a7d59d6b2e Merge pull request #5620 from mempool/nymkappa/pool-hashrate 2024-10-30 18:51:36 +09:00
nymkappa
a257bcc12a [mining] show pools estimated hashrate on 3d and 1w timeframes 2024-10-30 10:46:44 +01:00
wiz
66c0ea7ca3 Merge pull request #5500 from mempool/mononaut/esplora-monitoring-reorg
Fix reorg to lower height on /monitoring status page
2024-10-30 18:28:35 +09:00
wiz
7692b2af66 Merge pull request #5511 from mempool/mononaut/summary-indexing-limit
respect INDEXING_BLOCKS_AMOUNT during summary indexing
2024-10-30 18:27:26 +09:00
wiz
397f53f42d Merge branch 'master' into mononaut/esplora-monitoring-reorg 2024-10-30 18:18:16 +09:00
wiz
2ea76d9c38 Merge pull request #5394 from mempool/mononaut/use-acceleration-websocket
[accelerator] get acceleration updates over websocket
2024-10-30 18:17:26 +09:00
nymkappa
59ac27b104 [ui] fix loading indicator zindex 2024-10-30 09:59:41 +01:00
nymkappa
d27bb7e156 [mining] return 100 blocks per page instead of 10 for pool block list 2024-10-30 09:54:13 +01:00
Vojtěch Strnad
4eadfc0a3b Add missing fake_scripthash to the data filter 2024-10-29 07:25:36 +01:00
wiz
76cfa3ca47 Merge pull request #5615 from mempool/mononaut/adjust-app-imports 2024-10-27 13:55:58 +09:00
Mononaut
3a4a4d9ffd don't allow overriding critical @app imports 2024-10-27 02:39:55 +00:00
softsimon
aa9888a2fe Related Transactions 2024-10-25 11:58:01 +07:00
softsimon
3909148d6e Polish CPFP button 2024-10-25 11:52:01 +07:00
softsimon
e6980a832b Merge branch 'master' into mononaut/use-acceleration-websocket 2024-10-07 15:58:13 +09:00
Mononaut
be49f70b09 [accelerator] get acceleration updates over websocket 2024-09-23 17:17:28 +00:00
Mononaut
7865574bd4 block summary indexing respect INDEXING_BLOCKS_AMOUNT 2024-09-11 21:48:26 +00:00
Mononaut
e513f05c09 Fix reorg to lower height on /monitoring status page 2024-09-05 20:55:48 +00:00
39 changed files with 459 additions and 119 deletions

View File

@@ -42,6 +42,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
@@ -321,6 +322,20 @@ class BitcoinRoutes {
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) {
try {
const block = await blocks.$getBlock(req.params.hash);

View File

@@ -412,8 +412,16 @@ class Blocks {
}
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
// Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@@ -1216,6 +1224,11 @@ class Blocks {
return summary.transactions;
}
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
const txs = await this.$getStrippedBlockTransactions(hash);
return txs.find(tx => tx.txid === txid) || null;
}
/**
* Get 15 blocks
*

View File

@@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache';
import blocks from './blocks';
@@ -207,7 +208,7 @@ class Mempool {
return txTimes;
}
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
@@ -354,7 +355,7 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
if (accelerationDelta.length) {
hasChange = true;
}
@@ -399,58 +400,11 @@ class Mempool {
return this.accelerations;
}
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
try {
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
this.accelerations = newAccelerationMap;
return changed;
return accelerationDelta;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];

View File

@@ -459,7 +459,7 @@ class MiningRoutes {
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(accelerationApi.accelerations || []);
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}

View File

@@ -136,9 +136,13 @@ class Mining {
poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);

View File

@@ -1,7 +1,10 @@
import { WebSocket } from 'ws';
import config from '../../config';
import logger from '../../logger';
import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
@@ -37,14 +40,23 @@ export interface AccelerationHistory {
};
class AccelerationApi {
private ws: WebSocket | null = null;
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private _accelerations: Acceleration[] | null = null;
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
private _accelerations: Record<string, Acceleration> = {};
private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public get accelerations(): Acceleration[] | null {
public constructor() {}
public getAccelerations(): Record<string, Acceleration> {
return this._accelerations;
}
@@ -72,11 +84,18 @@ class AccelerationApi {
}
}
public async $updateAccelerations(): Promise<Acceleration[] | null> {
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
if (this.useWebsocket && this.websocketConnected) {
return this._accelerations;
}
if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations();
if (accelerations) {
this._accelerations = accelerations;
const latestAccelerations = {};
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations;
}
} else {
@@ -85,7 +104,7 @@ class AccelerationApi {
return null;
}
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
@@ -120,7 +139,11 @@ class AccelerationApi {
}
}
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
const latestAccelerations = {};
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations;
}
@@ -152,6 +175,131 @@ class AccelerationApi {
}
return anyAccelerated;
}
// get a list of accelerations that have changed between two sets of accelerations
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
const changed: string[] = [];
const mempoolCache = mempool.getMempool();
for (const acceleration of Object.values(newAccelerationMap)) {
// skip transactions we don't know about
if (!mempoolCache[acceleration.txid]) {
continue;
}
if (oldAccelerationMap[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(oldAccelerationMap)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
return changed;
}
private handleWebsocketMessage(msg: any): void {
if (msg?.accelerations !== null) {
const latestAccelerations = {};
for (const acc of msg?.accelerations || []) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
websocketHandler.handleAccelerationsChanged(this._accelerations);
}
}
public async connectWebsocket(): Promise<void> {
if (this.startedWebsocketLoop) {
return;
}
while (this.useWebsocket) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(this.websocketPath);
this.websocketConnected = true;
this.ws.on('open', () => {
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
this.ws?.send(JSON.stringify({
'watch-accelerations': true
}));
});
this.ws.on('error', (error) => {
logger.err(`Acceleration websocket error on ${this.websocketPath}: ` + error);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Acceleration websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const msg = (isBinary ? data : data.toString()) as string;
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
}
});
this.ws.on('ping', () => {
logger.debug('received ping from acceleration websocket server');
});
this.ws.on('pong', () => {
logger.debug('received pong from acceleration websocket server');
this.lastPong = Date.now();
});
} else {
if (this.lastPing > this.lastPong && Date.now() - this.lastPing > 10000) {
logger.warn('No pong received within 10 seconds, terminating connection');
this.ws.terminate();
this.ws = null;
this.websocketConnected = false;
} else if (Date.now() - this.lastPing > 30000) {
logger.debug('sending ping to acceleration websocket server');
this.ws.ping();
this.lastPing = Date.now();
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new AccelerationApi();

View File

@@ -22,6 +22,7 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
import Audit from './audit';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
@@ -60,6 +61,8 @@ class WebsocketHandler {
private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0;
private accelerations: Record<string, Acceleration> = {};
constructor() { }
addWebsocketServer(wss: WebSocket.Server) {
@@ -495,6 +498,42 @@ class WebsocketHandler {
}
}
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server has been set');
}
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
if (!websocketAccelerationDelta.length) {
return;
}
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
try {
const response = JSON.stringify({
accelerations: accelerationUpdate,
});
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
} catch (e) {
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
}
}
handleReorg(): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@@ -571,7 +610,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
const accelerations = accelerationApi.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
@@ -679,10 +718,13 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
// pre-compute acceleration delta
const accelerationUpdate = {
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged

View File

@@ -233,11 +233,11 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const newAccelerations = await accelerationApi.$updateAccelerations();
const latestAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
}
indexer.$run();
if (config.WALLETS.ENABLED) {
@@ -318,8 +318,10 @@ class Server {
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
}
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
}
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
bitcoinCoreRoutes.initRoutes(this.app);

View File

@@ -501,7 +501,7 @@ class BlocksRepository {
}
query += ` ORDER BY height DESC
LIMIT 10`;
LIMIT 100`;
try {
const [rows]: any[] = await DB.query(query, params);

View File

@@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppModule } from '@app/app.module';
import { AppModule } from './app.module';
import { AppComponent } from '@components/app/app.component';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ZoneService } from '@app/services/zone.service';

View File

@@ -3,7 +3,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppRoutingModule } from '@app/app-routing.module';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from '@components/app/app.component';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { OrdApiService } from '@app/services/ord-api.service';

View File

@@ -172,10 +172,6 @@
background-color: var(--tertiary);
}
.btn-small-height {
line-height: 1;
}
.summary-row {
display: flex;
flex-direction: row;

View File

@@ -20,7 +20,7 @@
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
}
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div>
@@ -36,7 +36,7 @@
<tr>
<td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start">
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
</div>
</td>
</tr>

View File

@@ -9,7 +9,7 @@ import { AudioService } from '@app/services/audio.service';
import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, combineLatest } from 'rxjs';
import { SeoService } from '@app/services/seo.service';
import { environment } from '@app/../environments/environment';
import { environment } from '@environments/environment';
import { AssetsService } from '@app/services/assets.service';
import { moveDec } from '@app/bitcoin.utils';

View File

@@ -9,6 +9,7 @@ import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-blocks-list',
@@ -49,6 +50,7 @@ export class BlocksList implements OnInit {
private ogService: OpenGraphService,
private route: ActivatedRoute,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
@Inject(LOCALE_ID) private locale: string,
) {
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
@@ -182,7 +184,7 @@ export class BlocksList implements OnInit {
}
pageChange(page: number): void {
this.router.navigate(['blocks', page]);
this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]);
}
trackByBlock(index: number, block: BlockExtended): number {

View File

@@ -73,7 +73,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' },
];
goggleFlags = 0n;
goggleMode: FilterMode = 'and';

View File

@@ -1,7 +1,7 @@
.sticky-loading {
position: absolute;
right: 10px;
z-index: 99;
z-index: 1000;
font-size: 14px;
@media (width >= 992px) {
left: 32px;

View File

@@ -267,7 +267,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (event.key === prevKey) {
if (this.mempoolBlocks[this.markIndex - 1]) {
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
this.router.navigate([this.relativeUrlPipe.transform('/mempool-block/'), this.markIndex - 1]);
} else {
const blocks = this.stateService.blocksSubject$.getValue();
for (const block of (blocks || [])) {

View File

@@ -90,9 +90,9 @@
<th class="d-none d-md-table-cell" i18n="mining.rank">Rank</th>
<th class=""></th>
<th class="" i18n="mining.pool-name">Pool</th>
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
<th class="" *ngIf="['24h', '3d', '1w'].includes(this.miningWindowPreference)" i18n="mining.hashrate">Hashrate</th>
<th class="" i18n="master-page.blocks">Blocks</th>
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': this.miningWindowPreference === '24h'}" i18n="latest-blocks.avg_health"
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': ['24h', '3d', '1w'].includes(this.miningWindowPreference)}" i18n="latest-blocks.avg_health"
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
@@ -105,12 +105,13 @@
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'">
</td>
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{
miningStats.miningUnits.hashrateUnit }}</td>
<td class="" *ngIf="'24h' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="" *ngIf="'3d' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="" *ngIf="'1w' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="d-flex justify-content-center">
{{ pool.blockCount }}<span class="d-none d-md-table-cell">&nbsp;({{ pool.share }}%)</span>
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': this.miningWindowPreference === '24h'}">
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': ['24h', '3d', '1w'].includes(this.miningWindowPreference)}">
<a
class="health-badge badge"
[class.badge-success]="pool.avgMatchRate >= 99"
@@ -136,8 +137,9 @@
<td class="d-none d-md-table-cell"></td>
<td class="text-right"></td>
<td class=""><b i18n="mining.all-miners">All miners</b></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate | number: '1.2-2' }} {{
miningStats.miningUnits.hashrateUnit }}</b></td>
<td class="" *ngIf="'24h' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate| number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="" *ngIf="'3d' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="" *ngIf="'1w' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class=""><b>{{ miningStats.blockCount }}</b></td>
<td *ngIf="auditAvailable"></td>
<td *ngIf="auditAvailable"></td>

View File

@@ -161,9 +161,12 @@ export class PoolRankingComponent implements OnInit {
borderColor: '#000',
formatter: () => {
const i = pool.blockCount.toString();
if (this.miningWindowPreference === '24h') {
if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) {
let hashrate = pool.lastEstimatedHashrate;
if ('3d' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate3d; }
if ('1w' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate1w; }
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit +
hashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit +
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
@@ -200,13 +203,10 @@ export class PoolRankingComponent implements OnInit {
borderColor: '#000',
formatter: () => {
const i = totalBlockOther.toString();
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + totalEstimatedHashrateOther.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit + `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
$localize`${ i }:INTERPOLATION: blocks`;
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + $localize`${ i }:INTERPOLATION: blocks`;
}
}
},
@@ -292,6 +292,8 @@ export class PoolRankingComponent implements OnInit {
getEmptyMiningStat(): MiningStats {
return {
lastEstimatedHashrate: 0,
lastEstimatedHashrate3d: 0,
lastEstimatedHashrate1w: 0,
blockCount: 0,
totalEmptyBlock: 0,
totalEmptyBlockRatio: '',

View File

@@ -9,7 +9,7 @@
<ng-container *ngIf="(hosts$ | async) as hosts">
<div class="status-panel">
<table class="status-table table table-borderless table-striped" *ngIf="(tip$ | async) as tip">
<table class="status-table table table-borderless table-striped">
<tbody>
<tr>
<th class="rank"></th>
@@ -27,7 +27,7 @@
<td class="updated">{{ getLastUpdateSeconds(host) }}</td>
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '')) }}</td>
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '')) }}</td>
</tr>
</tbody>
</table>

View File

@@ -1,6 +1,6 @@
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
import { WebsocketService } from '@app/services/websocket.service';
import { Observable, Subject, map } from 'rxjs';
import { Observable, Subject, map, tap } from 'rxjs';
import { StateService } from '@app/services/state.service';
import { HealthCheckHost } from '@interfaces/websocket.interface';
import { DomSanitizer } from '@angular/platform-browser';
@@ -13,7 +13,7 @@ import { DomSanitizer } from '@angular/platform-browser';
})
export class ServerHealthComponent implements OnInit {
hosts$: Observable<HealthCheckHost[]>;
tip$: Subject<number>;
maxHeight: number;
interval: number;
now: number = Date.now();
@@ -44,9 +44,14 @@ export class ServerHealthComponent implements OnInit {
host.flag = this.parseFlag(host.host);
}
return hosts;
}),
tap(hosts => {
let newMaxHeight = 0;
for (const host of hosts) {
newMaxHeight = Math.max(newMaxHeight, host.latestHeight);
}
})
);
this.tip$ = this.stateService.chainTip$;
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
this.interval = window.setInterval(() => {

View File

@@ -267,7 +267,7 @@
}
</div>
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP</button>
}
</td>
</tr>

View File

@@ -69,7 +69,9 @@
<!-- CPFP Details -->
<ng-template [ngIf]="showCpfpDetails">
<br>
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
<div class="title">
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
</div>
<div class="box cpfp-details">
<table class="table table-fixed table-borderless table-striped">
<thead>

View File

@@ -66,10 +66,6 @@
color: white;
}
.btn-small-height {
line-height: 1;
}
.arrow-green {
color: var(--success);
}

View File

@@ -406,6 +406,30 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const auditAvailable = this.isAuditAvailable(height);
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
const fetchAudit = auditAvailable && !isCoinbase;
const addFirstSeen = (audit: TxAuditStatus | null, hash: string, height: number, txid: string, useFullSummary: boolean) => {
if (
this.isFirstSeenAvailable(height)
&& !audit?.firstSeen // firstSeen is not already in audit
&& (!audit || audit?.seen) // audit is disabled or tx is already seen (meaning 'firstSeen' is in block summary)
) {
return useFullSummary ?
this.apiService.getStrippedBlockTransactions$(hash).pipe(
map(strippedTxs => {
return { audit, firstSeen: strippedTxs.find(tx => tx.txid === txid)?.time };
}),
catchError(() => of({ audit }))
) :
this.apiService.getStrippedBlockTransaction$(hash, txid).pipe(
map(strippedTx => {
return { audit, firstSeen: strippedTx?.time };
}),
catchError(() => of({ audit }))
);
}
return of({ audit });
};
if (fetchAudit) {
// If block audit is already cached, use it to get transaction audit
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
@@ -428,24 +452,31 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
accelerated: isAccelerated,
firstSeen,
};
}),
switchMap(audit => addFirstSeen(audit, hash, height, txid, true)),
catchError(() => {
return of({ audit: null });
})
)
} else {
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
retry({ count: 3, delay: 2000 }),
switchMap(audit => addFirstSeen(audit, hash, height, txid, false)),
catchError(() => {
return of(null);
return of({ audit: null });
})
)
}
} else {
return of(isCoinbase ? { coinbase: true } : null);
const audit = isCoinbase ? { coinbase: true } : null;
return addFirstSeen(audit, hash, height, txid, this.apiService.getBlockSummaryLoaded(hash));
}
}),
).subscribe(auditStatus => {
this.auditStatus = auditStatus;
if (this.auditStatus?.firstSeen) {
this.transactionTime = this.auditStatus.firstSeen;
this.auditStatus = auditStatus?.audit;
const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen'];
if (firstSeen) {
this.transactionTime = firstSeen;
}
this.setIsAccelerated();
});
@@ -922,6 +953,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return false;
}
break;
case 'testnet4':
if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) {
return false;
}
break;
case 'signet':
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
return false;
@@ -935,6 +971,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return true;
}
isFirstSeenAvailable(blockHeight: number): boolean {
if (this.stateService.env.BASE_MODULE !== 'mempool') {
return false;
}
switch (this.stateService.network) {
case 'testnet':
if (this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
break;
case 'testnet4':
if (this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
break;
case 'signet':
if (this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
break;
default:
if (this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
}
return false;
}
resetTransaction() {
this.firstLoad = false;
this.gotInitialPosition = false;

View File

@@ -78,7 +78,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' },
];
goggleFlags = 0n;
goggleMode: FilterMode = 'and';

View File

@@ -143,6 +143,8 @@ export interface SinglePoolStats {
rank: number;
share: number;
lastEstimatedHashrate: number;
lastEstimatedHashrate3d: number;
lastEstimatedHashrate1w: number;
emptyBlockRatio: string;
logo: string;
slug: string;
@@ -152,6 +154,8 @@ export interface SinglePoolStats {
export interface PoolsStats {
blockCount: number;
lastEstimatedHashrate: number;
lastEstimatedHashrate3d: number;
lastEstimatedHashrate1w: number;
pools: SinglePoolStats[];
}

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { RouterModule } from '@angular/router';
import { GraphsModule } from '@app/graphs/graphs.module';
import { PreviewsRoutingModule } from '@app/previews.routing.module';
import { PreviewsRoutingModule } from './previews.routing.module';
import { TransactionPreviewComponent } from '@components/transaction/transaction-preview.component';
import { BlockPreviewComponent } from '@components/block/block-preview.component';
import { AddressPreviewComponent } from '@components/address/address-preview.component';

View File

@@ -18,6 +18,7 @@ export class ApiService {
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
public blockSummaryLoaded: { [hash: string]: boolean } = {};
public blockAuditLoaded: { [hash: string]: boolean } = {};
constructor(
@@ -318,9 +319,14 @@ export class ApiService {
}
getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> {
this.setBlockSummaryLoaded(hash);
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary');
}
getStrippedBlockTransaction$(hash: string, txid: string): Observable<TransactionStripped> {
return this.httpClient.get<TransactionStripped>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/tx/' + txid + '/summary');
}
getDifficultyAdjustments$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` +
@@ -567,4 +573,12 @@ export class ApiService {
getBlockAuditLoaded(hash) {
return this.blockAuditLoaded[hash];
}
async setBlockSummaryLoaded(hash: string) {
this.blockSummaryLoaded[hash] = true;
}
getBlockSummaryLoaded(hash) {
return this.blockSummaryLoaded[hash];
}
}

View File

@@ -13,6 +13,8 @@ export interface MiningUnits {
export interface MiningStats {
lastEstimatedHashrate: number;
lastEstimatedHashrate3d: number;
lastEstimatedHashrate1w: number;
blockCount: number;
totalEmptyBlock: number;
totalEmptyBlockRatio: string;
@@ -129,6 +131,8 @@ export class MiningService {
return {
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
lastEstimatedHashrate: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider,
lastEstimatedHashrate3d: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate3d / hashrateDivider,
lastEstimatedHashrate1w: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate1w / hashrateDivider,
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
...poolStat
@@ -137,6 +141,8 @@ export class MiningService {
return {
lastEstimatedHashrate: stats.lastEstimatedHashrate / hashrateDivider,
lastEstimatedHashrate3d: stats.lastEstimatedHashrate3d / hashrateDivider,
lastEstimatedHashrate1w: stats.lastEstimatedHashrate1w / hashrateDivider,
blockCount: stats.blockCount,
totalEmptyBlock: totalEmptyBlock,
totalEmptyBlockRatio: totalEmptyBlockRatio,

View File

@@ -68,7 +68,12 @@ export interface Env {
AUDIT: boolean;
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET4_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
MAINNET_TX_FIRST_SEEN_START_HEIGHT: number;
TESTNET_TX_FIRST_SEEN_START_HEIGHT: number;
TESTNET4_TX_FIRST_SEEN_START_HEIGHT: number;
SIGNET_TX_FIRST_SEEN_START_HEIGHT: number;
HISTORICAL_PRICE: boolean;
ACCELERATOR: boolean;
ACCELERATOR_BUTTON: boolean;
@@ -107,7 +112,12 @@ const defaultEnv: Env = {
'AUDIT': false,
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET4_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
'MAINNET_TX_FIRST_SEEN_START_HEIGHT': 0,
'TESTNET_TX_FIRST_SEEN_START_HEIGHT': 0,
'TESTNET4_TX_FIRST_SEEN_START_HEIGHT': 0,
'SIGNET_TX_FIRST_SEEN_START_HEIGHT': 0,
'HISTORICAL_PRICE': true,
'ACCELERATOR': false,
'ACCELERATOR_BUTTON': true,

View File

@@ -1403,4 +1403,8 @@ a {
color: var(--fg);
background-color: var(--primary);
border-color: var(--primary);
}
}
.btn-small-height {
line-height: 1;
}

View File

@@ -1089,6 +1089,7 @@ case $OS in
echo "[*] Installing syslog configuration"
osSudo "${ROOT_USER}" mkdir -p /usr/local/etc/syslog.d
osSudo "${ROOT_USER}" install -c -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/mempool-logger" /usr/local/bin/mempool-logger
osSudo "${ROOT_USER}" install -c -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/mempool-slacker" /usr/local/bin/mempool-slacker
osSudo "${ROOT_USER}" install -c -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/syslog.conf" /usr/local/etc/syslog.d/mempool.conf
echo "[*] Installing redis configuration"

View File

@@ -13,6 +13,7 @@
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911,
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609,
"MAINNET_TX_FIRST_SEEN_START_HEIGHT": 838316,
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,

19
production/mempool-slacker Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
if [ -z "$SLACK_SYSLOG_WEBHOOK_URL" ]; then
echo "Error: SLACK_SYSLOG_WEBHOOK_URL environment variable is not set" >&2
exit 1
fi
trap "" PIPE
while read input; do
if [ ! -z "${input}" ]; then
escaped_input=$(echo "$input" | sed 's/"/\\"/g')
curl -X POST \
-H 'Content-type: application/json' \
--data "{\"text\":\"\`\`\`${escaped_input}\`\`\`\"}" \
-s \
"$SLACK_SYSLOG_WEBHOOK_URL" \
>/dev/null 2>&1 &
fi
done

View File

@@ -55,10 +55,10 @@ location /api/block/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @esplora-api-cache-forever;
}
# other API responses cannot be cached
# we cache for 1s to mitigate DoS attacks
location /api/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @esplora-api-cache-disabled;
try_files /dev/null @esplora-api-cache-minimal;
}
###########
@@ -171,6 +171,23 @@ location @esplora-api-cache-disabled {
expires -1;
}
location @esplora-api-cache-minimal {
proxy_pass $esploraMainnet;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache apihot;
proxy_cache_valid 200 1s;
proxy_redirect off;
expires 1s;
}
location @esplora-api-cache-forever {
proxy_pass $esploraMainnet;

View File

@@ -47,10 +47,10 @@ location /testnet/api/block/ {
rewrite ^/testnet/api/(.*) /$1 break;
try_files /dev/null @esplora-testnet-api-cache-forever;
}
# other API responses cannot be cached
# we cache for 1s to mitigate DoS attacks
location /testnet/api/ {
rewrite ^/testnet/api/(.*) /$1 break;
try_files /dev/null @esplora-testnet-api-cache-disabled;
try_files /dev/null @esplora-testnet-api-cache-minimal;
}
###########
@@ -160,3 +160,20 @@ location @esplora-testnet-api-cache-forever {
expires 30d;
}
location @esplora-testnet-api-cache-minimal {
proxy_pass $esploraTestnet;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update off;
proxy_cache_use_stale error;
proxy_cache apihot;
proxy_cache_valid 200 1s;
proxy_redirect off;
expires 1s;
}

View File

@@ -1,4 +1,4 @@
local7.>=err |/usr/local/bin/sudo -u mempool /usr/local/bin/mempool-logger mempool.ops alerts
local7.>=err |/usr/local/bin/sudo -u mempool /usr/local/bin/mempool-slacker
local7.>=info |/usr/local/bin/sudo -u mempool /usr/local/bin/mempool-logger mempool.ops node100
local7.>=info /var/log/mempool
local7.* /var/log/mempool.debug