Merge branch 'master' into simon/block-tip-hash-api
This commit is contained in:
commit
411e9c2e89
@ -13,6 +13,7 @@
|
|||||||
"INITIAL_BLOCKS_AMOUNT": 8,
|
"INITIAL_BLOCKS_AMOUNT": 8,
|
||||||
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
"INDEXING_BLOCKS_AMOUNT": 11000,
|
"INDEXING_BLOCKS_AMOUNT": 11000,
|
||||||
|
"BLOCKS_SUMMARIES_INDEXING": false,
|
||||||
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
||||||
"USE_SECOND_NODE_FOR_MINFEE": false,
|
"USE_SECOND_NODE_FOR_MINFEE": false,
|
||||||
"EXTERNAL_ASSETS": [],
|
"EXTERNAL_ASSETS": [],
|
||||||
|
@ -14,6 +14,7 @@ export interface AbstractBitcoinApi {
|
|||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||||
|
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||||
}
|
}
|
||||||
export interface BitcoinRpcCredentials {
|
export interface BitcoinRpcCredentials {
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -148,6 +148,15 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return outSpends;
|
return outSpends;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||||
|
const outspends: IEsploraApi.Outspend[][] = [];
|
||||||
|
for (const tx of txId) {
|
||||||
|
const outspend = await this.$getOutspends(tx);
|
||||||
|
outspends.push(outspend);
|
||||||
|
}
|
||||||
|
return outspends;
|
||||||
|
}
|
||||||
|
|
||||||
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
||||||
// 120 is the default block span in Core
|
// 120 is the default block span in Core
|
||||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
||||||
|
@ -66,8 +66,18 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$getOutspends(): Promise<IEsploraApi.Outspend[]> {
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
||||||
throw new Error('Method not implemented.');
|
return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||||
|
const outspends: IEsploraApi.Outspend[][] = [];
|
||||||
|
for (const tx of txId) {
|
||||||
|
const outspend = await this.$getOutspends(tx);
|
||||||
|
outspends.push(outspend);
|
||||||
|
}
|
||||||
|
return outspends;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import indexer from '../indexer';
|
|||||||
import fiatConversion from './fiat-conversion';
|
import fiatConversion from './fiat-conversion';
|
||||||
import RatesRepository from '../repositories/RatesRepository';
|
import RatesRepository from '../repositories/RatesRepository';
|
||||||
import poolsParser from './pools-parser';
|
import poolsParser from './pools-parser';
|
||||||
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
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
|
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||||
*/
|
*/
|
||||||
public async $generateBlockDatabase() {
|
public async $generateBlockDatabase(): Promise<boolean> {
|
||||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
|
if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -292,7 +348,7 @@ class Blocks {
|
|||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
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 progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||||
const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds);
|
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`);
|
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) {
|
} catch (e) {
|
||||||
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
loadingIndicators.setProgress('block-indexing', 100);
|
loadingIndicators.setProgress('block-indexing', 100);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chainValid = await BlocksRepository.$validateChain();
|
const chainValid = await BlocksRepository.$validateChain();
|
||||||
if (!chainValid) {
|
if (!chainValid) {
|
||||||
indexer.reindex();
|
indexer.reindex();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateBlocks() {
|
public async $updateBlocks() {
|
||||||
@ -387,11 +446,19 @@ class Blocks {
|
|||||||
// We assume there won't be a reorg with more than 10 block depth
|
// We assume there won't be a reorg with more than 10 block depth
|
||||||
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
await HashratesRepository.$deleteLastEntries();
|
await HashratesRepository.$deleteLastEntries();
|
||||||
|
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
for (let i = 10; i >= 0; --i) {
|
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);
|
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) {
|
if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) {
|
||||||
@ -477,14 +544,34 @@ class Blocks {
|
|||||||
return blockExtended;
|
return blockExtended;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getStrippedBlockTransactions(hash: string): Promise<TransactionStripped[]> {
|
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache: boolean = false,
|
||||||
// Check the memory cache
|
skipDBLookup: boolean = false): Promise<TransactionStripped[]>
|
||||||
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
|
{
|
||||||
if (cachedSummary) {
|
if (skipMemoryCache === false) {
|
||||||
return cachedSummary.transactions;
|
// 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 block = await bitcoinClient.getBlock(hash, 2);
|
||||||
const summary = this.summarizeBlock(block);
|
const summary = this.summarizeBlock(block);
|
||||||
|
|
||||||
|
// Index the response if needed
|
||||||
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
|
await BlocksSummariesRepository.$saveSummary(block.height, summary);
|
||||||
|
}
|
||||||
|
|
||||||
return summary.transactions;
|
return summary.transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,4 +177,11 @@ export class Common {
|
|||||||
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
|
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static blocksSummariesIndexingEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
Common.indexingEnabled() &&
|
||||||
|
config.MEMPOOL.BLOCKS_SUMMARIES_INDEXING === true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 19;
|
private static currentVersion = 20;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -217,6 +217,10 @@ class DatabaseMigration {
|
|||||||
if (databaseSchemaVersion < 19) {
|
if (databaseSchemaVersion < 19) {
|
||||||
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
|
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) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -512,6 +516,16 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) 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[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates'];
|
const allowedTables = ['blocks', 'hashrates'];
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ interface IConfig {
|
|||||||
INITIAL_BLOCKS_AMOUNT: number;
|
INITIAL_BLOCKS_AMOUNT: number;
|
||||||
MEMPOOL_BLOCKS_AMOUNT: number;
|
MEMPOOL_BLOCKS_AMOUNT: number;
|
||||||
INDEXING_BLOCKS_AMOUNT: number;
|
INDEXING_BLOCKS_AMOUNT: number;
|
||||||
|
BLOCKS_SUMMARIES_INDEXING: boolean;
|
||||||
PRICE_FEED_UPDATE_INTERVAL: number;
|
PRICE_FEED_UPDATE_INTERVAL: number;
|
||||||
USE_SECOND_NODE_FOR_MINFEE: boolean;
|
USE_SECOND_NODE_FOR_MINFEE: boolean;
|
||||||
EXTERNAL_ASSETS: string[];
|
EXTERNAL_ASSETS: string[];
|
||||||
@ -104,6 +105,7 @@ const defaults: IConfig = {
|
|||||||
'INITIAL_BLOCKS_AMOUNT': 8,
|
'INITIAL_BLOCKS_AMOUNT': 8,
|
||||||
'MEMPOOL_BLOCKS_AMOUNT': 8,
|
'MEMPOOL_BLOCKS_AMOUNT': 8,
|
||||||
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
|
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
|
||||||
|
'BLOCKS_SUMMARIES_INDEXING': false,
|
||||||
'PRICE_FEED_UPDATE_INTERVAL': 600,
|
'PRICE_FEED_UPDATE_INTERVAL': 600,
|
||||||
'USE_SECOND_NODE_FOR_MINFEE': false,
|
'USE_SECOND_NODE_FOR_MINFEE': false,
|
||||||
'EXTERNAL_ASSETS': [],
|
'EXTERNAL_ASSETS': [],
|
||||||
|
@ -195,6 +195,7 @@ class Server {
|
|||||||
setUpHttpApiRoutes() {
|
setUpHttpApiRoutes() {
|
||||||
this.app
|
this.app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes)
|
.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 + 'cpfp/:txId', routes.getCpfpInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
|
||||||
|
@ -29,10 +29,17 @@ class Indexer {
|
|||||||
this.indexerRunning = true;
|
this.indexerRunning = true;
|
||||||
|
|
||||||
try {
|
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 this.$resetHashratesIndexingState();
|
||||||
await mining.$generateNetworkHashrateHistory();
|
await mining.$generateNetworkHashrateHistory();
|
||||||
await mining.$generatePoolHashrateHistory();
|
await mining.$generatePoolHashrateHistory();
|
||||||
|
await blocks.$generateBlocksSummariesDatabase();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.reindex();
|
this.reindex();
|
||||||
logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
@ -6,6 +6,7 @@ import { prepareBlock } from '../utils/blocks-utils';
|
|||||||
import PoolsRepository from './PoolsRepository';
|
import PoolsRepository from './PoolsRepository';
|
||||||
import HashratesRepository from './HashratesRepository';
|
import HashratesRepository from './HashratesRepository';
|
||||||
import { escape } from 'mysql2';
|
import { escape } from 'mysql2';
|
||||||
|
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
||||||
|
|
||||||
class BlocksRepository {
|
class BlocksRepository {
|
||||||
/**
|
/**
|
||||||
@ -495,6 +496,7 @@ class BlocksRepository {
|
|||||||
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
|
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`);
|
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 this.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||||
|
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||||
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
|
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -652,6 +654,19 @@ class BlocksRepository {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of blocks that have been indexed
|
||||||
|
*/
|
||||||
|
public async $getIndexedBlocks(): Promise<any[]> {
|
||||||
|
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();
|
export default new BlocksRepository();
|
||||||
|
59
backend/src/repositories/BlocksSummariesRepository.ts
Normal file
59
backend/src/repositories/BlocksSummariesRepository.ts
Normal file
@ -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<BlockSummary | undefined> {
|
||||||
|
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<string[]> {
|
||||||
|
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();
|
||||||
|
|
@ -120,6 +120,30 @@ class Routes {
|
|||||||
res.json(times);
|
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) {
|
public getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
res.status(501).send(`Invalid transaction ID.`);
|
||||||
@ -729,7 +753,7 @@ class Routes {
|
|||||||
public async getStrippedBlockTransactions(req: Request, res: Response) {
|
public async getStrippedBlockTransactions(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
|
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);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
@ -19,7 +19,8 @@
|
|||||||
"EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__,
|
"EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__,
|
||||||
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
|
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
"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": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
|
@ -14,6 +14,7 @@ __MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
|
|||||||
__MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
|
__MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
|
||||||
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
|
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||||
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
|
__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_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
|
||||||
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
|
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
|
||||||
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]}
|
__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_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_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_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_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_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
|
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
.loader-wrapper {
|
.loader-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
background: #181b2d7f;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -68,6 +68,21 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.start();
|
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 {
|
enter(transactions: TransactionStripped[], direction: string): void {
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.enter(transactions, direction);
|
this.scene.enter(transactions, direction);
|
||||||
|
@ -29,10 +29,6 @@ export default class BlockScene {
|
|||||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
|
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 {
|
resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
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
|
// Animate new block entering scene
|
||||||
enter(txs: TransactionStripped[], direction) {
|
enter(txs: TransactionStripped[], direction) {
|
||||||
this.replace(txs, direction);
|
this.replace(txs, direction);
|
||||||
|
@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
|
|||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
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 { 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 { StateService } from '../../services/state.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||||
@ -33,7 +33,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
strippedTransactions: TransactionStripped[];
|
strippedTransactions: TransactionStripped[];
|
||||||
overviewTransitionDirection: string;
|
overviewTransitionDirection: string;
|
||||||
isLoadingOverview = true;
|
isLoadingOverview = true;
|
||||||
isAwaitingOverview = true;
|
|
||||||
error: any;
|
error: any;
|
||||||
blockSubsidy: number;
|
blockSubsidy: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
@ -54,6 +53,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
networkChangedSubscription: Subscription;
|
networkChangedSubscription: Subscription;
|
||||||
queryParamsSubscription: Subscription;
|
queryParamsSubscription: Subscription;
|
||||||
|
nextBlockSubscription: Subscription = undefined;
|
||||||
|
nextBlockSummarySubscription: Subscription = undefined;
|
||||||
|
nextBlockTxListSubscription: Subscription = undefined;
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||||
|
|
||||||
@ -124,6 +126,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
return of(history.state.data.block);
|
return of(history.state.data.block);
|
||||||
} else {
|
} else {
|
||||||
this.isLoadingBlock = true;
|
this.isLoadingBlock = true;
|
||||||
|
this.isLoadingOverview = true;
|
||||||
|
|
||||||
let blockInCache: BlockExtended;
|
let blockInCache: BlockExtended;
|
||||||
if (isBlockHeight) {
|
if (isBlockHeight) {
|
||||||
@ -152,6 +155,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
tap((block: BlockExtended) => {
|
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.block = block;
|
||||||
this.blockHeight = block.height;
|
this.blockHeight = block.height;
|
||||||
const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left';
|
const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left';
|
||||||
@ -170,13 +181,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.transactions = null;
|
this.transactions = null;
|
||||||
this.transactionsError = null;
|
this.transactionsError = null;
|
||||||
this.isLoadingOverview = true;
|
this.isLoadingOverview = true;
|
||||||
this.isAwaitingOverview = true;
|
this.overviewError = null;
|
||||||
this.overviewError = true;
|
|
||||||
if (this.blockGraph) {
|
|
||||||
this.blockGraph.exit(direction);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
debounceTime(300),
|
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
|
||||||
shareReplay(1)
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
this.transactionSubscription = block$.pipe(
|
this.transactionSubscription = block$.pipe(
|
||||||
@ -194,11 +201,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.transactions = transactions;
|
this.transactions = transactions;
|
||||||
this.isLoadingTransactions = false;
|
this.isLoadingTransactions = false;
|
||||||
|
|
||||||
if (!this.isAwaitingOverview && this.blockGraph && this.strippedTransactions && this.overviewTransitionDirection) {
|
|
||||||
this.isLoadingOverview = false;
|
|
||||||
this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
@ -226,18 +228,19 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
||||||
this.isAwaitingOverview = false;
|
|
||||||
this.strippedTransactions = transactions;
|
this.strippedTransactions = transactions;
|
||||||
this.overviewTransitionDirection = direction;
|
this.isLoadingOverview = false;
|
||||||
if (!this.isLoadingTransactions && this.blockGraph) {
|
if (this.blockGraph) {
|
||||||
this.isLoadingOverview = false;
|
this.blockGraph.destroy();
|
||||||
this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false);
|
this.blockGraph.setup(this.strippedTransactions);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
this.isAwaitingOverview = false;
|
if (this.blockGraph) {
|
||||||
|
this.blockGraph.destroy();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||||
@ -273,6 +276,19 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription.unsubscribe();
|
||||||
this.networkChangedSubscription.unsubscribe();
|
this.networkChangedSubscription.unsubscribe();
|
||||||
this.queryParamsSubscription.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
|
// TODO - Refactor this.fees/this.reward for liquid because it is not
|
||||||
|
@ -27,9 +27,6 @@
|
|||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> 1M
|
|
||||||
</label>
|
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> 3M
|
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> 3M
|
||||||
</label>
|
</label>
|
||||||
|
@ -47,6 +47,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
formatNumber = formatNumber;
|
formatNumber = formatNumber;
|
||||||
timespan = '';
|
timespan = '';
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
maResolution: number = 30;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
@ -122,9 +123,24 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
++diffIndex;
|
++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({
|
this.prepareChartOptions({
|
||||||
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
|
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
|
||||||
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
|
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
|
||||||
|
hashrateMa: hashrateMa,
|
||||||
});
|
});
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}),
|
}),
|
||||||
@ -160,6 +176,14 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
title: title,
|
title: title,
|
||||||
animation: false,
|
animation: false,
|
||||||
color: [
|
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, [
|
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||||
{ offset: 0, color: '#F4511E' },
|
{ offset: 0, color: '#F4511E' },
|
||||||
{ offset: 0.25, color: '#FB8C00' },
|
{ offset: 0.25, color: '#FB8C00' },
|
||||||
@ -167,10 +191,9 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
{ offset: 0.75, color: '#FDD835' },
|
{ offset: 0.75, color: '#FDD835' },
|
||||||
{ offset: 1, color: '#7CB342' }
|
{ offset: 1, color: '#7CB342' }
|
||||||
]),
|
]),
|
||||||
'#D81B60',
|
|
||||||
],
|
],
|
||||||
grid: {
|
grid: {
|
||||||
top: 20,
|
top: this.widget ? 20 : 40,
|
||||||
bottom: this.widget ? 30 : 70,
|
bottom: this.widget ? 30 : 70,
|
||||||
right: this.right,
|
right: this.right,
|
||||||
left: this.left,
|
left: this.left,
|
||||||
@ -192,6 +215,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
formatter: (ticks) => {
|
formatter: (ticks) => {
|
||||||
let hashrateString = '';
|
let hashrateString = '';
|
||||||
let difficultyString = '';
|
let difficultyString = '';
|
||||||
|
let hashrateStringMA = '';
|
||||||
let hashratePowerOfTen: any = selectPowerOfTen(1);
|
let hashratePowerOfTen: any = selectPowerOfTen(1);
|
||||||
|
|
||||||
for (const tick of ticks) {
|
for (const tick of ticks) {
|
||||||
@ -201,7 +225,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
|
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||||
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
|
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<br>`;
|
||||||
} else if (tick.seriesIndex === 1) { // Difficulty
|
} else if (tick.seriesIndex === 1) { // Difficulty
|
||||||
let difficultyPowerOfTen = hashratePowerOfTen;
|
let difficultyPowerOfTen = hashratePowerOfTen;
|
||||||
let difficulty = tick.data[1];
|
let difficulty = tick.data[1];
|
||||||
@ -209,7 +233,14 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
|
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||||
difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider);
|
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}<br>`;
|
||||||
|
} 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 `
|
return `
|
||||||
<b style="color: white; margin-left: 2px">${date}</b><br>
|
<b style="color: white; margin-left: 2px">${date}</b><br>
|
||||||
<span>${hashrateString}</span><br>
|
|
||||||
<span>${difficultyString}</span>
|
<span>${difficultyString}</span>
|
||||||
|
<span>${hashrateString}</span>
|
||||||
|
<span>${hashrateStringMA}</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -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)',
|
inactiveColor: 'rgb(110, 112, 121)',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
},
|
},
|
||||||
icon: 'roundRect',
|
icon: 'roundRect',
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#D81B60',
|
color: '#FFB300',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -270,8 +310,12 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: false,
|
lineStyle: {
|
||||||
}
|
type: 'dotted',
|
||||||
|
color: '#ffffff66',
|
||||||
|
opacity: 0.25,
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
min: (value) => {
|
min: (value) => {
|
||||||
@ -288,12 +332,8 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
show: false,
|
||||||
type: 'dotted',
|
}
|
||||||
color: '#ffffff66',
|
|
||||||
opacity: 0.25,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
series: data.hashrates.length === 0 ? [] : [
|
series: data.hashrates.length === 0 ? [] : [
|
||||||
@ -305,7 +345,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
data: data.hashrates,
|
data: data.hashrates,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 2,
|
width: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -319,6 +359,18 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 3,
|
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 : [{
|
dataZoom: this.widget ? null : [{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div [formGroup]="languageForm" class="text-small text-center mt-4">
|
<div [formGroup]="languageForm" class="text-small text-center">
|
||||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 130px;" (change)="changeLanguage()">
|
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 130px;" (change)="changeLanguage()">
|
||||||
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
|
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -5,8 +5,9 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter
|
|||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { AssetsService } from 'src/app/services/assets.service';
|
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 { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||||
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transactions-list',
|
selector: 'app-transactions-list',
|
||||||
@ -30,7 +31,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
latestBlock$: Observable<BlockExtended>;
|
latestBlock$: Observable<BlockExtended>;
|
||||||
outspendsSubscription: Subscription;
|
outspendsSubscription: Subscription;
|
||||||
refreshOutspends$: ReplaySubject<{ [str: string]: Observable<Outspend[]>}> = new ReplaySubject();
|
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
||||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||||
outspends: Outspend[][] = [];
|
outspends: Outspend[][] = [];
|
||||||
assetsMinimal: any;
|
assetsMinimal: any;
|
||||||
@ -38,6 +39,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
|
private apiService: ApiService,
|
||||||
private assetsService: AssetsService,
|
private assetsService: AssetsService,
|
||||||
private ref: ChangeDetectorRef,
|
private ref: ChangeDetectorRef,
|
||||||
) { }
|
) { }
|
||||||
@ -55,20 +57,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
this.outspendsSubscription = merge(
|
this.outspendsSubscription = merge(
|
||||||
this.refreshOutspends$
|
this.refreshOutspends$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((observableObject) => forkJoin(observableObject)),
|
switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)),
|
||||||
map((outspends: any) => {
|
tap((outspends: Outspend[][]) => {
|
||||||
const newOutspends: Outspend[] = [];
|
this.outspends = this.outspends.concat(outspends);
|
||||||
for (const i in outspends) {
|
|
||||||
if (outspends.hasOwnProperty(i)) {
|
|
||||||
newOutspends.push(outspends[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.outspends = this.outspends.concat(newOutspends);
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
this.stateService.utxoSpent$
|
this.stateService.utxoSpent$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((utxoSpent) => {
|
tap((utxoSpent) => {
|
||||||
for (const i in utxoSpent) {
|
for (const i in utxoSpent) {
|
||||||
this.outspends[0][i] = {
|
this.outspends[0][i] = {
|
||||||
spent: true,
|
spent: true,
|
||||||
@ -96,7 +92,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
const observableObject = {};
|
|
||||||
this.transactions.forEach((tx, i) => {
|
this.transactions.forEach((tx, i) => {
|
||||||
tx['@voutLimit'] = true;
|
tx['@voutLimit'] = true;
|
||||||
tx['@vinLimit'] = true;
|
tx['@vinLimit'] = true;
|
||||||
@ -117,10 +113,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
tx['addressValue'] = addressIn - addressOut;
|
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() {
|
onScroll() {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
<div class="container-xl dashboard-container">
|
<div class="container-xl dashboard-container">
|
||||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
||||||
<ng-template [ngIf]="collapseLevel === 'three'" [ngIfElse]="expanded">
|
<ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
||||||
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
<div class="col card-wrapper">
|
||||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body less-padding">
|
<div class="card-body less-padding">
|
||||||
@ -10,172 +10,135 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
<div class="col">
|
||||||
<app-difficulty></app-difficulty>
|
<app-difficulty></app-difficulty>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
</ng-container>
|
||||||
<div class="card">
|
<div class="col">
|
||||||
<div class="card-body">
|
<div class="card graph-card">
|
||||||
|
<div class="card-body pl-0">
|
||||||
|
<div style="padding-left: 1.25rem;">
|
||||||
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #expanded>
|
|
||||||
<ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
|
||||||
<div class="col card-wrapper">
|
|
||||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body less-padding">
|
|
||||||
<app-fees-box class="d-block"></app-fees-box>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<app-difficulty></app-difficulty>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<div class="col">
|
|
||||||
<div class="card graph-card">
|
|
||||||
<div class="card-body pl-0">
|
|
||||||
<div style="padding-left: 1.25rem;">
|
|
||||||
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs">
|
|
||||||
<ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
|
||||||
<div class="mempool-graph">
|
|
||||||
<app-mempool-graph
|
|
||||||
[template]="'widget'"
|
|
||||||
[limitFee]="150"
|
|
||||||
[limitFilterFee]="1"
|
|
||||||
[data]="mempoolStats.value?.mempool"
|
|
||||||
[windowPreferenceOverride]="'2h'"
|
|
||||||
></app-mempool-graph>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #liquidPegs>
|
|
||||||
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="card graph-card">
|
|
||||||
<div class="card-body">
|
|
||||||
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
|
||||||
<hr>
|
<hr>
|
||||||
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph">
|
|
||||||
<table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable">
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let group of featuredAssets">
|
|
||||||
<td class="asset-icon">
|
|
||||||
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
|
|
||||||
<img class="assetIcon" [src]="'/api/v1/asset/' + group.asset + '/icon'">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="asset-title">
|
|
||||||
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
|
|
||||||
</td>
|
|
||||||
<td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<ng-template #mempoolGraph>
|
|
||||||
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
|
||||||
<app-incoming-transactions-graph
|
|
||||||
[left]="50"
|
|
||||||
[data]="mempoolStats.value?.weightPerSecond"
|
|
||||||
[windowPreferenceOverride]="'2h'"
|
|
||||||
></app-incoming-transactions-graph>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs">
|
||||||
|
<ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
||||||
|
<div class="mempool-graph">
|
||||||
|
<app-mempool-graph
|
||||||
|
[template]="'widget'"
|
||||||
|
[limitFee]="150"
|
||||||
|
[limitFilterFee]="1"
|
||||||
|
[data]="mempoolStats.value?.mempool"
|
||||||
|
[windowPreferenceOverride]="'2h'"
|
||||||
|
></app-mempool-graph>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #liquidPegs>
|
||||||
|
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-template [ngIf]="collapseLevel === 'one'">
|
</div>
|
||||||
<div class="col" style="max-height: 410px">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card graph-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
<hr>
|
||||||
<span> </span>
|
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph">
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
<table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable">
|
||||||
</a>
|
|
||||||
<table class="table lastest-blocks-table">
|
|
||||||
<thead>
|
|
||||||
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
|
||||||
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
|
||||||
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
|
|
||||||
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
|
||||||
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
<tr *ngFor="let group of featuredAssets">
|
||||||
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
<td class="asset-icon">
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
|
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
|
<img class="assetIcon" [src]="'/api/v1/asset/' + group.asset + '/icon'">
|
||||||
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
|
||||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
|
||||||
onError="this.src = './resources/mining-pools/default.svg'">
|
|
||||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
<td class="asset-title">
|
||||||
<td class="table-cell-size">
|
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
|
||||||
<div class="progress">
|
|
||||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
|
||||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
|
<td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-template #mempoolGraph>
|
||||||
|
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
||||||
|
<app-incoming-transactions-graph
|
||||||
|
[left]="50"
|
||||||
|
[data]="mempoolStats.value?.weightPerSecond"
|
||||||
|
[windowPreferenceOverride]="'2h'"
|
||||||
|
></app-incoming-transactions-graph>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" style="max-height: 410px">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title" i18n="dashboard.latest-transactions">Latest transactions</h5>
|
|
||||||
<table class="table latest-transactions">
|
|
||||||
<thead>
|
|
||||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
|
||||||
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
|
||||||
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''" i18n="dashboard.latest-transactions.USD">USD</th>
|
|
||||||
<th class="table-cell-fees" i18n="dashboard.latest-transactions.fee">Fee</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
|
||||||
<td class="table-cell-txid"><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
|
|
||||||
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
|
||||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
|
||||||
<td class="table-cell-fees">{{ transaction.fee / transaction.vsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm d-block mx-auto" (click)="toggleCollapsed()">
|
|
||||||
<div [ngSwitch]="collapseLevel">
|
|
||||||
<fa-icon *ngSwitchCase="'three'" [icon]="['fas', 'angle-down']" [fixedWidth]="true" i18n-title="dashboard.expand" title="Expand"></fa-icon>
|
|
||||||
<fa-icon *ngSwitchDefault [icon]="['fas', 'angle-up']" [fixedWidth]="true" i18n-title="dashboard.collapse" title="Collapse"></fa-icon>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<div class="col" style="max-height: 410px">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||||
|
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||||
|
<span> </span>
|
||||||
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<table class="table lastest-blocks-table">
|
||||||
|
<thead>
|
||||||
|
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
||||||
|
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
||||||
|
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
|
||||||
|
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
||||||
|
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
||||||
|
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||||
|
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
|
||||||
|
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
|
||||||
|
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||||
|
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||||
|
onError="this.src = './resources/mining-pools/default.svg'">
|
||||||
|
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
||||||
|
<td class="table-cell-size">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
||||||
|
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col" style="max-height: 410px">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title" i18n="dashboard.latest-transactions">Latest transactions</h5>
|
||||||
|
<table class="table latest-transactions">
|
||||||
|
<thead>
|
||||||
|
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||||
|
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
||||||
|
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''" i18n="dashboard.latest-transactions.USD">USD</th>
|
||||||
|
<th class="table-cell-fees" i18n="dashboard.latest-transactions.fee">Fee</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
||||||
|
<td class="table-cell-txid"><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
|
||||||
|
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
||||||
|
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||||
|
<td class="table-cell-fees">{{ transaction.fee / transaction.vsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class=""> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<app-language-selector></app-language-selector>
|
<app-language-selector></app-language-selector>
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ interface MempoolStatsData {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit {
|
export class DashboardComponent implements OnInit {
|
||||||
collapseLevel: string;
|
|
||||||
featuredAssets$: Observable<any>;
|
featuredAssets$: Observable<any>;
|
||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||||
@ -63,7 +62,6 @@ export class DashboardComponent implements OnInit {
|
|||||||
this.seoService.resetTitle();
|
this.seoService.resetTitle();
|
||||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
||||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||||
this.collapseLevel = this.storageService.getValue('dashboard-collapsed') || 'one';
|
|
||||||
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
|
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
|
||||||
@ -230,15 +228,4 @@ export class DashboardComponent implements OnInit {
|
|||||||
trackByBlock(index: number, block: BlockExtended) {
|
trackByBlock(index: number, block: BlockExtended) {
|
||||||
return block.height;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
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 { Observable } from 'rxjs';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||||
|
import { Outspend } from '../interfaces/electrs.interface';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -74,6 +75,14 @@ export class ApiService {
|
|||||||
return this.httpClient.get<number[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/transaction-times', { params });
|
return this.httpClient.get<number[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/transaction-times', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOutspendsBatched$(txIds: string[]): Observable<Outspend[][]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
txIds.forEach((txId: string) => {
|
||||||
|
params = params.append('txId[]', txId);
|
||||||
|
});
|
||||||
|
return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params });
|
||||||
|
}
|
||||||
|
|
||||||
requestDonation$(amount: number, orderId: string): Observable<any> {
|
requestDonation$(amount: number, orderId: string): Observable<any> {
|
||||||
const params = {
|
const params = {
|
||||||
amount: amount,
|
amount: amount,
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"CLEAR_PROTECTION_MINUTES": 5,
|
"CLEAR_PROTECTION_MINUTES": 5,
|
||||||
"POLL_RATE_MS": 1000,
|
"POLL_RATE_MS": 1000,
|
||||||
"INDEXING_BLOCKS_AMOUNT": -1,
|
"INDEXING_BLOCKS_AMOUNT": -1,
|
||||||
|
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||||
"USE_SECOND_NODE_FOR_MINFEE": true
|
"USE_SECOND_NODE_FOR_MINFEE": true
|
||||||
},
|
},
|
||||||
"SYSLOG" : {
|
"SYSLOG" : {
|
||||||
|
@ -4,14 +4,42 @@ location /api/v1/statistics {
|
|||||||
location /api/v1/mining {
|
location /api/v1/mining {
|
||||||
try_files /dev/null @mempool-api-v1-warmcache;
|
try_files /dev/null @mempool-api-v1-warmcache;
|
||||||
}
|
}
|
||||||
|
location /api/v1/block {
|
||||||
|
try_files /dev/null @mempool-api-v1-forevercache;
|
||||||
|
}
|
||||||
location /api/v1 {
|
location /api/v1 {
|
||||||
try_files /dev/null @mempool-api-v1-coldcache;
|
try_files /dev/null @mempool-api-v1-coldcache;
|
||||||
}
|
}
|
||||||
|
location /api/block {
|
||||||
|
rewrite ^/api/(.*) /$1 break;
|
||||||
|
try_files /dev/null @electrs-api-forevercache;
|
||||||
|
}
|
||||||
location /api/ {
|
location /api/ {
|
||||||
rewrite ^/api/(.*) /$1 break;
|
rewrite ^/api/(.*) /$1 break;
|
||||||
try_files /dev/null @electrs-api-nocache;
|
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 {
|
location @mempool-api-v1-warmcache {
|
||||||
proxy_pass $mempoolBackend;
|
proxy_pass $mempoolBackend;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@ -46,6 +74,7 @@ location @mempool-api-v1-coldcache {
|
|||||||
proxy_cache api;
|
proxy_cache api;
|
||||||
proxy_cache_valid 200 10s;
|
proxy_cache_valid 200 10s;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
|
|
||||||
expires 10s;
|
expires 10s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,4 +110,27 @@ location @electrs-api-nocache {
|
|||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_buffering 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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user