Merge branch 'master' into simon/frontend-packages-29-12
This commit is contained in:
		
						commit
						2b9975ded9
					
				
							
								
								
									
										10
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -31,7 +31,7 @@ jobs: | ||||
|         run: | | ||||
|           sudo swapoff /mnt/swapfile | ||||
|           sudo rm -v /mnt/swapfile | ||||
|           sudo fallocate -l 10G /mnt/swapfile | ||||
|           sudo fallocate -l 13G /mnt/swapfile | ||||
|           sudo chmod 600 /mnt/swapfile | ||||
|           sudo mkswap /mnt/swapfile | ||||
|           sudo swapon /mnt/swapfile | ||||
| @ -68,24 +68,24 @@ jobs: | ||||
|         run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin | ||||
| 
 | ||||
|       - name: Checkout project | ||||
|         uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0 | ||||
|         uses: actions/checkout@v3 | ||||
| 
 | ||||
|       - name: Init repo for Dockerization | ||||
|         run: docker/init.sh "$TAG" | ||||
| 
 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0 | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|         id: qemu | ||||
| 
 | ||||
|       - name: Setup Docker buildx action | ||||
|         uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2.2.1 | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|         id: buildx | ||||
| 
 | ||||
|       - name: Available platforms | ||||
|         run: echo ${{ steps.buildx.outputs.platforms }} | ||||
| 
 | ||||
|       - name: Cache Docker layers | ||||
|         uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11 | ||||
|         uses: actions/cache@v3 | ||||
|         id: cache | ||||
|         with: | ||||
|           path: /tmp/.buildx-cache | ||||
|  | ||||
| @ -27,7 +27,7 @@ | ||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", | ||||
|     "ADVANCED_GBT_AUDIT": false, | ||||
|     "ADVANCED_GBT_MEMPOOL": false, | ||||
|     "TRANSACTION_INDEXING": false | ||||
|     "CPFP_INDEXING": false | ||||
|   }, | ||||
|   "CORE_RPC": { | ||||
|     "HOST": "127.0.0.1", | ||||
|  | ||||
| @ -26,9 +26,9 @@ | ||||
|     "INDEXING_BLOCKS_AMOUNT": 14, | ||||
|     "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", | ||||
|     "POOLS_JSON_URL": "__POOLS_JSON_URL__", | ||||
|     "ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__", | ||||
|     "ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__", | ||||
|     "TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__" | ||||
|     "ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__", | ||||
|     "ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__", | ||||
|     "CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__" | ||||
|   }, | ||||
|   "CORE_RPC": { | ||||
|     "HOST": "__CORE_RPC_HOST__", | ||||
|  | ||||
| @ -40,7 +40,7 @@ describe('Mempool Backend Config', () => { | ||||
|         POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', | ||||
|         ADVANCED_GBT_AUDIT: false, | ||||
|         ADVANCED_GBT_MEMPOOL: false, | ||||
|         TRANSACTION_INDEXING: false, | ||||
|         CPFP_INDEXING: false, | ||||
|       }); | ||||
| 
 | ||||
|       expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); | ||||
|  | ||||
| @ -1,10 +1,5 @@ | ||||
| import config from '../config'; | ||||
| import bitcoinApi from './bitcoin/bitcoin-api-factory'; | ||||
| import { Common } from './common'; | ||||
| import { TransactionExtended, MempoolBlockWithTransactions, AuditScore } from '../mempool.interfaces'; | ||||
| import blocksRepository from '../repositories/BlocksRepository'; | ||||
| import blocksAuditsRepository from '../repositories/BlocksAuditsRepository'; | ||||
| import blocks from '../api/blocks'; | ||||
| import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; | ||||
| 
 | ||||
| const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
 | ||||
| 
 | ||||
|  | ||||
| @ -2,6 +2,7 @@ import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import os from 'os'; | ||||
| import { IBackendInfo } from '../mempool.interfaces'; | ||||
| import config from '../config'; | ||||
| 
 | ||||
| class BackendInfo { | ||||
|   private backendInfo: IBackendInfo; | ||||
| @ -22,7 +23,8 @@ class BackendInfo { | ||||
|     this.backendInfo = { | ||||
|       hostname: os.hostname(), | ||||
|       version: versionInfo.version, | ||||
|       gitCommit: versionInfo.gitCommit | ||||
|       gitCommit: versionInfo.gitCommit, | ||||
|       lightning: config.LIGHTNING.ENABLED | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -17,4 +17,6 @@ function bitcoinApiFactory(): AbstractBitcoinApi { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const bitcoinCoreApi = new BitcoinApi(bitcoinClient); | ||||
| 
 | ||||
| export default bitcoinApiFactory(); | ||||
|  | ||||
| @ -402,7 +402,8 @@ class BitcoinRoutes { | ||||
|   private async getLegacyBlocks(req: Request, res: Response) { | ||||
|     try { | ||||
|       const returnBlocks: IEsploraApi.Block[] = []; | ||||
|       const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight(); | ||||
|       const tip = blocks.getCurrentBlockHeight(); | ||||
|       const fromHeight = Math.min(parseInt(req.params.height, 10) || tip, tip); | ||||
| 
 | ||||
|       // Check if block height exist in local cache to skip the hash lookup
 | ||||
|       const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight); | ||||
|  | ||||
| @ -22,12 +22,10 @@ import poolsParser from './pools-parser'; | ||||
| import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; | ||||
| import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; | ||||
| import cpfpRepository from '../repositories/CpfpRepository'; | ||||
| import transactionRepository from '../repositories/TransactionRepository'; | ||||
| import mining from './mining/mining'; | ||||
| import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; | ||||
| import PricesRepository from '../repositories/PricesRepository'; | ||||
| import priceUpdater from '../tasks/price-updater'; | ||||
| import { Block } from 'bitcoinjs-lib'; | ||||
| 
 | ||||
| class Blocks { | ||||
|   private blocks: BlockExtended[] = []; | ||||
| @ -101,12 +99,23 @@ class Blocks { | ||||
|           transactions.push(tx); | ||||
|           transactionsFetched++; | ||||
|         } catch (e) { | ||||
|           if (i === 0) { | ||||
|             const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);  | ||||
|             logger.err(msg); | ||||
|             throw new Error(msg); | ||||
|           } else { | ||||
|             logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|           try { | ||||
|             if (config.MEMPOOL.BACKEND === 'esplora') { | ||||
|               // Try again with core
 | ||||
|               const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true); | ||||
|               transactions.push(tx); | ||||
|               transactionsFetched++; | ||||
|             } else { | ||||
|               throw e; | ||||
|             } | ||||
|           } catch (e) { | ||||
|             if (i === 0) { | ||||
|               const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);  | ||||
|               logger.err(msg); | ||||
|               throw new Error(msg); | ||||
|             } else { | ||||
|               logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| @ -296,7 +305,7 @@ class Blocks { | ||||
|           const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); | ||||
|           const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); | ||||
|           const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100; | ||||
|           logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`); | ||||
|           logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining); | ||||
|           timer = new Date().getTime() / 1000; | ||||
|           indexedThisRun = 0; | ||||
|         } | ||||
| @ -309,12 +318,12 @@ class Blocks { | ||||
|         newlyIndexed++; | ||||
|       } | ||||
|       if (newlyIndexed > 0) { | ||||
|         logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`); | ||||
|         logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); | ||||
|       } else { | ||||
|         logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`); | ||||
|         logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||
|       logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| @ -329,9 +338,10 @@ class Blocks { | ||||
| 
 | ||||
|     try { | ||||
|       // Get all indexed block hash
 | ||||
|       const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks(); | ||||
|       const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks(); | ||||
|       logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`); | ||||
| 
 | ||||
|       if (!unindexedBlocks?.length) { | ||||
|       if (!unindexedBlockHeights?.length) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
| @ -340,30 +350,26 @@ class Blocks { | ||||
|       let countThisRun = 0; | ||||
|       let timer = new Date().getTime() / 1000; | ||||
|       const startedAt = new Date().getTime() / 1000; | ||||
| 
 | ||||
|       for (const block of unindexedBlocks) { | ||||
|       for (const height of unindexedBlockHeights) { | ||||
|         // Logging
 | ||||
|         const hash = await bitcoinApi.$getBlockHash(height); | ||||
|         const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer); | ||||
|         if (elapsedSeconds > 5) { | ||||
|           const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); | ||||
|           const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds); | ||||
|           const progress = Math.round(count / unindexedBlocks.length * 10000) / 100; | ||||
|           logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`); | ||||
|           const blockPerSeconds = (countThisRun / elapsedSeconds); | ||||
|           const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100; | ||||
|           logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`); | ||||
|           timer = new Date().getTime() / 1000; | ||||
|           countThisRun = 0; | ||||
|         } | ||||
| 
 | ||||
|         await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block
 | ||||
|         await this.$indexCPFP(hash, height); // Calculate and save CPFP data for transactions in this block
 | ||||
| 
 | ||||
|         // Logging
 | ||||
|         count++; | ||||
|         countThisRun++; | ||||
|       } | ||||
|       if (count > 0) { | ||||
|         logger.notice(`CPFP indexing completed: indexed ${count} blocks`); | ||||
|       } else { | ||||
|         logger.debug(`CPFP indexing completed: indexed ${count} blocks`); | ||||
|       } | ||||
|       logger.notice(`CPFP indexing completed: indexed ${count} blocks`); | ||||
|     } catch (e) { | ||||
|       logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||
|       throw e; | ||||
| @ -385,7 +391,7 @@ class Blocks { | ||||
| 
 | ||||
|       const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); | ||||
| 
 | ||||
|       logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`); | ||||
|       logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`, logger.tags.mining); | ||||
|       loadingIndicators.setProgress('block-indexing', 0); | ||||
| 
 | ||||
|       const chunkSize = 10000; | ||||
| @ -405,7 +411,7 @@ class Blocks { | ||||
|           continue; | ||||
|         } | ||||
| 
 | ||||
|         logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`); | ||||
|         logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining); | ||||
| 
 | ||||
|         for (const blockHeight of missingBlockHeights) { | ||||
|           if (blockHeight < lastBlockToIndex) { | ||||
| @ -418,7 +424,7 @@ class Blocks { | ||||
|             const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); | ||||
|             const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); | ||||
|             const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100; | ||||
|             logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`); | ||||
|             logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining); | ||||
|             timer = new Date().getTime() / 1000; | ||||
|             indexedThisRun = 0; | ||||
|             loadingIndicators.setProgress('block-indexing', progress, false); | ||||
| @ -435,13 +441,13 @@ class Blocks { | ||||
|         currentBlockHeight -= chunkSize; | ||||
|       } | ||||
|       if (newlyIndexed > 0) { | ||||
|         logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`); | ||||
|         logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); | ||||
|       } else { | ||||
|         logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`); | ||||
|         logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); | ||||
|       } | ||||
|       loadingIndicators.setProgress('block-indexing', 100); | ||||
|     } catch (e) { | ||||
|       logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); | ||||
|       loadingIndicators.setProgress('block-indexing', 100); | ||||
|       throw e; | ||||
|     } | ||||
| @ -519,7 +525,7 @@ class Blocks { | ||||
|             for (let i = 10; i >= 0; --i) { | ||||
|               const newBlock = await this.$indexBlock(lastBlock['height'] - i); | ||||
|               await this.$getStrippedBlockTransactions(newBlock.id, true, true); | ||||
|               if (config.MEMPOOL.TRANSACTION_INDEXING) { | ||||
|               if (config.MEMPOOL.CPFP_INDEXING) { | ||||
|                 await this.$indexCPFP(newBlock.id, lastBlock['height'] - i); | ||||
|               } | ||||
|             } | ||||
| @ -537,7 +543,7 @@ class Blocks { | ||||
|               priceId: lastestPriceId, | ||||
|             }]); | ||||
|           } else { | ||||
|             logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`) | ||||
|             logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining); | ||||
|             setTimeout(() => { | ||||
|               indexer.runSingleTask('blocksPrices'); | ||||
|             }, 10000); | ||||
| @ -547,7 +553,7 @@ class Blocks { | ||||
|           if (Common.blocksSummariesIndexingEnabled() === true) { | ||||
|             await this.$getStrippedBlockTransactions(blockExtended.id, true); | ||||
|           } | ||||
|           if (config.MEMPOOL.TRANSACTION_INDEXING) { | ||||
|           if (config.MEMPOOL.CPFP_INDEXING) { | ||||
|             this.$indexCPFP(blockExtended.id, this.currentBlockHeight); | ||||
|           } | ||||
|         } | ||||
| @ -677,7 +683,12 @@ class Blocks { | ||||
|   } | ||||
| 
 | ||||
|   public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> { | ||||
|     let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight(); | ||||
| 
 | ||||
|     let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; | ||||
|     if (currentHeight > this.currentBlockHeight) { | ||||
|       limit -= currentHeight - this.currentBlockHeight; | ||||
|       currentHeight = this.currentBlockHeight; | ||||
|     } | ||||
|     const returnBlocks: BlockExtended[] = []; | ||||
| 
 | ||||
|     if (currentHeight < 0) { | ||||
| @ -741,34 +752,15 @@ class Blocks { | ||||
|   } | ||||
| 
 | ||||
|   public async $indexCPFP(hash: string, height: number): Promise<void> { | ||||
|     let transactions; | ||||
|     if (Common.blocksSummariesIndexingEnabled()) { | ||||
|       transactions = await this.$getStrippedBlockTransactions(hash); | ||||
|       const rawBlock = await bitcoinApi.$getRawBlock(hash); | ||||
|       const block = Block.fromBuffer(rawBlock); | ||||
|       const txMap = {}; | ||||
|       for (const tx of block.transactions || []) { | ||||
|         txMap[tx.getId()] = tx; | ||||
|       } | ||||
|       for (const tx of transactions) { | ||||
|         // convert from bitcoinjs to esplora vin format
 | ||||
|         if (txMap[tx.txid]?.ins) { | ||||
|           tx.vin = txMap[tx.txid].ins.map(vin => { | ||||
|             return { | ||||
|               txid: vin.hash.slice().reverse().toString('hex') | ||||
|             }; | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       const block = await bitcoinClient.getBlock(hash, 2); | ||||
|       transactions = block.tx.map(tx => { | ||||
|         tx.vsize = tx.weight / 4; | ||||
|         tx.fee *= 100_000_000; | ||||
|         return tx; | ||||
|       }); | ||||
|     } | ||||
|   | ||||
|     const block = await bitcoinClient.getBlock(hash, 2); | ||||
|     const transactions = block.tx.map(tx => { | ||||
|       tx.vsize = tx.weight / 4; | ||||
|       tx.fee *= 100_000_000; | ||||
|       return tx; | ||||
|     }); | ||||
| 
 | ||||
|     const clusters: any[] = []; | ||||
| 
 | ||||
|     let cluster: TransactionStripped[] = []; | ||||
|     let ancestors: { [txid: string]: boolean } = {}; | ||||
|     for (let i = transactions.length - 1; i >= 0; i--) { | ||||
| @ -782,10 +774,12 @@ class Blocks { | ||||
|         }); | ||||
|         const effectiveFeePerVsize = totalFee / totalVSize; | ||||
|         if (cluster.length > 1) { | ||||
|           await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }), effectiveFeePerVsize); | ||||
|           for (const tx of cluster) { | ||||
|             await transactionRepository.$setCluster(tx.txid, cluster[0].txid); | ||||
|           } | ||||
|           clusters.push({ | ||||
|             root: cluster[0].txid, | ||||
|             height, | ||||
|             txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }), | ||||
|             effectiveFeePerVsize, | ||||
|           }); | ||||
|         } | ||||
|         cluster = []; | ||||
|         ancestors = {}; | ||||
| @ -795,7 +789,10 @@ class Blocks { | ||||
|         ancestors[vin.txid] = true; | ||||
|       }); | ||||
|     } | ||||
|     await blocksRepository.$setCPFPIndexed(hash); | ||||
|     const result = await cpfpRepository.$batchSaveClusters(clusters); | ||||
|     if (!result) { | ||||
|       await cpfpRepository.$insertProgressMarker(height); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -190,7 +190,7 @@ export class Common { | ||||
|   static cpfpIndexingEnabled(): boolean { | ||||
|     return ( | ||||
|       Common.indexingEnabled() && | ||||
|       config.MEMPOOL.TRANSACTION_INDEXING === true | ||||
|       config.MEMPOOL.CPFP_INDEXING === true | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -2,9 +2,12 @@ import config from '../config'; | ||||
| import DB from '../database'; | ||||
| import logger from '../logger'; | ||||
| import { Common } from './common'; | ||||
| import blocksRepository from '../repositories/BlocksRepository'; | ||||
| import cpfpRepository from '../repositories/CpfpRepository'; | ||||
| import { RowDataPacket } from 'mysql2'; | ||||
| 
 | ||||
| class DatabaseMigration { | ||||
|   private static currentVersion = 49; | ||||
|   private static currentVersion = 52; | ||||
|   private queryTimeout = 3600_000; | ||||
|   private statisticsAddedIndexed = false; | ||||
|   private uniqueLogs: string[] = []; | ||||
| @ -442,6 +445,29 @@ class DatabaseMigration { | ||||
|       await this.$executeQuery('TRUNCATE TABLE `blocks_audits`'); | ||||
|       await this.updateToSchemaVersion(49); | ||||
|     } | ||||
| 
 | ||||
|     if (databaseSchemaVersion < 50) { | ||||
|       await this.$executeQuery('ALTER TABLE `blocks` DROP COLUMN `cpfp_indexed`'); | ||||
|       await this.updateToSchemaVersion(50); | ||||
|     } | ||||
| 
 | ||||
|     if (databaseSchemaVersion < 51) { | ||||
|       await this.$executeQuery('ALTER TABLE `cpfp_clusters` ADD INDEX `height` (`height`)'); | ||||
|       await this.updateToSchemaVersion(51); | ||||
|     } | ||||
| 
 | ||||
|     if (databaseSchemaVersion < 52) { | ||||
|       await this.$executeQuery(this.getCreateCompactCPFPTableQuery(), await this.$checkIfTableExists('compact_cpfp_clusters')); | ||||
|       await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions')); | ||||
|       try { | ||||
|         await this.$convertCompactCpfpTables(); | ||||
|         await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`'); | ||||
|         await this.$executeQuery('DROP TABLE IF EXISTS `transactions`'); | ||||
|         await this.updateToSchemaVersion(52); | ||||
|       } catch(e) { | ||||
|         logger.warn('' + (e instanceof Error ? e.message : e)); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -913,6 +939,25 @@ class DatabaseMigration { | ||||
|     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | ||||
|   } | ||||
| 
 | ||||
|   private getCreateCompactCPFPTableQuery(): string { | ||||
|     return `CREATE TABLE IF NOT EXISTS compact_cpfp_clusters (
 | ||||
|       root binary(32) NOT NULL, | ||||
|       height int(10) NOT NULL, | ||||
|       txs BLOB DEFAULT NULL, | ||||
|       fee_rate float unsigned, | ||||
|       PRIMARY KEY (root), | ||||
|       INDEX (height) | ||||
|     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | ||||
|   } | ||||
| 
 | ||||
|   private getCreateCompactTransactionsTableQuery(): string { | ||||
|     return `CREATE TABLE IF NOT EXISTS compact_transactions (
 | ||||
|       txid binary(32) NOT NULL, | ||||
|       cluster binary(32) DEFAULT NULL, | ||||
|       PRIMARY KEY (txid) | ||||
|     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | ||||
|   } | ||||
| 
 | ||||
|   public async $truncateIndexedData(tables: string[]) { | ||||
|     const allowedTables = ['blocks', 'hashrates', 'prices']; | ||||
| 
 | ||||
| @ -933,6 +978,49 @@ class DatabaseMigration { | ||||
|       logger.warn(`Unable to erase indexed data`); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $convertCompactCpfpTables(): Promise<void> { | ||||
|     try { | ||||
|       const batchSize = 250; | ||||
|       const maxHeight = await blocksRepository.$mostRecentBlockHeight() || 0; | ||||
|       const [minHeightRows]: any = await DB.query(`SELECT MIN(height) AS minHeight from cpfp_clusters`); | ||||
|       const minHeight = (minHeightRows.length && minHeightRows[0].minHeight != null) ? minHeightRows[0].minHeight : maxHeight; | ||||
|       let height = maxHeight; | ||||
| 
 | ||||
|       // Logging
 | ||||
|       let timer = new Date().getTime() / 1000; | ||||
|       const startedAt = new Date().getTime() / 1000; | ||||
| 
 | ||||
|       while (height > minHeight) { | ||||
|         const [rows] = await DB.query( | ||||
|           ` | ||||
|             SELECT * from cpfp_clusters | ||||
|             WHERE height <= ? AND height > ? | ||||
|             ORDER BY height | ||||
|           `,
 | ||||
|           [height, height - batchSize] | ||||
|         ) as RowDataPacket[][]; | ||||
|         if (rows?.length) { | ||||
|           await cpfpRepository.$batchSaveClusters(rows.map(row => { | ||||
|             return { | ||||
|               root: row.root, | ||||
|               height: row.height, | ||||
|               txs: JSON.parse(row.txs), | ||||
|               effectiveFeePerVsize: row.fee_rate, | ||||
|             }; | ||||
|           })); | ||||
|         } | ||||
| 
 | ||||
|         const elapsed = new Date().getTime() / 1000 - timer; | ||||
|         const runningFor = new Date().getTime() / 1000 - startedAt; | ||||
|         logger.debug(`Migrated cpfp data from block ${height} to ${height - batchSize} in ${elapsed.toFixed(2)} seconds | total elapsed: ${runningFor.toFixed(2)} seconds`); | ||||
|         timer = new Date().getTime() / 1000; | ||||
|         height -= batchSize; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.warn(`Failed to migrate cpfp transaction data`); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default new DatabaseMigration(); | ||||
|  | ||||
| @ -670,9 +670,7 @@ class ChannelsApi { | ||||
|         AND status != 2 | ||||
|       `);
 | ||||
|       if (result[0].changedRows ?? 0 > 0) { | ||||
|         logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`); | ||||
|       } else { | ||||
|         logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`); | ||||
|         logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`, logger.tags.ln); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); | ||||
|  | ||||
| @ -685,9 +685,7 @@ class NodesApi { | ||||
|         ) | ||||
|       `);
 | ||||
|       if (result[0].changedRows ?? 0 > 0) { | ||||
|         logger.info(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`); | ||||
|       } else { | ||||
|         logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`); | ||||
|         logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`, logger.tags.ln); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('$setNodesInactive() error: ' + (e instanceof Error ? e.message : e)); | ||||
|  | ||||
| @ -41,13 +41,70 @@ class NodesRoutes { | ||||
|       let nodes: any[] = []; | ||||
|       switch (config.MEMPOOL.NETWORK) { | ||||
|         case 'testnet': | ||||
|           nodesList = ['032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584']; | ||||
|           nodesList = [ | ||||
|             '032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', | ||||
|             '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', | ||||
|             '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', | ||||
|             '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', | ||||
|             '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', | ||||
|             '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', | ||||
|             '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', | ||||
|             '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', | ||||
|             '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', | ||||
|             '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', | ||||
|             '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', | ||||
|             '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584', | ||||
|             '0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c', | ||||
|             '029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5', | ||||
|             '02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075', | ||||
|             '030b0ca1ea7b1075716d2a555630e6fd47ef11bc7391fe68963ec06cf370a5e382', | ||||
|             '031adb9eb2d66693f85fa31a4adca0319ba68219f3ad5f9a2ef9b34a6b40755fa1', | ||||
|             '02ccd07faa47eda810ecf5591ccf5ca50f6c1034d0d175052898d32a00b9bae24f', | ||||
|           ]; | ||||
|           break; | ||||
|         case 'signet': | ||||
|           nodesList = ['03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7']; | ||||
|           nodesList = [ | ||||
|             '03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', | ||||
|             '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', | ||||
|             '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', | ||||
|             '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', | ||||
|             '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', | ||||
|             '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', | ||||
|             '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', | ||||
|             '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', | ||||
|             '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', | ||||
|             '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', | ||||
|             '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', | ||||
|             '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7', | ||||
|             '02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070', | ||||
|             '02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45', | ||||
|             '038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097', | ||||
|             '0242c7f7d315095f37ad1421ae0a2fc967d4cbe65b61b079c5395a769436959853', | ||||
|             '02a909e70eb03742f12666ebb1f56ac42a5fbaab0c0e8b5b1df4aa9f10f8a09240', | ||||
|             '03a26efa12489803c07f3ac2f1dba63812e38f0f6e866ce3ebb34df7de1f458cd2', | ||||
|           ]; | ||||
|           break; | ||||
|         default: | ||||
|           nodesList = ['03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43']; | ||||
|           nodesList = [ | ||||
|             '03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', | ||||
|             '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', | ||||
|             '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', | ||||
|             '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', | ||||
|             '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', | ||||
|             '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', | ||||
|             '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', | ||||
|             '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', | ||||
|             '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', | ||||
|             '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', | ||||
|             '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', | ||||
|             '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43', | ||||
|             '02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06', | ||||
|             '0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e', | ||||
|             '03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7', | ||||
|             '038d118996b3eaa15dcd317b32a539c9ecfdd7698f204acf8a087336af655a9192', | ||||
|             '02a928903d93d78877dacc3642b696128a3636e9566dd42d2d132325b2c8891c09', | ||||
|             '0328cd17f3a9d3d90b532ade0d1a67e05eb8a51835b3dce0a2e38eac04b5a62a57', | ||||
|           ]; | ||||
|       } | ||||
| 
 | ||||
|       for (let pubKey of nodesList) { | ||||
|  | ||||
| @ -141,13 +141,13 @@ export default class CLightningClient extends EventEmitter implements AbstractLi | ||||
|       // main data directory provided, default to using the bitcoin mainnet subdirectory
 | ||||
|       // to be removed in v0.2.0
 | ||||
|       else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) { | ||||
|         logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`) | ||||
|         logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`) | ||||
|         logger.warn(`${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`, logger.tags.ln) | ||||
|         logger.warn(`specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`, logger.tags.ln) | ||||
|         rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc') | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     logger.debug(`[CLightningClient] Connecting to ${rpcPath}`); | ||||
|     logger.debug(`Connecting to ${rpcPath}`, logger.tags.ln); | ||||
| 
 | ||||
|     super(); | ||||
|     this.rpcPath = rpcPath; | ||||
| @ -172,19 +172,19 @@ export default class CLightningClient extends EventEmitter implements AbstractLi | ||||
| 
 | ||||
|     this.clientConnectionPromise = new Promise<void>(resolve => { | ||||
|       _self.client.on('connect', () => { | ||||
|         logger.info(`[CLightningClient] Lightning client connected`); | ||||
|         logger.info(`CLightning client connected`, logger.tags.ln); | ||||
|         _self.reconnectWait = 1; | ||||
|         resolve(); | ||||
|       }); | ||||
| 
 | ||||
|       _self.client.on('end', () => { | ||||
|         logger.err('[CLightningClient] Lightning client connection closed, reconnecting'); | ||||
|         logger.err(`CLightning client connection closed, reconnecting`, logger.tags.ln); | ||||
|         _self.increaseWaitTime(); | ||||
|         _self.reconnect(); | ||||
|       }); | ||||
| 
 | ||||
|       _self.client.on('error', error => { | ||||
|         logger.err(`[CLightningClient] Lightning client connection error: ${error}`); | ||||
|         logger.err(`CLightning client connection error: ${error}`, logger.tags.ln); | ||||
|         _self.increaseWaitTime(); | ||||
|         _self.reconnect(); | ||||
|       }); | ||||
| @ -196,7 +196,6 @@ export default class CLightningClient extends EventEmitter implements AbstractLi | ||||
|         return; | ||||
|       } | ||||
|       const data = JSON.parse(line); | ||||
|       // logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`);
 | ||||
|       _self.emit('res:' + data.id, data); | ||||
|     }); | ||||
|   } | ||||
| @ -217,7 +216,7 @@ export default class CLightningClient extends EventEmitter implements AbstractLi | ||||
|     } | ||||
| 
 | ||||
|     this.reconnectTimeout = setTimeout(() => { | ||||
|       logger.debug('[CLightningClient] Trying to reconnect...'); | ||||
|       logger.debug(`Trying to reconnect...`, logger.tags.ln); | ||||
| 
 | ||||
|       _self.client.connect(_self.rpcPath); | ||||
|       _self.reconnectTimeout = null; | ||||
| @ -235,7 +234,6 @@ export default class CLightningClient extends EventEmitter implements AbstractLi | ||||
|       id: '' + callInt | ||||
|     }; | ||||
| 
 | ||||
|     // logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`);
 | ||||
| 
 | ||||
|     // Wait for the client to connect
 | ||||
|     return this.clientConnectionPromise | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { ILightningApi } from '../lightning-api.interface'; | ||||
| import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher'; | ||||
| import logger from '../../../logger'; | ||||
| import { Common } from '../../common'; | ||||
| import config from '../../../config'; | ||||
| 
 | ||||
| /** | ||||
|  * Convert a clightning "listnode" entry to a lnd node entry | ||||
| @ -40,7 +41,7 @@ export function convertNode(clNode: any): ILightningApi.Node { | ||||
|  * Convert clightning "listchannels" response to lnd "describegraph.edges" format | ||||
|  */ | ||||
| export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise<ILightningApi.Channel[]> { | ||||
|   logger.info('Converting clightning nodes and channels to lnd graph format'); | ||||
|   logger.debug(`Converting clightning nodes and channels to lnd graph format`, logger.tags.ln); | ||||
| 
 | ||||
|   let loggerTimer = new Date().getTime() / 1000; | ||||
|   let channelProcessed = 0; | ||||
| @ -62,8 +63,8 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P | ||||
|     } | ||||
| 
 | ||||
|     const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); | ||||
|     if (elapsedSeconds > 10) { | ||||
|       logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`); | ||||
|     if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|       logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`, logger.tags.ln); | ||||
|       loggerTimer = new Date().getTime() / 1000; | ||||
|     } | ||||
| 
 | ||||
| @ -76,7 +77,7 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P | ||||
|     consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id])); | ||||
| 
 | ||||
|     const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); | ||||
|     if (elapsedSeconds > 10) { | ||||
|     if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|       logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`); | ||||
|       loggerTimer = new Date().getTime() / 1000; | ||||
|     } | ||||
|  | ||||
| @ -265,9 +265,9 @@ class Mining { | ||||
|       } | ||||
|       await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate()); | ||||
|       if (newlyIndexed > 0) { | ||||
|         logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`); | ||||
|         logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); | ||||
|       } else { | ||||
|         logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`); | ||||
|         logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); | ||||
|       } | ||||
|       loadingIndicators.setProgress('weekly-hashrate-indexing', 100); | ||||
|     } catch (e) { | ||||
| @ -370,14 +370,14 @@ class Mining { | ||||
| 
 | ||||
|       await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate()); | ||||
|       if (newlyIndexed > 0) { | ||||
|         logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`); | ||||
|         logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); | ||||
|       } else { | ||||
|         logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`); | ||||
|         logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); | ||||
|       } | ||||
|       loadingIndicators.setProgress('daily-hashrate-indexing', 100); | ||||
|     } catch (e) { | ||||
|       loadingIndicators.setProgress('daily-hashrate-indexing', 100); | ||||
|       logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||
|       logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| @ -449,9 +449,9 @@ class Mining { | ||||
|     } | ||||
| 
 | ||||
|     if (totalIndexed > 0) { | ||||
|       logger.notice(`Indexed ${totalIndexed} difficulty adjustments`); | ||||
|       logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); | ||||
|     } else { | ||||
|       logger.debug(`Indexed ${totalIndexed} difficulty adjustments`); | ||||
|       logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -61,7 +61,7 @@ class PoolsParser { | ||||
|         poolNames.push(poolsDuplicated[i].name); | ||||
|       } | ||||
|     } | ||||
|     logger.debug(`Found ${poolNames.length} unique mining pools`); | ||||
|     logger.debug(`Found ${poolNames.length} unique mining pools`, logger.tags.mining); | ||||
| 
 | ||||
|     // Get existing pools from the db
 | ||||
|     let existingPools; | ||||
| @ -72,7 +72,7 @@ class PoolsParser { | ||||
|         existingPools = []; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('Cannot get existing pools from the database, skipping pools.json import'); | ||||
|       logger.err('Cannot get existing pools from the database, skipping pools.json import', logger.tags.mining); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
| @ -99,7 +99,7 @@ class PoolsParser { | ||||
|         slug = poolsJson['slugs'][poolNames[i]]; | ||||
|       } catch (e) { | ||||
|         if (this.slugWarnFlag === false) { | ||||
|           logger.warn(`pools.json does not seem to contain the 'slugs' object`); | ||||
|           logger.warn(`pools.json does not seem to contain the 'slugs' object`, logger.tags.mining); | ||||
|           this.slugWarnFlag = true; | ||||
|         } | ||||
|       } | ||||
| @ -107,7 +107,7 @@ class PoolsParser { | ||||
|       if (slug === undefined) { | ||||
|         // Only keep alphanumerical
 | ||||
|         slug = poolNames[i].replace(/[^a-z0-9]/gi, '').toLowerCase(); | ||||
|         logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`); | ||||
|         logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`, logger.tags.mining); | ||||
|       } | ||||
| 
 | ||||
|       const poolObj = { | ||||
| @ -143,9 +143,9 @@ class PoolsParser { | ||||
|             'addresses': allAddresses, | ||||
|             'slug': slug | ||||
|           }); | ||||
|           logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`); | ||||
|           logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`, logger.tags.mining); | ||||
|         } else { | ||||
|           logger.debug(`Add '${finalPoolName}' mining pool`); | ||||
|           logger.debug(`Add '${finalPoolName}' mining pool`, logger.tags.mining); | ||||
|           finalPoolDataAdd.push(poolObj); | ||||
|         } | ||||
|       } | ||||
| @ -160,14 +160,14 @@ class PoolsParser { | ||||
|     } | ||||
| 
 | ||||
|     if (config.DATABASE.ENABLED === false) { // Don't run db operations
 | ||||
|       logger.info('Mining pools.json import completed (no database)'); | ||||
|       logger.info('Mining pools.json import completed (no database)', logger.tags.mining); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0 || | ||||
|       finalPoolDataRename.length > 0 | ||||
|     ) {     | ||||
|       logger.debug(`Update pools table now`); | ||||
|       logger.debug(`Update pools table now`, logger.tags.mining); | ||||
| 
 | ||||
|       // Add new mining pools into the database
 | ||||
|       let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES '; | ||||
| @ -217,9 +217,9 @@ class PoolsParser { | ||||
|           await DB.query({ sql: query, timeout: 120000 }); | ||||
|         } | ||||
|         await this.insertUnknownPool(); | ||||
|         logger.info('Mining pools.json import completed'); | ||||
|         logger.info('Mining pools.json import completed', logger.tags.mining); | ||||
|       } catch (e) { | ||||
|         logger.err(`Cannot import pools in the database`); | ||||
|         logger.err(`Cannot import pools in the database`, logger.tags.mining); | ||||
|         throw e; | ||||
|       } | ||||
|     } | ||||
| @ -227,7 +227,7 @@ class PoolsParser { | ||||
|     try { | ||||
|       await this.insertUnknownPool(); | ||||
|     } catch (e) { | ||||
|       logger.err(`Cannot insert unknown pool in the database`); | ||||
|       logger.err(`Cannot insert unknown pool in the database`, logger.tags.mining); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| @ -252,7 +252,7 @@ class PoolsParser { | ||||
|         `);
 | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('Unable to insert "Unknown" mining pool'); | ||||
|       logger.err('Unable to insert "Unknown" mining pool', logger.tags.mining); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -272,17 +272,17 @@ class PoolsParser { | ||||
|     for (const updatedPool of finalPoolDataUpdate) { | ||||
|       const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`); | ||||
|       if (pool.length > 0) { | ||||
|         logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`); | ||||
|         logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`, logger.tags.mining); | ||||
|         await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Ignore early days of Bitcoin as there were not mining pool yet
 | ||||
|     logger.notice('Deleting blocks with unknown mining pool from height 130635 for future re-indexing'); | ||||
|     logger.notice(`Deleting blocks with unknown mining pool from height 130635 for future re-indexing`, logger.tags.mining); | ||||
|     const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`); | ||||
|     await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`); | ||||
| 
 | ||||
|     logger.notice('Truncating hashrates for future re-indexing'); | ||||
|     logger.notice(`Truncating hashrates for future re-indexing`, logger.tags.mining); | ||||
|     await DB.query(`DELETE FROM hashrates`); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,7 @@ | ||||
| import bitcoinApi from './bitcoin/bitcoin-api-factory'; | ||||
| import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces'; | ||||
| import { IEsploraApi } from './bitcoin/esplora-api.interface'; | ||||
| import config from '../config'; | ||||
| import { Common } from './common'; | ||||
| import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; | ||||
| 
 | ||||
| class TransactionUtils { | ||||
|   constructor() { } | ||||
| @ -21,8 +20,19 @@ class TransactionUtils { | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false): Promise<TransactionExtended> { | ||||
|     const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts); | ||||
|   /** | ||||
|    * @param txId  | ||||
|    * @param addPrevouts  | ||||
|    * @param lazyPrevouts  | ||||
|    * @param forceCore - See https://github.com/mempool/mempool/issues/2904
 | ||||
|    */ | ||||
|   public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<TransactionExtended> { | ||||
|     let transaction: IEsploraApi.Transaction; | ||||
|     if (forceCore === true) { | ||||
|       transaction  = await bitcoinCoreApi.$getRawTransaction(txId, true); | ||||
|     } else { | ||||
|       transaction  = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts); | ||||
|     } | ||||
|     return this.extendTransaction(transaction); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -31,7 +31,7 @@ interface IConfig { | ||||
|     POOLS_JSON_TREE_URL: string, | ||||
|     ADVANCED_GBT_AUDIT: boolean; | ||||
|     ADVANCED_GBT_MEMPOOL: boolean; | ||||
|     TRANSACTION_INDEXING: boolean; | ||||
|     CPFP_INDEXING: boolean; | ||||
|   }; | ||||
|   ESPLORA: { | ||||
|     REST_API_URL: string; | ||||
| @ -152,7 +152,7 @@ const defaults: IConfig = { | ||||
|     'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', | ||||
|     'ADVANCED_GBT_AUDIT': false, | ||||
|     'ADVANCED_GBT_MEMPOOL': false, | ||||
|     'TRANSACTION_INDEXING': false, | ||||
|     'CPFP_INDEXING': false, | ||||
|   }, | ||||
|   'ESPLORA': { | ||||
|     'REST_API_URL': 'http://127.0.0.1:3000', | ||||
|  | ||||
| @ -32,22 +32,27 @@ class Logger { | ||||
|     local7: 23 | ||||
|   }; | ||||
| 
 | ||||
|   public tags = { | ||||
|     mining: 'Mining', | ||||
|     ln: 'Lightning', | ||||
|   };   | ||||
| 
 | ||||
|   // @ts-ignore
 | ||||
|   public emerg: ((msg: string) => void); | ||||
|   public emerg: ((msg: string, tag?: string) => void); | ||||
|   // @ts-ignore
 | ||||
|   public alert: ((msg: string) => void); | ||||
|   public alert: ((msg: string, tag?: string) => void); | ||||
|   // @ts-ignore
 | ||||
|   public crit: ((msg: string) => void); | ||||
|   public crit: ((msg: string, tag?: string) => void); | ||||
|   // @ts-ignore
 | ||||
|   public err: ((msg: string) => void); | ||||
|   public err: ((msg: string, tag?: string) => void); | ||||
|   // @ts-ignore
 | ||||
|   public warn: ((msg: string) => void); | ||||
|   public warn: ((msg: string, tag?: string) => void); | ||||
|   // @ts-ignore
 | ||||
|   public notice: ((msg: string) => void); | ||||
|   public notice: ((msg: string, tag?: string) => void); | ||||
|   // @ts-ignore
 | ||||
|   public info: ((msg: string) => void); | ||||
|   public info: ((msg: string, tag?: string) => void); | ||||
|   // @ts-ignore
 | ||||
|   public debug: ((msg: string) => void); | ||||
|   public debug: ((msg: string, tag?: string) => void); | ||||
| 
 | ||||
|   private name = 'mempool'; | ||||
|   private client: dgram.Socket; | ||||
| @ -66,8 +71,8 @@ class Logger { | ||||
| 
 | ||||
|   private addprio(prio): void { | ||||
|     this[prio] = (function(_this) { | ||||
|       return function(msg) { | ||||
|         return _this.msg(prio, msg); | ||||
|       return function(msg, tag?: string) { | ||||
|         return _this.msg(prio, msg, tag); | ||||
|       }; | ||||
|     })(this); | ||||
|   } | ||||
| @ -85,7 +90,7 @@ class Logger { | ||||
|     return ''; | ||||
|   } | ||||
| 
 | ||||
|   private msg(priority, msg) { | ||||
|   private msg(priority, msg, tag?: string) { | ||||
|     let consolemsg, prionum, syslogmsg; | ||||
|     if (typeof msg === 'string' && msg.length > 0) { | ||||
|       while (msg[msg.length - 1].charCodeAt(0) === 10) { | ||||
| @ -94,10 +99,10 @@ class Logger { | ||||
|     } | ||||
|     const network = this.network ? ' <' + this.network + '>' : ''; | ||||
|     prionum = Logger.priorities[priority] || Logger.priorities.info; | ||||
|     consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`; | ||||
|     consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${tag ? '[' + tag + '] ' : ''}${msg}`; | ||||
| 
 | ||||
|     if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) { | ||||
|       syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`; | ||||
|       syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${tag ? '[' + tag + '] ' : ''}${msg}`; | ||||
|       this.syslog(syslogmsg); | ||||
|     } | ||||
|     if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) { | ||||
|  | ||||
| @ -274,6 +274,7 @@ export interface IBackendInfo { | ||||
|   hostname: string; | ||||
|   gitCommit: string; | ||||
|   version: string; | ||||
|   lightning: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface IDifficultyAdjustment { | ||||
| @ -337,4 +338,4 @@ export interface IOldestNodes { | ||||
|   updatedAt?: number, | ||||
|   city?: any, | ||||
|   country?: any, | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,8 @@ import HashratesRepository from './HashratesRepository'; | ||||
| import { escape } from 'mysql2'; | ||||
| import BlocksSummariesRepository from './BlocksSummariesRepository'; | ||||
| import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository'; | ||||
| import bitcoinClient from '../api/bitcoin/bitcoin-client'; | ||||
| import config from '../config'; | ||||
| 
 | ||||
| class BlocksRepository { | ||||
|   /** | ||||
| @ -667,16 +669,32 @@ class BlocksRepository { | ||||
|    */ | ||||
|    public async $getCPFPUnindexedBlocks(): Promise<any[]> { | ||||
|     try { | ||||
|       const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`); | ||||
|       return rows; | ||||
|       const blockchainInfo = await bitcoinClient.getBlockchainInfo(); | ||||
|       const currentBlockHeight = blockchainInfo.blocks; | ||||
|       let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight); | ||||
|       if (indexingBlockAmount <= -1) { | ||||
|         indexingBlockAmount = currentBlockHeight + 1; | ||||
|       } | ||||
|       const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); | ||||
| 
 | ||||
|       const [rows]: any[] = await DB.query(` | ||||
|         SELECT height | ||||
|         FROM compact_cpfp_clusters | ||||
|         WHERE height <= ? AND height >= ? | ||||
|         ORDER BY height DESC; | ||||
|       `, [currentBlockHeight, minHeight]);
 | ||||
| 
 | ||||
|       const indexedHeights = {}; | ||||
|       rows.forEach((row) => { indexedHeights[row.height] = true; }); | ||||
|       const allHeights: number[] = Array.from(Array(currentBlockHeight - minHeight + 1).keys(), n => n + minHeight).reverse(); | ||||
|       const unindexedHeights = allHeights.filter(x => !indexedHeights[x]); | ||||
| 
 | ||||
|       return unindexedHeights; | ||||
|     } catch (e) { | ||||
|       logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $setCPFPIndexed(hash: string): Promise<void> { | ||||
|     await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]); | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -1,34 +1,151 @@ | ||||
| import cluster, { Cluster } from 'cluster'; | ||||
| import { RowDataPacket } from 'mysql2'; | ||||
| import DB from '../database'; | ||||
| import logger from '../logger'; | ||||
| import { Ancestor } from '../mempool.interfaces'; | ||||
| import transactionRepository from '../repositories/TransactionRepository'; | ||||
| 
 | ||||
| class CpfpRepository { | ||||
|   public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<void> { | ||||
|   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.reduce((acc, tx) => { | ||||
|       return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); | ||||
|     }, true); | ||||
|     if (equalFee) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       const txsJson = JSON.stringify(txs); | ||||
|       const packedTxs = Buffer.from(this.pack(txs)); | ||||
|       await DB.query( | ||||
|         ` | ||||
|           INSERT INTO cpfp_clusters(root, height, txs, fee_rate) | ||||
|           VALUE (?, ?, ?, ?) | ||||
|           INSERT INTO compact_cpfp_clusters(root, height, txs, fee_rate) | ||||
|           VALUE (UNHEX(?), ?, ?, ?) | ||||
|           ON DUPLICATE KEY UPDATE | ||||
|             height = ?, | ||||
|             txs = ?, | ||||
|             fee_rate = ? | ||||
|         `,
 | ||||
|         [txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height] | ||||
|         [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: any, effectiveFeePerVsize: number}[]): Promise<boolean> { | ||||
|     try { | ||||
|       const clusterValues: any[] = []; | ||||
|       const txs: any[] = []; | ||||
| 
 | ||||
|       for (const cluster of clusters) { | ||||
|         if (cluster.txs?.length > 1) { | ||||
|           const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100; | ||||
|           const equalFee = cluster.txs.reduce((acc, tx) => { | ||||
|             return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); | ||||
|           }, true); | ||||
|           if (!equalFee) { | ||||
|             clusterValues.push([ | ||||
|               cluster.root, | ||||
|               cluster.height, | ||||
|               Buffer.from(this.pack(cluster.txs)), | ||||
|               cluster.effectiveFeePerVsize | ||||
|             ]); | ||||
|             for (const tx of cluster.txs) { | ||||
|               txs.push({ txid: tx.txid, cluster: cluster.root }); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!clusterValues.length) { | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       const maxChunk = 100; | ||||
|       let chunkIndex = 0; | ||||
|       // insert transactions in batches of up to 100 rows
 | ||||
|       while (chunkIndex < txs.length) { | ||||
|         const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk); | ||||
|         await transactionRepository.$batchSetCluster(chunk); | ||||
|         chunkIndex += maxChunk; | ||||
|       } | ||||
| 
 | ||||
|       chunkIndex = 0; | ||||
|       // insert clusters in batches of up to 100 rows
 | ||||
|       while (chunkIndex < clusterValues.length) { | ||||
|         const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk); | ||||
|         let query = ` | ||||
|             INSERT IGNORE INTO compact_cpfp_clusters(root, height, txs, fee_rate) | ||||
|             VALUES | ||||
|         `;
 | ||||
|         query += chunk.map(chunk => { | ||||
|           return (' (UNHEX(?), ?, ?, ?)'); | ||||
|         }) + ';'; | ||||
|         const values = chunk.flat(); | ||||
|         await DB.query( | ||||
|           query, | ||||
|           values | ||||
|         ); | ||||
|         chunkIndex += maxChunk; | ||||
|       } | ||||
|       return true; | ||||
|     } catch (e: any) { | ||||
|       logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getCluster(clusterRoot: string): Promise<Cluster> { | ||||
|     const [clusterRows]: any = await DB.query( | ||||
|       ` | ||||
|         SELECT * | ||||
|         FROM compact_cpfp_clusters | ||||
|         WHERE root = UNHEX(?) | ||||
|       `,
 | ||||
|       [clusterRoot] | ||||
|     ); | ||||
|     const cluster = clusterRows[0]; | ||||
|     cluster.txs = this.unpack(cluster.txs); | ||||
|     return cluster; | ||||
|   } | ||||
| 
 | ||||
|   public async $deleteClustersFrom(height: number): Promise<void> { | ||||
|     logger.info(`Delete newer cpfp clusters from height ${height} from the database`); | ||||
|     try { | ||||
|       const [rows] = await DB.query( | ||||
|         ` | ||||
|           SELECT txs, height, root from compact_cpfp_clusters | ||||
|           WHERE height >= ? | ||||
|         `,
 | ||||
|         [height] | ||||
|       ) as RowDataPacket[][]; | ||||
|       if (rows?.length) { | ||||
|         for (let clusterToDelete of rows) { | ||||
|           const txs = this.unpack(clusterToDelete.txs); | ||||
|           for (let tx of txs) { | ||||
|             await transactionRepository.$removeTransaction(tx.txid); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       await DB.query( | ||||
|         ` | ||||
|           DELETE from cpfp_clusters | ||||
|           DELETE from compact_cpfp_clusters | ||||
|           WHERE height >= ? | ||||
|         `,
 | ||||
|         [height] | ||||
| @ -38,6 +155,70 @@ class CpfpRepository { | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // insert a dummy row to mark that we've indexed as far as this block
 | ||||
|   public async $insertProgressMarker(height: number): Promise<void> { | ||||
|     try { | ||||
|       const [rows]: any = await DB.query( | ||||
|         ` | ||||
|           SELECT root | ||||
|           FROM compact_cpfp_clusters | ||||
|           WHERE height = ? | ||||
|         `,
 | ||||
|         [height] | ||||
|       ); | ||||
|       if (!rows?.length) { | ||||
|         const rootBuffer = Buffer.alloc(32); | ||||
|         rootBuffer.writeInt32LE(height); | ||||
|         await DB.query( | ||||
|           ` | ||||
|             INSERT INTO compact_cpfp_clusters(root, height, fee_rate) | ||||
|             VALUE (?, ?, ?) | ||||
|           `,
 | ||||
|           [rootBuffer, height, 0] | ||||
|         ); | ||||
|       } | ||||
|     } catch (e: any) { | ||||
|       logger.err(`Cannot insert cpfp progress marker. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public pack(txs: Ancestor[]): ArrayBuffer { | ||||
|     const buf = new ArrayBuffer(44 * txs.length); | ||||
|     const view = new DataView(buf); | ||||
|     txs.forEach((tx, i) => { | ||||
|       const offset = i * 44; | ||||
|       for (let x = 0; x < 32; x++) { | ||||
|         // store txid in little-endian
 | ||||
|         view.setUint8(offset + (31 - x), parseInt(tx.txid.slice(x * 2, (x * 2) + 2), 16)); | ||||
|       } | ||||
|       view.setUint32(offset + 32, tx.weight); | ||||
|       view.setBigUint64(offset + 36, BigInt(Math.round(tx.fee))); | ||||
|     }); | ||||
|     return buf; | ||||
|   } | ||||
| 
 | ||||
|   public unpack(buf: Buffer): Ancestor[] { | ||||
|     if (!buf) { | ||||
|       return []; | ||||
|     } | ||||
| 
 | ||||
|     const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); | ||||
|     const txs: Ancestor[] = []; | ||||
|     const view = new DataView(arrayBuffer); | ||||
|     for (let offset = 0; offset < arrayBuffer.byteLength; offset += 44) { | ||||
|       const txid = Array.from(new Uint8Array(arrayBuffer, offset, 32)).reverse().map(b => b.toString(16).padStart(2, '0')).join(''); | ||||
|       const weight = view.getUint32(offset + 32); | ||||
|       const fee = Number(view.getBigUint64(offset + 36)); | ||||
|       txs.push({ | ||||
|         txid, | ||||
|         weight, | ||||
|         fee | ||||
|       }); | ||||
|     } | ||||
|     return txs; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default new CpfpRepository(); | ||||
| @ -1,6 +1,7 @@ | ||||
| import DB from '../database'; | ||||
| import logger from '../logger'; | ||||
| import { Ancestor, CpfpInfo } from '../mempool.interfaces'; | ||||
| import cpfpRepository from './CpfpRepository'; | ||||
| 
 | ||||
| interface CpfpSummary { | ||||
|   txid: string; | ||||
| @ -12,20 +13,20 @@ interface CpfpSummary { | ||||
| } | ||||
| 
 | ||||
| class TransactionRepository { | ||||
|   public async $setCluster(txid: string, cluster: string): Promise<void> { | ||||
|   public async $setCluster(txid: string, clusterRoot: string): Promise<void> { | ||||
|     try { | ||||
|       await DB.query( | ||||
|         ` | ||||
|           INSERT INTO transactions | ||||
|           INSERT INTO compact_transactions | ||||
|           ( | ||||
|             txid, | ||||
|             cluster | ||||
|           ) | ||||
|           VALUE (?, ?) | ||||
|           VALUE (UNHEX(?), UNHEX(?)) | ||||
|           ON DUPLICATE KEY UPDATE | ||||
|             cluster = ? | ||||
|             cluster = UNHEX(?) | ||||
|         ;`,
 | ||||
|         [txid, cluster, cluster] | ||||
|         [txid, clusterRoot, clusterRoot] | ||||
|       ); | ||||
|     } catch (e: any) { | ||||
|       logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
| @ -33,20 +34,45 @@ class TransactionRepository { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> { | ||||
|   public async $batchSetCluster(txs): Promise<void> { | ||||
|     try { | ||||
|       let query = ` | ||||
|         SELECT * | ||||
|         FROM transactions | ||||
|         LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster | ||||
|         WHERE transactions.txid = ? | ||||
|           INSERT IGNORE INTO compact_transactions | ||||
|           ( | ||||
|             txid, | ||||
|             cluster | ||||
|           ) | ||||
|           VALUES | ||||
|       `;
 | ||||
|       const [rows]: any = await DB.query(query, [txid]); | ||||
|       if (rows.length) { | ||||
|         rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[]; | ||||
|         if (rows[0]?.txs?.length) { | ||||
|           return this.convertCpfp(rows[0]); | ||||
|         } | ||||
|       query += txs.map(tx => { | ||||
|         return (' (UNHEX(?), UNHEX(?))'); | ||||
|       }) + ';'; | ||||
|       const values = txs.map(tx => [tx.txid, tx.cluster]).flat(); | ||||
|       await DB.query( | ||||
|         query, | ||||
|         values | ||||
|       ); | ||||
|     } catch (e: any) { | ||||
|       logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> { | ||||
|     try { | ||||
|       const [txRows]: any = await DB.query( | ||||
|         ` | ||||
|           SELECT HEX(txid) as id, HEX(cluster) as root | ||||
|           FROM compact_transactions | ||||
|           WHERE txid = UNHEX(?) | ||||
|         `,
 | ||||
|         [txid] | ||||
|       ); | ||||
|       if (txRows.length && txRows[0].root != null) { | ||||
|         const txid = txRows[0].id.toLowerCase(); | ||||
|         const clusterId = txRows[0].root.toLowerCase(); | ||||
|         const cluster = await cpfpRepository.$getCluster(clusterId); | ||||
|         return this.convertCpfp(txid, cluster); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
| @ -54,12 +80,23 @@ class TransactionRepository { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private convertCpfp(cpfp: CpfpSummary): CpfpInfo { | ||||
|   public async $removeTransaction(txid: string): Promise<void> { | ||||
|     await DB.query( | ||||
|       ` | ||||
|         DELETE FROM compact_transactions | ||||
|         WHERE txid = UNHEX(?) | ||||
|       `,
 | ||||
|       [txid] | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   private convertCpfp(txid, cluster): CpfpInfo { | ||||
|     const descendants: Ancestor[] = []; | ||||
|     const ancestors: Ancestor[] = []; | ||||
|     let matched = false; | ||||
|     for (const tx of cpfp.txs) { | ||||
|       if (tx.txid === cpfp.txid) { | ||||
| 
 | ||||
|     for (const tx of cluster.txs) { | ||||
|       if (tx.txid === txid) { | ||||
|         matched = true; | ||||
|       } else if (!matched) { | ||||
|         descendants.push(tx); | ||||
| @ -70,7 +107,6 @@ class TransactionRepository { | ||||
|     return { | ||||
|       descendants, | ||||
|       ancestors, | ||||
|       effectiveFeePerVsize: cpfp.fee_rate | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -23,7 +23,7 @@ class NetworkSyncService { | ||||
|   constructor() {} | ||||
| 
 | ||||
|   public async $startService(): Promise<void> { | ||||
|     logger.info('Starting lightning network sync service'); | ||||
|     logger.info(`Starting lightning network sync service`, logger.tags.ln); | ||||
| 
 | ||||
|     this.loggerTimer = new Date().getTime() / 1000; | ||||
| 
 | ||||
| @ -33,11 +33,11 @@ class NetworkSyncService { | ||||
|   private async $runTasks(): Promise<void> { | ||||
|     const taskStartTime = Date.now(); | ||||
|     try { | ||||
|       logger.info(`Updating nodes and channels`); | ||||
|       logger.debug(`Updating nodes and channels`, logger.tags.ln); | ||||
| 
 | ||||
|       const networkGraph = await lightningApi.$getNetworkGraph(); | ||||
|       if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) { | ||||
|         logger.info(`LN Network graph is empty, retrying in 10 seconds`); | ||||
|         logger.info(`LN Network graph is empty, retrying in 10 seconds`, logger.tags.ln); | ||||
|         setTimeout(() => { this.$runTasks(); }, 10000); | ||||
|         return; | ||||
|       } | ||||
| @ -55,7 +55,7 @@ class NetworkSyncService { | ||||
|       } | ||||
| 
 | ||||
|     } catch (e) { | ||||
|       logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err(`$runTasks() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); | ||||
|     } | ||||
| 
 | ||||
|     setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime))); | ||||
| @ -79,8 +79,8 @@ class NetworkSyncService { | ||||
|       ++progress; | ||||
| 
 | ||||
|       const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); | ||||
|       if (elapsedSeconds > 10) { | ||||
|         logger.info(`Updating node ${progress}/${nodes.length}`); | ||||
|       if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|         logger.debug(`Updating node ${progress}/${nodes.length}`, logger.tags.ln); | ||||
|         this.loggerTimer = new Date().getTime() / 1000; | ||||
|       } | ||||
| 
 | ||||
| @ -106,7 +106,7 @@ class NetworkSyncService { | ||||
|         deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes); | ||||
|       } | ||||
|     } | ||||
|     logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`); | ||||
|     logger.debug(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`); | ||||
| 
 | ||||
|     // If a channel if not present in the graph, mark it as inactive
 | ||||
|     await nodesApi.$setNodesInactive(graphNodesPubkeys); | ||||
| @ -138,18 +138,18 @@ class NetworkSyncService { | ||||
|         ++progress; | ||||
| 
 | ||||
|         const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); | ||||
|         if (elapsedSeconds > 10) { | ||||
|           logger.info(`Updating channel ${progress}/${channels.length}`); | ||||
|         if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|           logger.debug(`Updating channel ${progress}/${channels.length}`, logger.tags.ln); | ||||
|           this.loggerTimer = new Date().getTime() / 1000; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       logger.info(`${progress} channels updated`); | ||||
|       logger.debug(`${progress} channels updated`, logger.tags.ln); | ||||
| 
 | ||||
|       // If a channel if not present in the graph, mark it as inactive
 | ||||
|       await channelsApi.$setChannelsInactive(graphChannelsIds); | ||||
|     } catch (e) { | ||||
|       logger.err(`Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||
|       logger.err(` Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.ln); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -184,26 +184,28 @@ class NetworkSyncService { | ||||
|         if (lowest < node.first_seen) { | ||||
|           const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`; | ||||
|           const params = [lowest, node.public_key]; | ||||
|           ++updated; | ||||
|           await DB.query(query, params); | ||||
|         } | ||||
|         ++progress; | ||||
|         const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); | ||||
|         if (elapsedSeconds > 10) { | ||||
|           logger.info(`Updating node first seen date ${progress}/${nodes.length}`); | ||||
|         if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|           logger.debug(`Updating node first seen date ${progress}/${nodes.length}`, logger.tags.ln); | ||||
|           this.loggerTimer = new Date().getTime() / 1000; | ||||
|           ++updated; | ||||
|         } | ||||
|       } | ||||
|       logger.info(`Updated ${updated} node first seen dates`); | ||||
|       if (updated > 0) { | ||||
|         logger.debug(`Updated ${updated} node first seen dates`, logger.tags.ln); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err(`$updateNodeFirstSeen() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $lookUpCreationDateFromChain(): Promise<void> { | ||||
|     let progress = 0; | ||||
| 
 | ||||
|     logger.info(`Running channel creation date lookup`); | ||||
|     logger.debug(`Running channel creation date lookup`, logger.tags.ln); | ||||
|     try { | ||||
|       const channels = await channelsApi.$getChannelsWithoutCreatedDate(); | ||||
|       for (const channel of channels) { | ||||
| @ -214,14 +216,17 @@ class NetworkSyncService { | ||||
|         ); | ||||
|         ++progress; | ||||
|         const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); | ||||
|         if (elapsedSeconds > 10) { | ||||
|           logger.info(`Updating channel creation date ${progress}/${channels.length}`); | ||||
|         if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|           logger.debug(`Updating channel creation date ${progress}/${channels.length}`, logger.tags.ln); | ||||
|           this.loggerTimer = new Date().getTime() / 1000; | ||||
|         } | ||||
|       } | ||||
|       logger.info(`Updated ${channels.length} channels' creation date`); | ||||
| 
 | ||||
|       if (channels.length > 0) { | ||||
|         logger.debug(`Updated ${channels.length} channels' creation date`, logger.tags.ln); | ||||
|       }       | ||||
|     } catch (e) { | ||||
|       logger.err('$lookUpCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err(`$lookUpCreationDateFromChain() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -230,7 +235,7 @@ class NetworkSyncService { | ||||
|    * mark that channel as inactive | ||||
|    */ | ||||
|   private async $deactivateChannelsWithoutActiveNodes(): Promise<void> { | ||||
|     logger.info(`Find channels which nodes are offline`); | ||||
|     logger.debug(`Find channels which nodes are offline`, logger.tags.ln); | ||||
| 
 | ||||
|     try { | ||||
|       const result = await DB.query<ResultSetHeader>(` | ||||
| @ -253,12 +258,10 @@ class NetworkSyncService { | ||||
|         `);
 | ||||
| 
 | ||||
|       if (result[0].changedRows ?? 0 > 0) { | ||||
|         logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`); | ||||
|       } else { | ||||
|         logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`); | ||||
|         logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`, logger.tags.ln); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('$deactivateChannelsWithoutActiveNodes() error: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err(`$deactivateChannelsWithoutActiveNodes() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -277,13 +280,13 @@ class NetworkSyncService { | ||||
|       } else { | ||||
|         log += ` for the first time`; | ||||
|       } | ||||
|       logger.info(log); | ||||
|       logger.info(`${log}`, logger.tags.ln); | ||||
| 
 | ||||
|       const channels = await channelsApi.$getChannelsByStatus([0, 1]); | ||||
|       for (const channel of channels) { | ||||
|         const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout); | ||||
|         if (spendingTx.spent === true && spendingTx.status?.confirmed === true) { | ||||
|           logger.debug('Marking channel: ' + channel.id + ' as closed.'); | ||||
|           logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln); | ||||
|           await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`, | ||||
|             [spendingTx.status.block_time, channel.id]); | ||||
|           if (spendingTx.txid && !channel.closing_transaction_id) { | ||||
| @ -293,16 +296,16 @@ class NetworkSyncService { | ||||
| 
 | ||||
|         ++progress; | ||||
|         const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); | ||||
|         if (elapsedSeconds > 10) { | ||||
|           logger.info(`Checking if channel has been closed ${progress}/${channels.length}`); | ||||
|         if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|           logger.info(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln); | ||||
|           this.loggerTimer = new Date().getTime() / 1000; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.closedChannelsScanBlock = blocks.getCurrentBlockHeight(); | ||||
|       logger.info(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`); | ||||
|       logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln); | ||||
|     } catch (e) { | ||||
|       logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,7 @@ import { Common } from '../../api/common'; | ||||
| 
 | ||||
| class LightningStatsUpdater { | ||||
|   public async $startService(): Promise<void> { | ||||
|     logger.info('Starting Lightning Stats service'); | ||||
|     logger.info(`Starting Lightning Stats service`, logger.tags.ln); | ||||
| 
 | ||||
|     await this.$runTasks(); | ||||
|     LightningStatsImporter.$run(); | ||||
| @ -27,7 +27,7 @@ class LightningStatsUpdater { | ||||
|     const networkGraph = await lightningApi.$getNetworkGraph(); | ||||
|     await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); | ||||
|      | ||||
|     logger.info(`Updated latest network stats`); | ||||
|     logger.debug(`Updated latest network stats`, logger.tags.ln); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -21,10 +21,10 @@ class FundingTxFetcher { | ||||
|       try { | ||||
|         this.fundingTxCache = JSON.parse(await fsPromises.readFile(CACHE_FILE_NAME, 'utf-8')); | ||||
|       } catch (e) { | ||||
|         logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`); | ||||
|         logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`, logger.tags.ln); | ||||
|         this.fundingTxCache = {}; | ||||
|       } | ||||
|       logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`); | ||||
|       logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`, logger.tags.ln); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -44,26 +44,27 @@ class FundingTxFetcher { | ||||
|       ++channelProcessed; | ||||
| 
 | ||||
|       let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); | ||||
|       if (elapsedSeconds > 10) { | ||||
|       if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|         elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer); | ||||
|         logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` + | ||||
|           `(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` + | ||||
|           `elapsed: ${elapsedSeconds} seconds` | ||||
|           `elapsed: ${elapsedSeconds} seconds`, | ||||
|           logger.tags.ln | ||||
|         ); | ||||
|         loggerTimer = new Date().getTime() / 1000; | ||||
|       } | ||||
| 
 | ||||
|       elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer); | ||||
|       if (elapsedSeconds > 60) { | ||||
|         logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); | ||||
|         logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln); | ||||
|         fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); | ||||
|         cacheTimer = new Date().getTime() / 1000; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (this.channelNewlyProcessed > 0) { | ||||
|       logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`); | ||||
|       logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); | ||||
|       logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`, logger.tags.ln); | ||||
|       logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln); | ||||
|       fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -14,7 +14,7 @@ export async function $lookupNodeLocation(): Promise<void> { | ||||
|   let nodesUpdated = 0; | ||||
|   let geoNamesInserted = 0; | ||||
| 
 | ||||
|   logger.info(`Running node location updater using Maxmind`); | ||||
|   logger.debug(`Running node location updater using Maxmind`, logger.tags.ln); | ||||
|   try { | ||||
|     const nodes = await nodesApi.$getAllNodes(); | ||||
|     const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY); | ||||
| @ -152,8 +152,8 @@ export async function $lookupNodeLocation(): Promise<void> { | ||||
| 
 | ||||
|           ++progress; | ||||
|           const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); | ||||
|           if (elapsedSeconds > 10) { | ||||
|             logger.info(`Updating node location data ${progress}/${nodes.length}`); | ||||
|           if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { | ||||
|             logger.debug(`Updating node location data ${progress}/${nodes.length}`); | ||||
|             loggerTimer = new Date().getTime() / 1000; | ||||
|           } | ||||
|         } | ||||
| @ -161,9 +161,7 @@ export async function $lookupNodeLocation(): Promise<void> { | ||||
|     } | ||||
| 
 | ||||
|     if (nodesUpdated > 0) { | ||||
|       logger.info(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`); | ||||
|     } else { | ||||
|       logger.debug(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`); | ||||
|       logger.debug(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`, logger.tags.ln); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     logger.err('$lookupNodeLocation() error: ' + (e instanceof Error ? e.message : e)); | ||||
|  | ||||
| @ -8,7 +8,6 @@ import { isIP } from 'net'; | ||||
| import { Common } from '../../../api/common'; | ||||
| import channelsApi from '../../../api/explorer/channels.api'; | ||||
| import nodesApi from '../../../api/explorer/nodes.api'; | ||||
| import { ResultSetHeader } from 'mysql2'; | ||||
| 
 | ||||
| const fsPromises = promises; | ||||
| 
 | ||||
| @ -17,7 +16,7 @@ class LightningStatsImporter { | ||||
| 
 | ||||
|   async $run(): Promise<void> { | ||||
|     const [channels]: any[] = await DB.query('SELECT short_id from channels;'); | ||||
|     logger.info('Caching funding txs for currently existing channels'); | ||||
|     logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln); | ||||
|     await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); | ||||
| 
 | ||||
|     if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) { | ||||
| @ -108,7 +107,7 @@ class LightningStatsImporter { | ||||
| 
 | ||||
|       const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id); | ||||
|       if (!tx) { | ||||
|         logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`); | ||||
|         logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`, logger.tags.ln); | ||||
|         continue; | ||||
|       } | ||||
| 
 | ||||
| @ -321,7 +320,7 @@ class LightningStatsImporter { | ||||
|       try { | ||||
|         fileList = await fsPromises.readdir(this.topologiesFolder); | ||||
|       } catch (e) { | ||||
|         logger.err(`Unable to open topology folder at ${this.topologiesFolder}`); | ||||
|         logger.err(`Unable to open topology folder at ${this.topologiesFolder}`, logger.tags.ln); | ||||
|         throw e; | ||||
|       } | ||||
|       // Insert history from the most recent to the oldest
 | ||||
| @ -359,7 +358,7 @@ class LightningStatsImporter { | ||||
|           continue; | ||||
|         } | ||||
| 
 | ||||
|         logger.debug(`Reading ${this.topologiesFolder}/${filename}`); | ||||
|         logger.debug(`Reading ${this.topologiesFolder}/${filename}`, logger.tags.ln); | ||||
|         let fileContent = ''; | ||||
|         try { | ||||
|           fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8'); | ||||
| @ -368,7 +367,7 @@ class LightningStatsImporter { | ||||
|             totalProcessed++; | ||||
|             continue; | ||||
|           } | ||||
|           logger.err(`Unable to open ${this.topologiesFolder}/${filename}`); | ||||
|           logger.err(`Unable to open ${this.topologiesFolder}/${filename}`, logger.tags.ln); | ||||
|           totalProcessed++; | ||||
|           continue; | ||||
|         } | ||||
| @ -378,7 +377,7 @@ class LightningStatsImporter { | ||||
|           graph = JSON.parse(fileContent); | ||||
|           graph = await this.cleanupTopology(graph); | ||||
|         } catch (e) { | ||||
|           logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`); | ||||
|           logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); | ||||
|           totalProcessed++; | ||||
|           continue; | ||||
|         } | ||||
| @ -390,20 +389,20 @@ class LightningStatsImporter { | ||||
|         } | ||||
| 
 | ||||
|         if (!logStarted) { | ||||
|           logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`); | ||||
|           logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`, logger.tags.ln); | ||||
|           logStarted = true; | ||||
|         } | ||||
|          | ||||
|         const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; | ||||
|         logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`); | ||||
|         logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`, logger.tags.ln); | ||||
| 
 | ||||
|         totalProcessed++; | ||||
| 
 | ||||
|         if (processed > 10) { | ||||
|           logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`); | ||||
|           logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`, logger.tags.ln); | ||||
|           processed = 0; | ||||
|         } else { | ||||
|           logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`); | ||||
|           logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`, logger.tags.ln); | ||||
|         } | ||||
|         await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2))); | ||||
|         const stat = await this.computeNetworkStats(timestamp, graph, true); | ||||
| @ -412,10 +411,10 @@ class LightningStatsImporter { | ||||
|       } | ||||
| 
 | ||||
|       if (totalProcessed > 0) { | ||||
|         logger.info(`Lightning network stats historical import completed`); | ||||
|         logger.notice(`Lightning network stats historical import completed`, logger.tags.ln); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`); | ||||
|       logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -32,9 +32,9 @@ class PoolsUpdater { | ||||
|     this.lastRun = now; | ||||
| 
 | ||||
|     if (config.SOCKS5PROXY.ENABLED) { | ||||
|       logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`); | ||||
|       logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`, logger.tags.mining); | ||||
|     } else { | ||||
|       logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`); | ||||
|       logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`, logger.tags.mining); | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
| @ -53,9 +53,9 @@ class PoolsUpdater { | ||||
|       } | ||||
| 
 | ||||
|       if (this.currentSha === undefined) { | ||||
|         logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`); | ||||
|         logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`, logger.tags.mining); | ||||
|       } else { | ||||
|         logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`); | ||||
|         logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`, logger.tags.mining); | ||||
|       } | ||||
|       const poolsJson = await this.query(this.poolsUrl); | ||||
|       if (poolsJson === undefined) { | ||||
| @ -63,11 +63,11 @@ class PoolsUpdater { | ||||
|       } | ||||
|       await poolsParser.migratePoolsJson(poolsJson); | ||||
|       await this.updateDBSha(githubSha); | ||||
|       logger.notice('PoolsUpdater completed'); | ||||
|       logger.notice(`PoolsUpdater completed`, logger.tags.mining); | ||||
| 
 | ||||
|     } catch (e) { | ||||
|       this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
 | ||||
|       logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err(`PoolsUpdater failed. Will try again in 24h. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -81,7 +81,7 @@ class PoolsUpdater { | ||||
|         await DB.query('DELETE FROM state where name="pools_json_sha"'); | ||||
|         await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); | ||||
|       } catch (e) { | ||||
|         logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|         logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @ -94,7 +94,7 @@ class PoolsUpdater { | ||||
|       const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); | ||||
|       return (rows.length > 0 ? rows[0].string : undefined); | ||||
|     } catch (e) { | ||||
|       logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|       logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); | ||||
|       return undefined; | ||||
|     } | ||||
|   } | ||||
| @ -113,7 +113,7 @@ class PoolsUpdater { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`); | ||||
|     logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`, logger.tags.mining); | ||||
|     return undefined; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -91,7 +91,7 @@ class KrakenApi implements PriceFeed { | ||||
|     } | ||||
| 
 | ||||
|     if (Object.keys(priceHistory).length > 0) { | ||||
|       logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`); | ||||
|       logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -82,7 +82,7 @@ class PriceUpdater { | ||||
|         await this.$updatePrice(); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`); | ||||
|       logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); | ||||
|     } | ||||
| 
 | ||||
|     this.running = false; | ||||
| @ -115,14 +115,14 @@ class PriceUpdater { | ||||
|             if (price > 0) { | ||||
|               prices.push(price); | ||||
|             } | ||||
|             logger.debug(`${feed.name} BTC/${currency} price: ${price}`); | ||||
|             logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining); | ||||
|           } catch (e) { | ||||
|             logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||
|             logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       if (prices.length === 1) { | ||||
|         logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`); | ||||
|         logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`, logger.tags.mining); | ||||
|       } | ||||
| 
 | ||||
|       // Compute average price, non weighted
 | ||||
| @ -175,9 +175,9 @@ class PriceUpdater { | ||||
|       ++insertedCount; | ||||
|     } | ||||
|     if (insertedCount > 0) { | ||||
|       logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`); | ||||
|       logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`, logger.tags.mining); | ||||
|     } else { | ||||
|       logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`); | ||||
|       logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`, logger.tags.mining); | ||||
|     } | ||||
| 
 | ||||
|     // Insert Kraken weekly prices
 | ||||
| @ -198,7 +198,7 @@ class PriceUpdater { | ||||
|   private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> { | ||||
|     const existingPriceTimes = await PricesRepository.$getPricesTimes(); | ||||
| 
 | ||||
|     logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database, this may take a while`); | ||||
|     logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining); | ||||
| 
 | ||||
|     const historicalPrices: PriceHistory[] = []; | ||||
| 
 | ||||
| @ -207,7 +207,7 @@ class PriceUpdater { | ||||
|       try { | ||||
|         historicalPrices.push(await feed.$fetchRecentPrice(this.currencies, type)); | ||||
|       } catch (e) { | ||||
|         logger.err(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`); | ||||
|         logger.err(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| @ -252,9 +252,9 @@ class PriceUpdater { | ||||
|     } | ||||
| 
 | ||||
|     if (totalInserted > 0) { | ||||
|       logger.notice(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`); | ||||
|       logger.notice(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining); | ||||
|     } else { | ||||
|       logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`); | ||||
|       logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -100,12 +100,18 @@ Below we list all settings from `mempool-config.json` and the corresponding over | ||||
|     "BLOCK_WEIGHT_UNITS": 4000000, | ||||
|     "INITIAL_BLOCKS_AMOUNT": 8, | ||||
|     "MEMPOOL_BLOCKS_AMOUNT": 8, | ||||
|     "BLOCKS_SUMMARIES_INDEXING": false, | ||||
|     "PRICE_FEED_UPDATE_INTERVAL": 600, | ||||
|     "USE_SECOND_NODE_FOR_MINFEE": false, | ||||
|     "EXTERNAL_ASSETS": ["https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"], | ||||
|     "STDOUT_LOG_MIN_PRIORITY": "info", | ||||
|     "INDEXING_BLOCKS_AMOUNT": false, | ||||
|     "AUTOMATIC_BLOCK_REINDEXING": false, | ||||
|     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json", | ||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master" | ||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", | ||||
|     "ADVANCED_GBT_AUDIT": false, | ||||
|     "ADVANCED_GBT_MEMPOOL": false, | ||||
|     "CPFP_INDEXING": false, | ||||
|   }, | ||||
| ``` | ||||
| 
 | ||||
| @ -125,15 +131,25 @@ Corresponding `docker-compose.yml` overrides: | ||||
|       MEMPOOL_BLOCK_WEIGHT_UNITS: "" | ||||
|       MEMPOOL_INITIAL_BLOCKS_AMOUNT: "" | ||||
|       MEMPOOL_MEMPOOL_BLOCKS_AMOUNT: "" | ||||
|       MEMPOOL_BLOCKS_SUMMARIES_INDEXING: "" | ||||
|       MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: "" | ||||
|       MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: "" | ||||
|       MEMPOOL_EXTERNAL_ASSETS: "" | ||||
|       MEMPOOL_STDOUT_LOG_MIN_PRIORITY: "" | ||||
|       MEMPOOL_INDEXING_BLOCKS_AMOUNT: "" | ||||
|       MEMPOOL_AUTOMATIC_BLOCK_REINDEXING: "" | ||||
|       MEMPOOL_POOLS_JSON_URL: "" | ||||
|       MEMPOOL_POOLS_JSON_TREE_URL: "" | ||||
|       MEMPOOL_ADVANCED_GBT_AUDIT: "" | ||||
|       MEMPOOL_ADVANCED_GBT_MEMPOOL: "" | ||||
|       MEMPOOL_CPFP_INDEXING: "" | ||||
|       ... | ||||
| ``` | ||||
| 
 | ||||
| `ADVANCED_GBT_AUDIT` AND `ADVANCED_GBT_MEMPOOL` enable a more accurate (but slower) block prediction algorithm for the block audit feature and the projected mempool-blocks respectively. | ||||
| 
 | ||||
| `CPFP_INDEXING` enables indexing CPFP (Child Pays For Parent) information for the last `INDEXING_BLOCKS_AMOUNT` blocks. | ||||
| 
 | ||||
| <br/> | ||||
| 
 | ||||
| `mempool-config.json`: | ||||
|  | ||||
| @ -22,7 +22,10 @@ | ||||
|     "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__", | ||||
|     "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__, | ||||
|     "BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__, | ||||
|     "AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__ | ||||
|     "AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__, | ||||
|     "ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__, | ||||
|     "ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__, | ||||
|     "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__ | ||||
|   }, | ||||
|   "CORE_RPC": { | ||||
|     "HOST": "__CORE_RPC_HOST__", | ||||
|  | ||||
| @ -27,6 +27,9 @@ __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false} | ||||
| __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false} | ||||
| __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json} | ||||
| __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} | ||||
| __MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false} | ||||
| __MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false} | ||||
| __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} | ||||
| 
 | ||||
| # CORE_RPC | ||||
| __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} | ||||
| @ -136,6 +139,8 @@ sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT_ | ||||
| sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json | ||||
| 
 | ||||
| sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json | ||||
| sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json | ||||
|  | ||||
| @ -31,6 +31,9 @@ __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network} | ||||
| __BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets} | ||||
| __MINING_DASHBOARD__=${MINING_DASHBOARD:=true} | ||||
| __LIGHTNING__=${LIGHTNING:=false} | ||||
| __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} | ||||
| __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} | ||||
| __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} | ||||
| 
 | ||||
| # Export as environment variables to be used by envsubst | ||||
| export __TESTNET_ENABLED__ | ||||
| @ -52,6 +55,9 @@ export __LIQUID_WEBSITE_URL__ | ||||
| export __BISQ_WEBSITE_URL__ | ||||
| export __MINING_DASHBOARD__ | ||||
| export __LIGHTNING__ | ||||
| export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ | ||||
| export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ | ||||
| export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ | ||||
| 
 | ||||
| folder=$(find /var/www/mempool -name "config.js" | xargs dirname) | ||||
| echo ${folder} | ||||
|  | ||||
| @ -7,7 +7,6 @@ describe('Liquid', () => { | ||||
|     cy.intercept('/liquid/api/blocks/').as('blocks'); | ||||
|     cy.intercept('/liquid/api/tx/**/outspends').as('outspends'); | ||||
|     cy.intercept('/liquid/api/block/**/txs/**').as('block-txs'); | ||||
|     cy.intercept('/resources/pools.json').as('pools'); | ||||
| 
 | ||||
|     Cypress.Commands.add('waitForBlockData', () => { | ||||
|       cy.wait('@socket'); | ||||
|  | ||||
| @ -7,7 +7,6 @@ describe('Liquid Testnet', () => { | ||||
|     cy.intercept('/liquidtestnet/api/blocks/').as('blocks'); | ||||
|     cy.intercept('/liquidtestnet/api/tx/**/outspends').as('outspends'); | ||||
|     cy.intercept('/liquidtestnet/api/block/**/txs/**').as('block-txs'); | ||||
|     cy.intercept('/resources/pools.json').as('pools'); | ||||
| 
 | ||||
|     Cypress.Commands.add('waitForBlockData', () => { | ||||
|       cy.wait('@socket'); | ||||
|  | ||||
| @ -41,7 +41,6 @@ describe('Mainnet', () => { | ||||
|     // cy.intercept('/api/v1/block/*/summary').as('block-summary');
 | ||||
|     // cy.intercept('/api/v1/outspends/*').as('outspends');
 | ||||
|     // cy.intercept('/api/tx/*/outspends').as('tx-outspends');
 | ||||
|     // cy.intercept('/resources/pools.json').as('pools');
 | ||||
| 
 | ||||
|     // Search Auto Complete
 | ||||
|     cy.intercept('/api/address-prefix/1wiz').as('search-1wiz'); | ||||
|  | ||||
| @ -76,7 +76,7 @@ PROXY_CONFIG = [ | ||||
| 
 | ||||
| if (configContent && configContent.BASE_MODULE == "liquid") { | ||||
|     PROXY_CONFIG.push({ | ||||
|         context: ['/resources/pools.json', | ||||
|         context: [ | ||||
|             '/resources/assets.json', '/resources/assets.minimal.json', | ||||
|             '/resources/assets-testnet.json', '/resources/assets-testnet.minimal.json'], | ||||
|         target: "https://liquid.network", | ||||
| @ -85,7 +85,7 @@ if (configContent && configContent.BASE_MODULE == "liquid") { | ||||
|     }); | ||||
| } else { | ||||
|     PROXY_CONFIG.push({ | ||||
|         context: ['/resources/pools.json', '/resources/assets.json', '/resources/assets.minimal.json', '/resources/worldmap.json'], | ||||
|         context: ['/resources/assets.json', '/resources/assets.minimal.json', '/resources/worldmap.json'], | ||||
|         target: "https://mempool.space", | ||||
|         secure: false, | ||||
|         changeOrigin: true, | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { AppRoutingModule } from './app-routing.module'; | ||||
| import { AppComponent } from './components/app/app.component'; | ||||
| import { ElectrsApiService } from './services/electrs-api.service'; | ||||
| import { StateService } from './services/state.service'; | ||||
| import { CacheService } from './services/cache.service'; | ||||
| import { EnterpriseService } from './services/enterprise.service'; | ||||
| import { WebsocketService } from './services/websocket.service'; | ||||
| import { AudioService } from './services/audio.service'; | ||||
| @ -23,6 +24,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'; | ||||
| const providers = [ | ||||
|   ElectrsApiService, | ||||
|   StateService, | ||||
|   CacheService, | ||||
|   WebsocketService, | ||||
|   AudioService, | ||||
|   SeoService, | ||||
|  | ||||
| @ -36,7 +36,9 @@ export class AddressLabelsComponent implements OnChanges { | ||||
| 
 | ||||
|   handleChannel() { | ||||
|     const type = this.vout ? 'open' : 'close'; | ||||
|     this.label = `Channel ${type}: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`; | ||||
|     const leftNodeName = this.channel.node_left.alias || this.channel.node_left.public_key.substring(0, 10); | ||||
|     const rightNodeName = this.channel.node_right.alias || this.channel.node_right.public_key.substring(0, 10); | ||||
|     this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`; | ||||
|   } | ||||
| 
 | ||||
|   handleVin() { | ||||
|  | ||||
| @ -42,6 +42,10 @@ export class AppComponent implements OnInit { | ||||
|     if (event.target instanceof HTMLInputElement) { | ||||
|       return; | ||||
|     } | ||||
|     // prevent arrow key horizontal scrolling
 | ||||
|     if(["ArrowLeft","ArrowRight"].indexOf(event.code) > -1) { | ||||
|       event.preventDefault(); | ||||
|     } | ||||
|     this.stateService.keyNavigation$.next(event); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -112,6 +112,7 @@ | ||||
|             [flip]="false" | ||||
|             (txClickEvent)="onTxClick($event)" | ||||
|           ></app-block-overview-graph> | ||||
|           <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| @ -213,15 +214,21 @@ | ||||
|     <div class="row"> | ||||
|       <div class="col-sm"> | ||||
|         <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3> | ||||
|         <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75" | ||||
|           [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" | ||||
|           (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !auditEnabled"></app-block-overview-graph>  | ||||
|         <div class="block-graph-wrapper"> | ||||
|           <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75" | ||||
|             [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" | ||||
|             (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !auditEnabled"></app-block-overview-graph> | ||||
|           <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="col-sm" *ngIf="!isMobile"> | ||||
|         <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3> | ||||
|         <app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75" | ||||
|           [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" | ||||
|           (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !auditEnabled"></app-block-overview-graph> | ||||
|         <div class="block-graph-wrapper"> | ||||
|           <app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75" | ||||
|             [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" | ||||
|             (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !auditEnabled"></app-block-overview-graph> | ||||
|           <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -343,5 +350,17 @@ | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #emptyBlockInfo> | ||||
|   <a | ||||
|     *ngIf="network === '' && block && block.height > 100000 && block.tx_count <= 1" | ||||
|     class="info-bubble-link badge badge-primary" | ||||
|     [routerLink]="['/docs/faq/' | relativeUrl]" | ||||
|     fragment="why-empty-blocks" | ||||
|   > | ||||
|     <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon> | ||||
|     <span i18n="block.empty-block-explanation">Why is this block empty?</span> | ||||
|   </a> | ||||
| </ng-template> | ||||
| 
 | ||||
| <br> | ||||
| <br> | ||||
|  | ||||
| @ -202,4 +202,24 @@ h1 { | ||||
|   &.active, &:hover { | ||||
|     border-color: white; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .block-graph-wrapper { | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .info-bubble-link { | ||||
|   position: absolute; | ||||
|   display: block; | ||||
|   top: 2em; | ||||
|   left: 50%; | ||||
|   margin: auto; | ||||
|   text-align: center; | ||||
|   padding: 0.5em 1em; | ||||
|   font-size: 80%; | ||||
|   transform: translateX(-50%); | ||||
| 
 | ||||
|   .ng-fa-icon { | ||||
|     margin-right: 1em; | ||||
|   } | ||||
| } | ||||
| @ -138,7 +138,6 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|         this.page = 1; | ||||
|         this.error = undefined; | ||||
|         this.fees = undefined; | ||||
|         this.stateService.markBlock$.next({}); | ||||
|         this.auditDataMissing = false; | ||||
| 
 | ||||
|         if (history.state.data && history.state.data.blockHeight) { | ||||
|  | ||||
| @ -1,36 +1,55 @@ | ||||
| <div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate"> | ||||
|   <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" > | ||||
|     <div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)"> | ||||
|       <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" | ||||
|         class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a> | ||||
|       <div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height"> | ||||
|         <a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a> | ||||
| <div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [style.left]="static ? (offset || 0) + 'px' : null" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate"> | ||||
|   <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn"> | ||||
|     <ng-container *ngIf="block && !block.loading && !block.placeholder; else placeholderBlock"> | ||||
|       <div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)"> | ||||
|         <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" | ||||
|           class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height"> | ||||
|           <a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a> | ||||
|         </div> | ||||
|         <div class="block-body"> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-fees'" class="fees"> | ||||
|             ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||
|           </div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="block?.extras?.feeRange"> | ||||
|             {{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||
|           </div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="!block?.extras?.feeRange"> | ||||
|               | ||||
|           </div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-total-fees'" *ngIf="showMiningInfo" class="block-size"> | ||||
|             <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|           </div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + 'block-size'" *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count"> | ||||
|             <ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container> | ||||
|             <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> | ||||
|             <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template> | ||||
|           </div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-time'" class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div> | ||||
|         </div> | ||||
|         <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined"> | ||||
|           <a [attr.data-cy]="'bitcoin-block-' + i + '-pool'" class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]"> | ||||
|             {{ block.extras.pool.name}}</a> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="block-body"> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-fees'" class="fees"> | ||||
|           ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||
|     </ng-container> | ||||
|     <ng-template #placeholderBlock> | ||||
|       <ng-container *ngIf="block && block.placeholder; else loadingBlock"> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block placeholder-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"> | ||||
|   | ||||
|         </div> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span"> | ||||
|           {{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||
|       </ng-container> | ||||
|     </ng-template> | ||||
|     <ng-template #loadingBlock> | ||||
|       <ng-container *ngIf="block && block.loading"> | ||||
|         <div class="flashing"> | ||||
|           <div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"></div> | ||||
|         </div> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-total-fees'" *ngIf="showMiningInfo" class="block-size"> | ||||
|           <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
|         </div> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + 'block-size'" *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count"> | ||||
|           <ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container> | ||||
|           <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> | ||||
|           <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template> | ||||
|         </div> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-time'" class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div> | ||||
|       </div> | ||||
|       <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined"> | ||||
|         <a [attr.data-cy]="'bitcoin-block-' + i + '-pool'" class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]"> | ||||
|           {{ block.extras.pool.name}}</a> | ||||
|       </div> | ||||
|     </div> | ||||
|       </ng-container> | ||||
|     </ng-template> | ||||
|   </div> | ||||
| <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div> | ||||
| <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loadingBlocksTemplate> | ||||
|  | ||||
| @ -25,6 +25,10 @@ | ||||
|   transition: background 2s, left 2s, transform 1s; | ||||
| } | ||||
| 
 | ||||
| .mined-block.placeholder-block { | ||||
|   background: none !important; | ||||
| } | ||||
| 
 | ||||
| .block-size { | ||||
|   font-size: 16px; | ||||
|   font-weight: bold; | ||||
| @ -96,6 +100,16 @@ | ||||
|   transform-origin: top; | ||||
| } | ||||
| 
 | ||||
| .bitcoin-block.placeholder-block::after { | ||||
|   content: none; | ||||
|   background: 0; | ||||
| } | ||||
| 
 | ||||
| .bitcoin-block.placeholder-block::before { | ||||
|   content: none; | ||||
|   background: 0; | ||||
| } | ||||
| 
 | ||||
| .black-background { | ||||
|   background-color: #11131f; | ||||
|   z-index: 100; | ||||
|  | ||||
| @ -1,10 +1,16 @@ | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Observable, Subscription } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { specialBlocks } from '../../app.constants'; | ||||
| import { BlockExtended } from '../../interfaces/node-api.interface'; | ||||
| import { Location } from '@angular/common'; | ||||
| import { config } from 'process'; | ||||
| import { CacheService } from 'src/app/services/cache.service'; | ||||
| 
 | ||||
| interface BlockchainBlock extends BlockExtended { | ||||
|   placeholder?: boolean; | ||||
|   loading?: boolean; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-blockchain-blocks', | ||||
| @ -12,13 +18,19 @@ import { config } from 'process'; | ||||
|   styleUrls: ['./blockchain-blocks.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
| export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   @Input() static: boolean = false; | ||||
|   @Input() offset: number = 0; | ||||
|   @Input() height: number = 0; | ||||
|   @Input() count: number = 8; | ||||
|    | ||||
|   specialBlocks = specialBlocks; | ||||
|   network = ''; | ||||
|   blocks: BlockExtended[] = []; | ||||
|   blocks: BlockchainBlock[] = []; | ||||
|   emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); | ||||
|   markHeight: number; | ||||
|   blocksSubscription: Subscription; | ||||
|   blockPageSubscription: Subscription; | ||||
|   networkSubscription: Subscription; | ||||
|   tabHiddenSubscription: Subscription; | ||||
|   markBlockSubscription: Subscription; | ||||
| @ -31,7 +43,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|   arrowVisible = false; | ||||
|   arrowLeftPx = 30; | ||||
|   blocksFilled = false; | ||||
|   transition = '1s'; | ||||
|   arrowTransition = '1s'; | ||||
|   showMiningInfo = false; | ||||
|   timeLtrSubscription: Subscription; | ||||
|   timeLtr: boolean; | ||||
| @ -47,6 +59,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     public cacheService: CacheService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|     private location: Location, | ||||
|   ) { | ||||
| @ -75,44 +88,52 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|     this.loadingBlocks$ = this.stateService.isLoadingWebSocket$; | ||||
|     this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); | ||||
|     this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); | ||||
|     this.blocksSubscription = this.stateService.blocks$ | ||||
|       .subscribe(([block, txConfirmed]) => { | ||||
|         if (this.blocks.some((b) => b.height === block.height)) { | ||||
|           return; | ||||
|         } | ||||
|     if (!this.static) { | ||||
|       this.blocksSubscription = this.stateService.blocks$ | ||||
|         .subscribe(([block, txConfirmed]) => { | ||||
|           if (this.blocks.some((b) => b.height === block.height)) { | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|         if (this.blocks.length && block.height !== this.blocks[0].height + 1) { | ||||
|           this.blocks = []; | ||||
|           this.blocksFilled = false; | ||||
|         } | ||||
|           if (this.blocks.length && block.height !== this.blocks[0].height + 1) { | ||||
|             this.blocks = []; | ||||
|             this.blocksFilled = false; | ||||
|           } | ||||
| 
 | ||||
|         this.blocks.unshift(block); | ||||
|         this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT); | ||||
|           this.blocks.unshift(block); | ||||
|           this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT); | ||||
| 
 | ||||
|         if (this.blocksFilled && !this.tabHidden && block.extras) { | ||||
|           block.extras.stage = block.extras.matchRate >= 66 ? 1 : 2; | ||||
|         } | ||||
|           if (txConfirmed) { | ||||
|             this.markHeight = block.height; | ||||
|             this.moveArrowToPosition(true, true); | ||||
|           } else { | ||||
|             this.moveArrowToPosition(true, false); | ||||
|           } | ||||
| 
 | ||||
|         if (txConfirmed) { | ||||
|           this.markHeight = block.height; | ||||
|           this.moveArrowToPosition(true, true); | ||||
|         } else { | ||||
|           this.moveArrowToPosition(true, false); | ||||
|         } | ||||
| 
 | ||||
|         this.blockStyles = []; | ||||
|         this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); | ||||
|         setTimeout(() => { | ||||
|           this.blockStyles = []; | ||||
|           this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); | ||||
|           this.cd.markForCheck(); | ||||
|         }, 50); | ||||
|           if (this.blocksFilled) { | ||||
|             this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205))); | ||||
|             setTimeout(() => { | ||||
|               this.blockStyles = []; | ||||
|               this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); | ||||
|               this.cd.markForCheck(); | ||||
|             }, 50); | ||||
|           } else { | ||||
|             this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); | ||||
|           } | ||||
| 
 | ||||
|         if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) { | ||||
|           this.blocksFilled = true; | ||||
|           if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) { | ||||
|             this.blocksFilled = true; | ||||
|           } | ||||
|           this.cd.markForCheck(); | ||||
|         }); | ||||
|     } else { | ||||
|       this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => { | ||||
|         if (block.height <= this.height && block.height > this.height - this.count) { | ||||
|           this.onBlockLoaded(block); | ||||
|         } | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.markBlockSubscription = this.stateService.markBlock$ | ||||
|       .subscribe((state) => { | ||||
| @ -123,10 +144,26 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|         this.moveArrowToPosition(false); | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
| 
 | ||||
|       if (this.static) { | ||||
|         this.updateStaticBlocks(); | ||||
|       } | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (this.static) { | ||||
|       const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1); | ||||
|       this.updateStaticBlocks(animateSlide); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.blocksSubscription.unsubscribe(); | ||||
|     if (this.blocksSubscription) { | ||||
|       this.blocksSubscription.unsubscribe(); | ||||
|     } | ||||
|     if (this.blockPageSubscription) { | ||||
|       this.blockPageSubscription.unsubscribe(); | ||||
|     } | ||||
|     this.networkSubscription.unsubscribe(); | ||||
|     this.tabHiddenSubscription.unsubscribe(); | ||||
|     this.markBlockSubscription.unsubscribe(); | ||||
| @ -142,13 +179,13 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|     const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight); | ||||
|     if (blockindex > -1) { | ||||
|       if (!animate) { | ||||
|         this.transition = 'inherit'; | ||||
|         this.arrowTransition = 'inherit'; | ||||
|       } | ||||
|       this.arrowVisible = true; | ||||
|       if (newBlockFromLeft) { | ||||
|         this.arrowLeftPx = blockindex * 155 + 30 - 205; | ||||
|         setTimeout(() => { | ||||
|           this.transition = '2s'; | ||||
|           this.arrowTransition = '2s'; | ||||
|           this.arrowLeftPx = blockindex * 155 + 30; | ||||
|           this.cd.markForCheck(); | ||||
|         }, 50); | ||||
| @ -156,45 +193,117 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|         this.arrowLeftPx = blockindex * 155 + 30; | ||||
|         if (!animate) { | ||||
|           setTimeout(() => { | ||||
|             this.transition = '2s'; | ||||
|             this.arrowTransition = '2s'; | ||||
|             this.cd.markForCheck(); | ||||
|           }); | ||||
|           }, 50); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       this.arrowVisible = false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   trackByBlocksFn(index: number, item: BlockExtended) { | ||||
|   trackByBlocksFn(index: number, item: BlockchainBlock) { | ||||
|     return item.height; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForBlock(block: BlockExtended) { | ||||
|   updateStaticBlocks(animateSlide: boolean = false) { | ||||
|     // reset blocks
 | ||||
|     this.blocks = []; | ||||
|     this.blockStyles = []; | ||||
|     while (this.blocks.length < this.count) { | ||||
|       const height = this.height - this.blocks.length; | ||||
|       let block; | ||||
|       if (height >= 0) { | ||||
|         this.cacheService.loadBlock(height); | ||||
|         block = this.cacheService.getCachedBlock(height) || null; | ||||
|       } | ||||
|       this.blocks.push(block || { | ||||
|         placeholder: height < 0, | ||||
|         loading: height >= 0, | ||||
|         id: '', | ||||
|         height, | ||||
|         version: 0, | ||||
|         timestamp: 0, | ||||
|         bits: 0, | ||||
|         nonce: 0, | ||||
|         difficulty: 0, | ||||
|         merkle_root: '', | ||||
|         tx_count: 0, | ||||
|         size: 0, | ||||
|         weight: 0, | ||||
|         previousblockhash: '', | ||||
|       }); | ||||
|     } | ||||
|     this.blocks = this.blocks.slice(0, this.count); | ||||
|     this.blockStyles = []; | ||||
|     this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0))); | ||||
|     this.cd.markForCheck(); | ||||
|     if (animateSlide) { | ||||
|       // animate blocks slide right
 | ||||
|       setTimeout(() => { | ||||
|         this.blockStyles = []; | ||||
|         this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); | ||||
|         this.cd.markForCheck(); | ||||
|       }, 50); | ||||
|       this.moveArrowToPosition(true, true); | ||||
|     } else { | ||||
|       this.moveArrowToPosition(false, false); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onBlockLoaded(block: BlockExtended) { | ||||
|     const blockIndex = this.height - block.height; | ||||
|     if (blockIndex >= 0 && blockIndex < this.blocks.length) { | ||||
|       this.blocks[blockIndex] = block; | ||||
|       this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex); | ||||
|     } | ||||
|     this.cd.markForCheck(); | ||||
|   } | ||||
| 
 | ||||
|   getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) { | ||||
|     if (!block || block.placeholder) { | ||||
|       return this.getStyleForPlaceholderBlock(index, animateEnterFrom); | ||||
|     } else if (block.loading) { | ||||
|       return this.getStyleForLoadingBlock(index, animateEnterFrom); | ||||
|     } | ||||
|     const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100; | ||||
|     let addLeft = 0; | ||||
| 
 | ||||
|     if (block?.extras?.stage === 1) { | ||||
|       block.extras.stage = 2; | ||||
|       addLeft = -205; | ||||
|     if (animateEnterFrom) { | ||||
|       addLeft = animateEnterFrom || 0; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       left: addLeft + 155 * this.blocks.indexOf(block) + 'px', | ||||
|       left: addLeft + 155 * index + 'px', | ||||
|       background: `repeating-linear-gradient(
 | ||||
|         #2d3348, | ||||
|         #2d3348 ${greenBackgroundHeight}%, | ||||
|         ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, | ||||
|         ${this.gradientColors[this.network][1]} 100% | ||||
|       )`,
 | ||||
|       transition: animateEnterFrom ? 'background 2s, transform 1s' : null, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForEmptyBlock(block: BlockExtended) { | ||||
|     let addLeft = 0; | ||||
|   getStyleForLoadingBlock(index: number, animateEnterFrom: number = 0) { | ||||
|     const addLeft = animateEnterFrom || 0; | ||||
| 
 | ||||
|     if (block?.extras?.stage === 1) { | ||||
|       block.extras.stage = 2; | ||||
|       addLeft = -205; | ||||
|     } | ||||
|     return { | ||||
|       left: addLeft + (155 * index) + 'px', | ||||
|       background: "#2d3348", | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) { | ||||
|     const addLeft = animateEnterFrom || 0; | ||||
|     return { | ||||
|       left: addLeft + (155 * index) + 'px', | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForEmptyBlock(block: BlockExtended, animateEnterFrom: number = 0) { | ||||
|     const addLeft = animateEnterFrom || 0; | ||||
| 
 | ||||
|     return { | ||||
|       left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px', | ||||
| @ -219,7 +328,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|         weight: 0, | ||||
|         previousblockhash: '', | ||||
|         matchRate: 0, | ||||
|         stage: 0, | ||||
|       }); | ||||
|     } | ||||
|     return emptyBlocks; | ||||
|  | ||||
| @ -2,10 +2,14 @@ | ||||
|   <div class="position-container" [ngClass]="network ? network : ''"> | ||||
|     <span> | ||||
|       <div class="blocks-wrapper"> | ||||
|         <app-mempool-blocks></app-mempool-blocks> | ||||
|         <app-blockchain-blocks></app-blockchain-blocks> | ||||
|         <div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div> | ||||
|         <app-mempool-blocks [hidden]="pageIndex > 0"></app-mempool-blocks> | ||||
|         <app-blockchain-blocks [hidden]="pageIndex > 0"></app-blockchain-blocks> | ||||
|         <ng-container *ngFor="let page of pages; trackBy: trackByPageFn"> | ||||
|           <app-blockchain-blocks [static]="true" [offset]="page.offset" [height]="page.height" [count]="blocksPerPage"></app-blockchain-blocks> | ||||
|         </ng-container> | ||||
|       </div> | ||||
|       <div id="divider"> | ||||
|       <div id="divider" [hidden]="pageIndex > 0"> | ||||
|         <button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button> | ||||
|       </div> | ||||
|     </span> | ||||
|  | ||||
| @ -72,6 +72,15 @@ | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .scroll-spacer { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 1px; | ||||
|   height: 1px; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .loading-block { | ||||
|   position: absolute; | ||||
|   text-align: center; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| 
 | ||||
| @ -9,6 +9,11 @@ import { StateService } from '../../services/state.service'; | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class BlockchainComponent implements OnInit, OnDestroy { | ||||
|   @Input() pages: any[] = []; | ||||
|   @Input() pageIndex: number; | ||||
|   @Input() blocksPerPage: number = 8; | ||||
|   @Input() minScrollWidth: number = 0; | ||||
| 
 | ||||
|   network: string; | ||||
|   timeLtrSubscription: Subscription; | ||||
|   timeLtr: boolean = this.stateService.timeLtr.value; | ||||
| @ -29,6 +34,10 @@ export class BlockchainComponent implements OnInit, OnDestroy { | ||||
|     this.timeLtrSubscription.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   trackByPageFn(index: number, item: { index: number }) { | ||||
|     return item.index; | ||||
|   } | ||||
| 
 | ||||
|   toggleTimeDirection() { | ||||
|     this.ltrTransitionEnabled = true; | ||||
|     this.stateService.timeLtr.next(!this.timeLtr); | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|       <app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results> | ||||
|     </div> | ||||
|     <div> | ||||
|       <button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary"> | ||||
|       <button [disabled]="isSearching" type="submit" class="btn btn-block btn-purple"> | ||||
|         <fa-icon *ngIf="!(isTypeaheading$ | async) else searchLoading" [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon> | ||||
|       </button> | ||||
|     </div> | ||||
|  | ||||
| @ -43,9 +43,6 @@ form { | ||||
|   @media (min-width: 1200px) { | ||||
|     min-width: 300px; | ||||
|   } | ||||
|   input { | ||||
|     border: 0px; | ||||
|   } | ||||
|   .btn { | ||||
|     width: 100px; | ||||
|   } | ||||
|  | ||||
| @ -11,8 +11,9 @@ | ||||
| <div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer | ||||
|   (mousedown)="onMouseDown($event)" | ||||
|   (dragstart)="onDragStart($event)" | ||||
|   (scroll)="onScroll($event)" | ||||
| > | ||||
| <app-blockchain></app-blockchain> | ||||
|   <app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth"></app-blockchain> | ||||
| </div> | ||||
| 
 | ||||
| <router-outlet></router-outlet> | ||||
|  | ||||
| @ -19,16 +19,51 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|   blockchainScrollLeftInit: number; | ||||
|   timeLtrSubscription: Subscription; | ||||
|   timeLtr: boolean = this.stateService.timeLtr.value; | ||||
|   chainTipSubscription: Subscription; | ||||
|   chainTip: number = -1; | ||||
|   markBlockSubscription: Subscription; | ||||
|   @ViewChild('blockchainContainer') blockchainContainer: ElementRef; | ||||
| 
 | ||||
|   isMobile: boolean = false; | ||||
|   blockWidth = 155; | ||||
|   blocksPerPage: number = 1; | ||||
|   pageWidth: number; | ||||
|   firstPageWidth: number; | ||||
|   minScrollWidth: number; | ||||
|   pageIndex: number = 0; | ||||
|   pages: any[] = []; | ||||
|   pendingMark: number | void = null; | ||||
| 
 | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.firstPageWidth = 40 + (this.blockWidth * this.stateService.env.KEEP_BLOCKS_AMOUNT); | ||||
|     this.onResize(); | ||||
|     this.updatePages(); | ||||
|     this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { | ||||
|       this.timeLtr = !!ltr; | ||||
|     }); | ||||
|     this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => { | ||||
|       this.chainTip = height; | ||||
|       this.updatePages(); | ||||
|       if (this.pendingMark != null) { | ||||
|         this.scrollToBlock(this.pendingMark); | ||||
|         this.pendingMark = null; | ||||
|       } | ||||
|     }); | ||||
|     this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => { | ||||
|       if (mark?.blockHeight != null) { | ||||
|         if (this.chainTip >=0) { | ||||
|           if (!this.blockInViewport(mark.blockHeight)) { | ||||
|             this.scrollToBlock(mark.blockHeight); | ||||
|           } | ||||
|         } else { | ||||
|           this.pendingMark = mark.blockHeight; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     this.stateService.blocks$ | ||||
|       .subscribe((blocks: any) => { | ||||
|         if (this.stateService.network !== '') { | ||||
| @ -55,6 +90,34 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     this.isMobile = window.innerWidth <= 767.98; | ||||
|     let firstVisibleBlock; | ||||
|     let offset; | ||||
|     if (this.blockchainContainer?.nativeElement != null) { | ||||
|       this.pages.forEach(page => { | ||||
|         const left = page.offset - this.getConvertedScrollOffset(); | ||||
|         const right = left + this.pageWidth; | ||||
|         if (left <= 0 && right > 0) { | ||||
|           const blockIndex = Math.max(0, Math.floor(left / -this.blockWidth)); | ||||
|           firstVisibleBlock = page.height - blockIndex; | ||||
|           offset = left + (blockIndex * this.blockWidth); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.blocksPerPage = Math.ceil(window.innerWidth / this.blockWidth); | ||||
|     this.pageWidth = this.blocksPerPage * this.blockWidth; | ||||
|     this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2); | ||||
| 
 | ||||
|     if (firstVisibleBlock != null) { | ||||
|       this.scrollToBlock(firstVisibleBlock, offset); | ||||
|     } else { | ||||
|       this.updatePages(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onMouseDown(event: MouseEvent) { | ||||
|     this.mouseDragStartX = event.clientX; | ||||
|     this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft; | ||||
| @ -70,7 +133,7 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|     if (this.mouseDragStartX != null) { | ||||
|       this.stateService.setBlockScrollingInProgress(true); | ||||
|       this.blockchainContainer.nativeElement.scrollLeft = | ||||
|         this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX | ||||
|         this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX; | ||||
|     } | ||||
|   } | ||||
|   @HostListener('document:mouseup', []) | ||||
| @ -79,7 +142,149 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|     this.stateService.setBlockScrollingInProgress(false); | ||||
|   } | ||||
| 
 | ||||
|   onScroll(e) { | ||||
|     const middlePage = this.pageIndex === 0 ? this.pages[0] : this.pages[1]; | ||||
|     // compensate for css transform
 | ||||
|     const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5); | ||||
|     const backThreshold = middlePage.offset + (this.pageWidth * 0.5) + translation; | ||||
|     const forwardThreshold = middlePage.offset - (this.pageWidth * 0.5) + translation; | ||||
|     const scrollLeft = this.getConvertedScrollOffset(); | ||||
|     if (scrollLeft > backThreshold) { | ||||
|       if (this.shiftPagesBack()) { | ||||
|         this.addConvertedScrollOffset(-this.pageWidth); | ||||
|         this.blockchainScrollLeftInit -= this.pageWidth; | ||||
|       } | ||||
|     } else if (scrollLeft < forwardThreshold) { | ||||
|       if (this.shiftPagesForward()) { | ||||
|         this.addConvertedScrollOffset(this.pageWidth); | ||||
|         this.blockchainScrollLeftInit += this.pageWidth; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   scrollToBlock(height, blockOffset = 0) { | ||||
|     if (!this.blockchainContainer?.nativeElement) { | ||||
|       setTimeout(() => { this.scrollToBlock(height, blockOffset); }, 50); | ||||
|       return; | ||||
|     } | ||||
|     const targetHeight = this.isMobile ? height - 1 : height; | ||||
|     const viewingPageIndex = this.getPageIndexOf(targetHeight); | ||||
|     const pages = []; | ||||
|     this.pageIndex = Math.max(viewingPageIndex - 1, 0); | ||||
|     let viewingPage = this.getPageAt(viewingPageIndex); | ||||
|     const isLastPage = viewingPage.height < this.blocksPerPage; | ||||
|     if (isLastPage) { | ||||
|       this.pageIndex = Math.max(viewingPageIndex - 2, 0); | ||||
|       viewingPage = this.getPageAt(viewingPageIndex); | ||||
|     } | ||||
|     const left = viewingPage.offset - this.getConvertedScrollOffset(); | ||||
|     const blockIndex = viewingPage.height - targetHeight; | ||||
|     const targetOffset = (this.blockWidth * blockIndex) + left; | ||||
|     let deltaOffset = targetOffset - blockOffset; | ||||
| 
 | ||||
|     if (isLastPage) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex - 2)); | ||||
|     } | ||||
|     if (viewingPageIndex > 1) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex - 1)); | ||||
|     } | ||||
|     if (viewingPageIndex > 0) { | ||||
|       pages.push(viewingPage); | ||||
|     } | ||||
|     if (!isLastPage) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex + 1)); | ||||
|     } | ||||
|     if (viewingPageIndex === 0) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex + 2)); | ||||
|     } | ||||
| 
 | ||||
|     this.pages = pages; | ||||
|     this.addConvertedScrollOffset(deltaOffset); | ||||
|   } | ||||
| 
 | ||||
|   updatePages() { | ||||
|     const pages = []; | ||||
|     if (this.pageIndex > 0) { | ||||
|       pages.push(this.getPageAt(this.pageIndex)); | ||||
|     } | ||||
|     pages.push(this.getPageAt(this.pageIndex + 1)); | ||||
|     pages.push(this.getPageAt(this.pageIndex + 2)); | ||||
|     this.pages = pages; | ||||
|   } | ||||
| 
 | ||||
|   shiftPagesBack(): boolean { | ||||
|     const nextPage = this.getPageAt(this.pageIndex + 3); | ||||
|     if (nextPage.height >= 0) { | ||||
|       this.pageIndex++; | ||||
|       this.pages.forEach(page => page.offset -= this.pageWidth); | ||||
|       if (this.pageIndex !== 1) { | ||||
|         this.pages.shift(); | ||||
|       } | ||||
|       this.pages.push(this.getPageAt(this.pageIndex + 2)); | ||||
|      return true; | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   shiftPagesForward(): boolean { | ||||
|     if (this.pageIndex > 0) { | ||||
|       this.pageIndex--; | ||||
|       this.pages.forEach(page => page.offset += this.pageWidth); | ||||
|       this.pages.pop(); | ||||
|       if (this.pageIndex) { | ||||
|         this.pages.unshift(this.getPageAt(this.pageIndex)); | ||||
|       } | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   getPageAt(index: number) { | ||||
|     const height = this.chainTip - 8 - ((index - 1) * this.blocksPerPage) | ||||
|     return { | ||||
|       offset: this.firstPageWidth + (this.pageWidth * (index - 1 - this.pageIndex)), | ||||
|       height: height, | ||||
|       depth: this.chainTip - height, | ||||
|       index: index, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getPageIndexOf(height: number): number { | ||||
|     const delta = this.chainTip - 8 - height; | ||||
|     return Math.max(0, Math.floor(delta / this.blocksPerPage) + 1); | ||||
|   } | ||||
| 
 | ||||
|   blockInViewport(height: number): boolean { | ||||
|     const firstHeight = this.pages[0].height; | ||||
|     const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5); | ||||
|     const firstX = this.pages[0].offset - this.getConvertedScrollOffset() + translation; | ||||
|     const xPos = firstX + ((firstHeight - height) * 155); | ||||
|     return xPos > -55 && xPos < (window.innerWidth - 100); | ||||
|   } | ||||
| 
 | ||||
|   getConvertedScrollOffset(): number { | ||||
|     if (this.timeLtr) { | ||||
|       return -this.blockchainContainer?.nativeElement?.scrollLeft || 0; | ||||
|     } else { | ||||
|       return this.blockchainContainer?.nativeElement?.scrollLeft || 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   addConvertedScrollOffset(offset: number): void { | ||||
|     if (!this.blockchainContainer?.nativeElement) { | ||||
|       return; | ||||
|     } | ||||
|     if (this.timeLtr) { | ||||
|       this.blockchainContainer.nativeElement.scrollLeft -= offset; | ||||
|     } else { | ||||
|       this.blockchainContainer.nativeElement.scrollLeft += offset; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.timeLtrSubscription.unsubscribe(); | ||||
|     this.chainTipSubscription.unsubscribe(); | ||||
|     this.markBlockSubscription.unsubscribe(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { | ||||
| import { Transaction, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { of, merge, Subscription, Observable, Subject, from } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { CacheService } from '../../services/cache.service'; | ||||
| import { OpenGraphService } from '../../services/opengraph.service'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| @ -45,6 +46,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { | ||||
|     private route: ActivatedRoute, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private apiService: ApiService, | ||||
|     private seoService: SeoService, | ||||
|     private openGraphService: OpenGraphService, | ||||
| @ -97,7 +99,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { | ||||
|         }), | ||||
|         switchMap(() => { | ||||
|           let transactionObservable$: Observable<Transaction>; | ||||
|           const cached = this.stateService.getTxFromCache(this.txId); | ||||
|           const cached = this.cacheService.getTxFromCache(this.txId); | ||||
|           if (cached && cached.fee !== -1) { | ||||
|             transactionObservable$ = of(cached); | ||||
|           } else { | ||||
|  | ||||
| @ -13,6 +13,7 @@ import { | ||||
| import { Transaction } from '../../interfaces/electrs.interface'; | ||||
| import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { CacheService } from '../../services/cache.service'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { AudioService } from '../../services/audio.service'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| @ -74,6 +75,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     private relativeUrlPipe: RelativeUrlPipe, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private websocketService: WebsocketService, | ||||
|     private audioService: AudioService, | ||||
|     private apiService: ApiService, | ||||
| @ -131,26 +133,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|           this.cpfpInfo = null; | ||||
|           return; | ||||
|         } | ||||
|         if (cpfpInfo.effectiveFeePerVsize) { | ||||
|           this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; | ||||
|         } else { | ||||
|           const lowerFeeParents = cpfpInfo.ancestors.filter( | ||||
|             (parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize | ||||
|           ); | ||||
|           let totalWeight = | ||||
|             this.tx.weight + | ||||
|             lowerFeeParents.reduce((prev, val) => prev + val.weight, 0); | ||||
|           let totalFees = | ||||
|             this.tx.fee + | ||||
|             lowerFeeParents.reduce((prev, val) => prev + val.fee, 0); | ||||
| 
 | ||||
|           if (cpfpInfo?.bestDescendant) { | ||||
|             totalWeight += cpfpInfo?.bestDescendant.weight; | ||||
|             totalFees += cpfpInfo?.bestDescendant.fee; | ||||
|           } | ||||
| 
 | ||||
|           this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4); | ||||
|         // merge ancestors/descendants
 | ||||
|         const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])]; | ||||
|         if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) { | ||||
|           relatives.push(cpfpInfo.bestDescendant); | ||||
|         } | ||||
|         let totalWeight = | ||||
|           this.tx.weight + | ||||
|           relatives.reduce((prev, val) => prev + val.weight, 0); | ||||
|         let totalFees = | ||||
|           this.tx.fee + | ||||
|           relatives.reduce((prev, val) => prev + val.fee, 0); | ||||
| 
 | ||||
|         this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4); | ||||
| 
 | ||||
|         if (!this.tx.status.confirmed) { | ||||
|           this.stateService.markBlock$.next({ | ||||
|             txFeePerVSize: this.tx.effectiveFeePerVsize, | ||||
| @ -203,7 +199,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|         }), | ||||
|         switchMap(() => { | ||||
|           let transactionObservable$: Observable<Transaction>; | ||||
|           const cached = this.stateService.getTxFromCache(this.txId); | ||||
|           const cached = this.cacheService.getTxFromCache(this.txId); | ||||
|           if (cached && cached.fee !== -1) { | ||||
|             transactionObservable$ = of(cached); | ||||
|           } else { | ||||
| @ -302,7 +298,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|         this.waitingForTransaction = false; | ||||
|       } | ||||
|       this.rbfTransaction = rbfTransaction; | ||||
|       this.stateService.setTxCache([this.rbfTransaction]); | ||||
|       this.cacheService.setTxCache([this.rbfTransaction]); | ||||
|     }); | ||||
| 
 | ||||
|     this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { CacheService } from '../../services/cache.service'; | ||||
| import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; | ||||
| import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| @ -44,6 +45,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private apiService: ApiService, | ||||
|     private assetsService: AssetsService, | ||||
| @ -123,7 +125,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
|       } | ||||
| 
 | ||||
|       this.transactionsLength = this.transactions.length; | ||||
|       this.stateService.setTxCache(this.transactions); | ||||
|       this.cacheService.setTxCache(this.transactions); | ||||
| 
 | ||||
|       this.transactions.forEach((tx) => { | ||||
|         tx['@voutLimit'] = true; | ||||
|  | ||||
| @ -24,7 +24,6 @@ export interface CpfpInfo { | ||||
|   ancestors: Ancestor[]; | ||||
|   descendants?: Ancestor[]; | ||||
|   bestDescendant?: BestDescendant | null; | ||||
|   effectiveFeePerVsize?: number; | ||||
| } | ||||
| 
 | ||||
| export interface DifficultyAdjustment { | ||||
| @ -122,8 +121,6 @@ export interface BlockExtension { | ||||
|     name: string; | ||||
|     slug: string; | ||||
|   } | ||||
| 
 | ||||
|   stage?: number; // Frontend only
 | ||||
| } | ||||
| 
 | ||||
| export interface BlockExtended extends Block { | ||||
|  | ||||
							
								
								
									
										105
									
								
								frontend/src/app/services/cache.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								frontend/src/app/services/cache.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { firstValueFrom, Subject, Subscription} from 'rxjs'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { BlockExtended } from '../interfaces/node-api.interface'; | ||||
| import { StateService } from './state.service'; | ||||
| import { ApiService } from './api.service'; | ||||
| 
 | ||||
| const BLOCK_CACHE_SIZE = 500; | ||||
| const KEEP_RECENT_BLOCKS = 50; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class CacheService { | ||||
|   loadedBlocks$ = new Subject<BlockExtended>(); | ||||
|   tip: number = 0; | ||||
| 
 | ||||
|   txCache: { [txid: string]: Transaction } = {}; | ||||
| 
 | ||||
|   blockCache: { [height: number]: BlockExtended } = {}; | ||||
|   blockLoading: { [height: number]: boolean } = {}; | ||||
|   copiesInBlockQueue: { [height: number]: number } = {}; | ||||
|   blockPriorities: number[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
|     private apiService: ApiService, | ||||
|   ) { | ||||
|     this.stateService.blocks$.subscribe(([block]) => { | ||||
|       this.addBlockToCache(block); | ||||
|       this.clearBlocks(); | ||||
|     }); | ||||
|     this.stateService.chainTip$.subscribe((height) => { | ||||
|       this.tip = height; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   setTxCache(transactions) { | ||||
|     this.txCache = {}; | ||||
|     transactions.forEach(tx => { | ||||
|       this.txCache[tx.txid] = tx; | ||||
|     }); | ||||
|   } | ||||
|   | ||||
|   getTxFromCache(txid) { | ||||
|     if (this.txCache && this.txCache[txid]) { | ||||
|       return this.txCache[txid]; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   addBlockToCache(block: BlockExtended) { | ||||
|     this.blockCache[block.height] = block; | ||||
|     this.bumpBlockPriority(block.height); | ||||
|   } | ||||
| 
 | ||||
|   async loadBlock(height) { | ||||
|     if (!this.blockCache[height] && !this.blockLoading[height]) { | ||||
|       const chunkSize = 10; | ||||
|       const maxHeight = Math.ceil(height / chunkSize) * chunkSize; | ||||
|       for (let i = 0; i < chunkSize; i++) { | ||||
|         this.blockLoading[maxHeight - i] = true; | ||||
|       } | ||||
|       const result = await firstValueFrom(this.apiService.getBlocks$(maxHeight)); | ||||
|       for (let i = 0; i < chunkSize; i++) { | ||||
|         delete this.blockLoading[maxHeight - i]; | ||||
|       } | ||||
|       if (result && result.length) { | ||||
|         result.forEach(block => { | ||||
|           this.addBlockToCache(block); | ||||
|           this.loadedBlocks$.next(block); | ||||
|         }); | ||||
|       } | ||||
|       this.clearBlocks(); | ||||
|     } else { | ||||
|       this.bumpBlockPriority(height); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // increase the priority of a block, to delay removal
 | ||||
|   bumpBlockPriority(height) { | ||||
|     this.blockPriorities.push(height); | ||||
|     this.copiesInBlockQueue[height] = (this.copiesInBlockQueue[height] || 0) + 1; | ||||
|   } | ||||
| 
 | ||||
|   // remove lowest priority blocks from the cache
 | ||||
|   clearBlocks() { | ||||
|     while (Object.keys(this.blockCache).length > (BLOCK_CACHE_SIZE + KEEP_RECENT_BLOCKS) && this.blockPriorities.length > KEEP_RECENT_BLOCKS) { | ||||
|       const height = this.blockPriorities.shift(); | ||||
|       if (this.copiesInBlockQueue[height] > 1) { | ||||
|         this.copiesInBlockQueue[height]--; | ||||
|       } else if ((this.tip - height) < KEEP_RECENT_BLOCKS) { | ||||
|         this.bumpBlockPriority(height); | ||||
|       } else { | ||||
|         delete this.blockCache[height]; | ||||
|         delete this.copiesInBlockQueue[height]; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getCachedBlock(height) { | ||||
|     return this.blockCache[height]; | ||||
|   } | ||||
| } | ||||
| @ -104,6 +104,7 @@ export class StateService { | ||||
|   backendInfo$ = new ReplaySubject<IBackendInfo>(1); | ||||
|   loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1); | ||||
|   recommendedFees$ = new ReplaySubject<Recommendedfees>(1); | ||||
|   chainTip$ = new ReplaySubject<number>(-1); | ||||
| 
 | ||||
|   live2Chart$ = new Subject<OptimizedMempoolStats>(); | ||||
| 
 | ||||
| @ -111,15 +112,13 @@ export class StateService { | ||||
|   connectionState$ = new BehaviorSubject<0 | 1 | 2>(2); | ||||
|   isTabHidden$: Observable<boolean>; | ||||
| 
 | ||||
|   markBlock$ = new ReplaySubject<MarkBlockState>(); | ||||
|   markBlock$ = new BehaviorSubject<MarkBlockState>({}); | ||||
|   keyNavigation$ = new Subject<KeyboardEvent>(); | ||||
| 
 | ||||
|   blockScrolling$: Subject<boolean> = new Subject<boolean>(); | ||||
|   timeLtr: BehaviorSubject<boolean>; | ||||
|   hideFlow: BehaviorSubject<boolean>; | ||||
| 
 | ||||
|   txCache: { [txid: string]: Transaction } = {}; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(PLATFORM_ID) private platformId: any, | ||||
|     @Inject(LOCALE_ID) private locale: string, | ||||
| @ -274,18 +273,15 @@ export class StateService { | ||||
|     return this.network === 'liquid' || this.network === 'liquidtestnet'; | ||||
|   } | ||||
| 
 | ||||
|   setTxCache(transactions) { | ||||
|     this.txCache = {}; | ||||
|     transactions.forEach(tx => { | ||||
|       this.txCache[tx.txid] = tx; | ||||
|     }); | ||||
|   resetChainTip() { | ||||
|     this.latestBlockHeight = -1; | ||||
|     this.chainTip$.next(-1); | ||||
|   } | ||||
|   | ||||
|   getTxFromCache(txid) { | ||||
|     if (this.txCache && this.txCache[txid]) { | ||||
|       return this.txCache[txid]; | ||||
|     } else { | ||||
|       return null; | ||||
| 
 | ||||
|   updateChainTip(height) { | ||||
|     if (height > this.latestBlockHeight) { | ||||
|       this.latestBlockHeight = height; | ||||
|       this.chainTip$.next(height); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -70,7 +70,7 @@ export class WebsocketService { | ||||
|         clearTimeout(this.onlineCheckTimeout); | ||||
|         clearTimeout(this.onlineCheckTimeoutTwo); | ||||
| 
 | ||||
|         this.stateService.latestBlockHeight = -1; | ||||
|         this.stateService.resetChainTip(); | ||||
| 
 | ||||
|         this.websocketSubject.complete(); | ||||
|         this.subscription.unsubscribe(); | ||||
| @ -224,12 +224,14 @@ export class WebsocketService { | ||||
|   handleResponse(response: WebsocketResponse) { | ||||
|     if (response.blocks && response.blocks.length) { | ||||
|       const blocks = response.blocks; | ||||
|       let maxHeight = 0; | ||||
|       blocks.forEach((block: BlockExtended) => { | ||||
|         if (block.height > this.stateService.latestBlockHeight) { | ||||
|           this.stateService.latestBlockHeight = block.height; | ||||
|           maxHeight = Math.max(maxHeight, block.height); | ||||
|           this.stateService.blocks$.next([block, false]); | ||||
|         } | ||||
|       }); | ||||
|       this.stateService.updateChainTip(maxHeight); | ||||
|     } | ||||
| 
 | ||||
|     if (response.tx) { | ||||
| @ -238,7 +240,7 @@ export class WebsocketService { | ||||
| 
 | ||||
|     if (response.block) { | ||||
|       if (response.block.height > this.stateService.latestBlockHeight) { | ||||
|         this.stateService.latestBlockHeight = response.block.height; | ||||
|         this.stateService.updateChainTip(response.block.height); | ||||
|         this.stateService.blocks$.next([response.block, !!response.txConfirmed]); | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -115,10 +115,38 @@ body { | ||||
| } | ||||
| 
 | ||||
| .form-control { | ||||
|   color: #495057; | ||||
|   color: #fff; | ||||
|   background-color: #2d3348; | ||||
|   border: 1px solid rgba(17, 19, 31, 0.2); | ||||
| } | ||||
| 
 | ||||
| .form-control:focus { | ||||
|   color: #000; | ||||
|   color: #fff; | ||||
|   background-color: #2d3348; | ||||
| } | ||||
| 
 | ||||
| .btn-purple { | ||||
|   background-color: #653b9c; | ||||
|   border-color: #653b9c; | ||||
| } | ||||
| 
 | ||||
| .btn-purple:not(:disabled):not(.disabled):active, .btn-purple:not(:disabled):not(.disabled).active, .show > .btn-purple.dropdown-toggle { | ||||
|   color: #fff; | ||||
|   background-color: #4d2d77; | ||||
|   border-color: #472a6e; | ||||
| } | ||||
| 
 | ||||
| .btn-purple:focus, .btn-purple.focus { | ||||
|   color: #fff; | ||||
|   background-color: #533180; | ||||
|   border-color: #4d2d77; | ||||
|   box-shadow: 0 0 0 0.2rem rgb(124 88 171 / 50%); | ||||
| } | ||||
| 
 | ||||
| .btn-purple:hover { | ||||
|   color: #fff; | ||||
|   background-color: #533180; | ||||
|   border-color: #4d2d77; | ||||
| } | ||||
| 
 | ||||
| .form-control.form-control-secondary { | ||||
|  | ||||
| @ -54,9 +54,13 @@ function downloadMiningPoolLogos() { | ||||
|    | ||||
|     response.on('end', () => { | ||||
|       let response_body = Buffer.concat(chunks_of_data); | ||||
|       const poolLogos = JSON.parse(response_body.toString()); | ||||
|       for (const poolLogo of poolLogos) { | ||||
|           download(`${PATH}/mining-pools/${poolLogo.name}`, poolLogo.download_url); | ||||
|       try { | ||||
|         const poolLogos = JSON.parse(response_body.toString()); | ||||
|         for (const poolLogo of poolLogos) { | ||||
|             download(`${PATH}/mining-pools/${poolLogo.name}`, poolLogo.download_url); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         console.error(`Unable to download mining pool logos. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`); | ||||
|       } | ||||
|     }); | ||||
|    | ||||
| @ -66,7 +70,6 @@ function downloadMiningPoolLogos() { | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| const poolsJsonUrl = 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'; | ||||
| let assetsJsonUrl = 'https://raw.githubusercontent.com/mempool/asset_registry_db/master/index.json'; | ||||
| let assetsMinimalJsonUrl = 'https://raw.githubusercontent.com/mempool/asset_registry_db/master/index.minimal.json'; | ||||
| 
 | ||||
| @ -82,8 +85,6 @@ console.log('Downloading assets'); | ||||
| download(PATH + 'assets.json', assetsJsonUrl); | ||||
| console.log('Downloading assets minimal'); | ||||
| download(PATH + 'assets.minimal.json', assetsMinimalJsonUrl); | ||||
| console.log('Downloading mining pools info'); | ||||
| download(PATH + 'pools.json', poolsJsonUrl); | ||||
| console.log('Downloading testnet assets'); | ||||
| download(PATH + 'assets-testnet.json', testnetAssetsJsonUrl); | ||||
| console.log('Downloading testnet assets minimal'); | ||||
|  | ||||
| @ -36,6 +36,12 @@ zmqpubrawtx=tcp://127.0.0.1:8335 | ||||
| #addnode=[2401:b140:2::92:204]:8333 | ||||
| #addnode=[2401:b140:2::92:205]:8333 | ||||
| #addnode=[2401:b140:2::92:206]:8333 | ||||
| #addnode=[2401:b140:3::92:201]:8333 | ||||
| #addnode=[2401:b140:3::92:202]:8333 | ||||
| #addnode=[2401:b140:3::92:203]:8333 | ||||
| #addnode=[2401:b140:3::92:204]:8333 | ||||
| #addnode=[2401:b140:3::92:205]:8333 | ||||
| #addnode=[2401:b140:3::92:206]:8333 | ||||
| 
 | ||||
| [test] | ||||
| daemon=1 | ||||
| @ -57,6 +63,12 @@ zmqpubrawtx=tcp://127.0.0.1:18335 | ||||
| #addnode=[2401:b140:2::92:204]:18333 | ||||
| #addnode=[2401:b140:2::92:205]:18333 | ||||
| #addnode=[2401:b140:2::92:206]:18333 | ||||
| #addnode=[2401:b140:3::92:201]:18333 | ||||
| #addnode=[2401:b140:3::92:202]:18333 | ||||
| #addnode=[2401:b140:3::92:203]:18333 | ||||
| #addnode=[2401:b140:3::92:204]:18333 | ||||
| #addnode=[2401:b140:3::92:205]:18333 | ||||
| #addnode=[2401:b140:3::92:206]:18333 | ||||
| 
 | ||||
| [signet] | ||||
| daemon=1 | ||||
| @ -78,3 +90,9 @@ zmqpubrawtx=tcp://127.0.0.1:38335 | ||||
| #addnode=[2401:b140:2::92:204]:38333 | ||||
| #addnode=[2401:b140:2::92:205]:38333 | ||||
| #addnode=[2401:b140:2::92:206]:38333 | ||||
| #addnode=[2401:b140:3::92:201]:38333 | ||||
| #addnode=[2401:b140:3::92:202]:38333 | ||||
| #addnode=[2401:b140:3::92:203]:38333 | ||||
| #addnode=[2401:b140:3::92:204]:38333 | ||||
| #addnode=[2401:b140:3::92:205]:38333 | ||||
| #addnode=[2401:b140:3::92:206]:38333 | ||||
|  | ||||
| @ -251,6 +251,7 @@ MEMPOOL_BISQ_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') | ||||
| MEMPOOL_HOME=/mempool | ||||
| MEMPOOL_USER=mempool | ||||
| MEMPOOL_GROUP=mempool | ||||
| MEMPOOL_MYSQL_CREDENTIALS="${MEMPOOL_HOME}/.mysql_credentials" | ||||
| # name of Tor hidden service in torrc | ||||
| MEMPOOL_TOR_HS=mempool | ||||
| 
 | ||||
| @ -1009,6 +1010,7 @@ osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_ | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-build-all upgrade | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-kill-all stop | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-start-all start | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-reset-all reset | ||||
| 
 | ||||
| 
 | ||||
| case $OS in | ||||
| @ -1869,7 +1871,7 @@ grant all on mempool_bisq.* to '${MEMPOOL_BISQ_USER}'@'localhost' identified by | ||||
| _EOF_ | ||||
| 
 | ||||
| echo "[*] save MySQL credentials" | ||||
| cat > ${MEMPOOL_HOME}/mysql_credentials << _EOF_ | ||||
| cat > "${MEMPOOL_MYSQL_CREDENTIALS}" << _EOF_ | ||||
| declare -x MEMPOOL_MAINNET_USER="${MEMPOOL_MAINNET_USER}" | ||||
| declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}" | ||||
| declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}" | ||||
| @ -1889,6 +1891,7 @@ declare -x MEMPOOL_LIQUIDTESTNET_PASS="${MEMPOOL_LIQUIDTESTNET_PASS}" | ||||
| declare -x MEMPOOL_BISQ_USER="${MEMPOOL_BISQ_USER}" | ||||
| declare -x MEMPOOL_BISQ_PASS="${MEMPOOL_BISQ_PASS}" | ||||
| _EOF_ | ||||
| chown "${MEMPOOL_USER}:${MEMPOOL_GROUP}" "${MEMPOOL_MYSQL_CREDENTIALS}" | ||||
| 
 | ||||
| ##### nginx | ||||
| 
 | ||||
|  | ||||
| @ -12,7 +12,7 @@ ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2) | ||||
| ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2) | ||||
| 
 | ||||
| # get mysql credentials | ||||
| MYSQL_CRED_FILE=${HOME}/mempool/mysql_credentials | ||||
| MYSQL_CRED_FILE=${HOME}/.mysql_credentials | ||||
| if [ -f "${MYSQL_CRED_FILE}" ];then | ||||
|     . ${MYSQL_CRED_FILE} | ||||
| fi | ||||
|  | ||||
| @ -9,5 +9,6 @@ | ||||
|   "MEMPOOL_WEBSITE_URL": "https://mempool.space", | ||||
|   "LIQUID_WEBSITE_URL": "https://liquid.network", | ||||
|   "BISQ_WEBSITE_URL": "https://bisq.markets", | ||||
|   "ITEMS_PER_PAGE": 25 | ||||
|   "ITEMS_PER_PAGE": 25, | ||||
|   "LIGHTNING": true | ||||
| } | ||||
|  | ||||
							
								
								
									
										3
									
								
								production/mempool-reset-all
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								production/mempool-reset-all
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,3 @@ | ||||
| #!/usr/bin/env zsh | ||||
| rm $HOME/*/backend/mempool-config.json | ||||
| rm $HOME/*/frontend/mempool-frontend-config.json | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user