Merge branch 'master' into simon/block-transactions-error-catcher
This commit is contained in:
		
						commit
						1880e3a59b
					
				| @ -15,6 +15,7 @@ import BitcoinApi from './bitcoin/bitcoin-api'; | ||||
| import { prepareBlock } from '../utils/blocks-utils'; | ||||
| import BlocksRepository from '../repositories/BlocksRepository'; | ||||
| import HashratesRepository from '../repositories/HashratesRepository'; | ||||
| import indexer from '../indexer'; | ||||
| 
 | ||||
| class Blocks { | ||||
|   private blocks: BlockExtended[] = []; | ||||
| @ -23,9 +24,6 @@ class Blocks { | ||||
|   private lastDifficultyAdjustmentTime = 0; | ||||
|   private previousDifficultyRetarget = 0; | ||||
|   private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; | ||||
|   private blockIndexingStarted = false; | ||||
|   public blockIndexingCompleted = false; | ||||
|   public reindexFlag = false; | ||||
| 
 | ||||
|   constructor() { } | ||||
| 
 | ||||
| @ -197,24 +195,15 @@ class Blocks { | ||||
|    * [INDEXING] Index all blocks metadata for the mining dashboard | ||||
|    */ | ||||
|   public async $generateBlockDatabase() { | ||||
|     if (this.blockIndexingStarted && !this.reindexFlag) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.reindexFlag = false; | ||||
| 
 | ||||
|     const blockchainInfo = await bitcoinClient.getBlockchainInfo(); | ||||
|     if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
 | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.blockIndexingStarted = true; | ||||
|     this.blockIndexingCompleted = false; | ||||
| 
 | ||||
|     try { | ||||
|       let currentBlockHeight = blockchainInfo.blocks; | ||||
| 
 | ||||
|       let indexingBlockAmount = config.MEMPOOL.INDEXING_BLOCKS_AMOUNT; | ||||
|       let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks); | ||||
|       if (indexingBlockAmount <= -1) { | ||||
|         indexingBlockAmount = currentBlockHeight + 1; | ||||
|       } | ||||
| @ -275,14 +264,14 @@ class Blocks { | ||||
|       loadingIndicators.setProgress('block-indexing', 100); | ||||
|     } catch (e) { | ||||
|       logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|       this.blockIndexingStarted = false; | ||||
|       loadingIndicators.setProgress('block-indexing', 100); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const chainValid = await BlocksRepository.$validateChain(); | ||||
|     this.reindexFlag = !chainValid; | ||||
|     this.blockIndexingCompleted = chainValid; | ||||
|     if (!chainValid) { | ||||
|       indexer.reindex(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $updateBlocks() { | ||||
| @ -299,6 +288,8 @@ class Blocks { | ||||
|       logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`); | ||||
|       this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT; | ||||
|       fastForwarded = true; | ||||
|       logger.info(`Re-indexing skipped blocks and corresponding hashrates data`); | ||||
|       indexer.reindex(); // Make sure to index the skipped blocks #1619
 | ||||
|     } | ||||
| 
 | ||||
|     if (!this.lastDifficultyAdjustmentTime) { | ||||
| @ -426,10 +417,7 @@ class Blocks { | ||||
|     return blockExtended; | ||||
|   } | ||||
| 
 | ||||
|   public async $getBlocksExtras(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> { | ||||
|     // Note - This API is breaking if indexing is not available. For now it is okay because we only
 | ||||
|     // use it for the mining pages, and mining pages should not be available if indexing is turned off.
 | ||||
|     // I'll need to fix it before we refactor the block(s) related pages
 | ||||
|   public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> { | ||||
|     try { | ||||
|       let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight(); | ||||
|       const returnBlocks: BlockExtended[] = []; | ||||
|  | ||||
| @ -4,15 +4,11 @@ import PoolsRepository from '../repositories/PoolsRepository'; | ||||
| import HashratesRepository from '../repositories/HashratesRepository'; | ||||
| import bitcoinClient from './bitcoin/bitcoin-client'; | ||||
| import logger from '../logger'; | ||||
| import blocks from './blocks'; | ||||
| import { Common } from './common'; | ||||
| import loadingIndicators from './loading-indicators'; | ||||
| import { escape } from 'mysql2'; | ||||
| 
 | ||||
| class Mining { | ||||
|   hashrateIndexingStarted = false; | ||||
|   weeklyHashrateIndexingStarted = false; | ||||
| 
 | ||||
|   constructor() { | ||||
|   } | ||||
| 
 | ||||
| @ -153,14 +149,9 @@ class Mining { | ||||
|    * [INDEXING] Generate weekly mining pool hashrate history | ||||
|    */ | ||||
|   public async $generatePoolHashrateHistory(): Promise<void> { | ||||
|     if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted || this.weeklyHashrateIndexingStarted) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const now = new Date(); | ||||
| 
 | ||||
|     try { | ||||
|       this.weeklyHashrateIndexingStarted = true; | ||||
|       const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing'); | ||||
| 
 | ||||
|       // Run only if:
 | ||||
| @ -168,11 +159,9 @@ class Mining { | ||||
|       // * we started a new week (around Monday midnight)
 | ||||
|       const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate(); | ||||
|       if (!runIndexing) { | ||||
|         this.weeklyHashrateIndexingStarted = false; | ||||
|         return; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.weeklyHashrateIndexingStarted = false; | ||||
|       throw e; | ||||
|     } | ||||
| 
 | ||||
| @ -192,6 +181,7 @@ class Mining { | ||||
|       const startedAt = new Date().getTime() / 1000; | ||||
|       let timer = new Date().getTime() / 1000; | ||||
| 
 | ||||
|       logger.debug(`Indexing weekly mining pool hashrate`); | ||||
|       loadingIndicators.setProgress('weekly-hashrate-indexing', 0); | ||||
| 
 | ||||
|       while (toTimestamp > genesisTimestamp) { | ||||
| @ -256,7 +246,6 @@ class Mining { | ||||
|         ++indexedThisRun; | ||||
|         ++totalIndexed; | ||||
|       } | ||||
|       this.weeklyHashrateIndexingStarted = false; | ||||
|       await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate()); | ||||
|       if (newlyIndexed > 0) { | ||||
|         logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`); | ||||
| @ -264,7 +253,6 @@ class Mining { | ||||
|       loadingIndicators.setProgress('weekly-hashrate-indexing', 100); | ||||
|     } catch (e) { | ||||
|       loadingIndicators.setProgress('weekly-hashrate-indexing', 100); | ||||
|       this.weeklyHashrateIndexingStarted = false; | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| @ -273,22 +261,14 @@ class Mining { | ||||
|    * [INDEXING] Generate daily hashrate data | ||||
|    */ | ||||
|   public async $generateNetworkHashrateHistory(): Promise<void> { | ||||
|     if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       this.hashrateIndexingStarted = true; | ||||
| 
 | ||||
|       // We only run this once a day around midnight
 | ||||
|       const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing'); | ||||
|       const now = new Date().getUTCDate(); | ||||
|       if (now === latestRunDate) { | ||||
|         this.hashrateIndexingStarted = false; | ||||
|         return; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.hashrateIndexingStarted = false; | ||||
|       throw e; | ||||
|     } | ||||
| 
 | ||||
| @ -306,6 +286,7 @@ class Mining { | ||||
|       const startedAt = new Date().getTime() / 1000; | ||||
|       let timer = new Date().getTime() / 1000; | ||||
| 
 | ||||
|       logger.debug(`Indexing daily network hashrate`); | ||||
|       loadingIndicators.setProgress('daily-hashrate-indexing', 0); | ||||
| 
 | ||||
|       while (toTimestamp > genesisTimestamp) { | ||||
| @ -377,14 +358,12 @@ class Mining { | ||||
|       await HashratesRepository.$saveHashrates(hashrates); | ||||
| 
 | ||||
|       await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate()); | ||||
|       this.hashrateIndexingStarted = false; | ||||
|       if (newlyIndexed > 0) { | ||||
|         logger.info(`Indexed ${newlyIndexed} day of network hashrate`); | ||||
|       } | ||||
|       loadingIndicators.setProgress('daily-hashrate-indexing', 100); | ||||
|     } catch (e) { | ||||
|       loadingIndicators.setProgress('daily-hashrate-indexing', 100); | ||||
|       this.hashrateIndexingStarted = false; | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -27,8 +27,8 @@ import icons from './api/liquid/icons'; | ||||
| import { Common } from './api/common'; | ||||
| import mining from './api/mining'; | ||||
| import HashratesRepository from './repositories/HashratesRepository'; | ||||
| import BlocksRepository from './repositories/BlocksRepository'; | ||||
| import poolsUpdater from './tasks/pools-updater'; | ||||
| import indexer from './indexer'; | ||||
| 
 | ||||
| class Server { | ||||
|   private wss: WebSocket.Server | undefined; | ||||
| @ -99,7 +99,7 @@ class Server { | ||||
|         } | ||||
|         await databaseMigration.$initializeOrMigrateDatabase(); | ||||
|         if (Common.indexingEnabled()) { | ||||
|           await this.$resetHashratesIndexingState(); | ||||
|           await indexer.$resetHashratesIndexingState(); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         throw new Error(e instanceof Error ? e.message : 'Error'); | ||||
| @ -154,7 +154,7 @@ class Server { | ||||
|       await poolsUpdater.updatePoolsJson(); | ||||
|       await blocks.$updateBlocks(); | ||||
|       await memPool.$updateMempool(); | ||||
|       this.$runIndexingWhenReady(); | ||||
|       indexer.$run(); | ||||
| 
 | ||||
|       setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS); | ||||
|       this.currentBackendRetryInterval = 5; | ||||
| @ -173,29 +173,6 @@ class Server { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async $resetHashratesIndexingState() { | ||||
|     try { | ||||
|       await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0); | ||||
|       await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0); | ||||
|     } catch (e) { | ||||
|       logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async $runIndexingWhenReady() { | ||||
|     if (!Common.indexingEnabled() || mempool.hasPriority()) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       await blocks.$generateBlockDatabase(); | ||||
|       await mining.$generateNetworkHashrateHistory(); | ||||
|       await mining.$generatePoolHashrateHistory(); | ||||
|     } catch (e) { | ||||
|       logger.err(`Indexing failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setUpWebsocketHandling() { | ||||
|     if (this.wss) { | ||||
|       websocketHandler.setWebsocketServer(this.wss); | ||||
| @ -337,8 +314,8 @@ class Server { | ||||
|     } | ||||
| 
 | ||||
|     this.app | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras', routes.getBlocksExtras) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras/:height', routes.getBlocksExtras) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes)) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes)) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock); | ||||
| 
 | ||||
|     if (config.MEMPOOL.BACKEND !== 'esplora') { | ||||
| @ -352,8 +329,6 @@ class Server { | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions) | ||||
|  | ||||
							
								
								
									
										52
									
								
								backend/src/indexer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								backend/src/indexer.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| import { Common } from './api/common'; | ||||
| import blocks from './api/blocks'; | ||||
| import mempool from './api/mempool'; | ||||
| import mining from './api/mining'; | ||||
| import logger from './logger'; | ||||
| import HashratesRepository from './repositories/HashratesRepository'; | ||||
| 
 | ||||
| class Indexer { | ||||
|   runIndexer = true; | ||||
|   indexerRunning = false; | ||||
| 
 | ||||
|   constructor() { | ||||
|   } | ||||
| 
 | ||||
|   public reindex() { | ||||
|     this.runIndexer = true; | ||||
|   } | ||||
| 
 | ||||
|   public async $run() { | ||||
|     if (!Common.indexingEnabled() || this.runIndexer === false || | ||||
|       this.indexerRunning === true || mempool.hasPriority() | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.runIndexer = false; | ||||
|     this.indexerRunning = true; | ||||
| 
 | ||||
|     try { | ||||
|       await blocks.$generateBlockDatabase(); | ||||
|       await this.$resetHashratesIndexingState(); | ||||
|       await mining.$generateNetworkHashrateHistory(); | ||||
|       await mining.$generatePoolHashrateHistory(); | ||||
|     } catch (e) { | ||||
|       this.reindex(); | ||||
|       logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
| 
 | ||||
|     this.indexerRunning = false; | ||||
|   } | ||||
| 
 | ||||
|   async $resetHashratesIndexingState() { | ||||
|     try { | ||||
|       await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0); | ||||
|       await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0); | ||||
|     } catch (e) { | ||||
|       logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default new Indexer(); | ||||
| @ -81,7 +81,7 @@ export interface TransactionStripped { | ||||
| 
 | ||||
| export interface BlockExtension { | ||||
|   totalFees?: number; | ||||
|   medianFee?: number; // Actually the median fee rate that we compute ourself
 | ||||
|   medianFee?: number; | ||||
|   feeRange?: number[]; | ||||
|   reward?: number; | ||||
|   coinbaseTx?: TransactionMinerInfo; | ||||
|  | ||||
| @ -475,9 +475,9 @@ class BlocksRepository { | ||||
|   public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> { | ||||
|     try { | ||||
|       let query = `SELECT
 | ||||
|         CAST(AVG(height) as INT) as avg_height, | ||||
|         CAST(AVG(height) as INT) as avgHeight, | ||||
|         CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, | ||||
|         CAST(AVG(fees) as INT) as avg_fees | ||||
|         CAST(AVG(fees) as INT) as avgFees | ||||
|         FROM blocks`;
 | ||||
| 
 | ||||
|       if (interval !== null) { | ||||
| @ -500,9 +500,9 @@ class BlocksRepository { | ||||
|   public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> { | ||||
|     try { | ||||
|       let query = `SELECT
 | ||||
|         CAST(AVG(height) as INT) as avg_height, | ||||
|         CAST(AVG(height) as INT) as avgHeight, | ||||
|         CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, | ||||
|         CAST(AVG(reward) as INT) as avg_rewards | ||||
|         CAST(AVG(reward) as INT) as avgRewards | ||||
|         FROM blocks`;
 | ||||
| 
 | ||||
|       if (interval !== null) { | ||||
| @ -525,15 +525,15 @@ class BlocksRepository { | ||||
|    public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> { | ||||
|     try { | ||||
|       let query = `SELECT
 | ||||
|         CAST(AVG(height) as INT) as avg_height, | ||||
|         CAST(AVG(height) as INT) as avgHeight, | ||||
|         CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[0]')) as INT) as avg_fee_0, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[1]')) as INT) as avg_fee_10, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[2]')) as INT) as avg_fee_25, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[3]')) as INT) as avg_fee_50, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[4]')) as INT) as avg_fee_75, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[5]')) as INT) as avg_fee_90, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[6]')) as INT) as avg_fee_100 | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[0]')) as INT) as avgFee_0, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[1]')) as INT) as avgFee_10, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[2]')) as INT) as avgFee_25, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[3]')) as INT) as avgFee_50, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[4]')) as INT) as avgFee_75, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[5]')) as INT) as avgFee_90, | ||||
|         CAST(AVG(JSON_EXTRACT(fee_span, '$[6]')) as INT) as avgFee_100 | ||||
|       FROM blocks`;
 | ||||
| 
 | ||||
|       if (interval !== null) { | ||||
| @ -556,9 +556,9 @@ class BlocksRepository { | ||||
|    public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> { | ||||
|     try { | ||||
|       let query = `SELECT
 | ||||
|         CAST(AVG(height) as INT) as avg_height, | ||||
|         CAST(AVG(height) as INT) as avgHeight, | ||||
|         CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, | ||||
|         CAST(AVG(size) as INT) as avg_size | ||||
|         CAST(AVG(size) as INT) as avgSize | ||||
|       FROM blocks`;
 | ||||
| 
 | ||||
|       if (interval !== null) { | ||||
| @ -581,9 +581,9 @@ class BlocksRepository { | ||||
|    public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> { | ||||
|     try { | ||||
|       let query = `SELECT
 | ||||
|         CAST(AVG(height) as INT) as avg_height, | ||||
|         CAST(AVG(height) as INT) as avgHeight, | ||||
|         CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, | ||||
|         CAST(AVG(weight) as INT) as avg_weight | ||||
|         CAST(AVG(weight) as INT) as avgWeight | ||||
|       FROM blocks`;
 | ||||
| 
 | ||||
|       if (interval !== null) { | ||||
|  | ||||
| @ -720,20 +720,22 @@ class Routes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async getBlocksExtras(req: Request, res: Response) { | ||||
|   public async getBlocks(req: Request, res: Response) { | ||||
|     try { | ||||
|       const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); | ||||
|       res.json(await blocks.$getBlocksExtras(height, 15)); | ||||
|       if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
 | ||||
|         const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); | ||||
|         res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); | ||||
|         res.json(await blocks.$getBlocks(height, 15)); | ||||
|       } else { // Liquid, Bisq
 | ||||
|         return await this.getLegacyBlocks(req, res); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async getBlocks(req: Request, res: Response) { | ||||
|   public async getLegacyBlocks(req: Request, res: Response) { | ||||
|     try { | ||||
|       loadingIndicators.setProgress('blocks', 0); | ||||
| 
 | ||||
|       const returnBlocks: IEsploraApi.Block[] = []; | ||||
|       const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight(); | ||||
| 
 | ||||
| @ -757,16 +759,15 @@ class Routes { | ||||
|           returnBlocks.push(block); | ||||
|           nextHash = block.previousblockhash; | ||||
|         } | ||||
|         loadingIndicators.setProgress('blocks', i / 10 * 100); | ||||
|       } | ||||
| 
 | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); | ||||
|       res.json(returnBlocks); | ||||
|     } catch (e) { | ||||
|       loadingIndicators.setProgress('blocks', 100); | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|    | ||||
|   public async getBlockTransactions(req: Request, res: Response) { | ||||
|     try { | ||||
|       loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); | ||||
|  | ||||
| @ -15,7 +15,7 @@ export function prepareBlock(block: any): BlockExtended { | ||||
|     weight: block.weight, | ||||
|     previousblockhash: block.previousblockhash, | ||||
|     extras: { | ||||
|       coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw, | ||||
|       coinbaseRaw: block.coinbase_raw ?? block.extras?.coinbaseRaw, | ||||
|       medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee, | ||||
|       feeRange: block.feeRange ?? block.fee_span, | ||||
|       reward: block.reward ?? block?.extras?.reward, | ||||
|  | ||||
| @ -7,7 +7,6 @@ import { AddressComponent } from './components/address/address.component'; | ||||
| import { MasterPageComponent } from './components/master-page/master-page.component'; | ||||
| import { AboutComponent } from './components/about/about.component'; | ||||
| import { StatusViewComponent } from './components/status-view/status-view.component'; | ||||
| import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component'; | ||||
| import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component'; | ||||
| import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component'; | ||||
| import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component'; | ||||
| @ -36,19 +35,20 @@ let routes: Routes = [ | ||||
|         component: MasterPageComponent, | ||||
|         children: [ | ||||
|           { | ||||
|             path: 'tx/push', | ||||
|             component: PushTransactionComponent, | ||||
|             path: 'mining/blocks', | ||||
|             redirectTo: 'blocks', | ||||
|             pathMatch: 'full' | ||||
|           }, | ||||
|           { | ||||
|             path: 'blocks', | ||||
|             component: LatestBlocksComponent, | ||||
|             path: 'tx/push', | ||||
|             component: PushTransactionComponent, | ||||
|           }, | ||||
|           { | ||||
|             path: 'about', | ||||
|             component: AboutComponent, | ||||
|           }, | ||||
|           { | ||||
|             path: 'mining/blocks', | ||||
|             path: 'blocks', | ||||
|             component: BlocksList, | ||||
|           }, | ||||
|           { | ||||
| @ -115,6 +115,11 @@ let routes: Routes = [ | ||||
|   { | ||||
|     path: 'signet', | ||||
|     children: [ | ||||
|       { | ||||
|         path: 'mining/blocks', | ||||
|         redirectTo: 'blocks', | ||||
|         pathMatch: 'full' | ||||
|       }, | ||||
|       { | ||||
|         path: '', | ||||
|         pathMatch: 'full', | ||||
| @ -128,16 +133,12 @@ let routes: Routes = [ | ||||
|             path: 'tx/push', | ||||
|             component: PushTransactionComponent, | ||||
|           }, | ||||
|           { | ||||
|             path: 'blocks', | ||||
|             component: LatestBlocksComponent, | ||||
|           }, | ||||
|           { | ||||
|             path: 'about', | ||||
|             component: AboutComponent, | ||||
|           }, | ||||
|           { | ||||
|             path: 'mining/blocks', | ||||
|             path: 'blocks', | ||||
|             component: BlocksList, | ||||
|           }, | ||||
|           { | ||||
| @ -211,19 +212,20 @@ let routes: Routes = [ | ||||
|     component: MasterPageComponent, | ||||
|     children: [ | ||||
|       { | ||||
|         path: 'tx/push', | ||||
|         component: PushTransactionComponent, | ||||
|         path: 'mining/blocks', | ||||
|         redirectTo: 'blocks', | ||||
|         pathMatch: 'full' | ||||
|       }, | ||||
|       { | ||||
|         path: 'blocks', | ||||
|         component: LatestBlocksComponent, | ||||
|         path: 'tx/push', | ||||
|         component: PushTransactionComponent, | ||||
|       }, | ||||
|       { | ||||
|         path: 'about', | ||||
|         component: AboutComponent, | ||||
|       }, | ||||
|       { | ||||
|         path: 'mining/blocks', | ||||
|         path: 'blocks', | ||||
|         component: BlocksList, | ||||
|       }, | ||||
|       { | ||||
| @ -321,16 +323,12 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | ||||
|               path: 'tx/push', | ||||
|               component: PushTransactionComponent, | ||||
|             }, | ||||
|             { | ||||
|               path: 'blocks', | ||||
|               component: LatestBlocksComponent, | ||||
|             }, | ||||
|             { | ||||
|               path: 'about', | ||||
|               component: AboutComponent, | ||||
|             }, | ||||
|             { | ||||
|               path: 'mining/blocks', | ||||
|               path: 'blocks', | ||||
|               component: BlocksList, | ||||
|             }, | ||||
|             { | ||||
| @ -429,16 +427,12 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | ||||
|           path: 'tx/push', | ||||
|           component: PushTransactionComponent, | ||||
|         }, | ||||
|         { | ||||
|           path: 'blocks', | ||||
|           component: LatestBlocksComponent, | ||||
|         }, | ||||
|         { | ||||
|           path: 'about', | ||||
|           component: AboutComponent, | ||||
|         }, | ||||
|         { | ||||
|           path: 'mining/blocks', | ||||
|           path: 'blocks', | ||||
|           component: BlocksList, | ||||
|         }, | ||||
|         { | ||||
|  | ||||
| @ -250,6 +250,10 @@ | ||||
|         <img class="image" src="/resources/profile/marina.svg" /> | ||||
|         <span>Marina</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/bitcoin-wallet/bitcoin-wallet/" target="_blank" title="Bitcoin Wallet (Schildbach)"> | ||||
|         <img class="image" src="/resources/profile/schildbach.svg" /> | ||||
|         <span>Schildbach</span> | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|  | ||||
| @ -89,13 +89,13 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|                 }; | ||||
|                 for (const rate of data.blockFeeRates) { | ||||
|                   const timestamp = rate.timestamp * 1000; | ||||
|                   seriesData['Min'].push([timestamp, rate.avg_fee_0, rate.avg_height]); | ||||
|                   seriesData['10th'].push([timestamp, rate.avg_fee_10, rate.avg_height]); | ||||
|                   seriesData['25th'].push([timestamp, rate.avg_fee_25, rate.avg_height]); | ||||
|                   seriesData['Median'].push([timestamp, rate.avg_fee_50, rate.avg_height]); | ||||
|                   seriesData['75th'].push([timestamp, rate.avg_fee_75, rate.avg_height]); | ||||
|                   seriesData['90th'].push([timestamp, rate.avg_fee_90, rate.avg_height]); | ||||
|                   seriesData['Max'].push([timestamp, rate.avg_fee_100, rate.avg_height]); | ||||
|                   seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]); | ||||
|                   seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]); | ||||
|                   seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]); | ||||
|                   seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]); | ||||
|                   seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]); | ||||
|                   seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]); | ||||
|                   seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]); | ||||
|                 } | ||||
| 
 | ||||
|                 // Prepare chart
 | ||||
|  | ||||
| @ -71,7 +71,7 @@ export class BlockFeesGraphComponent implements OnInit { | ||||
|             .pipe( | ||||
|               tap((response) => { | ||||
|                 this.prepareChartOptions({ | ||||
|                   blockFees: response.body.map(val => [val.timestamp * 1000, val.avg_fees / 100000000]), | ||||
|                   blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000]), | ||||
|                 }); | ||||
|                 this.isLoading = false; | ||||
|               }), | ||||
|  | ||||
| @ -69,7 +69,7 @@ export class BlockRewardsGraphComponent implements OnInit { | ||||
|             .pipe( | ||||
|               tap((response) => { | ||||
|                 this.prepareChartOptions({ | ||||
|                   blockRewards: response.body.map(val => [val.timestamp * 1000, val.avg_rewards / 100000000]), | ||||
|                   blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000]), | ||||
|                 }); | ||||
|                 this.isLoading = false; | ||||
|               }), | ||||
|  | ||||
| @ -83,8 +83,8 @@ export class BlockSizesWeightsGraphComponent implements OnInit { | ||||
|               tap((response) => { | ||||
|                 const data = response.body; | ||||
|                 this.prepareChartOptions({ | ||||
|                   sizes: data.sizes.map(val => [val.timestamp * 1000, val.avg_size / 1000000, val.avg_height]), | ||||
|                   weights: data.weights.map(val => [val.timestamp * 1000, val.avg_weight / 1000000, val.avg_height]), | ||||
|                   sizes: data.sizes.map(val => [val.timestamp * 1000, val.avgSize / 1000000, val.avgHeight]), | ||||
|                   weights: data.weights.map(val => [val.timestamp * 1000, val.avgWeight / 1000000, val.avgHeight]), | ||||
|                 }); | ||||
|                 this.isLoading = false; | ||||
|               }), | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <app-indexing-progress *ngIf="!widget"></app-indexing-progress> | ||||
| 
 | ||||
| <div class="container-xl" [class]="widget ? 'widget' : 'full-height'"> | ||||
| <div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !indexingAvailable}"> | ||||
|   <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1> | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
| @ -8,22 +8,22 @@ | ||||
|   <div style="min-height: 295px"> | ||||
|     <table class="table table-borderless"> | ||||
|       <thead> | ||||
|         <th class="height text-left" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</th> | ||||
|         <th class="pool text-left" [class]="widget ? 'widget' : ''" i18n="mining.pool-name">Pool</th> | ||||
|         <th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget">Timestamp</th> | ||||
|         <th class="mined" i18n="latest-blocks.mined" *ngIf="!widget">Mined</th> | ||||
|         <th class="reward text-right" i18n="latest-blocks.reward" [class]="widget ? 'widget' : ''">Reward</th> | ||||
|         <th class="fees text-right" i18n="latest-blocks.fees" *ngIf="!widget">Fees</th> | ||||
|         <th class="txs text-right" i18n="dashboard.txs" [class]="widget ? 'widget' : ''">TXs</th> | ||||
|         <th class="size" i18n="latest-blocks.size" *ngIf="!widget">Size</th> | ||||
|         <th class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="latest-blocks.height">Height</th> | ||||
|         <th *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="mining.pool-name">Pool</th> | ||||
|         <th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th> | ||||
|         <th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th> | ||||
|         <th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Reward</th> | ||||
|         <th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th> | ||||
|         <th *ngIf="indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">TXs</th> | ||||
|         <th *ngIf="!indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Transactions</th> | ||||
|         <th class="size" i18n="latest-blocks.size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Size</th> | ||||
|       </thead> | ||||
|       <tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> | ||||
|         <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock"> | ||||
|           <td class="text-left" [class]="widget ? 'widget' : ''"> | ||||
|             <a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height | ||||
|               }}</a> | ||||
|             <a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height }}</a> | ||||
|           </td> | ||||
|           <td class="pool text-left" [class]="widget ? 'widget' : ''"> | ||||
|           <td  *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> | ||||
|             <div class="tooltip-custom"> | ||||
|               <a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]"> | ||||
|                 <img width="22" height="22" src="{{ block.extras.pool['logo'] }}" | ||||
| @ -36,19 +36,19 @@ | ||||
|           <td class="timestamp" *ngIf="!widget"> | ||||
|             ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} | ||||
|           </td> | ||||
|           <td class="mined" *ngIf="!widget"> | ||||
|           <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> | ||||
|             <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> | ||||
|           </td> | ||||
|           <td class="reward text-right" [class]="widget ? 'widget' : ''"> | ||||
|           <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> | ||||
|             <app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount> | ||||
|           </td> | ||||
|           <td class="fees text-right" *ngIf="!widget"> | ||||
|           <td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'"> | ||||
|             <app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount> | ||||
|           </td> | ||||
|           <td class="txs text-right" [class]="widget ? 'widget' : ''"> | ||||
|           <td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> | ||||
|             {{ block.tx_count | number }} | ||||
|           </td> | ||||
|           <td class="size" *ngIf="!widget"> | ||||
|           <td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> | ||||
|             <div class="progress"> | ||||
|               <div class="progress-bar progress-mempool" role="progressbar" | ||||
|                 [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div> | ||||
| @ -60,29 +60,29 @@ | ||||
|       <ng-template #skeleton> | ||||
|         <tbody> | ||||
|           <tr *ngFor="let item of skeletonLines"> | ||||
|             <td class="height text-left" [class]="widget ? 'widget' : ''"> | ||||
|             <td class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|             <td class="pool text-left" [class]="widget ? 'widget' : ''"> | ||||
|             <td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> | ||||
|               <img width="1" height="25" style="opacity: 0"> | ||||
|               <span class="skeleton-loader" style="max-width: 125px"></span> | ||||
|             </td> | ||||
|             <td class="timestamp" *ngIf="!widget"> | ||||
|             <td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> | ||||
|               <span class="skeleton-loader" style="max-width: 150px"></span> | ||||
|             </td> | ||||
|             <td class="mined" *ngIf="!widget"> | ||||
|             <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> | ||||
|               <span class="skeleton-loader" style="max-width: 125px"></span> | ||||
|             </td> | ||||
|             <td class="reward text-right" [class]="widget ? 'widget' : ''"> | ||||
|             <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|             <td class="fees text-right" *ngIf="!widget"> | ||||
|             <td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|             <td class="txs text-right" [class]="widget ? 'widget' : ''"> | ||||
|             <td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|             <td class="size" *ngIf="!widget"> | ||||
|             <td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|           </tr> | ||||
|  | ||||
| @ -6,6 +6,9 @@ | ||||
|   padding-left: 0px; | ||||
|   padding-bottom: 0px; | ||||
| } | ||||
| .container-xl.legacy { | ||||
|   max-width: 1140px; | ||||
| } | ||||
| 
 | ||||
| .container { | ||||
|   max-width: 100%; | ||||
| @ -58,6 +61,9 @@ tr, td, th { | ||||
|     width: 10%; | ||||
|   } | ||||
| } | ||||
| .height.legacy { | ||||
|   width: 15%; | ||||
| } | ||||
| 
 | ||||
| .timestamp { | ||||
|   width: 18%; | ||||
| @ -65,6 +71,9 @@ tr, td, th { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| .timestamp.legacy { | ||||
|   width: 20%; | ||||
| } | ||||
| 
 | ||||
| .mined { | ||||
|   width: 13%; | ||||
| @ -72,6 +81,12 @@ tr, td, th { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| .mined.legacy { | ||||
|   width: 15%; | ||||
|   @media (max-width: 576px) { | ||||
|     display: table-cell; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .txs { | ||||
|   padding-right: 40px; | ||||
| @ -88,6 +103,10 @@ tr, td, th { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| .txs.legacy { | ||||
|   padding-right: 80px; | ||||
|   width: 10%; | ||||
| } | ||||
| 
 | ||||
| .fees { | ||||
|   width: 10%; | ||||
| @ -126,6 +145,12 @@ tr, td, th { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| .size.legacy { | ||||
|   width: 20%; | ||||
|   @media (max-width: 576px) { | ||||
|     display: table-cell; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* Tooltip text */ | ||||
| .tooltip-custom { | ||||
|  | ||||
| @ -17,6 +17,7 @@ export class BlocksList implements OnInit { | ||||
| 
 | ||||
|   blocks$: Observable<any[]> = undefined; | ||||
| 
 | ||||
|   indexingAvailable = false; | ||||
|   isLoading = true; | ||||
|   fromBlockHeight = undefined; | ||||
|   paginationMaxSize: number; | ||||
| @ -35,6 +36,9 @@ export class BlocksList implements OnInit { | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && | ||||
|       this.stateService.env.MINING_DASHBOARD === true); | ||||
| 
 | ||||
|     if (!this.widget) { | ||||
|       this.websocketService.want(['blocks']); | ||||
|     } | ||||
| @ -55,17 +59,19 @@ export class BlocksList implements OnInit { | ||||
|                 this.isLoading = false; | ||||
|               }), | ||||
|               map(blocks => { | ||||
|                 for (const block of blocks) { | ||||
|                   // @ts-ignore: Need to add an extra field for the template
 | ||||
|                   block.extras.pool.logo = `./resources/mining-pools/` + | ||||
|                     block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||
|                 if (this.indexingAvailable) { | ||||
|                   for (const block of blocks) { | ||||
|                     // @ts-ignore: Need to add an extra field for the template
 | ||||
|                     block.extras.pool.logo = `./resources/mining-pools/` + | ||||
|                       block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||
|                   } | ||||
|                 } | ||||
|                 if (this.widget) { | ||||
|                   return blocks.slice(0, 6); | ||||
|                 } | ||||
|                 return blocks; | ||||
|               }), | ||||
|               retryWhen(errors => errors.pipe(delayWhen(() => timer(1000)))) | ||||
|               retryWhen(errors => errors.pipe(delayWhen(() => timer(10000)))) | ||||
|             ) | ||||
|           }) | ||||
|       ), | ||||
| @ -81,9 +87,11 @@ export class BlocksList implements OnInit { | ||||
|             return blocks[0]; | ||||
|           } | ||||
|           this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1; | ||||
|           // @ts-ignore: Need to add an extra field for the template
 | ||||
|           blocks[1][0].extras.pool.logo = `./resources/mining-pools/` + | ||||
|             blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||
|           if (this.stateService.env.MINING_DASHBOARD) { | ||||
|             // @ts-ignore: Need to add an extra field for the template
 | ||||
|             blocks[1][0].extras.pool.logo = `./resources/mining-pools/` + | ||||
|               blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||
|           } | ||||
|           acc.unshift(blocks[1][0]); | ||||
|           acc = acc.slice(0, this.widget ? 6 : 15); | ||||
|           return acc; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <div style="min-height: 295px"> | ||||
| <div style="min-height: 335px"> | ||||
|   <table class="table latest-adjustments"> | ||||
|     <thead> | ||||
|       <tr> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <div class="mb-3 d-flex menu" style="padding: 0px 35px;"> | ||||
| <div *ngIf="stateService.env.MINING_DASHBOARD" class="mb-3 d-flex menu" style="padding: 0px 35px;"> | ||||
|   <a routerLinkActive="active" class="btn btn-primary w-50 mr-1" | ||||
|     [routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a> | ||||
|   <div ngbDropdown *ngIf="stateService.env.MINING_DASHBOARD" class="w-50"> | ||||
|   <div ngbDropdown class="w-50"> | ||||
|     <button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button> | ||||
|     <div ngbDropdownMenu aria-labelledby="dropdownBasic1"> | ||||
|       <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]" | ||||
|  | ||||
| @ -1,57 +0,0 @@ | ||||
| <div class="container-xl"> | ||||
|   <h1 class="float-left" i18n="master-page.blocks">Blocks</h1> | ||||
|   <br> | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
| 
 | ||||
|   <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()"> | ||||
|     <thead> | ||||
|       <th style="width: 15%;" i18n="latest-blocks.height">Height</th> | ||||
|       <th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th> | ||||
|       <th style="width: 20%;" i18n="latest-blocks.mined">Mined</th> | ||||
|       <th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</th> | ||||
|       <th style="width: 20%;" i18n="latest-blocks.size">Size</th> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock"> | ||||
|         <td><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> | ||||
|         <td class="d-none d-md-block">‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td> | ||||
|         <td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td> | ||||
|         <td class="d-none d-lg-block">{{ block.tx_count | number }}</td> | ||||
|         <td> | ||||
|           <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> | ||||
|       <ng-template [ngIf]="isLoading"> | ||||
|         <tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]"> | ||||
|           <td><span class="skeleton-loader"></span></td> | ||||
|           <td class="d-none d-md-block"><span class="skeleton-loader"></span></td> | ||||
|           <td><span class="skeleton-loader"></span></td> | ||||
|           <td class="d-none d-lg-block"><span class="skeleton-loader"></span></td> | ||||
|           <td><span class="skeleton-loader"></span></td> | ||||
|         </tr> | ||||
|       <ng-container *ngIf="(blocksLoadingStatus$ | async) as blocksLoadingStatus"> | ||||
|       <tr> | ||||
|         <td colspan="5"> | ||||
|           <div class="progress progress-dark"> | ||||
|             <div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': blocksLoadingStatus + '%' }"></div> | ||||
|           </div> | ||||
|         </td> | ||||
|       </tr> | ||||
|       </ng-container> | ||||
|     </ng-template> | ||||
|     </tbody> | ||||
|   </table> | ||||
| 
 | ||||
|   <ng-template [ngIf]="error"> | ||||
|     <div class="text-center"> | ||||
|       <span>Error loading blocks</span> | ||||
|       <br> | ||||
|       <i>{{ error.error }}</i> | ||||
|     </div> | ||||
|   </ng-template> | ||||
| 
 | ||||
| </div> | ||||
| @ -1,14 +0,0 @@ | ||||
| .progress { | ||||
|   background-color: #2d3348; | ||||
| } | ||||
| 
 | ||||
| @media (min-width: 768px) { | ||||
|   .d-md-block { | ||||
|       display: table-cell !important; | ||||
|   } | ||||
| } | ||||
| @media (min-width: 992px) { | ||||
|   .d-lg-block { | ||||
|       display: table-cell !important; | ||||
|   } | ||||
| } | ||||
| @ -1,140 +0,0 @@ | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { Block } from '../../interfaces/electrs.interface'; | ||||
| import { Subscription, Observable, merge, of } from 'rxjs'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| import { WebsocketService } from 'src/app/services/websocket.service'; | ||||
| import { map } from 'rxjs/operators'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-latest-blocks', | ||||
|   templateUrl: './latest-blocks.component.html', | ||||
|   styleUrls: ['./latest-blocks.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush | ||||
| }) | ||||
| export class LatestBlocksComponent implements OnInit, OnDestroy { | ||||
|   network$: Observable<string>; | ||||
|   error: any; | ||||
|   blocks: any[] = []; | ||||
|   blockSubscription: Subscription; | ||||
|   isLoading = true; | ||||
|   interval: any; | ||||
|   blocksLoadingStatus$: Observable<number>; | ||||
| 
 | ||||
|   latestBlockHeight: number; | ||||
| 
 | ||||
|   heightOfPageUntilBlocks = 150; | ||||
|   heightOfBlocksTableChunk = 470; | ||||
| 
 | ||||
|   constructor( | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     public stateService: StateService, | ||||
|     private seoService: SeoService, | ||||
|     private websocketService: WebsocketService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`); | ||||
|     this.websocketService.want(['blocks']); | ||||
| 
 | ||||
|     this.network$ = merge(of(''), this.stateService.networkChanged$); | ||||
| 
 | ||||
|     this.blocksLoadingStatus$ = this.stateService.loadingIndicators$ | ||||
|       .pipe( | ||||
|         map((indicators) => indicators['blocks'] !== undefined ? indicators['blocks'] : 0) | ||||
|       ); | ||||
| 
 | ||||
|     this.blockSubscription = this.stateService.blocks$ | ||||
|       .subscribe(([block]) => { | ||||
|         if (block === null || !this.blocks.length) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         this.latestBlockHeight = block.height; | ||||
| 
 | ||||
|         if (block.height === this.blocks[0].height) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         // If we are out of sync, reload the blocks instead
 | ||||
|         if (block.height > this.blocks[0].height + 1) { | ||||
|           this.loadInitialBlocks(); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         if (block.height <= this.blocks[0].height) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         this.blocks.pop(); | ||||
|         this.blocks.unshift(block); | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
| 
 | ||||
|     this.loadInitialBlocks(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     clearInterval(this.interval); | ||||
|     this.blockSubscription.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   loadInitialBlocks() { | ||||
|     this.electrsApiService.listBlocks$() | ||||
|       .subscribe((blocks) => { | ||||
|         this.blocks = blocks; | ||||
|         this.isLoading = false; | ||||
|         this.error = undefined; | ||||
| 
 | ||||
|         this.latestBlockHeight = blocks[0].height; | ||||
| 
 | ||||
|         const spaceForBlocks = window.innerHeight - this.heightOfPageUntilBlocks; | ||||
|         const chunks = Math.ceil(spaceForBlocks / this.heightOfBlocksTableChunk) - 1; | ||||
|         if (chunks > 0) { | ||||
|           this.loadMore(chunks); | ||||
|         } | ||||
|         this.cd.markForCheck(); | ||||
|       }, | ||||
|       (error) => { | ||||
|         console.log(error); | ||||
|         this.error = error; | ||||
|         this.isLoading = false; | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   loadMore(chunks = 0) { | ||||
|     if (this.isLoading) { | ||||
|       return; | ||||
|     } | ||||
|     const height = this.blocks[this.blocks.length - 1].height - 1; | ||||
|     if (height < 0) { | ||||
|       return; | ||||
|     } | ||||
|     this.isLoading = true; | ||||
|     this.electrsApiService.listBlocks$(height) | ||||
|       .subscribe((blocks) => { | ||||
|         this.blocks = this.blocks.concat(blocks); | ||||
|         this.isLoading = false; | ||||
|         this.error = undefined; | ||||
| 
 | ||||
|         const chunksLeft = chunks - 1; | ||||
|         if (chunksLeft > 0) { | ||||
|           this.loadMore(chunksLeft); | ||||
|         } | ||||
|         this.cd.markForCheck(); | ||||
|       }, | ||||
|       (error) => { | ||||
|         console.log(error); | ||||
|         this.error = error; | ||||
|         this.isLoading = false; | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   trackByBlock(index: number, block: Block) { | ||||
|     return block.height; | ||||
|   } | ||||
| } | ||||
| @ -51,7 +51,7 @@ | ||||
|         <div class="card-body"> | ||||
|           <h5 class="card-title" i18n="dashboard.latest-blocks">Latest blocks</h5> | ||||
|           <app-blocks-list [widget]=true></app-blocks-list> | ||||
|           <div><a [routerLink]="['/mining/blocks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> | ||||
|           <div><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
| @ -136,7 +136,7 @@ | ||||
|                 </tr> | ||||
|               </tbody> | ||||
|             </table> | ||||
|             <div class=""><a href="" [routerLink]="[(stateService.env.MINING_DASHBOARD ? '/mining/blocks' : '/blocks') | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> | ||||
|             <div class=""><a href="" [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
| @ -198,9 +198,7 @@ | ||||
|     display: none; | ||||
|     text-align: right; | ||||
|     width: 20%; | ||||
|     @media (min-width: 376px) { | ||||
|       display: table-cell; | ||||
|     } | ||||
|     display: table-cell; | ||||
|   } | ||||
|   .table-cell-size { | ||||
|     display: none; | ||||
|  | ||||
| @ -3834,34 +3834,34 @@ export const restApiDocsData = [ | ||||
|           curl: [`1w`], | ||||
|           response: `[
 | ||||
|   { | ||||
|     "avg_height": 735644, | ||||
|     "avgHeight": 735644, | ||||
|     "timestamp": 1652119111, | ||||
|     "avg_fees": 24212890 | ||||
|     "avgFees": 24212890 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 735646, | ||||
|     "avgHeight": 735646, | ||||
|     "timestamp": 1652120252, | ||||
|     "avg_fees": 21655996 | ||||
|     "avgFees": 21655996 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 735648, | ||||
|     "avgHeight": 735648, | ||||
|     "timestamp": 1652121214, | ||||
|     "avg_fees": 20678859 | ||||
|     "avgFees": 20678859 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 735649, | ||||
|     "avgHeight": 735649, | ||||
|     "timestamp": 1652121757, | ||||
|     "avg_fees": 21020140 | ||||
|     "avgFees": 21020140 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 735650, | ||||
|     "avgHeight": 735650, | ||||
|     "timestamp": 1652122367, | ||||
|     "avg_fees": 23064200 | ||||
|     "avgFees": 23064200 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 735652, | ||||
|     "avgHeight": 735652, | ||||
|     "timestamp": 1652122893, | ||||
|     "avg_fees": 17620723 | ||||
|     "avgFees": 17620723 | ||||
|   }, | ||||
|   ... | ||||
| ]` | ||||
| @ -3872,14 +3872,14 @@ export const restApiDocsData = [ | ||||
|           curl: [`1w`], | ||||
|           response: `[
 | ||||
|   { | ||||
|     "avg_height": 2224253, | ||||
|     "avgHeight": 2224253, | ||||
|     "timestamp": 1652346420, | ||||
|     "avg_fees": 211686 | ||||
|     "avgFees": 211686 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 2224254, | ||||
|     "avgHeight": 2224254, | ||||
|     "timestamp": 1652346850, | ||||
|     "avg_fees": 2565952 | ||||
|     "avgFees": 2565952 | ||||
|   }, | ||||
|   ... | ||||
| ]` | ||||
| @ -3890,14 +3890,14 @@ export const restApiDocsData = [ | ||||
|           curl: [`1w`], | ||||
|           response: `[
 | ||||
|   { | ||||
|     "avg_height": 89978, | ||||
|     "avgHeight": 89978, | ||||
|     "timestamp": 1652346573, | ||||
|     "avg_fees": 1071 | ||||
|     "avgFees": 1071 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 89979, | ||||
|     "avgHeight": 89979, | ||||
|     "timestamp": 1652346970, | ||||
|     "avg_fees": 1224 | ||||
|     "avgFees": 1224 | ||||
|   }, | ||||
|   ... | ||||
| ]` | ||||
| @ -3933,29 +3933,29 @@ export const restApiDocsData = [ | ||||
|           curl: [`1d`], | ||||
|           response: `[
 | ||||
|   { | ||||
|     "avg_height": 599992, | ||||
|     "avgHeight": 599992, | ||||
|     "timestamp": 1571438412, | ||||
|     "avg_rewards": 1260530933 | ||||
|     "avgRewards": 1260530933 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 600000, | ||||
|     "avgHeight": 600000, | ||||
|     "timestamp": 1571443398, | ||||
|     "avg_rewards": 1264314538 | ||||
|     "avgRewards": 1264314538 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 725441, | ||||
|     "avgHeight": 725441, | ||||
|     "timestamp": 1646139035, | ||||
|     "avg_rewards": 637067563 | ||||
|     "avgRewards": 637067563 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 725585, | ||||
|     "avgHeight": 725585, | ||||
|     "timestamp": 1646222444, | ||||
|     "avg_rewards": 646519104 | ||||
|     "avgRewards": 646519104 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 725727, | ||||
|     "avgHeight": 725727, | ||||
|     "timestamp": 1646308374, | ||||
|     "avg_rewards": 638709605 | ||||
|     "avgRewards": 638709605 | ||||
|   }, | ||||
|   ... | ||||
| ]` | ||||
| @ -3966,14 +3966,14 @@ export const restApiDocsData = [ | ||||
|           curl: [`1d`], | ||||
|           response: `[
 | ||||
|   { | ||||
|     "avg_height": 12, | ||||
|     "avgHeight": 12, | ||||
|     "timestamp": 1296689648, | ||||
|     "avg_rewards": 5000000000 | ||||
|     "avgRewards": 5000000000 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 269, | ||||
|     "avgHeight": 269, | ||||
|     "timestamp": 1296717674, | ||||
|     "avg_rewards": 5000091820 | ||||
|     "avgRewards": 5000091820 | ||||
|   }, | ||||
|   ... | ||||
| ]` | ||||
| @ -3984,14 +3984,14 @@ export const restApiDocsData = [ | ||||
|           curl: [`1d`], | ||||
|           response: `[
 | ||||
|   { | ||||
|     "avg_height": 183, | ||||
|     "avgHeight": 183, | ||||
|     "timestamp": 1598962247, | ||||
|     "avg_rewards": 5000000000 | ||||
|     "avgRewards": 5000000000 | ||||
|   }, | ||||
|   { | ||||
|     "avg_height": 576, | ||||
|     "avgHeight": 576, | ||||
|     "timestamp": 1599047892, | ||||
|     "avg_rewards": 5000000000 | ||||
|     "avgRewards": 5000000000 | ||||
|   }, | ||||
|   ... | ||||
| ]` | ||||
| @ -4029,37 +4029,37 @@ export const restApiDocsData = [ | ||||
|   "oldestIndexedBlockTimestamp": 1571434851, | ||||
|   "blockFeeRates": [ | ||||
|     { | ||||
|       "avg_height": 732152, | ||||
|       "avgHeight": 732152, | ||||
|       "timestamp": 1650132959, | ||||
|       "avg_fee_0": 1, | ||||
|       "avg_fee_10": 2, | ||||
|       "avg_fee_25": 2, | ||||
|       "avg_fee_50": 3, | ||||
|       "avg_fee_75": 4, | ||||
|       "avg_fee_90": 8, | ||||
|       "avg_fee_100": 393 | ||||
|       "avgFee_0": 1, | ||||
|       "avgFee_10": 2, | ||||
|       "avgFee_25": 2, | ||||
|       "avgFee_50": 3, | ||||
|       "avgFee_75": 4, | ||||
|       "avgFee_90": 8, | ||||
|       "avgFee_100": 393 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 732158, | ||||
|       "avgHeight": 732158, | ||||
|       "timestamp": 1650134432, | ||||
|       "avg_fee_0": 1, | ||||
|       "avg_fee_10": 1, | ||||
|       "avg_fee_25": 2, | ||||
|       "avg_fee_50": 4, | ||||
|       "avg_fee_75": 6, | ||||
|       "avg_fee_90": 10, | ||||
|       "avg_fee_100": 240 | ||||
|       "avgFee_0": 1, | ||||
|       "avgFee_10": 1, | ||||
|       "avgFee_25": 2, | ||||
|       "avgFee_50": 4, | ||||
|       "avgFee_75": 6, | ||||
|       "avgFee_90": 10, | ||||
|       "avgFee_100": 240 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 732161, | ||||
|       "avgHeight": 732161, | ||||
|       "timestamp": 1650135818, | ||||
|       "avg_fee_0": 1, | ||||
|       "avg_fee_10": 1, | ||||
|       "avg_fee_25": 1, | ||||
|       "avg_fee_50": 2, | ||||
|       "avg_fee_75": 5, | ||||
|       "avg_fee_90": 8, | ||||
|       "avg_fee_100": 251 | ||||
|       "avgFee_0": 1, | ||||
|       "avgFee_10": 1, | ||||
|       "avgFee_25": 1, | ||||
|       "avgFee_50": 2, | ||||
|       "avgFee_75": 5, | ||||
|       "avgFee_90": 8, | ||||
|       "avgFee_100": 251 | ||||
|     }, | ||||
|     ... | ||||
|   ] | ||||
| @ -4073,26 +4073,26 @@ export const restApiDocsData = [ | ||||
|   "oldestIndexedBlockTimestamp": 1296688602, | ||||
|   "blockFeeRates": [ | ||||
|     { | ||||
|       "avg_height": 2196306, | ||||
|       "avgHeight": 2196306, | ||||
|       "timestamp": 1650360168, | ||||
|       "avg_fee_0": 1, | ||||
|       "avg_fee_10": 1, | ||||
|       "avg_fee_25": 1, | ||||
|       "avg_fee_50": 1, | ||||
|       "avg_fee_75": 2, | ||||
|       "avg_fee_90": 28, | ||||
|       "avg_fee_100": 2644 | ||||
|       "avgFee_0": 1, | ||||
|       "avgFee_10": 1, | ||||
|       "avgFee_25": 1, | ||||
|       "avgFee_50": 1, | ||||
|       "avgFee_75": 2, | ||||
|       "avgFee_90": 28, | ||||
|       "avgFee_100": 2644 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 2196308, | ||||
|       "avgHeight": 2196308, | ||||
|       "timestamp": 1650361209, | ||||
|       "avg_fee_0": 1, | ||||
|       "avg_fee_10": 1, | ||||
|       "avg_fee_25": 1, | ||||
|       "avg_fee_50": 4, | ||||
|       "avg_fee_75": 12, | ||||
|       "avg_fee_90": 65, | ||||
|       "avg_fee_100": 102 | ||||
|       "avgFee_0": 1, | ||||
|       "avgFee_10": 1, | ||||
|       "avgFee_25": 1, | ||||
|       "avgFee_50": 4, | ||||
|       "avgFee_75": 12, | ||||
|       "avgFee_90": 65, | ||||
|       "avgFee_100": 102 | ||||
|     }, | ||||
|     ... | ||||
|   ] | ||||
| @ -4106,26 +4106,26 @@ export const restApiDocsData = [ | ||||
|   "oldestIndexedBlockTimestamp": 1598918400, | ||||
|   "blockFeeRates": [ | ||||
|     { | ||||
|       "avg_height": 86620, | ||||
|       "avgHeight": 86620, | ||||
|       "timestamp": 1650360010, | ||||
|       "avg_fee_0": 1, | ||||
|       "avg_fee_10": 1, | ||||
|       "avg_fee_25": 1, | ||||
|       "avg_fee_50": 1, | ||||
|       "avg_fee_75": 1, | ||||
|       "avg_fee_90": 1, | ||||
|       "avg_fee_100": 1 | ||||
|       "avgFee_0": 1, | ||||
|       "avgFee_10": 1, | ||||
|       "avgFee_25": 1, | ||||
|       "avgFee_50": 1, | ||||
|       "avgFee_75": 1, | ||||
|       "avgFee_90": 1, | ||||
|       "avgFee_100": 1 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 86623, | ||||
|       "avgHeight": 86623, | ||||
|       "timestamp": 1650361330, | ||||
|       "avg_fee_0": 1, | ||||
|       "avg_fee_10": 1, | ||||
|       "avg_fee_25": 1, | ||||
|       "avg_fee_50": 1, | ||||
|       "avg_fee_75": 1, | ||||
|       "avg_fee_90": 1, | ||||
|       "avg_fee_100": 1 | ||||
|       "avgFee_0": 1, | ||||
|       "avgFee_10": 1, | ||||
|       "avgFee_25": 1, | ||||
|       "avgFee_50": 1, | ||||
|       "avgFee_75": 1, | ||||
|       "avgFee_90": 1, | ||||
|       "avgFee_100": 1 | ||||
|     }, | ||||
|     ... | ||||
|   ] | ||||
| @ -4163,47 +4163,47 @@ export const restApiDocsData = [ | ||||
|           response: `{
 | ||||
|   "sizes": [ | ||||
|     { | ||||
|       "avg_height": 576650, | ||||
|       "avgHeight": 576650, | ||||
|       "timestamp": 1558212081, | ||||
|       "avg_size": 1271404 | ||||
|       "avgSize": 1271404 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 576715, | ||||
|       "avgHeight": 576715, | ||||
|       "timestamp": 1558246272, | ||||
|       "avg_size": 1105893 | ||||
|       "avgSize": 1105893 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 576797, | ||||
|       "avgHeight": 576797, | ||||
|       "timestamp": 1558289379, | ||||
|       "avg_size": 1141071 | ||||
|       "avgSize": 1141071 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 576885, | ||||
|       "avgHeight": 576885, | ||||
|       "timestamp": 1558330184, | ||||
|       "avg_size": 1108166 | ||||
|       "avgSize": 1108166 | ||||
|     }, | ||||
|     ... | ||||
|   ], | ||||
|   "weights": [ | ||||
|     { | ||||
|       "avg_height": 576650, | ||||
|       "avgHeight": 576650, | ||||
|       "timestamp": 1558212081, | ||||
|       "avg_weight": 3994002 | ||||
|       "avgWeight": 3994002 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 576715, | ||||
|       "avgHeight": 576715, | ||||
|       "timestamp": 1558246272, | ||||
|       "avg_weight": 3756312 | ||||
|       "avgWeight": 3756312 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 576797, | ||||
|       "avgHeight": 576797, | ||||
|       "timestamp": 1558289379, | ||||
|       "avg_weight": 3719625 | ||||
|       "avgWeight": 3719625 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 576885, | ||||
|       "avgHeight": 576885, | ||||
|       "timestamp": 1558330184, | ||||
|       "avg_weight": 3631381 | ||||
|       "avgWeight": 3631381 | ||||
|     }, | ||||
|     ... | ||||
|   ] | ||||
| @ -4216,27 +4216,27 @@ export const restApiDocsData = [ | ||||
|           response: `{
 | ||||
|   "sizes": [ | ||||
|     { | ||||
|       "avg_height": 1517188, | ||||
|       "avgHeight": 1517188, | ||||
|       "timestamp": 1558262730, | ||||
|       "avg_size": 25089 | ||||
|       "avgSize": 25089 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 1517275, | ||||
|       "avgHeight": 1517275, | ||||
|       "timestamp": 1558290933, | ||||
|       "avg_size": 21679 | ||||
|       "avgSize": 21679 | ||||
|     }, | ||||
|     ... | ||||
|   ], | ||||
|   "weights": [ | ||||
|     { | ||||
|       "avg_height": 1517188, | ||||
|       "avgHeight": 1517188, | ||||
|       "timestamp": 1558262730, | ||||
|       "avg_weight": 74921 | ||||
|       "avgWeight": 74921 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 1517275, | ||||
|       "avgHeight": 1517275, | ||||
|       "timestamp": 1558290933, | ||||
|       "avg_weight": 65164 | ||||
|       "avgWeight": 65164 | ||||
|     }, | ||||
|     ... | ||||
|   ] | ||||
| @ -4249,27 +4249,27 @@ export const restApiDocsData = [ | ||||
|           response: `{
 | ||||
|   "sizes": [ | ||||
|     { | ||||
|       "avg_height": 83, | ||||
|       "avgHeight": 83, | ||||
|       "timestamp": 1598937527, | ||||
|       "avg_size": 329 | ||||
|       "avgSize": 329 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 266, | ||||
|       "avgHeight": 266, | ||||
|       "timestamp": 1598982991, | ||||
|       "avg_size": 330 | ||||
|       "avgSize": 330 | ||||
|     }, | ||||
|     ... | ||||
|   ], | ||||
|   "weights": [ | ||||
|     { | ||||
|       "avg_height": 83, | ||||
|       "avgHeight": 83, | ||||
|       "timestamp": 1598937527, | ||||
|       "avg_weight": 1209 | ||||
|       "avgWeight": 1209 | ||||
|     }, | ||||
|     { | ||||
|       "avg_height": 266, | ||||
|       "avgHeight": 266, | ||||
|       "timestamp": 1598982991, | ||||
|       "avg_weight": 1212 | ||||
|       "avgWeight": 1212 | ||||
|     }, | ||||
|     ... | ||||
|   ] | ||||
|  | ||||
| @ -149,7 +149,7 @@ export class ApiService { | ||||
| 
 | ||||
|   getBlocks$(from: number): Observable<BlockExtended[]> { | ||||
|     return this.httpClient.get<BlockExtended[]>( | ||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/blocks-extras` + | ||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/blocks` + | ||||
|       (from !== undefined ? `/${from}` : ``) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -48,7 +48,6 @@ import { TransactionsListComponent } from '../components/transactions-list/trans | ||||
| import { BlockComponent } from '../components/block/block.component'; | ||||
| import { AddressComponent } from '../components/address/address.component'; | ||||
| import { SearchFormComponent } from '../components/search-form/search-form.component'; | ||||
| import { LatestBlocksComponent } from '../components/latest-blocks/latest-blocks.component'; | ||||
| import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; | ||||
| import { FooterComponent } from '../components/footer/footer.component'; | ||||
| import { TimeSpanComponent } from '../components/time-span/time-span.component'; | ||||
| @ -113,7 +112,6 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index | ||||
|     BlockComponent, | ||||
|     TransactionsListComponent, | ||||
|     AddressComponent, | ||||
|     LatestBlocksComponent, | ||||
|     SearchFormComponent, | ||||
|     TimeSpanComponent, | ||||
|     AddressLabelsComponent, | ||||
| @ -208,7 +206,6 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index | ||||
|     BlockComponent, | ||||
|     TransactionsListComponent, | ||||
|     AddressComponent, | ||||
|     LatestBlocksComponent, | ||||
|     SearchFormComponent, | ||||
|     TimeSpanComponent, | ||||
|     AddressLabelsComponent, | ||||
|  | ||||
							
								
								
									
										1
									
								
								frontend/src/resources/profile/schildbach.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/profile/schildbach.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="-0.015 -0.015 1.03 1.03" preserveAspectRatio="xMidYMid"><defs><filter id="b"><feGaussianBlur in="SourceAlpha" result="blur-out" stdDeviation=".5"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="c"><feGaussianBlur in="SourceAlpha" result="blur-out" stdDeviation="1.3"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter><linearGradient id="a" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#f9b949"/><stop offset="100%" style="stop-color:#f7931a"/></linearGradient></defs><path d="M63.036 39.741c-4.274 17.143-21.637 27.576-38.782 23.301C7.116 58.768-3.317 41.404.959 24.262 5.23 7.117 22.594-3.317 39.734.957c17.144 4.274 27.576 21.64 23.302 38.784z" style="fill:url(#a)" filter="url(#b)" transform="scale(.01563)"/><path d="M46.1 27.441c.638-4.258-2.604-6.547-7.037-8.074l1.438-5.768-3.511-.875-1.4 5.616c-.923-.23-1.871-.447-2.813-.662l1.41-5.653-3.51-.875-1.438 5.766c-.764-.174-1.514-.346-2.242-.527l.004-.018-4.842-1.209-.934 3.75s2.605.597 2.55.634c1.422.355 1.679 1.296 1.636 2.042l-1.638 6.571c.098.025.225.061.365.117l-.371-.092-2.296 9.205c-.174.432-.615 1.08-1.61.834.036.051-2.551-.637-2.551-.637l-1.743 4.019 4.569 1.139c.85.213 1.683.436 2.503.646l-1.453 5.834 3.507.875 1.439-5.772c.958.26 1.888.5 2.798.726l-1.434 5.745 3.51.875 1.454-5.823c5.987 1.133 10.489.676 12.384-4.739 1.527-4.36-.076-6.875-3.226-8.515 2.294-.529 4.022-2.038 4.483-5.155zM38.08 38.69c-1.085 4.36-8.426 2.003-10.806 1.412l1.928-7.729c2.38.594 10.012 1.77 8.878 6.317zm1.086-11.312c-.99 3.966-7.1 1.951-9.082 1.457l1.748-7.01c1.982.494 8.365 1.416 7.334 5.553z" style="fill:#fff" filter="url(#c)" transform="scale(.01563)"/></svg> | ||||
| After Width: | Height: | Size: 1.7 KiB | 
| @ -4,6 +4,7 @@ slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/ | ||||
| 
 | ||||
| while true | ||||
| do for url in / \ | ||||
| 	'/api/v1/blocks' \ | ||||
| 	'/api/v1/statistics/2h' \ | ||||
| 	'/api/v1/statistics/24h' \ | ||||
| 	'/api/v1/statistics/1w' \ | ||||
| @ -36,7 +37,6 @@ do for url in / \ | ||||
| 	'/api/v1/mining/hashrate/pools/3y' \ | ||||
| 	'/api/v1/mining/hashrate/pools/all' \ | ||||
| 	'/api/v1/mining/reward-stats/144' \ | ||||
| 	'/api/v1/mining/blocks-extras' \ | ||||
| 	'/api/v1/mining/blocks/fees/24h' \ | ||||
| 	'/api/v1/mining/blocks/fees/3d' \ | ||||
| 	'/api/v1/mining/blocks/fees/1w' \ | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user