Merge branch 'master' into simon/block-tip-hash-api

This commit is contained in:
wiz 2022-06-23 20:50:01 +09:00 committed by GitHub
commit 411e9c2e89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 595 additions and 241 deletions

View File

@ -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": [],

View File

@ -14,6 +14,7 @@ export interface AbstractBitcoinApi {
$getAddressPrefix(prefix: string): string[];
$sendRawTransaction(rawTransaction: string): Promise<string>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
}
export interface BitcoinRpcCredentials {
host: string;

View File

@ -148,6 +148,15 @@ class BitcoinApi implements AbstractBitcoinApi {
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> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View File

@ -66,8 +66,18 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.');
}
$getOutspends(): Promise<IEsploraApi.Outspend[]> {
throw new Error('Method not implemented.');
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
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;
}
}

View File

@ -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<boolean> {
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<TransactionStripped[]> {
// 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<TransactionStripped[]>
{
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;
}

View File

@ -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
);
}
}

View File

@ -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'];

View File

@ -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': [],

View File

@ -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)

View File

@ -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));

View File

@ -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<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();

View 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();

View File

@ -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);

View File

@ -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__",

View File

@ -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

View File

@ -26,6 +26,7 @@
.loader-wrapper {
position: absolute;
background: #181b2d7f;
left: 0;
right: 0;
top: 0;

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -27,9 +27,6 @@
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
<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">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> 3M
</label>

View File

@ -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<br>`;
} 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}<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 `
<b style="color: white; margin-left: 2px">${date}</b><br>
<span>${hashrateString}</span><br>
<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)',
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 : [{

View File

@ -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()">
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
</select>

View File

@ -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<BlockExtended>;
outspendsSubscription: Subscription;
refreshOutspends$: ReplaySubject<{ [str: string]: Observable<Outspend[]>}> = new ReplaySubject();
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
showDetails$ = new BehaviorSubject<boolean>(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() {

View File

@ -1,8 +1,8 @@
<div class="container-xl dashboard-container">
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
<ng-template [ngIf]="collapseLevel === 'three'" [ngIfElse]="expanded">
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
<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">
@ -10,172 +10,135 @@
</div>
</div>
</div>
<div class="col" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
<div class="col">
<app-difficulty></app-difficulty>
</div>
<div class="col">
<div class="card">
<div class="card-body">
</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>
</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>
<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>
<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>
<ng-template [ngIf]="collapseLevel === 'one'">
<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>&nbsp;</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>
</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>
<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 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>
<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="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 + '%' }">&nbsp;</div>
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
</div>
<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>
<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>
</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>&nbsp;</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 + '%' }">&nbsp;</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="">&nbsp;</div>
</div>
</div>
</div>
</div>
<app-language-selector></app-language-selector>

View File

@ -33,7 +33,6 @@ interface MempoolStatsData {
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent implements OnInit {
collapseLevel: string;
featuredAssets$: Observable<any>;
network$: Observable<string>;
mempoolBlocksData$: Observable<MempoolBlocksData>;
@ -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);
}
}

View File

@ -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<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> {
const params = {
amount: amount,

View File

@ -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" : {

View File

@ -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;
}