Merge branch 'master' into simon/mempool-break-limit
This commit is contained in:
		
						commit
						52c813bcc7
					
				| @ -399,9 +399,13 @@ class BitcoinRoutes { | |||||||
| 
 | 
 | ||||||
|   private async getBlockAuditSummary(req: Request, res: Response) { |   private async getBlockAuditSummary(req: Request, res: Response) { | ||||||
|     try { |     try { | ||||||
|       const transactions = await blocks.$getBlockAuditSummary(req.params.hash); |       const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); | ||||||
|  |       if (auditSummary) { | ||||||
|         res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); |         res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); | ||||||
|       res.json(transactions); |         res.json(auditSummary); | ||||||
|  |       } else { | ||||||
|  |         return res.status(404).send(`audit not available`); | ||||||
|  |       } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       res.status(500).send(e instanceof Error ? e.message : e); |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -158,6 +158,13 @@ class Blocks { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { | ||||||
|  |     return { | ||||||
|  |       id: hash, | ||||||
|  |       transactions: Common.stripTransactions(transactions), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock { |   private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock { | ||||||
|     block.tx.forEach(tx => { |     block.tx.forEach(tx => { | ||||||
|       tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0); |       tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0); | ||||||
| @ -646,7 +653,7 @@ class Blocks { | |||||||
|       } |       } | ||||||
|       const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions); |       const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions); | ||||||
|       const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); |       const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); | ||||||
|       const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock); |       const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); | ||||||
|       this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); |       this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); | ||||||
| 
 | 
 | ||||||
|       // start async callbacks
 |       // start async callbacks
 | ||||||
| @ -668,12 +675,13 @@ class Blocks { | |||||||
|             for (let i = 10; i >= 0; --i) { |             for (let i = 10; i >= 0; --i) { | ||||||
|               const newBlock = await this.$indexBlock(lastBlock.height - i); |               const newBlock = await this.$indexBlock(lastBlock.height - i); | ||||||
|               this.updateTimerProgress(timer, `reindexed block`); |               this.updateTimerProgress(timer, `reindexed block`); | ||||||
|               await this.$getStrippedBlockTransactions(newBlock.id, true, true); |               let cpfpSummary; | ||||||
|               this.updateTimerProgress(timer, `reindexed block summary`); |  | ||||||
|               if (config.MEMPOOL.CPFP_INDEXING) { |               if (config.MEMPOOL.CPFP_INDEXING) { | ||||||
|                 await this.$indexCPFP(newBlock.id, lastBlock.height - i); |                 cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i); | ||||||
|                 this.updateTimerProgress(timer, `reindexed block cpfp`); |                 this.updateTimerProgress(timer, `reindexed block cpfp`); | ||||||
|               } |               } | ||||||
|  |               await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height); | ||||||
|  |               this.updateTimerProgress(timer, `reindexed block summary`); | ||||||
|             } |             } | ||||||
|             await mining.$indexDifficultyAdjustments(); |             await mining.$indexDifficultyAdjustments(); | ||||||
|             await DifficultyAdjustmentsRepository.$deleteLastAdjustment(); |             await DifficultyAdjustmentsRepository.$deleteLastAdjustment(); | ||||||
| @ -704,7 +712,7 @@ class Blocks { | |||||||
| 
 | 
 | ||||||
|           // Save blocks summary for visualization if it's enabled
 |           // Save blocks summary for visualization if it's enabled
 | ||||||
|           if (Common.blocksSummariesIndexingEnabled() === true) { |           if (Common.blocksSummariesIndexingEnabled() === true) { | ||||||
|             await this.$getStrippedBlockTransactions(blockExtended.id, true); |             await this.$getStrippedBlockTransactions(blockExtended.id, true, false, cpfpSummary, blockExtended.height); | ||||||
|             this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`); |             this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`); | ||||||
|           } |           } | ||||||
|           if (config.MEMPOOL.CPFP_INDEXING) { |           if (config.MEMPOOL.CPFP_INDEXING) { | ||||||
| @ -730,6 +738,11 @@ class Blocks { | |||||||
|         this.currentDifficulty = block.difficulty; |         this.currentDifficulty = block.difficulty; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       // wait for pending async callbacks to finish
 | ||||||
|  |       this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`); | ||||||
|  |       await Promise.all(callbackPromises); | ||||||
|  |       this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`); | ||||||
|  | 
 | ||||||
|       this.blocks.push(blockExtended); |       this.blocks.push(blockExtended); | ||||||
|       if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) { |       if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) { | ||||||
|         this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4); |         this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4); | ||||||
| @ -746,11 +759,6 @@ class Blocks { | |||||||
|         diskCache.$saveCacheToDisk(); |         diskCache.$saveCacheToDisk(); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // wait for pending async callbacks to finish
 |  | ||||||
|       this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`); |  | ||||||
|       await Promise.all(callbackPromises); |  | ||||||
|       this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`); |  | ||||||
| 
 |  | ||||||
|       handledBlocks++; |       handledBlocks++; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -827,7 +835,7 @@ class Blocks { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false, |   public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false, | ||||||
|     skipDBLookup = false): Promise<TransactionStripped[]> |     skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise<TransactionStripped[]> | ||||||
|   { |   { | ||||||
|     if (skipMemoryCache === false) { |     if (skipMemoryCache === false) { | ||||||
|       // Check the memory cache
 |       // Check the memory cache
 | ||||||
| @ -845,13 +853,35 @@ class Blocks { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     let height = blockHeight; | ||||||
|  |     let summary: BlockSummary; | ||||||
|  |     if (cpfpSummary) { | ||||||
|  |       summary = { | ||||||
|  |         id: hash, | ||||||
|  |         transactions: cpfpSummary.transactions.map(tx => { | ||||||
|  |           return { | ||||||
|  |             txid: tx.txid, | ||||||
|  |             fee: tx.fee, | ||||||
|  |             vsize: tx.vsize, | ||||||
|  |             value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)), | ||||||
|  |             rate: tx.effectiveFeePerVsize | ||||||
|  |           }; | ||||||
|  |         }), | ||||||
|  |       }; | ||||||
|  |     } else { | ||||||
|       // Call Core RPC
 |       // Call Core RPC
 | ||||||
|       const block = await bitcoinClient.getBlock(hash, 2); |       const block = await bitcoinClient.getBlock(hash, 2); | ||||||
|     const summary = this.summarizeBlock(block); |       summary = this.summarizeBlock(block); | ||||||
|  |       height = block.height; | ||||||
|  |     } | ||||||
|  |     if (height == null) { | ||||||
|  |       const block = await bitcoinApi.$getBlock(hash); | ||||||
|  |       height = block.height; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // Index the response if needed
 |     // Index the response if needed
 | ||||||
|     if (Common.blocksSummariesIndexingEnabled() === true) { |     if (Common.blocksSummariesIndexingEnabled() === true) { | ||||||
|       await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions); |       await BlocksSummariesRepository.$saveTransactions(height, hash, summary.transactions); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return summary.transactions; |     return summary.transactions; | ||||||
| @ -1007,19 +1037,11 @@ class Blocks { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $getBlockAuditSummary(hash: string): Promise<any> { |   public async $getBlockAuditSummary(hash: string): Promise<any> { | ||||||
|     let summary; |  | ||||||
|     if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { |     if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { | ||||||
|       summary = await BlocksAuditsRepository.$getBlockAudit(hash); |       return BlocksAuditsRepository.$getBlockAudit(hash); | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     // fallback to non-audited transaction summary
 |  | ||||||
|     if (!summary?.transactions?.length) { |  | ||||||
|       const strippedTransactions = await this.$getStrippedBlockTransactions(hash); |  | ||||||
|       summary = { |  | ||||||
|         transactions: strippedTransactions |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|     return summary; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public getLastDifficultyAdjustmentTime(): number { |   public getLastDifficultyAdjustmentTime(): number { | ||||||
| @ -1050,10 +1072,14 @@ class Blocks { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> { |   public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> { | ||||||
|  |     try { | ||||||
|       const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters); |       const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters); | ||||||
|       if (!result) { |       if (!result) { | ||||||
|         await cpfpRepository.$insertProgressMarker(height); |         await cpfpRepository.$insertProgressMarker(height); | ||||||
|       } |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       // not a fatal error, we'll try again next time the indexer runs
 | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -113,6 +113,10 @@ export class Common { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   static stripTransactions(txs: TransactionExtended[]): TransactionStripped[] { | ||||||
|  |     return txs.map(this.stripTransaction); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   static sleep$(ms: number): Promise<void> { |   static sleep$(ms: number): Promise<void> { | ||||||
|     return new Promise((resolve) => { |     return new Promise((resolve) => { | ||||||
|        setTimeout(() => { |        setTimeout(() => { | ||||||
|  | |||||||
| @ -143,7 +143,7 @@ class MempoolBlocks { | |||||||
|           const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0); |           const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0); | ||||||
|           if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) { |           if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) { | ||||||
|             onlineStats = true; |             onlineStats = true; | ||||||
|             feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5); |             feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); | ||||||
|             feeStatsCalculator.processNext(tx); |             feeStatsCalculator.processNext(tx); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @ -334,7 +334,7 @@ class MempoolBlocks { | |||||||
|     if (hasBlockStack) { |     if (hasBlockStack) { | ||||||
|       stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0); |       stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0); | ||||||
|       hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS; |       hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS; | ||||||
|       feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5); |       feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees, feeStats }[] = []; |     const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees, feeStats }[] = []; | ||||||
|  | |||||||
| @ -211,7 +211,7 @@ class StatisticsApi { | |||||||
|       CAST(avg(vsize_1800) as DOUBLE) as vsize_1800, |       CAST(avg(vsize_1800) as DOUBLE) as vsize_1800, | ||||||
|       CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \ |       CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \ | ||||||
|       FROM statistics \ |       FROM statistics \ | ||||||
|       WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \ |       ${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \ | ||||||
|       GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \ |       GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \ | ||||||
|       ORDER BY statistics.added DESC;`;
 |       ORDER BY statistics.added DESC;`;
 | ||||||
|   } |   } | ||||||
| @ -259,7 +259,7 @@ class StatisticsApi { | |||||||
|       vsize_1800, |       vsize_1800, | ||||||
|       vsize_2000 \ |       vsize_2000 \ | ||||||
|       FROM statistics \ |       FROM statistics \ | ||||||
|       WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \ |       ${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \ | ||||||
|       GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \ |       GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \ | ||||||
|       ORDER BY statistics.added DESC;`;
 |       ORDER BY statistics.added DESC;`;
 | ||||||
|   } |   } | ||||||
| @ -386,6 +386,17 @@ class StatisticsApi { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $listAll(): Promise<OptimizedStatistic[]> { | ||||||
|  |     try { | ||||||
|  |       const query = this.getQueryForDays(43200, 'all'); // 12h interval
 | ||||||
|  |       const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); | ||||||
|  |       return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$listAll() error' + (e instanceof Error ? e.message : e)); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] { |   private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] { | ||||||
|     return statistic.map((s) => { |     return statistic.map((s) => { | ||||||
|       return { |       return { | ||||||
|  | |||||||
| @ -15,10 +15,11 @@ class StatisticsRoutes { | |||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y')) |       .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y')) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y')) |       .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y')) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y')) |       .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y')) | ||||||
|  |       .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/all', this.$getStatisticsByTime.bind(this, 'all')) | ||||||
|     ; |     ; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) { |   private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y' | 'all', req: Request, res: Response) { | ||||||
|     res.header('Pragma', 'public'); |     res.header('Pragma', 'public'); | ||||||
|     res.header('Cache-control', 'public'); |     res.header('Cache-control', 'public'); | ||||||
|     res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); |     res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); | ||||||
| @ -26,10 +27,6 @@ class StatisticsRoutes { | |||||||
|     try { |     try { | ||||||
|       let result; |       let result; | ||||||
|       switch (time as string) { |       switch (time as string) { | ||||||
|         case '2h': |  | ||||||
|           result = await statisticsApi.$list2H(); |  | ||||||
|           res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); |  | ||||||
|           break; |  | ||||||
|         case '24h': |         case '24h': | ||||||
|           result = await statisticsApi.$list24H(); |           result = await statisticsApi.$list24H(); | ||||||
|           res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); |           res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); | ||||||
| @ -58,8 +55,13 @@ class StatisticsRoutes { | |||||||
|         case '4y': |         case '4y': | ||||||
|           result = await statisticsApi.$list4Y(); |           result = await statisticsApi.$list4Y(); | ||||||
|           break; |           break; | ||||||
|  |         case 'all': | ||||||
|  |           result = await statisticsApi.$listAll(); | ||||||
|  |           break; | ||||||
|         default: |         default: | ||||||
|           result = await statisticsApi.$list2H(); |           result = await statisticsApi.$list2H(); | ||||||
|  |           res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); | ||||||
|  |           break; | ||||||
|       } |       } | ||||||
|       res.json(result); |       res.json(result); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  | |||||||
| @ -22,6 +22,14 @@ import { deepClone } from '../utils/clone'; | |||||||
| import priceUpdater from '../tasks/price-updater'; | import priceUpdater from '../tasks/price-updater'; | ||||||
| import { ApiPrice } from '../repositories/PricesRepository'; | import { ApiPrice } from '../repositories/PricesRepository'; | ||||||
| 
 | 
 | ||||||
|  | // valid 'want' subscriptions
 | ||||||
|  | const wantable = [ | ||||||
|  |   'blocks', | ||||||
|  |   'mempool-blocks', | ||||||
|  |   'live-2h-chart', | ||||||
|  |   'stats', | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
| class WebsocketHandler { | class WebsocketHandler { | ||||||
|   private wss: WebSocket.Server | undefined; |   private wss: WebSocket.Server | undefined; | ||||||
|   private extraInitProperties = {}; |   private extraInitProperties = {}; | ||||||
| @ -30,7 +38,7 @@ class WebsocketHandler { | |||||||
|   private numConnected = 0; |   private numConnected = 0; | ||||||
|   private numDisconnected = 0; |   private numDisconnected = 0; | ||||||
| 
 | 
 | ||||||
|   private initData: { [key: string]: string } = {}; |   private socketData: { [key: string]: string } = {}; | ||||||
|   private serializedInitData: string = '{}'; |   private serializedInitData: string = '{}'; | ||||||
| 
 | 
 | ||||||
|   constructor() { } |   constructor() { } | ||||||
| @ -39,28 +47,28 @@ class WebsocketHandler { | |||||||
|     this.wss = wss; |     this.wss = wss; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setExtraInitProperties(property: string, value: any) { |   setExtraInitData(property: string, value: any) { | ||||||
|     this.extraInitProperties[property] = value; |     this.extraInitProperties[property] = value; | ||||||
|     this.setInitDataFields(this.extraInitProperties); |     this.updateSocketDataFields(this.extraInitProperties); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private setInitDataFields(data: { [property: string]: any }): void { |   private updateSocketDataFields(data: { [property: string]: any }): void { | ||||||
|     for (const property of Object.keys(data)) { |     for (const property of Object.keys(data)) { | ||||||
|       if (data[property] != null) { |       if (data[property] != null) { | ||||||
|         this.initData[property] = JSON.stringify(data[property]); |         this.socketData[property] = JSON.stringify(data[property]); | ||||||
|       } else { |       } else { | ||||||
|         delete this.initData[property]; |         delete this.socketData[property]; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     this.serializedInitData = '{' |     this.serializedInitData = '{' | ||||||
|       + Object.keys(this.initData).map(key => `"${key}": ${this.initData[key]}`).join(', ') |     + Object.keys(this.socketData).map(key => `"${key}": ${this.socketData[key]}`).join(', ') | ||||||
|     + '}'; |     + '}'; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private updateInitData(): void { |   private updateSocketData(): void { | ||||||
|     const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); |     const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); | ||||||
|     const da = difficultyAdjustment.getDifficultyAdjustment(); |     const da = difficultyAdjustment.getDifficultyAdjustment(); | ||||||
|     this.setInitDataFields({ |     this.updateSocketDataFields({ | ||||||
|       'mempoolInfo': memPool.getMempoolInfo(), |       'mempoolInfo': memPool.getMempoolInfo(), | ||||||
|       'vBytesPerSecond': memPool.getVBytesPerSecond(), |       'vBytesPerSecond': memPool.getVBytesPerSecond(), | ||||||
|       'blocks': _blocks, |       'blocks': _blocks, | ||||||
| @ -94,11 +102,33 @@ class WebsocketHandler { | |||||||
|           const parsedMessage: WebsocketResponse = JSON.parse(message); |           const parsedMessage: WebsocketResponse = JSON.parse(message); | ||||||
|           const response = {}; |           const response = {}; | ||||||
| 
 | 
 | ||||||
|           if (parsedMessage.action === 'want') { |           const wantNow = {}; | ||||||
|             client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1; |           if (parsedMessage && parsedMessage.action === 'want' && Array.isArray(parsedMessage.data)) { | ||||||
|             client['want-mempool-blocks'] = parsedMessage.data.indexOf('mempool-blocks') > -1; |             for (const sub of wantable) { | ||||||
|             client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1; |               const key = `want-${sub}`; | ||||||
|             client['want-stats'] = parsedMessage.data.indexOf('stats') > -1; |               const wants = parsedMessage.data.includes(sub); | ||||||
|  |               if (wants && client['wants'] && !client[key]) { | ||||||
|  |                 wantNow[key] = true; | ||||||
|  |               } | ||||||
|  |               client[key] = wants; | ||||||
|  |             } | ||||||
|  |             client['wants'] = true; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           // send initial data when a client first starts a subscription
 | ||||||
|  |           if (wantNow['want-blocks'] || (parsedMessage && parsedMessage['refresh-blocks'])) { | ||||||
|  |             response['blocks'] = this.socketData['blocks']; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (wantNow['want-mempool-blocks']) { | ||||||
|  |             response['mempool-blocks'] = this.socketData['mempool-blocks']; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (wantNow['want-stats']) { | ||||||
|  |             response['mempoolInfo'] = this.socketData['mempoolInfo']; | ||||||
|  |             response['vBytesPerSecond'] = this.socketData['vBytesPerSecond']; | ||||||
|  |             response['fees'] = this.socketData['fees']; | ||||||
|  |             response['da'] = this.socketData['da']; | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (parsedMessage && parsedMessage['track-tx']) { |           if (parsedMessage && parsedMessage['track-tx']) { | ||||||
| @ -109,21 +139,21 @@ class WebsocketHandler { | |||||||
|               if (parsedMessage['watch-mempool']) { |               if (parsedMessage['watch-mempool']) { | ||||||
|                 const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid); |                 const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid); | ||||||
|                 if (rbfCacheTxid) { |                 if (rbfCacheTxid) { | ||||||
|                   response['txReplaced'] = { |                   response['txReplaced'] = JSON.stringify({ | ||||||
|                     txid: rbfCacheTxid, |                     txid: rbfCacheTxid, | ||||||
|                   }; |                   }); | ||||||
|                   client['track-tx'] = null; |                   client['track-tx'] = null; | ||||||
|                 } else { |                 } else { | ||||||
|                   // It might have appeared before we had the time to start watching for it
 |                   // It might have appeared before we had the time to start watching for it
 | ||||||
|                   const tx = memPool.getMempool()[trackTxid]; |                   const tx = memPool.getMempool()[trackTxid]; | ||||||
|                   if (tx) { |                   if (tx) { | ||||||
|                     if (config.MEMPOOL.BACKEND === 'esplora') { |                     if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|                       response['tx'] = tx; |                       response['tx'] = JSON.stringify(tx); | ||||||
|                     } else { |                     } else { | ||||||
|                       // tx.prevout is missing from transactions when in bitcoind mode
 |                       // tx.prevout is missing from transactions when in bitcoind mode
 | ||||||
|                       try { |                       try { | ||||||
|                         const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); |                         const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); | ||||||
|                         response['tx'] = fullTx; |                         response['tx'] = JSON.stringify(fullTx); | ||||||
|                       } catch (e) { |                       } catch (e) { | ||||||
|                         logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e)); |                         logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e)); | ||||||
|                       } |                       } | ||||||
| @ -131,7 +161,7 @@ class WebsocketHandler { | |||||||
|                   } else { |                   } else { | ||||||
|                     try { |                     try { | ||||||
|                       const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true); |                       const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true); | ||||||
|                       response['tx'] = fullTx; |                       response['tx'] = JSON.stringify(fullTx); | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                       logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e)); |                       logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e)); | ||||||
|                       client['track-mempool-tx'] = parsedMessage['track-tx']; |                       client['track-mempool-tx'] = parsedMessage['track-tx']; | ||||||
| @ -141,10 +171,10 @@ class WebsocketHandler { | |||||||
|               } |               } | ||||||
|               const tx = memPool.getMempool()[trackTxid]; |               const tx = memPool.getMempool()[trackTxid]; | ||||||
|               if (tx && tx.position) { |               if (tx && tx.position) { | ||||||
|                 response['txPosition'] = { |                 response['txPosition'] = JSON.stringify({ | ||||||
|                   txid: trackTxid, |                   txid: trackTxid, | ||||||
|                   position: tx.position, |                   position: tx.position, | ||||||
|                 }; |                 }); | ||||||
|               } |               } | ||||||
|             } else { |             } else { | ||||||
|               client['track-tx'] = null; |               client['track-tx'] = null; | ||||||
| @ -177,10 +207,10 @@ class WebsocketHandler { | |||||||
|               const index = parsedMessage['track-mempool-block']; |               const index = parsedMessage['track-mempool-block']; | ||||||
|               client['track-mempool-block'] = index; |               client['track-mempool-block'] = index; | ||||||
|               const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); |               const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||||
|               response['projected-block-transactions'] = { |               response['projected-block-transactions'] = JSON.stringify({ | ||||||
|                 index: index, |                 index: index, | ||||||
|                 blockTransactions: mBlocksWithTransactions[index]?.transactions || [], |                 blockTransactions: mBlocksWithTransactions[index]?.transactions || [], | ||||||
|               }; |               }); | ||||||
|             } else { |             } else { | ||||||
|               client['track-mempool-block'] = null; |               client['track-mempool-block'] = null; | ||||||
|             } |             } | ||||||
| @ -189,23 +219,24 @@ class WebsocketHandler { | |||||||
|           if (parsedMessage && parsedMessage['track-rbf'] !== undefined) { |           if (parsedMessage && parsedMessage['track-rbf'] !== undefined) { | ||||||
|             if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) { |             if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) { | ||||||
|               client['track-rbf'] = parsedMessage['track-rbf']; |               client['track-rbf'] = parsedMessage['track-rbf']; | ||||||
|  |               response['rbfLatest'] = JSON.stringify(rbfCache.getRbfTrees(parsedMessage['track-rbf'] === 'fullRbf')); | ||||||
|             } else { |             } else { | ||||||
|               client['track-rbf'] = false; |               client['track-rbf'] = false; | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (parsedMessage.action === 'init') { |           if (parsedMessage.action === 'init') { | ||||||
|             if (!this.initData['blocks']?.length || !this.initData['da']) { |             if (!this.socketData['blocks']?.length || !this.socketData['da']) { | ||||||
|               this.updateInitData(); |               this.updateSocketData(); | ||||||
|             } |             } | ||||||
|             if (!this.initData['blocks']?.length) { |             if (!this.socketData['blocks']?.length) { | ||||||
|               return; |               return; | ||||||
|             } |             } | ||||||
|             client.send(this.serializedInitData); |             client.send(this.serializedInitData); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (parsedMessage.action === 'ping') { |           if (parsedMessage.action === 'ping') { | ||||||
|             response['pong'] = true; |             response['pong'] = JSON.stringify(true); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) { |           if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) { | ||||||
| @ -221,7 +252,8 @@ class WebsocketHandler { | |||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (Object.keys(response).length) { |           if (Object.keys(response).length) { | ||||||
|             client.send(JSON.stringify(response)); |             const serializedResponse = this.serializeResponse(response); | ||||||
|  |             client.send(serializedResponse); | ||||||
|           } |           } | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|           logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e)); |           logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e)); | ||||||
| @ -250,7 +282,7 @@ class WebsocketHandler { | |||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('WebSocket.Server is not set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.setInitDataFields({ 'loadingIndicators': indicators }); |     this.updateSocketDataFields({ 'loadingIndicators': indicators }); | ||||||
| 
 | 
 | ||||||
|     const response = JSON.stringify({ loadingIndicators: indicators }); |     const response = JSON.stringify({ loadingIndicators: indicators }); | ||||||
|     this.wss.clients.forEach((client) => { |     this.wss.clients.forEach((client) => { | ||||||
| @ -266,7 +298,7 @@ class WebsocketHandler { | |||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('WebSocket.Server is not set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.setInitDataFields({ 'conversions': conversionRates }); |     this.updateSocketDataFields({ 'conversions': conversionRates }); | ||||||
| 
 | 
 | ||||||
|     const response = JSON.stringify({ conversions: conversionRates }); |     const response = JSON.stringify({ conversions: conversionRates }); | ||||||
|     this.wss.clients.forEach((client) => { |     this.wss.clients.forEach((client) => { | ||||||
| @ -336,11 +368,21 @@ class WebsocketHandler { | |||||||
|     memPool.addToSpendMap(newTransactions); |     memPool.addToSpendMap(newTransactions); | ||||||
|     const recommendedFees = feeApi.getRecommendedFee(); |     const recommendedFees = feeApi.getRecommendedFee(); | ||||||
| 
 | 
 | ||||||
|  |     const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); | ||||||
|  | 
 | ||||||
|     // update init data
 |     // update init data
 | ||||||
|     this.updateInitData(); |     this.updateSocketDataFields({ | ||||||
|  |       'mempoolInfo': mempoolInfo, | ||||||
|  |       'vBytesPerSecond': vBytesPerSecond, | ||||||
|  |       'mempool-blocks': mBlocks, | ||||||
|  |       'transactions': latestTransactions, | ||||||
|  |       'loadingIndicators': loadingIndicators.getLoadingIndicators(), | ||||||
|  |       'da': da?.previousTime ? da : undefined, | ||||||
|  |       'fees': recommendedFees, | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|     // cache serialized objects to avoid stringify-ing the same thing for every client
 |     // cache serialized objects to avoid stringify-ing the same thing for every client
 | ||||||
|     const responseCache = { ...this.initData }; |     const responseCache = { ...this.socketData }; | ||||||
|     function getCachedResponse(key: string,  data): string { |     function getCachedResponse(key: string,  data): string { | ||||||
|       if (!responseCache[key]) { |       if (!responseCache[key]) { | ||||||
|         responseCache[key] = JSON.stringify(data); |         responseCache[key] = JSON.stringify(data); | ||||||
| @ -371,8 +413,6 @@ class WebsocketHandler { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); |  | ||||||
| 
 |  | ||||||
|     this.wss.clients.forEach(async (client) => { |     this.wss.clients.forEach(async (client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
| @ -490,7 +530,7 @@ class WebsocketHandler { | |||||||
|         if (rbfReplacedBy) { |         if (rbfReplacedBy) { | ||||||
|           response['rbfTransaction'] = JSON.stringify({ |           response['rbfTransaction'] = JSON.stringify({ | ||||||
|             txid: rbfReplacedBy, |             txid: rbfReplacedBy, | ||||||
|           }) |           }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const rbfChange = rbfChanges.map[client['track-tx']]; |         const rbfChange = rbfChanges.map[client['track-tx']]; | ||||||
| @ -524,9 +564,7 @@ class WebsocketHandler { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (Object.keys(response).length) { |       if (Object.keys(response).length) { | ||||||
|         const serializedResponse = '{' |         const serializedResponse = this.serializeResponse(response); | ||||||
|           + Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ') |  | ||||||
|           + '}'; |  | ||||||
|         client.send(serializedResponse); |         client.send(serializedResponse); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
| @ -562,14 +600,7 @@ class WebsocketHandler { | |||||||
|         const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); |         const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); | ||||||
|         const matchRate = Math.round(score * 100 * 100) / 100; |         const matchRate = Math.round(score * 100 * 100) / 100; | ||||||
| 
 | 
 | ||||||
|         const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { |         const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; | ||||||
|           return { |  | ||||||
|             txid: tx.txid, |  | ||||||
|             vsize: tx.vsize, |  | ||||||
|             fee: tx.fee ? Math.round(tx.fee) : 0, |  | ||||||
|             value: tx.value, |  | ||||||
|           }; |  | ||||||
|         }) : []; |  | ||||||
| 
 | 
 | ||||||
|         let totalFees = 0; |         let totalFees = 0; | ||||||
|         let totalWeight = 0; |         let totalWeight = 0; | ||||||
| @ -633,11 +664,19 @@ class WebsocketHandler { | |||||||
| 
 | 
 | ||||||
|     const da = difficultyAdjustment.getDifficultyAdjustment(); |     const da = difficultyAdjustment.getDifficultyAdjustment(); | ||||||
|     const fees = feeApi.getRecommendedFee(); |     const fees = feeApi.getRecommendedFee(); | ||||||
|  |     const mempoolInfo = memPool.getMempoolInfo(); | ||||||
| 
 | 
 | ||||||
|     // update init data
 |     // update init data
 | ||||||
|     this.updateInitData(); |     this.updateSocketDataFields({ | ||||||
|  |       'mempoolInfo': mempoolInfo, | ||||||
|  |       'blocks': [...blocks.getBlocks(), block].slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT), | ||||||
|  |       'mempool-blocks': mBlocks, | ||||||
|  |       'loadingIndicators': loadingIndicators.getLoadingIndicators(), | ||||||
|  |       'da': da?.previousTime ? da : undefined, | ||||||
|  |       'fees': fees, | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|     const responseCache = { ...this.initData }; |     const responseCache = { ...this.socketData }; | ||||||
|     function getCachedResponse(key, data): string { |     function getCachedResponse(key, data): string { | ||||||
|       if (!responseCache[key]) { |       if (!responseCache[key]) { | ||||||
|         responseCache[key] = JSON.stringify(data); |         responseCache[key] = JSON.stringify(data); | ||||||
| @ -645,23 +684,27 @@ class WebsocketHandler { | |||||||
|       return responseCache[key]; |       return responseCache[key]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const mempoolInfo = memPool.getMempoolInfo(); |  | ||||||
| 
 |  | ||||||
|     this.wss.clients.forEach((client) => { |     this.wss.clients.forEach((client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (!client['want-blocks']) { |       const response = {}; | ||||||
|         return; | 
 | ||||||
|  |       if (client['want-blocks']) { | ||||||
|  |         response['block'] = getCachedResponse('block', block); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const response = {}; |       if (client['want-stats']) { | ||||||
|       response['block'] = getCachedResponse('block', block); |  | ||||||
|         response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo); |         response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo); | ||||||
|       response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined); |         response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond()); | ||||||
|         response['fees'] = getCachedResponse('fees', fees); |         response['fees'] = getCachedResponse('fees', fees); | ||||||
| 
 | 
 | ||||||
|  |         if (da?.previousTime) { | ||||||
|  |           response['da'] = getCachedResponse('da', da); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (mBlocks && client['want-mempool-blocks']) { |       if (mBlocks && client['want-mempool-blocks']) { | ||||||
|         response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks); |         response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks); | ||||||
|       } |       } | ||||||
| @ -755,11 +798,19 @@ class WebsocketHandler { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const serializedResponse = '{' |       if (Object.keys(response).length) { | ||||||
|  |         const serializedResponse = this.serializeResponse(response); | ||||||
|  |         client.send(serializedResponse); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // takes a dictionary of JSON serialized values
 | ||||||
|  |   // and zips it together into a valid JSON object
 | ||||||
|  |   private serializeResponse(response): string { | ||||||
|  |     return '{' | ||||||
|         + Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ') |         + Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ') | ||||||
|         + '}'; |         + '}'; | ||||||
|       client.send(serializedResponse); |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private printLogs(): void { |   private printLogs(): void { | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | |   public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | | ||||||
|     OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]> |     OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]> | ||||||
|   { |   { | ||||||
|     this.checkDBFlag(); |     this.checkDBFlag(); | ||||||
|     let hardTimeout; |     let hardTimeout; | ||||||
| @ -45,7 +45,9 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr | |||||||
|           reject(new Error(`DB query failed to return, reject or time out within ${hardTimeout / 1000}s - ${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}`)); |           reject(new Error(`DB query failed to return, reject or time out within ${hardTimeout / 1000}s - ${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}`)); | ||||||
|         }, hardTimeout); |         }, hardTimeout); | ||||||
| 
 | 
 | ||||||
|         this.getPool().then(pool => { |         // Use a specific connection if provided, otherwise delegate to the pool
 | ||||||
|  |         const connectionPromise = connection ? Promise.resolve(connection) : this.getPool(); | ||||||
|  |         connectionPromise.then((pool: PoolConnection | Pool) => { | ||||||
|           return pool.query(query, params) as Promise<[T, FieldPacket[]]>; |           return pool.query(query, params) as Promise<[T, FieldPacket[]]>; | ||||||
|         }).then(result => { |         }).then(result => { | ||||||
|           resolve(result); |           resolve(result); | ||||||
| @ -61,6 +63,33 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | | ||||||
|  |     OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]> | ||||||
|  |   { | ||||||
|  |     const pool = await this.getPool(); | ||||||
|  |     const connection = await pool.getConnection(); | ||||||
|  |     try { | ||||||
|  |       await connection.beginTransaction(); | ||||||
|  | 
 | ||||||
|  |       const results: [T, FieldPacket[]][]  = []; | ||||||
|  |       for (const query of queries) { | ||||||
|  |         const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]]; | ||||||
|  |         results.push(result); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       await connection.commit(); | ||||||
|  | 
 | ||||||
|  |       return results; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       connection.rollback(); | ||||||
|  |       connection.release(); | ||||||
|  |       throw e; | ||||||
|  |     } finally { | ||||||
|  |       connection.release(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public async checkDbConnection() { |   public async checkDbConnection() { | ||||||
|     this.checkDBFlag(); |     this.checkDBFlag(); | ||||||
|     try { |     try { | ||||||
|  | |||||||
| @ -150,7 +150,7 @@ class Server { | |||||||
| 
 | 
 | ||||||
|     if (config.BISQ.ENABLED) { |     if (config.BISQ.ENABLED) { | ||||||
|       bisq.startBisqService(); |       bisq.startBisqService(); | ||||||
|       bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price)); |       bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price)); | ||||||
|       blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq)); |       blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq)); | ||||||
|       bisqMarkets.startBisqService(); |       bisqMarkets.startBisqService(); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -64,7 +64,6 @@ class BlocksAuditRepositories { | |||||||
|       const [rows]: any[] = await DB.query( |       const [rows]: any[] = await DB.query( | ||||||
|         `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
 |         `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
 | ||||||
|         blocks.weight, blocks.tx_count, |         blocks.weight, blocks.tx_count, | ||||||
|         transactions, |  | ||||||
|         template, |         template, | ||||||
|         missing_txs as missingTxs, |         missing_txs as missingTxs, | ||||||
|         added_txs as addedTxs, |         added_txs as addedTxs, | ||||||
| @ -76,7 +75,6 @@ class BlocksAuditRepositories { | |||||||
|         FROM blocks_audits |         FROM blocks_audits | ||||||
|         JOIN blocks ON blocks.hash = blocks_audits.hash |         JOIN blocks ON blocks.hash = blocks_audits.hash | ||||||
|         JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash |         JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash | ||||||
|         JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash |  | ||||||
|         WHERE blocks_audits.hash = "${hash}" |         WHERE blocks_audits.hash = "${hash}" | ||||||
|       `);
 |       `);
 | ||||||
|        |        | ||||||
| @ -85,13 +83,10 @@ class BlocksAuditRepositories { | |||||||
|         rows[0].addedTxs = JSON.parse(rows[0].addedTxs); |         rows[0].addedTxs = JSON.parse(rows[0].addedTxs); | ||||||
|         rows[0].freshTxs = JSON.parse(rows[0].freshTxs); |         rows[0].freshTxs = JSON.parse(rows[0].freshTxs); | ||||||
|         rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); |         rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); | ||||||
|         rows[0].transactions = JSON.parse(rows[0].transactions); |  | ||||||
|         rows[0].template = JSON.parse(rows[0].template); |         rows[0].template = JSON.parse(rows[0].template); | ||||||
| 
 | 
 | ||||||
|         if (rows[0].transactions.length) { |  | ||||||
|         return rows[0]; |         return rows[0]; | ||||||
|       } |       } | ||||||
|       } |  | ||||||
|       return null; |       return null; | ||||||
|     } catch (e: any) { |     } catch (e: any) { | ||||||
|       logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e)); |       logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  | |||||||
| @ -5,52 +5,10 @@ import { Ancestor, CpfpCluster } from '../mempool.interfaces'; | |||||||
| import transactionRepository from '../repositories/TransactionRepository'; | import transactionRepository from '../repositories/TransactionRepository'; | ||||||
| 
 | 
 | ||||||
| class CpfpRepository { | class CpfpRepository { | ||||||
|   public async $saveCluster(clusterRoot: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<boolean> { |  | ||||||
|     if (!txs[0]) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     // skip clusters of transactions with the same fees
 |  | ||||||
|     const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100; |  | ||||||
|     const equalFee = txs.length > 1 && txs.reduce((acc, tx) => { |  | ||||||
|       return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); |  | ||||||
|     }, true); |  | ||||||
|     if (equalFee) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|       const packedTxs = Buffer.from(this.pack(txs)); |  | ||||||
|       await DB.query( |  | ||||||
|         ` |  | ||||||
|           INSERT INTO compact_cpfp_clusters(root, height, txs, fee_rate) |  | ||||||
|           VALUE (UNHEX(?), ?, ?, ?) |  | ||||||
|           ON DUPLICATE KEY UPDATE |  | ||||||
|             height = ?, |  | ||||||
|             txs = ?, |  | ||||||
|             fee_rate = ? |  | ||||||
|         `,
 |  | ||||||
|         [clusterRoot, height, packedTxs, effectiveFeePerVsize, height, packedTxs, effectiveFeePerVsize] |  | ||||||
|       ); |  | ||||||
|       const maxChunk = 10; |  | ||||||
|       let chunkIndex = 0; |  | ||||||
|       while (chunkIndex < txs.length) { |  | ||||||
|         const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk).map(tx => { |  | ||||||
|           return { txid: tx.txid, cluster: clusterRoot }; |  | ||||||
|         }); |  | ||||||
|         await transactionRepository.$batchSetCluster(chunk); |  | ||||||
|         chunkIndex += maxChunk; |  | ||||||
|       } |  | ||||||
|       return true; |  | ||||||
|     } catch (e: any) { |  | ||||||
|       logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e)); |  | ||||||
|       throw e; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> { |   public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> { | ||||||
|     try { |     try { | ||||||
|       const clusterValues: any[] = []; |       const clusterValues: [string, number, Buffer, number][] = []; | ||||||
|       const txs: any[] = []; |       const txs: { txid: string, cluster: string }[] = []; | ||||||
| 
 | 
 | ||||||
|       for (const cluster of clusters) { |       for (const cluster of clusters) { | ||||||
|         if (cluster.txs?.length) { |         if (cluster.txs?.length) { | ||||||
| @ -76,6 +34,8 @@ class CpfpRepository { | |||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       const queries: { query, params }[] = []; | ||||||
|  | 
 | ||||||
|       const maxChunk = 100; |       const maxChunk = 100; | ||||||
|       let chunkIndex = 0; |       let chunkIndex = 0; | ||||||
|       // insert clusters in batches of up to 100 rows
 |       // insert clusters in batches of up to 100 rows
 | ||||||
| @ -89,10 +49,10 @@ class CpfpRepository { | |||||||
|           return (' (UNHEX(?), ?, ?, ?)'); |           return (' (UNHEX(?), ?, ?, ?)'); | ||||||
|         }) + ';'; |         }) + ';'; | ||||||
|         const values = chunk.flat(); |         const values = chunk.flat(); | ||||||
|         await DB.query( |         queries.push({ | ||||||
|           query, |           query, | ||||||
|           values |           params: values, | ||||||
|         ); |         }); | ||||||
|         chunkIndex += maxChunk; |         chunkIndex += maxChunk; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -100,10 +60,12 @@ class CpfpRepository { | |||||||
|       // insert transactions in batches of up to 100 rows
 |       // insert transactions in batches of up to 100 rows
 | ||||||
|       while (chunkIndex < txs.length) { |       while (chunkIndex < txs.length) { | ||||||
|         const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk); |         const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk); | ||||||
|         await transactionRepository.$batchSetCluster(chunk); |         queries.push(transactionRepository.buildBatchSetQuery(chunk)); | ||||||
|         chunkIndex += maxChunk; |         chunkIndex += maxChunk; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       await DB.$atomicQuery(queries); | ||||||
|  | 
 | ||||||
|       return true; |       return true; | ||||||
|     } catch (e: any) { |     } catch (e: any) { | ||||||
|       logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e)); |       logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  | |||||||
| @ -25,8 +25,7 @@ class TransactionRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $batchSetCluster(txs): Promise<void> { |   public buildBatchSetQuery(txs: { txid: string, cluster: string }[]): { query, params } { | ||||||
|     try { |  | ||||||
|     let query = ` |     let query = ` | ||||||
|           INSERT IGNORE INTO compact_transactions |           INSERT IGNORE INTO compact_transactions | ||||||
|           ( |           ( | ||||||
| @ -39,9 +38,18 @@ class TransactionRepository { | |||||||
|       return (' (UNHEX(?), UNHEX(?))'); |       return (' (UNHEX(?), UNHEX(?))'); | ||||||
|     }) + ';'; |     }) + ';'; | ||||||
|     const values = txs.map(tx => [tx.txid, tx.cluster]).flat(); |     const values = txs.map(tx => [tx.txid, tx.cluster]).flat(); | ||||||
|       await DB.query( |     return { | ||||||
|       query, |       query, | ||||||
|         values |       params: values, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $batchSetCluster(txs): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       const query = this.buildBatchSetQuery(txs); | ||||||
|  |       await DB.query( | ||||||
|  |         query.query, | ||||||
|  |         query.params, | ||||||
|       ); |       ); | ||||||
|     } catch (e: any) { |     } catch (e: any) { | ||||||
|       logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e)); |       logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  | |||||||
| @ -220,7 +220,7 @@ | |||||||
|         <img class="image" src="/resources/profile/mynodebtc.png" /> |         <img class="image" src="/resources/profile/mynodebtc.png" /> | ||||||
|         <span>myNode</span> |         <span>myNode</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo"> |       <a href="https://code.samourai.io/ronindojo/RoninDojo" target="_blank" title="RoninDojo"> | ||||||
|         <img class="image" src="/resources/profile/ronindojo.png" /> |         <img class="image" src="/resources/profile/ronindojo.png" /> | ||||||
|         <span>RoninDojo</span> |         <span>RoninDojo</span> | ||||||
|       </a> |       </a> | ||||||
|  | |||||||
| @ -63,7 +63,7 @@ | |||||||
|                     *ngIf="blockAudit?.matchRate != null; else nullHealth" |                     *ngIf="blockAudit?.matchRate != null; else nullHealth" | ||||||
|                   >{{ blockAudit?.matchRate }}%</span> |                   >{{ blockAudit?.matchRate }}%</span> | ||||||
|                   <ng-template #nullHealth> |                   <ng-template #nullHealth> | ||||||
|                     <ng-container *ngIf="!isLoadingAudit; else loadingHealth"> |                     <ng-container *ngIf="!isLoadingOverview; else loadingHealth"> | ||||||
|                       <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> |                       <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> | ||||||
|                     </ng-container> |                     </ng-container> | ||||||
|                   </ng-template> |                   </ng-template> | ||||||
|  | |||||||
| @ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/ | |||||||
| import { Location } from '@angular/common'; | import { Location } from '@angular/common'; | ||||||
| import { ActivatedRoute, ParamMap, Router } from '@angular/router'; | import { ActivatedRoute, ParamMap, Router } from '@angular/router'; | ||||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||||
| import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators'; | import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith } from 'rxjs/operators'; | ||||||
| import { Transaction, Vout } from '../../interfaces/electrs.interface'; | import { Transaction, Vout } from '../../interfaces/electrs.interface'; | ||||||
| import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs'; | import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { SeoService } from '../../services/seo.service'; | import { SeoService } from '../../services/seo.service'; | ||||||
| import { WebsocketService } from '../../services/websocket.service'; | import { WebsocketService } from '../../services/websocket.service'; | ||||||
| @ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy { | |||||||
|   strippedTransactions: TransactionStripped[]; |   strippedTransactions: TransactionStripped[]; | ||||||
|   overviewTransitionDirection: string; |   overviewTransitionDirection: string; | ||||||
|   isLoadingOverview = true; |   isLoadingOverview = true; | ||||||
|   isLoadingAudit = true; |  | ||||||
|   error: any; |   error: any; | ||||||
|   blockSubsidy: number; |   blockSubsidy: number; | ||||||
|   fees: number; |   fees: number; | ||||||
| @ -281,57 +280,35 @@ export class BlockComponent implements OnInit, OnDestroy { | |||||||
|       this.isLoadingOverview = false; |       this.isLoadingOverview = false; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (!this.auditSupported) { |  | ||||||
|     this.overviewSubscription = block$.pipe( |     this.overviewSubscription = block$.pipe( | ||||||
|         startWith(null), |       switchMap((block) => { | ||||||
|         pairwise(), |         return forkJoin([ | ||||||
|         switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id) |           this.apiService.getStrippedBlockTransactions$(block.id) | ||||||
|             .pipe( |             .pipe( | ||||||
|               catchError((err) => { |               catchError((err) => { | ||||||
|                 this.overviewError = err; |                 this.overviewError = err; | ||||||
|               return of([]); |                 return of(null); | ||||||
|             }), |               }) | ||||||
|             switchMap((transactions) => { |             ), | ||||||
|               if (prevBlock) { |           !this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id) | ||||||
|                 return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' }); |             .pipe( | ||||||
|               } else { |               catchError((err) => { | ||||||
|                 return of({ transactions, direction: 'down' }); |                 this.overviewError = err; | ||||||
|               } |                 return of(null); | ||||||
|               }) |               }) | ||||||
|             ) |             ) | ||||||
|         ), |         ]); | ||||||
|  |       }) | ||||||
|     ) |     ) | ||||||
|       .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => { |     .subscribe(([transactions, blockAudit]) => {       | ||||||
|  |       if (transactions) { | ||||||
|         this.strippedTransactions = transactions; |         this.strippedTransactions = transactions; | ||||||
|         this.isLoadingOverview = false; |       } else { | ||||||
|         this.setupBlockGraphs(); |         this.strippedTransactions = []; | ||||||
|       }, |  | ||||||
|       (error) => { |  | ||||||
|         this.error = error; |  | ||||||
|         this.isLoadingOverview = false; |  | ||||||
|       }); |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|     if (this.auditSupported) { |  | ||||||
|       this.auditSubscription = block$.pipe( |  | ||||||
|         startWith(null), |  | ||||||
|         pairwise(), |  | ||||||
|         switchMap(([prevBlock, block]) => { |  | ||||||
|           this.isLoadingAudit = true; |  | ||||||
|       this.blockAudit = null; |       this.blockAudit = null; | ||||||
|           return this.apiService.getBlockAudit$(block.id) |       if (transactions && blockAudit) { | ||||||
|             .pipe( |  | ||||||
|               catchError((err) => { |  | ||||||
|                 this.overviewError = err; |  | ||||||
|                 this.isLoadingAudit = false; |  | ||||||
|                 return of([]); |  | ||||||
|               }) |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         ), |  | ||||||
|         filter((response) => response != null), |  | ||||||
|         map((response) => { |  | ||||||
|           const blockAudit = response.body; |  | ||||||
|         const inTemplate = {}; |         const inTemplate = {}; | ||||||
|         const inBlock = {}; |         const inBlock = {}; | ||||||
|         const isAdded = {}; |         const isAdded = {}; | ||||||
| @ -347,7 +324,7 @@ export class BlockComponent implements OnInit, OnDestroy { | |||||||
|           for (const tx of blockAudit.template) { |           for (const tx of blockAudit.template) { | ||||||
|             inTemplate[tx.txid] = true; |             inTemplate[tx.txid] = true; | ||||||
|           } |           } | ||||||
|             for (const tx of blockAudit.transactions) { |           for (const tx of transactions) { | ||||||
|             inBlock[tx.txid] = true; |             inBlock[tx.txid] = true; | ||||||
|           } |           } | ||||||
|           for (const txid of blockAudit.addedTxs) { |           for (const txid of blockAudit.addedTxs) { | ||||||
| @ -375,7 +352,7 @@ export class BlockComponent implements OnInit, OnDestroy { | |||||||
|               this.numMissing++; |               this.numMissing++; | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|             for (const [index, tx] of blockAudit.transactions.entries()) { |           for (const [index, tx] of transactions.entries()) { | ||||||
|             tx.context = 'actual'; |             tx.context = 'actual'; | ||||||
|             if (index === 0) { |             if (index === 0) { | ||||||
|               tx.status = null; |               tx.status = null; | ||||||
| @ -389,36 +366,26 @@ export class BlockComponent implements OnInit, OnDestroy { | |||||||
|               this.numUnexpected++; |               this.numUnexpected++; | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|             for (const tx of blockAudit.transactions) { |           for (const tx of transactions) { | ||||||
|             inBlock[tx.txid] = true; |             inBlock[tx.txid] = true; | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0; |           blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0; | ||||||
|           blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0; |           blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0; | ||||||
|           blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0; |           blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0; | ||||||
| 
 |           this.blockAudit = blockAudit; | ||||||
|           this.setAuditAvailable(true); |           this.setAuditAvailable(true); | ||||||
|         } else { |         } else { | ||||||
|           this.setAuditAvailable(false); |           this.setAuditAvailable(false); | ||||||
|         } |         } | ||||||
|           return blockAudit; |       } else { | ||||||
|         }), |  | ||||||
|         catchError((err) => { |  | ||||||
|           console.log(err); |  | ||||||
|           this.error = err; |  | ||||||
|           this.isLoadingOverview = false; |  | ||||||
|           this.isLoadingAudit = false; |  | ||||||
|         this.setAuditAvailable(false); |         this.setAuditAvailable(false); | ||||||
|           return of(null); |  | ||||||
|         }), |  | ||||||
|       ).subscribe((blockAudit) => { |  | ||||||
|         this.blockAudit = blockAudit; |  | ||||||
|         this.setupBlockGraphs(); |  | ||||||
|         this.isLoadingOverview = false; |  | ||||||
|         this.isLoadingAudit = false; |  | ||||||
|       }); |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       this.isLoadingOverview = false; | ||||||
|  |       this.setupBlockGraphs(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     this.networkChangedSubscription = this.stateService.networkChanged$ |     this.networkChangedSubscription = this.stateService.networkChanged$ | ||||||
|       .subscribe((network) => this.network = network); |       .subscribe((network) => this.network = network); | ||||||
| 
 | 
 | ||||||
| @ -652,25 +619,32 @@ export class BlockComponent implements OnInit, OnDestroy { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   updateAuditAvailableFromBlockHeight(blockHeight: number): void { |   updateAuditAvailableFromBlockHeight(blockHeight: number): void { | ||||||
|     if (!this.auditSupported) { |     if (!this.isAuditAvailableFromBlockHeight(blockHeight)) { | ||||||
|       this.setAuditAvailable(false); |       this.setAuditAvailable(false); | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   isAuditAvailableFromBlockHeight(blockHeight: number): boolean { | ||||||
|  |     if (!this.auditSupported) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|     switch (this.stateService.network) { |     switch (this.stateService.network) { | ||||||
|       case 'testnet': |       case 'testnet': | ||||||
|         if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) { |         if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) { | ||||||
|           this.setAuditAvailable(false); |           return false; | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case 'signet': |       case 'signet': | ||||||
|         if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { |         if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { | ||||||
|           this.setAuditAvailable(false); |           return false; | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       default: |       default: | ||||||
|         if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) { |         if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) { | ||||||
|           this.setAuditAvailable(false); |           return false; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     return true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getMinBlockFee(block: BlockExtended): number { |   getMinBlockFee(block: BlockExtended): number { | ||||||
|  | |||||||
| @ -1,5 +1,9 @@ | |||||||
| import { OnChanges } from '@angular/core'; | import { OnChanges } from '@angular/core'; | ||||||
| import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; | import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; | ||||||
|  | import { TransactionStripped } from '../../interfaces/websocket.interface'; | ||||||
|  | import { StateService } from '../../services/state.service'; | ||||||
|  | import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe'; | ||||||
|  | import { selectPowerOfTen } from '../../bitcoin.utils'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-fee-distribution-graph', |   selector: 'app-fee-distribution-graph', | ||||||
| @ -7,47 +11,121 @@ import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core | |||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
| }) | }) | ||||||
| export class FeeDistributionGraphComponent implements OnInit, OnChanges { | export class FeeDistributionGraphComponent implements OnInit, OnChanges { | ||||||
|   @Input() data: any; |   @Input() feeRange: number[]; | ||||||
|  |   @Input() vsize: number; | ||||||
|  |   @Input() transactions: TransactionStripped[]; | ||||||
|   @Input() height: number | string = 210; |   @Input() height: number | string = 210; | ||||||
|   @Input() top: number | string = 20; |   @Input() top: number | string = 20; | ||||||
|   @Input() right: number | string = 22; |   @Input() right: number | string = 22; | ||||||
|   @Input() left: number | string = 30; |   @Input() left: number | string = 30; | ||||||
|  |   @Input() numSamples: number = 200; | ||||||
|  |   @Input() numLabels: number = 10; | ||||||
|  | 
 | ||||||
|  |   simple: boolean = false; | ||||||
|  |   data: number[][]; | ||||||
|  |   labelInterval: number = 50; | ||||||
| 
 | 
 | ||||||
|   mempoolVsizeFeesOptions: any; |   mempoolVsizeFeesOptions: any; | ||||||
|   mempoolVsizeFeesInitOptions = { |   mempoolVsizeFeesInitOptions = { | ||||||
|     renderer: 'svg' |     renderer: 'svg' | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   constructor() { } |   constructor( | ||||||
|  |     private stateService: StateService, | ||||||
|  |     private vbytesPipe: VbytesPipe, | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit() { |   ngOnInit(): void { | ||||||
|     this.mountChart(); |     this.mountChart(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnChanges() { |   ngOnChanges(): void { | ||||||
|  |     this.simple = !!this.feeRange?.length; | ||||||
|  |     this.prepareChart(); | ||||||
|     this.mountChart(); |     this.mountChart(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   mountChart() { |   prepareChart(): void { | ||||||
|  |     if (this.simple) { | ||||||
|  |       this.data = this.feeRange.map((rate, index) => [index * 10, rate]); | ||||||
|  |       this.labelInterval = 1; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.data = []; | ||||||
|  |     if (!this.transactions?.length) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const samples = []; | ||||||
|  |     const txs = this.transactions.map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; }); | ||||||
|  |     const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4; | ||||||
|  |     const sampleInterval = maxBlockVSize / this.numSamples; | ||||||
|  |     let cumVSize = 0; | ||||||
|  |     let sampleIndex = 0; | ||||||
|  |     let nextSample = 0; | ||||||
|  |     let txIndex = 0; | ||||||
|  |     this.labelInterval = this.numSamples / this.numLabels; | ||||||
|  |     while (nextSample <= maxBlockVSize) { | ||||||
|  |       if (txIndex >= txs.length) { | ||||||
|  |         samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0]); | ||||||
|  |         nextSample += sampleInterval; | ||||||
|  |         sampleIndex++; | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) { | ||||||
|  |         samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate]); | ||||||
|  |         nextSample += sampleInterval; | ||||||
|  |         sampleIndex++; | ||||||
|  |       } | ||||||
|  |       cumVSize += txs[txIndex].vsize; | ||||||
|  |       txIndex++; | ||||||
|  |     } | ||||||
|  |     this.data = samples.reverse(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   mountChart(): void { | ||||||
|     this.mempoolVsizeFeesOptions = { |     this.mempoolVsizeFeesOptions = { | ||||||
|       grid: { |       grid: { | ||||||
|         height: '210', |         height: '210', | ||||||
|         right: '20', |         right: '20', | ||||||
|         top: '22', |         top: '22', | ||||||
|         left: '30', |         left: '40', | ||||||
|       }, |       }, | ||||||
|       xAxis: { |       xAxis: { | ||||||
|         type: 'category', |         type: 'category', | ||||||
|         boundaryGap: false, |         boundaryGap: false, | ||||||
|  |         name: '% Weight', | ||||||
|  |         nameLocation: 'middle', | ||||||
|  |         nameGap: 0, | ||||||
|  |         nameTextStyle: { | ||||||
|  |           verticalAlign: 'top', | ||||||
|  |           padding: [30, 0, 0, 0], | ||||||
|  |         }, | ||||||
|  |         axisLabel: { | ||||||
|  |           interval: (index: number): boolean => { return index && (index % this.labelInterval === 0); }, | ||||||
|  |           formatter: (value: number): string => { return Number(value).toFixed(0); }, | ||||||
|  |         }, | ||||||
|  |         axisTick: { | ||||||
|  |           interval: (index:number): boolean => { return (index % this.labelInterval === 0); }, | ||||||
|  |         }, | ||||||
|       }, |       }, | ||||||
|       yAxis: { |       yAxis: { | ||||||
|         type: 'value', |         type: 'value', | ||||||
|  |         // name: 'Effective Fee Rate s/vb',
 | ||||||
|  |         // nameLocation: 'middle',
 | ||||||
|         splitLine: { |         splitLine: { | ||||||
|           lineStyle: { |           lineStyle: { | ||||||
|             type: 'dotted', |             type: 'dotted', | ||||||
|             color: '#ffffff66', |             color: '#ffffff66', | ||||||
|             opacity: 0.25, |             opacity: 0.25, | ||||||
|           } |           } | ||||||
|  |         }, | ||||||
|  |         axisLabel: { | ||||||
|  |           formatter: (value: number): string => { | ||||||
|  |             const selectedPowerOfTen = selectPowerOfTen(value); | ||||||
|  |             const newVal = Math.round(value / selectedPowerOfTen.divider); | ||||||
|  |             return `${newVal}${selectedPowerOfTen.unit}`; | ||||||
|  |           }, | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       series: [{ |       series: [{ | ||||||
| @ -58,14 +136,18 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges { | |||||||
|           position: 'top', |           position: 'top', | ||||||
|           color: '#ffffff', |           color: '#ffffff', | ||||||
|           textShadowBlur: 0, |           textShadowBlur: 0, | ||||||
|           formatter: (label: any) => { |           formatter: (label: { data: number[] }): string => { | ||||||
|             return Math.floor(label.data); |             const value = label.data[1]; | ||||||
|  |             const selectedPowerOfTen = selectPowerOfTen(value); | ||||||
|  |             const newVal = Math.round(value / selectedPowerOfTen.divider); | ||||||
|  |             return `${newVal}${selectedPowerOfTen.unit}`; | ||||||
|           }, |           }, | ||||||
|         }, |         }, | ||||||
|  |         showAllSymbol: false, | ||||||
|         smooth: true, |         smooth: true, | ||||||
|         lineStyle: { |         lineStyle: { | ||||||
|           color: '#D81B60', |           color: '#D81B60', | ||||||
|           width: 4, |           width: 1, | ||||||
|         }, |         }, | ||||||
|         itemStyle: { |         itemStyle: { | ||||||
|           color: '#b71c1c', |           color: '#b71c1c', | ||||||
|  | |||||||
| @ -39,11 +39,11 @@ | |||||||
|             </tr> |             </tr> | ||||||
|           </tbody> |           </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <app-fee-distribution-graph *ngIf="webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph> |         <app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph> | ||||||
|       </div> |       </div> | ||||||
|       <div class="col-md chart-container"> |       <div class="col-md chart-container"> | ||||||
|         <app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview> |         <app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview> | ||||||
|         <app-fee-distribution-graph *ngIf="!webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph> |         <app-fee-distribution-graph *ngIf="!webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { | |||||||
|   network$: Observable<string>; |   network$: Observable<string>; | ||||||
|   mempoolBlockIndex: number; |   mempoolBlockIndex: number; | ||||||
|   mempoolBlock$: Observable<MempoolBlock>; |   mempoolBlock$: Observable<MempoolBlock>; | ||||||
|  |   mempoolBlockTransactions$: Observable<TransactionStripped[]>; | ||||||
|   ordinal$: BehaviorSubject<string> = new BehaviorSubject(''); |   ordinal$: BehaviorSubject<string> = new BehaviorSubject(''); | ||||||
|   previewTx: TransactionStripped | void; |   previewTx: TransactionStripped | void; | ||||||
|   webGlEnabled: boolean; |   webGlEnabled: boolean; | ||||||
| @ -53,6 +54,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { | |||||||
|                 const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]); |                 const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]); | ||||||
|                 this.ordinal$.next(ordinal); |                 this.ordinal$.next(ordinal); | ||||||
|                 this.seoService.setTitle(ordinal); |                 this.seoService.setTitle(ordinal); | ||||||
|  |                 mempoolBlocks[this.mempoolBlockIndex].isStack = mempoolBlocks[this.mempoolBlockIndex].blockVSize > this.stateService.blockVSize; | ||||||
|                 return mempoolBlocks[this.mempoolBlockIndex]; |                 return mempoolBlocks[this.mempoolBlockIndex]; | ||||||
|               }) |               }) | ||||||
|             ); |             ); | ||||||
| @ -62,6 +64,8 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { | |||||||
|         }) |         }) | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|  |     this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap))); | ||||||
|  | 
 | ||||||
|     this.network$ = this.stateService.networkChanged$; |     this.network$ = this.stateService.networkChanged$; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -143,6 +143,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|           this.mempoolBlocksFull = JSON.parse(stringifiedBlocks); |           this.mempoolBlocksFull = JSON.parse(stringifiedBlocks); | ||||||
|           this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks)); |           this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks)); | ||||||
| 
 | 
 | ||||||
|  |           this.now = Date.now(); | ||||||
|  | 
 | ||||||
|           this.updateMempoolBlockStyles(); |           this.updateMempoolBlockStyles(); | ||||||
|           this.calculateTransactionPosition(); |           this.calculateTransactionPosition(); | ||||||
|           return this.mempoolBlocks; |           return this.mempoolBlocks; | ||||||
| @ -152,7 +154,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$ |     this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$ | ||||||
|       .pipe( |       .pipe( | ||||||
|         map((da) => { |         map((da) => { | ||||||
|           this.now = new Date().getTime(); |           this.now = Date.now(); | ||||||
|  |           this.cd.markForCheck(); | ||||||
|           return da; |           return da; | ||||||
|         }) |         }) | ||||||
|       ); |       ); | ||||||
|  | |||||||
| @ -20,7 +20,8 @@ | |||||||
|                 <fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon> |                 <fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon> | ||||||
|               </a> |               </a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="btn-group btn-group-toggle" name="radioBasic"> |             <div class="btn-toggle-rows" name="radioBasic"> | ||||||
|  |               <div class="btn-group btn-group-toggle"> | ||||||
|                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'"> |                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'"> | ||||||
|                   <input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H |                   <input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H | ||||||
|                   (LIVE) |                   (LIVE) | ||||||
| @ -38,6 +39,8 @@ | |||||||
|                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'"> |                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'"> | ||||||
|                   <input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M |                   <input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M | ||||||
|                 </label> |                 </label> | ||||||
|  |               </div> | ||||||
|  |               <div class="btn-group btn-group-toggle"> | ||||||
|                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'"> |                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'"> | ||||||
|                   <input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M |                   <input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M | ||||||
|                 </label> |                 </label> | ||||||
| @ -53,6 +56,10 @@ | |||||||
|                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'"> |                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'"> | ||||||
|                   <input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y |                   <input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y | ||||||
|                 </label> |                 </label> | ||||||
|  |                 <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'"> | ||||||
|  |                   <input type="radio" [value]="'all'" [routerLink]="['/graphs' | relativeUrl]" fragment="all" formControlName="dateSpan"><span i18n="all">All</span> | ||||||
|  |                 </label> | ||||||
|  |               </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="small-buttons"> |             <div class="small-buttons"> | ||||||
|               <div ngbDropdown #myDrop="ngbDropdown"> |               <div ngbDropdown #myDrop="ngbDropdown"> | ||||||
|  | |||||||
| @ -53,17 +53,17 @@ | |||||||
|   } |   } | ||||||
| } | } | ||||||
| .formRadioGroup.mining { | .formRadioGroup.mining { | ||||||
|   @media (min-width: 991px) { |   @media (min-width: 1035px) { | ||||||
|     position: relative; |     position: relative; | ||||||
|     top: -100px; |     top: -100px; | ||||||
|   } |   } | ||||||
|   @media (min-width: 830px) and (max-width: 991px) { |   @media (min-width: 830px) and (max-width: 1035px) { | ||||||
|     position: relative; |     position: relative; | ||||||
|     top: 0px; |     top: 0px; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| .formRadioGroup.no-menu { | .formRadioGroup.no-menu { | ||||||
|   @media (min-width: 991px) { |   @media (min-width: 1035px) { | ||||||
|     position: relative; |     position: relative; | ||||||
|     top: -33px; |     top: -33px; | ||||||
|   } |   } | ||||||
| @ -183,3 +183,43 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .btn-toggle-rows { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   align-items: stretch; | ||||||
|  |   justify-content: stretch; | ||||||
|  | 
 | ||||||
|  |   .btn-group { | ||||||
|  |     flex-grow: 1; | ||||||
|  |     flex-shrink: 1; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @media (min-width: 500px) { | ||||||
|  |     .btn-group:first-child > .btn:last-child { | ||||||
|  |       border-top-right-radius: 0; | ||||||
|  |       border-bottom-right-radius: 0; | ||||||
|  |     } | ||||||
|  |     .btn-group:last-child > .btn:first-child { | ||||||
|  |       border-top-left-radius: 0; | ||||||
|  |       border-bottom-left-radius: 0; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @media (max-width: 499px) { | ||||||
|  |     flex-direction: column; | ||||||
|  | 
 | ||||||
|  |     .btn-group:first-child > .btn:first-child { | ||||||
|  |       border-bottom-left-radius: 0; | ||||||
|  |     } | ||||||
|  |     .btn-group:first-child > .btn:last-child { | ||||||
|  |       border-bottom-right-radius: 0; | ||||||
|  |     } | ||||||
|  |     .btn-group:last-child > .btn:first-child { | ||||||
|  |       border-top-left-radius: 0; | ||||||
|  |     } | ||||||
|  |     .btn-group:last-child > .btn:last-child { | ||||||
|  |       border-top-right-radius: 0; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -72,8 +72,10 @@ export class StatisticsComponent implements OnInit { | |||||||
|     this.route |     this.route | ||||||
|       .fragment |       .fragment | ||||||
|       .subscribe((fragment) => { |       .subscribe((fragment) => { | ||||||
|         if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y'].indexOf(fragment) > -1) { |         if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y', 'all'].indexOf(fragment) > -1) { | ||||||
|           this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); |           this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); | ||||||
|  |         } else { | ||||||
|  |           this.radioGroupForm.controls.dateSpan.setValue('2h', { emitEvent: false }); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
| @ -114,7 +116,12 @@ export class StatisticsComponent implements OnInit { | |||||||
|         if (this.radioGroupForm.controls.dateSpan.value === '3y') { |         if (this.radioGroupForm.controls.dateSpan.value === '3y') { | ||||||
|           return this.apiService.list3YStatistics$(); |           return this.apiService.list3YStatistics$(); | ||||||
|         } |         } | ||||||
|  |         if (this.radioGroupForm.controls.dateSpan.value === '4y') { | ||||||
|           return this.apiService.list4YStatistics$(); |           return this.apiService.list4YStatistics$(); | ||||||
|  |         } | ||||||
|  |         if (this.radioGroupForm.controls.dateSpan.value === 'all') { | ||||||
|  |           return this.apiService.listAllTimeStatistics$(); | ||||||
|  |         } | ||||||
|       }) |       }) | ||||||
|     ) |     ) | ||||||
|     .subscribe((mempoolStats: any) => { |     .subscribe((mempoolStats: any) => { | ||||||
|  | |||||||
| @ -105,7 +105,7 @@ | |||||||
|                           <app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time> |                           <app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time> | ||||||
|                         </ng-template> |                         </ng-template> | ||||||
|                         <ng-template #timeEstimateDefault> |                         <ng-template #timeEstimateDefault> | ||||||
|                           <app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * this.mempoolPosition.block) + now + timeAvg" [fastRender]="false" [fixedRender]="true"></app-time> |                           <app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time> | ||||||
|                         </ng-template> |                         </ng-template> | ||||||
|                       </ng-template> |                       </ng-template> | ||||||
|                     </ng-template> |                     </ng-template> | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ import { WebsocketService } from '../../services/websocket.service'; | |||||||
| import { AudioService } from '../../services/audio.service'; | import { AudioService } from '../../services/audio.service'; | ||||||
| import { ApiService } from '../../services/api.service'; | import { ApiService } from '../../services/api.service'; | ||||||
| import { SeoService } from '../../services/seo.service'; | import { SeoService } from '../../services/seo.service'; | ||||||
| import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition } from '../../interfaces/node-api.interface'; | import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface'; | ||||||
| import { LiquidUnblinding } from './liquid-ublinding'; | import { LiquidUnblinding } from './liquid-ublinding'; | ||||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||||
| import { Price, PriceService } from '../../services/price.service'; | import { Price, PriceService } from '../../services/price.service'; | ||||||
| @ -65,7 +65,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|   fetchCachedTx$ = new Subject<string>(); |   fetchCachedTx$ = new Subject<string>(); | ||||||
|   isCached: boolean = false; |   isCached: boolean = false; | ||||||
|   now = Date.now(); |   now = Date.now(); | ||||||
|   timeAvg$: Observable<number>; |   da$: Observable<DifficultyAdjustment>; | ||||||
|   liquidUnblinding = new LiquidUnblinding(); |   liquidUnblinding = new LiquidUnblinding(); | ||||||
|   inputIndex: number; |   inputIndex: number; | ||||||
|   outputIndex: number; |   outputIndex: number; | ||||||
| @ -117,10 +117,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|       this.setFlowEnabled(); |       this.setFlowEnabled(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     this.timeAvg$ = timer(0, 1000) |     this.da$ = this.stateService.difficultyAdjustment$.pipe( | ||||||
|       .pipe( |       tap(() => { | ||||||
|         switchMap(() => this.stateService.difficultyAdjustment$), |         this.now = Date.now(); | ||||||
|         map((da) => da.timeAvg) |       }) | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => { |     this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => { | ||||||
| @ -236,6 +236,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { |     this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { | ||||||
|  |       this.now = Date.now(); | ||||||
|       if (txPosition && txPosition.txid === this.txId && txPosition.position) { |       if (txPosition && txPosition.txid === this.txId && txPosition.position) { | ||||||
|         this.mempoolPosition = txPosition.position; |         this.mempoolPosition = txPosition.position; | ||||||
|         if (this.tx && !this.tx.status.confirmed) { |         if (this.tx && !this.tx.status.confirmed) { | ||||||
| @ -434,12 +435,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => { |     this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => { | ||||||
|  |       this.now = Date.now(); | ||||||
|  | 
 | ||||||
|       if (!this.tx || this.mempoolPosition) { |       if (!this.tx || this.mempoolPosition) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       this.now = Date.now(); |  | ||||||
| 
 |  | ||||||
|       const txFeePerVSize = |       const txFeePerVSize = | ||||||
|         this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4); |         this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -153,6 +153,8 @@ export interface BlockExtended extends Block { | |||||||
| export interface BlockAudit extends BlockExtended { | export interface BlockAudit extends BlockExtended { | ||||||
|   missingTxs: string[], |   missingTxs: string[], | ||||||
|   addedTxs: string[], |   addedTxs: string[], | ||||||
|  |   freshTxs: string[], | ||||||
|  |   sigopTxs: string[], | ||||||
|   matchRate: number, |   matchRate: number, | ||||||
|   expectedFees: number, |   expectedFees: number, | ||||||
|   expectedWeight: number, |   expectedWeight: number, | ||||||
| @ -169,6 +171,7 @@ export interface TransactionStripped { | |||||||
|   vsize: number; |   vsize: number; | ||||||
|   value: number; |   value: number; | ||||||
|   status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; |   status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; | ||||||
|  |   context?: 'projected' | 'actual'; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface RbfTransaction extends TransactionStripped { | interface RbfTransaction extends TransactionStripped { | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ export interface WebsocketResponse { | |||||||
|   'track-rbf'?: string; |   'track-rbf'?: string; | ||||||
|   'watch-mempool'?: boolean; |   'watch-mempool'?: boolean; | ||||||
|   'track-bisq-market'?: string; |   'track-bisq-market'?: string; | ||||||
|  |   'refresh-blocks'?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ReplacedTransaction extends Transaction { | export interface ReplacedTransaction extends Transaction { | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; | import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; | ||||||
| import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, | import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, | ||||||
|   PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree } from '../interfaces/node-api.interface'; |   PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface'; | ||||||
| import { Observable } from 'rxjs'; | import { Observable } from 'rxjs'; | ||||||
| import { StateService } from './state.service'; | import { StateService } from './state.service'; | ||||||
| import { WebsocketResponse } from '../interfaces/websocket.interface'; | import { WebsocketResponse } from '../interfaces/websocket.interface'; | ||||||
| @ -72,6 +72,10 @@ export class ApiService { | |||||||
|     return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/4y'); |     return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/4y'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   listAllTimeStatistics$(): Observable<OptimizedMempoolStats[]> { | ||||||
|  |     return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/all'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getTransactionTimes$(txIds: string[]): Observable<number[]> { |   getTransactionTimes$(txIds: string[]): Observable<number[]> { | ||||||
|     let params = new HttpParams(); |     let params = new HttpParams(); | ||||||
|     txIds.forEach((txId: string) => { |     txIds.forEach((txId: string) => { | ||||||
| @ -245,9 +249,9 @@ export class ApiService { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getBlockAudit$(hash: string) : Observable<any> { |   getBlockAudit$(hash: string) : Observable<BlockAudit> { | ||||||
|     return this.httpClient.get<any>( |     return this.httpClient.get<BlockAudit>( | ||||||
|       this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`, { observe: 'response' } |       this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary` | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; | import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; | ||||||
| import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; | import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; | ||||||
| import { Transaction } from '../interfaces/electrs.interface'; | import { Transaction } from '../interfaces/electrs.interface'; | ||||||
| import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; | import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; | ||||||
| import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; | import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; | ||||||
| import { Router, NavigationStart } from '@angular/router'; | import { Router, NavigationStart } from '@angular/router'; | ||||||
| import { isPlatformBrowser } from '@angular/common'; | import { isPlatformBrowser } from '@angular/common'; | ||||||
| import { map, shareReplay } from 'rxjs/operators'; | import { map, scan, shareReplay, tap } from 'rxjs/operators'; | ||||||
| import { StorageService } from './storage.service'; | import { StorageService } from './storage.service'; | ||||||
| 
 | 
 | ||||||
| interface MarkBlockState { | interface MarkBlockState { | ||||||
| @ -100,6 +100,7 @@ export class StateService { | |||||||
|   mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); |   mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); | ||||||
|   mempoolBlockTransactions$ = new Subject<TransactionStripped[]>(); |   mempoolBlockTransactions$ = new Subject<TransactionStripped[]>(); | ||||||
|   mempoolBlockDelta$ = new Subject<MempoolBlockDelta>(); |   mempoolBlockDelta$ = new Subject<MempoolBlockDelta>(); | ||||||
|  |   liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; | ||||||
|   txReplaced$ = new Subject<ReplacedTransaction>(); |   txReplaced$ = new Subject<ReplacedTransaction>(); | ||||||
|   txRbfInfo$ = new Subject<RbfTree>(); |   txRbfInfo$ = new Subject<RbfTree>(); | ||||||
|   rbfLatest$ = new Subject<RbfTree[]>(); |   rbfLatest$ = new Subject<RbfTree[]>(); | ||||||
| @ -166,6 +167,30 @@ export class StateService { | |||||||
| 
 | 
 | ||||||
|     this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT); |     this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT); | ||||||
| 
 | 
 | ||||||
|  |     this.liveMempoolBlockTransactions$ = merge( | ||||||
|  |       this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })), | ||||||
|  |       this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })), | ||||||
|  |     ).pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: any): { [txid: string]: TransactionStripped } => { | ||||||
|  |       if (change.transactions) { | ||||||
|  |         const txMap = {} | ||||||
|  |         change.transactions.forEach(tx => { | ||||||
|  |           txMap[tx.txid] = tx; | ||||||
|  |         }) | ||||||
|  |         return txMap; | ||||||
|  |       } else { | ||||||
|  |         change.delta.changed.forEach(tx => { | ||||||
|  |           transactions[tx.txid].rate = tx.rate; | ||||||
|  |         }) | ||||||
|  |         change.delta.removed.forEach(txid => { | ||||||
|  |           delete transactions[txid]; | ||||||
|  |         }); | ||||||
|  |         change.delta.added.forEach(tx => { | ||||||
|  |           transactions[tx.txid] = tx; | ||||||
|  |         }); | ||||||
|  |         return transactions; | ||||||
|  |       } | ||||||
|  |     }, {})); | ||||||
|  | 
 | ||||||
|     if (this.env.BASE_MODULE === 'bisq') { |     if (this.env.BASE_MODULE === 'bisq') { | ||||||
|       this.network = this.env.BASE_MODULE; |       this.network = this.env.BASE_MODULE; | ||||||
|       this.networkChanged$.next(this.env.BASE_MODULE); |       this.networkChanged$.next(this.env.BASE_MODULE); | ||||||
|  | |||||||
| @ -235,6 +235,8 @@ export class WebsocketService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleResponse(response: WebsocketResponse) { |   handleResponse(response: WebsocketResponse) { | ||||||
|  |     let reinitBlocks = false; | ||||||
|  | 
 | ||||||
|     if (response.blocks && response.blocks.length) { |     if (response.blocks && response.blocks.length) { | ||||||
|       const blocks = response.blocks; |       const blocks = response.blocks; | ||||||
|       let maxHeight = 0; |       let maxHeight = 0; | ||||||
| @ -256,9 +258,11 @@ export class WebsocketService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (response.block) { |     if (response.block) { | ||||||
|       if (response.block.height > this.stateService.latestBlockHeight) { |       if (response.block.height === this.stateService.latestBlockHeight + 1) { | ||||||
|         this.stateService.updateChainTip(response.block.height); |         this.stateService.updateChainTip(response.block.height); | ||||||
|         this.stateService.blocks$.next([response.block, response.txConfirmed || '']); |         this.stateService.blocks$.next([response.block, response.txConfirmed || '']); | ||||||
|  |       } else if (response.block.height > this.stateService.latestBlockHeight + 1) { | ||||||
|  |         reinitBlocks = true; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (response.txConfirmed) { |       if (response.txConfirmed) { | ||||||
| @ -369,5 +373,9 @@ export class WebsocketService { | |||||||
|     if (response['git-commit']) { |     if (response['git-commit']) { | ||||||
|       this.stateService.backendInfo$.next(response['git-commit']); |       this.stateService.backendInfo$.next(response['git-commit']); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     if (reinitBlocks) { | ||||||
|  |       this.websocketSubject.next({'refresh-blocks': true}); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -500,7 +500,7 @@ html:lang(ru) .card-title { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .fee-distribution-chart { | .fee-distribution-chart { | ||||||
|   height: 250px; |   height: 265px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .fees-wrapper-tooltip-chart { | .fees-wrapper-tooltip-chart { | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ do for url in / \ | |||||||
| 	'/api/v1/statistics/2y' \ | 	'/api/v1/statistics/2y' \ | ||||||
| 	'/api/v1/statistics/3y' \ | 	'/api/v1/statistics/3y' \ | ||||||
| 	'/api/v1/statistics/4y' \ | 	'/api/v1/statistics/4y' \ | ||||||
|  | 	'/api/v1/statistics/all' \ | ||||||
| 	'/api/v1/mining/pools/24h' \ | 	'/api/v1/mining/pools/24h' \ | ||||||
| 	'/api/v1/mining/pools/3d' \ | 	'/api/v1/mining/pools/3d' \ | ||||||
| 	'/api/v1/mining/pools/1w' \ | 	'/api/v1/mining/pools/1w' \ | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user