diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 77b571136..eedbf3e4c 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -13,6 +13,7 @@ "INITIAL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8, "INDEXING_BLOCKS_AMOUNT": 11000, + "BLOCKS_SUMMARIES_INDEXING": false, "PRICE_FEED_UPDATE_INTERVAL": 600, "USE_SECOND_NODE_FOR_MINFEE": false, "EXTERNAL_ASSETS": [], diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 0c64a9d63..9802bcd71 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -14,6 +14,7 @@ export interface AbstractBitcoinApi { $getAddressPrefix(prefix: string): string[]; $sendRawTransaction(rawTransaction: string): Promise; $getOutspends(txId: string): Promise; + $getBatchedOutspends(txId: string[]): Promise; } export interface BitcoinRpcCredentials { host: string; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 1ff8486f4..7309256bd 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -148,6 +148,15 @@ class BitcoinApi implements AbstractBitcoinApi { return outSpends; } + async $getBatchedOutspends(txId: string[]): Promise { + const outspends: IEsploraApi.Outspend[][] = []; + for (const tx of txId) { + const outspend = await this.$getOutspends(tx); + outspends.push(outspend); + } + return outspends; + } + $getEstimatedHashrate(blockHeight: number): Promise { // 120 is the default block span in Core return this.bitcoindClient.getNetworkHashPs(120, blockHeight); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index fbed1c429..007b4131c 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -66,8 +66,18 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method not implemented.'); } - $getOutspends(): Promise { - throw new Error('Method not implemented.'); + $getOutspends(txId: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) + .then((response) => response.data); + } + + async $getBatchedOutspends(txId: string[]): Promise { + const outspends: IEsploraApi.Outspend[][] = []; + for (const tx of txId) { + const outspend = await this.$getOutspends(tx); + outspends.push(outspend); + } + return outspends; } } diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index c013bbdd3..571cc0f3b 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -20,6 +20,7 @@ import indexer from '../indexer'; import fiatConversion from './fiat-conversion'; import RatesRepository from '../repositories/RatesRepository'; import poolsParser from './pools-parser'; +import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; class Blocks { private blocks: BlockExtended[] = []; @@ -242,13 +243,68 @@ class Blocks { } } + /** + * [INDEXING] Index all blocks summaries for the block txs visualization + */ + public async $generateBlocksSummariesDatabase() { + if (Common.blocksSummariesIndexingEnabled() === false) { + return; + } + + try { + // Get all indexed block hash + const indexedBlocks = await blocksRepository.$getIndexedBlocks(); + const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId(); + + const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop + for (const hash of indexedBlockSummariesHashesArray) { + indexedBlockSummariesHashes[hash] = true; + } + + // Logging + let newlyIndexed = 0; + let totalIndexed = indexedBlockSummariesHashesArray.length; + let indexedThisRun = 0; + let timer = new Date().getTime() / 1000; + const startedAt = new Date().getTime() / 1000; + + for (const block of indexedBlocks) { + if (indexedBlockSummariesHashes[block.hash] === true) { + continue; + } + + // Logging + const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); + if (elapsedSeconds > 5) { + const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); + const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); + const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100; + const timeLeft = Math.round((indexedBlocks.length - totalIndexed) / blockPerSeconds); + logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`); + timer = new Date().getTime() / 1000; + indexedThisRun = 0; + } + + await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary + + // Logging + indexedThisRun++; + totalIndexed++; + newlyIndexed++; + } + logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`); + } catch (e) { + logger.err(`Blocks summaries indexing failed. Reason: ${(e instanceof Error ? e.message : e)}`); + } + } + /** * [INDEXING] Index all blocks metadata for the mining dashboard */ - public async $generateBlockDatabase() { + public async $generateBlockDatabase(): Promise { const blockchainInfo = await bitcoinClient.getBlockchainInfo(); if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync - return; + return false; } try { @@ -292,7 +348,7 @@ class Blocks { const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) { const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); - const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); + const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100; const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds); logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`); @@ -316,13 +372,16 @@ class Blocks { } catch (e) { logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e)); loadingIndicators.setProgress('block-indexing', 100); - return; + return false; } const chainValid = await BlocksRepository.$validateChain(); if (!chainValid) { indexer.reindex(); + return false; } + + return true; } public async $updateBlocks() { @@ -387,11 +446,19 @@ class Blocks { // We assume there won't be a reorg with more than 10 block depth await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10); await HashratesRepository.$deleteLastEntries(); + await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10); for (let i = 10; i >= 0; --i) { - await this.$indexBlock(lastBlock['height'] - i); + const newBlock = await this.$indexBlock(lastBlock['height'] - i); + await this.$getStrippedBlockTransactions(newBlock.id, true, true); } + logger.info(`Re-indexed 10 blocks and summaries`); } await blocksRepository.$saveBlockInDatabase(blockExtended); + + // Save blocks summary for visualization if it's enabled + if (Common.blocksSummariesIndexingEnabled() === true) { + await this.$getStrippedBlockTransactions(blockExtended.id, true); + } } } if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) { @@ -477,14 +544,34 @@ class Blocks { return blockExtended; } - public async $getStrippedBlockTransactions(hash: string): Promise { - // Check the memory cache - const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash); - if (cachedSummary) { - return cachedSummary.transactions; + public async $getStrippedBlockTransactions(hash: string, skipMemoryCache: boolean = false, + skipDBLookup: boolean = false): Promise + { + if (skipMemoryCache === false) { + // Check the memory cache + const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash); + if (cachedSummary) { + return cachedSummary.transactions; + } } + + // Check if it's indexed in db + if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) { + const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash); + if (indexedSummary !== undefined) { + return indexedSummary.transactions; + } + } + + // Call Core RPC const block = await bitcoinClient.getBlock(hash, 2); const summary = this.summarizeBlock(block); + + // Index the response if needed + if (Common.blocksSummariesIndexingEnabled() === true) { + await BlocksSummariesRepository.$saveSummary(block.height, summary); + } + return summary.transactions; } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index d4b57f204..d1c8cecbb 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -177,4 +177,11 @@ export class Common { config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0 ); } + + static blocksSummariesIndexingEnabled(): boolean { + return ( + Common.indexingEnabled() && + config.MEMPOOL.BLOCKS_SUMMARIES_INDEXING === true + ); + } } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 5ac63740b..2b7b6ddea 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 19; + private static currentVersion = 20; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -217,6 +217,10 @@ class DatabaseMigration { if (databaseSchemaVersion < 19) { await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates')); } + + if (databaseSchemaVersion < 20 && isBitcoin === true) { + await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries')); + } } catch (e) { throw e; } @@ -512,6 +516,16 @@ class DatabaseMigration { ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + private getCreateBlocksSummariesTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS blocks_summaries ( + height int(10) unsigned NOT NULL, + id varchar(65) NOT NULL, + transactions JSON NOT NULL, + PRIMARY KEY (id), + INDEX (height) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + public async $truncateIndexedData(tables: string[]) { const allowedTables = ['blocks', 'hashrates']; diff --git a/backend/src/config.ts b/backend/src/config.ts index e49da3dc9..44864d3b9 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -15,6 +15,7 @@ interface IConfig { INITIAL_BLOCKS_AMOUNT: number; MEMPOOL_BLOCKS_AMOUNT: number; INDEXING_BLOCKS_AMOUNT: number; + BLOCKS_SUMMARIES_INDEXING: boolean; PRICE_FEED_UPDATE_INTERVAL: number; USE_SECOND_NODE_FOR_MINFEE: boolean; EXTERNAL_ASSETS: string[]; @@ -104,6 +105,7 @@ const defaults: IConfig = { 'INITIAL_BLOCKS_AMOUNT': 8, 'MEMPOOL_BLOCKS_AMOUNT': 8, 'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks + 'BLOCKS_SUMMARIES_INDEXING': false, 'PRICE_FEED_UPDATE_INTERVAL': 600, 'USE_SECOND_NODE_FOR_MINFEE': false, 'EXTERNAL_ASSETS': [], diff --git a/backend/src/index.ts b/backend/src/index.ts index c61a45c69..e1ef5c7a5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -195,6 +195,7 @@ class Server { setUpHttpApiRoutes() { this.app .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes) + .get(config.MEMPOOL.API_URL_PREFIX + 'outspends', routes.$getBatchedOutspends) .get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', routes.getCpfpInfo) .get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange) .get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees) diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index 04c51ffe2..96cca9f7f 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -29,10 +29,17 @@ class Indexer { this.indexerRunning = true; try { - await blocks.$generateBlockDatabase(); + const chainValid = await blocks.$generateBlockDatabase(); + if (chainValid === false) { + // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration + this.indexerRunning = false; + return; + } + await this.$resetHashratesIndexingState(); await mining.$generateNetworkHashrateHistory(); await mining.$generatePoolHashrateHistory(); + await blocks.$generateBlocksSummariesDatabase(); } catch (e) { this.reindex(); logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e)); diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 4900e3bba..01b7622f3 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -6,6 +6,7 @@ import { prepareBlock } from '../utils/blocks-utils'; import PoolsRepository from './PoolsRepository'; import HashratesRepository from './HashratesRepository'; import { escape } from 'mysql2'; +import BlocksSummariesRepository from './BlocksSummariesRepository'; class BlocksRepository { /** @@ -495,6 +496,7 @@ class BlocksRepository { if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) { logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`); await this.$deleteBlocksFrom(blocks[idx - 1].height); + await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height); await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800); return false; } @@ -652,6 +654,19 @@ class BlocksRepository { throw e; } } + + /** + * Get a list of blocks that have been indexed + */ + public async $getIndexedBlocks(): Promise { + try { + const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`); + return rows; + } catch (e) { + logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } } export default new BlocksRepository(); diff --git a/backend/src/repositories/BlocksSummariesRepository.ts b/backend/src/repositories/BlocksSummariesRepository.ts new file mode 100644 index 000000000..66c6b97f2 --- /dev/null +++ b/backend/src/repositories/BlocksSummariesRepository.ts @@ -0,0 +1,59 @@ +import DB from '../database'; +import logger from '../logger'; +import { BlockSummary } from '../mempool.interfaces'; + +class BlocksSummariesRepository { + public async $getByBlockId(id: string): Promise { + try { + const [summary]: any[] = await DB.query(`SELECT * from blocks_summaries WHERE id = ?`, [id]); + if (summary.length > 0) { + summary[0].transactions = JSON.parse(summary[0].transactions); + return summary[0]; + } + } catch (e) { + logger.err(`Cannot get block summary for block id ${id}. Reason: ` + (e instanceof Error ? e.message : e)); + } + + return undefined; + } + + public async $saveSummary(height: number, summary: BlockSummary) { + try { + await DB.query(`INSERT INTO blocks_summaries VALUE (?, ?, ?)`, [height, summary.id, JSON.stringify(summary.transactions)]); + } catch (e: any) { + if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart + logger.debug(`Cannot save block summary for ${summary.id} because it has already been indexed, ignoring`); + } else { + logger.debug(`Cannot save block summary for ${summary.id}. Reason: ${e instanceof Error ? e.message : e}`); + throw e; + } + } + } + + public async $getIndexedSummariesId(): Promise { + try { + const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`); + return rows.map(row => row.id); + } catch (e) { + logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e)); + } + + return []; + } + + /** + * Delete blocks from the database from blockHeight + */ + public async $deleteBlocksFrom(blockHeight: number) { + logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`); + + try { + await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`); + } catch (e) { + logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e)); + } + } +} + +export default new BlocksSummariesRepository(); + diff --git a/backend/src/routes.ts b/backend/src/routes.ts index e8db4e25d..67f402f7f 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -120,6 +120,30 @@ class Routes { res.json(times); } + public async $getBatchedOutspends(req: Request, res: Response) { + if (!Array.isArray(req.query.txId)) { + res.status(500).send('Not an array'); + return; + } + if (req.query.txId.length > 50) { + res.status(400).send('Too many txids requested'); + return; + } + const txIds: string[] = []; + for (const _txId in req.query.txId) { + if (typeof req.query.txId[_txId] === 'string') { + txIds.push(req.query.txId[_txId].toString()); + } + } + + try { + const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds); + res.json(batchedOutspends); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public getCpfpInfo(req: Request, res: Response) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { res.status(501).send(`Invalid transaction ID.`); @@ -729,7 +753,7 @@ class Routes { public async getStrippedBlockTransactions(req: Request, res: Response) { try { const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); - res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transactions); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 9a929a4f0..daa4fddc3 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -19,7 +19,8 @@ "EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__, "USER_AGENT": "__MEMPOOL_USER_AGENT__", "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__", - "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__ + "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__, + "BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__ }, "CORE_RPC": { "HOST": "__CORE_RPC_HOST__", diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 5792da008..5c4213a1c 100644 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -14,6 +14,7 @@ __MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000} __MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8} __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8} __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000} +__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__=${MEMPOOL_BLOCKS_SUMMARIES_INDEXING:=false} __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600} __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false} __MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]} @@ -101,6 +102,7 @@ sed -i "s/__MEMPOOL_BLOCK_WEIGHT_UNITS__/${__MEMPOOL_BLOCK_WEIGHT_UNITS__}/g" me sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json +sed -i "s/__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__/${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}/g" mempool-config.json sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json 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 05b9b340a..58b53aebf 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 @@ -26,6 +26,7 @@ .loader-wrapper { position: absolute; + background: #181b2d7f; left: 0; right: 0; top: 0; 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 a458ebd5f..e2774ac03 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 @@ -68,6 +68,21 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy { this.start(); } + destroy(): void { + if (this.scene) { + this.scene.destroy(); + this.start(); + } + } + + // initialize the scene without any entry transition + setup(transactions: TransactionStripped[]): void { + if (this.scene) { + this.scene.setup(transactions); + this.start(); + } + } + enter(transactions: TransactionStripped[], direction: string): void { if (this.scene) { this.scene.enter(transactions, direction); diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index fc5bfff8e..af64c0f20 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -29,10 +29,6 @@ export default class BlockScene { this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }); } - destroy(): void { - Object.values(this.txs).forEach(tx => tx.destroy()); - } - resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void { this.width = width; this.height = height; @@ -46,6 +42,36 @@ export default class BlockScene { } } + // Destroy the current layout and clean up graphics sprites without any exit animation + destroy(): void { + Object.values(this.txs).forEach(tx => tx.destroy()); + this.txs = {}; + this.layout = null; + } + + // set up the scene with an initial set of transactions, without any transition animation + setup(txs: TransactionStripped[]) { + // clean up any old transactions + Object.values(this.txs).forEach(tx => { + tx.destroy(); + delete this.txs[tx.txid]; + }); + this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); + txs.forEach(tx => { + const txView = new TxView(tx, this.vertexArray); + this.txs[tx.txid] = txView; + this.place(txView); + this.saveGridToScreenPosition(txView); + this.applyTxUpdate(txView, { + display: { + position: txView.screenPosition, + color: txView.getColor() + }, + duration: 0 + }); + }); + } + // Animate new block entering scene enter(txs: TransactionStripped[], direction) { this.replace(txs, direction); diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 39c4042fb..3eb8c5bb0 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co import { Location } from '@angular/common'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; -import { switchMap, tap, debounceTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators'; +import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators'; import { Transaction, Vout } from '../../interfaces/electrs.interface'; -import { Observable, of, Subscription } from 'rxjs'; +import { Observable, of, Subscription, asyncScheduler } from 'rxjs'; import { StateService } from '../../services/state.service'; import { SeoService } from 'src/app/services/seo.service'; import { WebsocketService } from 'src/app/services/websocket.service'; @@ -33,7 +33,6 @@ export class BlockComponent implements OnInit, OnDestroy { strippedTransactions: TransactionStripped[]; overviewTransitionDirection: string; isLoadingOverview = true; - isAwaitingOverview = true; error: any; blockSubsidy: number; fees: number; @@ -54,6 +53,9 @@ export class BlockComponent implements OnInit, OnDestroy { blocksSubscription: Subscription; networkChangedSubscription: Subscription; queryParamsSubscription: Subscription; + nextBlockSubscription: Subscription = undefined; + nextBlockSummarySubscription: Subscription = undefined; + nextBlockTxListSubscription: Subscription = undefined; @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @@ -124,6 +126,7 @@ export class BlockComponent implements OnInit, OnDestroy { return of(history.state.data.block); } else { this.isLoadingBlock = true; + this.isLoadingOverview = true; let blockInCache: BlockExtended; if (isBlockHeight) { @@ -152,6 +155,14 @@ export class BlockComponent implements OnInit, OnDestroy { } }), tap((block: BlockExtended) => { + // Preload previous block summary (execute the http query so the response will be cached) + this.unsubscribeNextBlockSubscriptions(); + setTimeout(() => { + this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe(); + this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe(); + this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe(); + }, 100); + this.block = block; this.blockHeight = block.height; const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left'; @@ -170,13 +181,9 @@ export class BlockComponent implements OnInit, OnDestroy { this.transactions = null; this.transactionsError = null; this.isLoadingOverview = true; - this.isAwaitingOverview = true; - this.overviewError = true; - if (this.blockGraph) { - this.blockGraph.exit(direction); - } + this.overviewError = null; }), - debounceTime(300), + throttleTime(300, asyncScheduler, { leading: true, trailing: true }), shareReplay(1) ); this.transactionSubscription = block$.pipe( @@ -194,11 +201,6 @@ export class BlockComponent implements OnInit, OnDestroy { } this.transactions = transactions; this.isLoadingTransactions = false; - - if (!this.isAwaitingOverview && this.blockGraph && this.strippedTransactions && this.overviewTransitionDirection) { - this.isLoadingOverview = false; - this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false); - } }, (error) => { this.error = error; @@ -226,18 +228,19 @@ export class BlockComponent implements OnInit, OnDestroy { ), ) .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => { - this.isAwaitingOverview = false; this.strippedTransactions = transactions; - this.overviewTransitionDirection = direction; - if (!this.isLoadingTransactions && this.blockGraph) { - this.isLoadingOverview = false; - this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false); + this.isLoadingOverview = false; + if (this.blockGraph) { + this.blockGraph.destroy(); + this.blockGraph.setup(this.strippedTransactions); } }, (error) => { this.error = error; this.isLoadingOverview = false; - this.isAwaitingOverview = false; + if (this.blockGraph) { + this.blockGraph.destroy(); + } }); this.networkChangedSubscription = this.stateService.networkChanged$ @@ -273,6 +276,19 @@ export class BlockComponent implements OnInit, OnDestroy { this.blocksSubscription.unsubscribe(); this.networkChangedSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe(); + this.unsubscribeNextBlockSubscriptions(); + } + + unsubscribeNextBlockSubscriptions() { + if (this.nextBlockSubscription !== undefined) { + this.nextBlockSubscription.unsubscribe(); + } + if (this.nextBlockSummarySubscription !== undefined) { + this.nextBlockSummarySubscription.unsubscribe(); + } + if (this.nextBlockTxListSubscription !== undefined) { + this.nextBlockTxListSubscription.unsubscribe(); + } } // TODO - Refactor this.fees/this.reward for liquid because it is not diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html index 1a3d56b7b..e0ca6b98a 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -27,9 +27,6 @@
- diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 0278bad4d..b294927ba 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -47,6 +47,7 @@ export class HashrateChartComponent implements OnInit { formatNumber = formatNumber; timespan = ''; chartInstance: any = undefined; + maResolution: number = 30; constructor( @Inject(LOCALE_ID) public locale: string, @@ -122,9 +123,24 @@ export class HashrateChartComponent implements OnInit { ++diffIndex; } + this.maResolution = 30; + if (["3m", "6m"].includes(this.timespan)) { + this.maResolution = 7; + } + const hashrateMa = []; + for (let i = this.maResolution - 1; i < data.hashrates.length; ++i) { + let avg = 0; + for (let y = this.maResolution - 1; y >= 0; --y) { + avg += data.hashrates[i - y].avgHashrate; + } + avg /= this.maResolution; + hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]); + } + this.prepareChartOptions({ hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]), difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]), + hashrateMa: hashrateMa, }); this.isLoading = false; }), @@ -160,6 +176,14 @@ export class HashrateChartComponent implements OnInit { title: title, animation: false, color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E99' }, + { offset: 0.25, color: '#FB8C0099' }, + { offset: 0.5, color: '#FFB30099' }, + { offset: 0.75, color: '#FDD83599' }, + { offset: 1, color: '#7CB34299' } + ]), + '#D81B60', new graphic.LinearGradient(0, 0, 0, 0.65, [ { offset: 0, color: '#F4511E' }, { offset: 0.25, color: '#FB8C00' }, @@ -167,10 +191,9 @@ export class HashrateChartComponent implements OnInit { { offset: 0.75, color: '#FDD835' }, { offset: 1, color: '#7CB342' } ]), - '#D81B60', ], grid: { - top: 20, + top: this.widget ? 20 : 40, bottom: this.widget ? 30 : 70, right: this.right, left: this.left, @@ -192,6 +215,7 @@ export class HashrateChartComponent implements OnInit { formatter: (ticks) => { let hashrateString = ''; let difficultyString = ''; + let hashrateStringMA = ''; let hashratePowerOfTen: any = selectPowerOfTen(1); for (const tick of ticks) { @@ -201,7 +225,7 @@ export class HashrateChartComponent implements OnInit { hashratePowerOfTen = selectPowerOfTen(tick.data[1]); hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider); } - hashrateString = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s`; + hashrateString = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s
`; } else if (tick.seriesIndex === 1) { // Difficulty let difficultyPowerOfTen = hashratePowerOfTen; let difficulty = tick.data[1]; @@ -209,7 +233,14 @@ export class HashrateChartComponent implements OnInit { difficultyPowerOfTen = selectPowerOfTen(tick.data[1]); difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider); } - difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}`; + difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}
`; + } else if (tick.seriesIndex === 2) { // Hashrate MA + let hashrate = tick.data[1]; + if (this.isMobile()) { + hashratePowerOfTen = selectPowerOfTen(tick.data[1]); + hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider); + } + hashrateStringMA = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s`; } } @@ -217,8 +248,9 @@ export class HashrateChartComponent implements OnInit { return ` ${date}
- ${hashrateString}
${difficultyString} + ${hashrateString} + ${hashrateStringMA} `; } }, @@ -243,15 +275,23 @@ export class HashrateChartComponent implements OnInit { }, }, { - name: $localize`:@@25148835d92465353fc5fe8897c27d5369978e5a:Difficulty`, + name: $localize`::Difficulty`, + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: $localize`Hashrate` + ` (MA${this.maResolution})`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', }, icon: 'roundRect', itemStyle: { - color: '#D81B60', - } + color: '#FFB300', + }, }, ], }, @@ -270,8 +310,12 @@ export class HashrateChartComponent implements OnInit { } }, splitLine: { - show: false, - } + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, }, { min: (value) => { @@ -288,12 +332,8 @@ export class HashrateChartComponent implements OnInit { } }, splitLine: { - lineStyle: { - type: 'dotted', - color: '#ffffff66', - opacity: 0.25, - } - }, + show: false, + } } ], series: data.hashrates.length === 0 ? [] : [ @@ -305,7 +345,7 @@ export class HashrateChartComponent implements OnInit { data: data.hashrates, type: 'line', lineStyle: { - width: 2, + width: 1, }, }, { @@ -319,6 +359,18 @@ export class HashrateChartComponent implements OnInit { lineStyle: { width: 3, } + }, + { + zlevel: 2, + name: $localize`Hashrate` + ` (MA${this.maResolution})`, + showSymbol: false, + symbol: 'none', + data: data.hashrateMa, + type: 'line', + smooth: true, + lineStyle: { + width: 3, + } } ], dataZoom: this.widget ? null : [{ diff --git a/frontend/src/app/components/language-selector/language-selector.component.html b/frontend/src/app/components/language-selector/language-selector.component.html index 81fcc8985..dee1e3acb 100644 --- a/frontend/src/app/components/language-selector/language-selector.component.html +++ b/frontend/src/app/components/language-selector/language-selector.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index ba8ba60ba..d5ec36151 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -5,8 +5,9 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from 'src/environments/environment'; import { AssetsService } from 'src/app/services/assets.service'; -import { map, switchMap } from 'rxjs/operators'; +import { map, tap, switchMap } from 'rxjs/operators'; import { BlockExtended } from 'src/app/interfaces/node-api.interface'; +import { ApiService } from 'src/app/services/api.service'; @Component({ selector: 'app-transactions-list', @@ -30,7 +31,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { latestBlock$: Observable; outspendsSubscription: Subscription; - refreshOutspends$: ReplaySubject<{ [str: string]: Observable}> = new ReplaySubject(); + refreshOutspends$: ReplaySubject = new ReplaySubject(); showDetails$ = new BehaviorSubject(false); outspends: Outspend[][] = []; assetsMinimal: any; @@ -38,6 +39,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { constructor( public stateService: StateService, private electrsApiService: ElectrsApiService, + private apiService: ApiService, private assetsService: AssetsService, private ref: ChangeDetectorRef, ) { } @@ -55,20 +57,14 @@ export class TransactionsListComponent implements OnInit, OnChanges { this.outspendsSubscription = merge( this.refreshOutspends$ .pipe( - switchMap((observableObject) => forkJoin(observableObject)), - map((outspends: any) => { - const newOutspends: Outspend[] = []; - for (const i in outspends) { - if (outspends.hasOwnProperty(i)) { - newOutspends.push(outspends[i]); - } - } - this.outspends = this.outspends.concat(newOutspends); + switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)), + tap((outspends: Outspend[][]) => { + this.outspends = this.outspends.concat(outspends); }), ), this.stateService.utxoSpent$ .pipe( - map((utxoSpent) => { + tap((utxoSpent) => { for (const i in utxoSpent) { this.outspends[0][i] = { spent: true, @@ -96,7 +92,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { } }, 10); } - const observableObject = {}; + this.transactions.forEach((tx, i) => { tx['@voutLimit'] = true; tx['@vinLimit'] = true; @@ -117,10 +113,9 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx['addressValue'] = addressIn - addressOut; } - - observableObject[i] = this.electrsApiService.getOutspends$(tx.txid); }); - this.refreshOutspends$.next(observableObject); + + this.refreshOutspends$.next(this.transactions.map((tx) => tx.txid)); } onScroll() { diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 4eb95c1ca..c66750f70 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -1,8 +1,8 @@
- -
+ +
Transaction Fees
@@ -10,172 +10,135 @@
-
+
-
-
-
+ +
+
+
+
-
-
-
-
-
-
- -
-
-
- - - -
-
Transaction Fees
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
-
- - -
- -
-
-
- - - -
-
-
-
-
-
-
-
- - - - - - - - -
- - - - - {{ group.name }} -
-
- -
- -
-
+ + +
+ +
+
+
+ + +
- -
-
-
- -
Latest blocks
-   - -
- - - - - - - - + +
+
+
+ +
+
+
HeightMinedPoolTXsSize
- - - - + - - +
{{ block.height }} - - - {{ block.extras.pool.name }} +
+ + {{ block.tx_count | number }} -
-
 
-
-
+
+ {{ group.name }}
+ +
+ +
+
-
-
-
-
Latest transactions
- - - - - - - - - - - - - - - -
TXIDAmountUSDFee
{{ transaction.txid | shortenString : 10 }}Confidential{{ transaction.fee / transaction.vsize | feeRounding }} sat/vB
-
-
-
-
-
-
- - +
+
+
+ +
Latest blocks
+   + +
+ + + + + + + + + + + + + + + + + +
HeightMinedPoolTXsSize
{{ block.height }} + + + {{ block.extras.pool.name }} + + {{ block.tx_count | number }} +
+
 
+
+
+
+
+
+
+
+
+
+
Latest transactions
+ + + + + + + + + + + + + + + +
TXIDAmountUSDFee
{{ transaction.txid | shortenString : 10 }}Confidential{{ transaction.fee / transaction.vsize | feeRounding }} sat/vB
+
 
+
+
+
+
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 910d6a81a..68c4ff35a 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -33,7 +33,6 @@ interface MempoolStatsData { changeDetection: ChangeDetectionStrategy.OnPush }) export class DashboardComponent implements OnInit { - collapseLevel: string; featuredAssets$: Observable; network$: Observable; mempoolBlocksData$: Observable; @@ -63,7 +62,6 @@ export class DashboardComponent implements OnInit { this.seoService.resetTitle(); this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']); this.network$ = merge(of(''), this.stateService.networkChanged$); - this.collapseLevel = this.storageService.getValue('dashboard-collapsed') || 'one'; this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$ .pipe( map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100) @@ -230,15 +228,4 @@ export class DashboardComponent implements OnInit { trackByBlock(index: number, block: BlockExtended) { return block.height; } - - toggleCollapsed() { - if (this.collapseLevel === 'one') { - this.collapseLevel = 'two'; - } else if (this.collapseLevel === 'two') { - this.collapseLevel = 'three'; - } else { - this.collapseLevel = 'one'; - } - this.storageService.setValue('dashboard-collapsed', this.collapseLevel); - } } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 8202fbb49..bd32685fd 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, - PoolsStats, PoolStat, BlockExtended, TransactionStripped, RewardStats } from '../interfaces/node-api.interface'; + PoolStat, BlockExtended, TransactionStripped, RewardStats } from '../interfaces/node-api.interface'; import { Observable } from 'rxjs'; import { StateService } from './state.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; +import { Outspend } from '../interfaces/electrs.interface'; @Injectable({ providedIn: 'root' @@ -74,6 +75,14 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/transaction-times', { params }); } + getOutspendsBatched$(txIds: string[]): Observable { + let params = new HttpParams(); + txIds.forEach((txId: string) => { + params = params.append('txId[]', txId); + }); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params }); + } + requestDonation$(amount: number, orderId: string): Observable { const params = { amount: amount, diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 1cfeab20d..4575afdbe 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -9,6 +9,7 @@ "CLEAR_PROTECTION_MINUTES": 5, "POLL_RATE_MS": 1000, "INDEXING_BLOCKS_AMOUNT": -1, + "BLOCKS_SUMMARIES_INDEXING": true, "USE_SECOND_NODE_FOR_MINFEE": true }, "SYSLOG" : { diff --git a/production/nginx/location-api.conf b/production/nginx/location-api.conf index 253033206..9c88afc6e 100644 --- a/production/nginx/location-api.conf +++ b/production/nginx/location-api.conf @@ -4,14 +4,42 @@ location /api/v1/statistics { location /api/v1/mining { try_files /dev/null @mempool-api-v1-warmcache; } +location /api/v1/block { + try_files /dev/null @mempool-api-v1-forevercache; +} location /api/v1 { try_files /dev/null @mempool-api-v1-coldcache; } +location /api/block { + rewrite ^/api/(.*) /$1 break; + try_files /dev/null @electrs-api-forevercache; +} location /api/ { rewrite ^/api/(.*) /$1 break; try_files /dev/null @electrs-api-nocache; } +location @mempool-api-v1-forevercache { + proxy_pass $mempoolBackend; + proxy_http_version 1.1; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache_bypass $http_upgrade; + proxy_cache_background_update on; + proxy_cache_use_stale updating; + proxy_cache api; + proxy_cache_valid 200 30d; + proxy_redirect off; + + expires 30d; +} + location @mempool-api-v1-warmcache { proxy_pass $mempoolBackend; proxy_http_version 1.1; @@ -46,6 +74,7 @@ location @mempool-api-v1-coldcache { proxy_cache api; proxy_cache_valid 200 10s; proxy_redirect off; + expires 10s; } @@ -81,4 +110,27 @@ location @electrs-api-nocache { proxy_cache_bypass $http_upgrade; proxy_redirect off; proxy_buffering off; + + expires -1; +} + +location @electrs-api-forevercache { + proxy_pass $electrsBackend; + proxy_http_version 1.1; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache_bypass $http_upgrade; + proxy_cache_background_update on; + proxy_cache_use_stale updating; + proxy_cache api; + proxy_cache_valid 200 30d; + proxy_redirect off; + + expires 30d; }