Merge branch 'master' into nymkappa/update-doc
This commit is contained in:
		
						commit
						968a26d2cd
					
				
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -9,7 +9,7 @@ jobs: | |||||||
|     if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" |     if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         node: ["18", "20"] |         node: ["20", "21"] | ||||||
|         flavor: ["dev", "prod"] |         flavor: ["dev", "prod"] | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|     runs-on: "ubuntu-latest" |     runs-on: "ubuntu-latest" | ||||||
| @ -160,7 +160,7 @@ jobs: | |||||||
|     if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" |     if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         node: ["18", "20"] |         node: ["20", "21"] | ||||||
|         flavor: ["dev", "prod"] |         flavor: ["dev", "prod"] | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|     runs-on: "ubuntu-latest" |     runs-on: "ubuntu-latest" | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -100,6 +100,5 @@ jobs: | |||||||
|           --cache-to "type=local,dest=/tmp/.buildx-cache" \ |           --cache-to "type=local,dest=/tmp/.buildx-cache" \ | ||||||
|           --platform linux/amd64,linux/arm64 \ |           --platform linux/amd64,linux/arm64 \ | ||||||
|           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ |           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ | ||||||
|           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ |  | ||||||
|           --output "type=registry" ./${{ matrix.service }}/ \ |           --output "type=registry" ./${{ matrix.service }}/ \ | ||||||
|           --build-arg commitHash=$SHORT_SHA |           --build-arg commitHash=$SHORT_SHA | ||||||
|  | |||||||
| @ -40,6 +40,7 @@ class Blocks { | |||||||
|   private quarterEpochBlockTime: number | null = null; |   private quarterEpochBlockTime: number | null = null; | ||||||
|   private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; |   private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; | ||||||
|   private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = []; |   private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = []; | ||||||
|  |   private classifyingBlocks: boolean = false; | ||||||
| 
 | 
 | ||||||
|   private mainLoopTimeout: number = 120000; |   private mainLoopTimeout: number = 120000; | ||||||
| 
 | 
 | ||||||
| @ -568,6 +569,11 @@ class Blocks { | |||||||
|    * [INDEXING] Index transaction classification flags for Goggles |    * [INDEXING] Index transaction classification flags for Goggles | ||||||
|    */ |    */ | ||||||
|   public async $classifyBlocks(): Promise<void> { |   public async $classifyBlocks(): Promise<void> { | ||||||
|  |     if (this.classifyingBlocks) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.classifyingBlocks = true; | ||||||
|  | 
 | ||||||
|     // classification requires an esplora backend
 |     // classification requires an esplora backend
 | ||||||
|     if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') { |     if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') { | ||||||
|       return; |       return; | ||||||
| @ -679,6 +685,8 @@ class Blocks { | |||||||
|         indexedThisRun = 0; |         indexedThisRun = 0; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     this.classifyingBlocks = false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|  | |||||||
| @ -19,45 +19,90 @@ class RedisCache { | |||||||
|   private client; |   private client; | ||||||
|   private connected = false; |   private connected = false; | ||||||
|   private schemaVersion = 1; |   private schemaVersion = 1; | ||||||
|  |   private redisConfig: any; | ||||||
| 
 | 
 | ||||||
|  |   private pauseFlush: boolean = false; | ||||||
|   private cacheQueue: MempoolTransactionExtended[] = []; |   private cacheQueue: MempoolTransactionExtended[] = []; | ||||||
|  |   private removeQueue: string[] = []; | ||||||
|  |   private rbfCacheQueue: { type: string, txid: string, value: any }[] = []; | ||||||
|  |   private rbfRemoveQueue: { type: string, txid: string }[] = []; | ||||||
|   private txFlushLimit: number = 10000; |   private txFlushLimit: number = 10000; | ||||||
| 
 | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     if (config.REDIS.ENABLED) { |     if (config.REDIS.ENABLED) { | ||||||
|       const redisConfig = { |       this.redisConfig = { | ||||||
|         socket: { |         socket: { | ||||||
|           path: config.REDIS.UNIX_SOCKET_PATH |           path: config.REDIS.UNIX_SOCKET_PATH | ||||||
|         }, |         }, | ||||||
|         database: NetworkDB[config.MEMPOOL.NETWORK], |         database: NetworkDB[config.MEMPOOL.NETWORK], | ||||||
|       }; |       }; | ||||||
|       this.client = createClient(redisConfig); |  | ||||||
|       this.client.on('error', (e) => { |  | ||||||
|         logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`); |  | ||||||
|       }); |  | ||||||
|       this.$ensureConnected(); |       this.$ensureConnected(); | ||||||
|  |       setInterval(() => { this.$ensureConnected(); }, 10000); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $ensureConnected(): Promise<void> { |   private async $ensureConnected(): Promise<boolean> { | ||||||
|     if (!this.connected && config.REDIS.ENABLED) { |     if (!this.connected && config.REDIS.ENABLED) { | ||||||
|       return this.client.connect().then(async () => { |       try { | ||||||
|         this.connected = true; |         this.client = createClient(this.redisConfig); | ||||||
|         logger.info(`Redis client connected`); |         this.client.on('error', async (e) => { | ||||||
|  |           logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`); | ||||||
|  |           this.connected = false; | ||||||
|  |           await this.client.disconnect(); | ||||||
|  |         }); | ||||||
|  |         await this.client.connect().then(async () => { | ||||||
|  |           try { | ||||||
|             const version = await this.client.get('schema_version'); |             const version = await this.client.get('schema_version'); | ||||||
|  |             this.connected = true; | ||||||
|             if (version !== this.schemaVersion) { |             if (version !== this.schemaVersion) { | ||||||
|               // schema changed
 |               // schema changed
 | ||||||
|               // perform migrations or flush DB if necessary
 |               // perform migrations or flush DB if necessary
 | ||||||
|               logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`); |               logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`); | ||||||
|               await this.client.set('schema_version', this.schemaVersion); |               await this.client.set('schema_version', this.schemaVersion); | ||||||
|             } |             } | ||||||
|  |             logger.info(`Redis client connected`); | ||||||
|  |             return true; | ||||||
|  |           } catch (e) { | ||||||
|  |             this.connected = false; | ||||||
|  |             logger.warn('Failed to connect to Redis'); | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|         }); |         }); | ||||||
|  |         await this.$onConnected(); | ||||||
|  |         return true; | ||||||
|  |       } catch (e) { | ||||||
|  |         logger.warn('Error connecting to Redis: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       try { | ||||||
|  |         // test connection
 | ||||||
|  |         await this.client.get('schema_version'); | ||||||
|  |         return true; | ||||||
|  |       } catch (e) { | ||||||
|  |         logger.warn('Lost connection to Redis: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |         logger.warn('Attempting to reconnect in 10 seconds'); | ||||||
|  |         this.connected = false; | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $updateBlocks(blocks: BlockExtended[]) { |   private async $onConnected(): Promise<void> { | ||||||
|  |     await this.$flushTransactions(); | ||||||
|  |     await this.$removeTransactions([]); | ||||||
|  |     await this.$flushRbfQueues(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async $updateBlocks(blocks: BlockExtended[]): Promise<void> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       logger.warn(`Failed to update blocks in Redis cache: Redis is not connected`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       await this.client.set('blocks', JSON.stringify(blocks)); |       await this.client.set('blocks', JSON.stringify(blocks)); | ||||||
|       logger.debug(`Saved latest blocks to Redis cache`); |       logger.debug(`Saved latest blocks to Redis cache`); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @ -65,9 +110,15 @@ class RedisCache { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $updateBlockSummaries(summaries: BlockSummary[]) { |   async $updateBlockSummaries(summaries: BlockSummary[]): Promise<void> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       logger.warn(`Failed to update block summaries in Redis cache: Redis is not connected`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       await this.client.set('block-summaries', JSON.stringify(summaries)); |       await this.client.set('block-summaries', JSON.stringify(summaries)); | ||||||
|       logger.debug(`Saved latest block summaries to Redis cache`); |       logger.debug(`Saved latest block summaries to Redis cache`); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @ -75,30 +126,35 @@ class RedisCache { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $addTransaction(tx: MempoolTransactionExtended) { |   async $addTransaction(tx: MempoolTransactionExtended): Promise<void> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     this.cacheQueue.push(tx); |     this.cacheQueue.push(tx); | ||||||
|     if (this.cacheQueue.length >= this.txFlushLimit) { |     if (this.cacheQueue.length >= this.txFlushLimit) { | ||||||
|  |       if (!this.pauseFlush) { | ||||||
|         await this.$flushTransactions(); |         await this.$flushTransactions(); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|   async $flushTransactions() { |  | ||||||
|     const success = await this.$addTransactions(this.cacheQueue); |  | ||||||
|     if (success) { |  | ||||||
|       logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`); |  | ||||||
|       this.cacheQueue = []; |  | ||||||
|     } else { |  | ||||||
|       logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> { |   async $flushTransactions(): Promise<void> { | ||||||
|     if (!newTransactions.length) { |     if (!config.REDIS.ENABLED) { | ||||||
|       return true; |       return; | ||||||
|     } |     } | ||||||
|  |     if (!this.cacheQueue.length) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       logger.warn(`Failed to add ${this.cacheQueue.length} transactions to Redis cache: Redis not connected`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.pauseFlush = false; | ||||||
|  | 
 | ||||||
|  |     const toAdd = this.cacheQueue.slice(0, this.txFlushLimit); | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |       const msetData = toAdd.map(tx => { | ||||||
|       const msetData = newTransactions.map(tx => { |  | ||||||
|         const minified: any = { ...tx }; |         const minified: any = { ...tx }; | ||||||
|         delete minified.hex; |         delete minified.hex; | ||||||
|         for (const vin of minified.vin) { |         for (const vin of minified.vin) { | ||||||
| @ -112,30 +168,53 @@ class RedisCache { | |||||||
|         return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)]; |         return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)]; | ||||||
|       }); |       }); | ||||||
|       await this.client.MSET(msetData); |       await this.client.MSET(msetData); | ||||||
|       return true; |       // successful, remove transactions from cache queue
 | ||||||
|  |       this.cacheQueue = this.cacheQueue.slice(toAdd.length); | ||||||
|  |       logger.debug(`Saved ${toAdd.length} transactions to Redis cache, ${this.cacheQueue.length} left in queue`); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`); |       logger.warn(`Failed to add ${toAdd.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`); | ||||||
|       return false; |       this.pauseFlush = true; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $removeTransactions(transactions: string[]) { |   async $removeTransactions(transactions: string[]): Promise<void> { | ||||||
|     try { |     if (!config.REDIS.ENABLED) { | ||||||
|       await this.$ensureConnected(); |       return; | ||||||
|       const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE; |  | ||||||
|       for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) { |  | ||||||
|         const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength); |  | ||||||
|         await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`)); |  | ||||||
|         logger.debug(`Deleted ${slice.length} transactions from the Redis cache`); |  | ||||||
|     } |     } | ||||||
|  |     const toRemove = this.removeQueue.concat(transactions); | ||||||
|  |     this.removeQueue = []; | ||||||
|  |     let failed: string[] = []; | ||||||
|  |     let numRemoved = 0; | ||||||
|  |     if (this.connected) { | ||||||
|  |       const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE; | ||||||
|  |       for (let i = 0; i < Math.ceil(toRemove.length / sliceLength); i++) { | ||||||
|  |         const slice = toRemove.slice(i * sliceLength, (i + 1) * sliceLength); | ||||||
|  |         try { | ||||||
|  |           await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`)); | ||||||
|  |           numRemoved+= sliceLength; | ||||||
|  |           logger.debug(`Deleted ${slice.length} transactions from the Redis cache`); | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|       logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`); |           logger.warn(`Failed to remove ${slice.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`); | ||||||
|  |           failed = failed.concat(slice); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // concat instead of replace, in case more txs have been added in the meantime
 | ||||||
|  |       this.removeQueue = this.removeQueue.concat(failed); | ||||||
|  |     } else { | ||||||
|  |       this.removeQueue = this.removeQueue.concat(toRemove); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $setRbfEntry(type: string, txid: string, value: any): Promise<void> { |   async $setRbfEntry(type: string, txid: string, value: any): Promise<void> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       this.rbfCacheQueue.push({ type, txid, value }); | ||||||
|  |       logger.warn(`Failed to set RBF ${type} in Redis cache: Redis is not connected`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value)); |       await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value)); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`); |       logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`); | ||||||
| @ -143,17 +222,55 @@ class RedisCache { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $removeRbfEntry(type: string, txid: string): Promise<void> { |   async $removeRbfEntry(type: string, txid: string): Promise<void> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       this.rbfRemoveQueue.push({ type, txid }); | ||||||
|  |       logger.warn(`Failed to remove RBF ${type} from Redis cache: Redis is not connected`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       await this.client.unlink(`rbf:${type}:${txid}`); |       await this.client.unlink(`rbf:${type}:${txid}`); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`); |       logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $getBlocks(): Promise<BlockExtended[]> { |   private async $flushRbfQueues(): Promise<void> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     try { | ||||||
|  |       const toAdd = this.rbfCacheQueue; | ||||||
|  |       this.rbfCacheQueue = []; | ||||||
|  |       for (const { type, txid, value } of toAdd) { | ||||||
|  |         await this.$setRbfEntry(type, txid, value); | ||||||
|  |       } | ||||||
|  |       logger.debug(`Saved ${toAdd.length} queued RBF entries to the Redis cache`); | ||||||
|  |       const toRemove = this.rbfRemoveQueue; | ||||||
|  |       this.rbfRemoveQueue = []; | ||||||
|  |       for (const { type, txid } of toRemove) { | ||||||
|  |         await this.$removeRbfEntry(type, txid); | ||||||
|  |       } | ||||||
|  |       logger.debug(`Removed ${toRemove.length} queued RBF entries from the Redis cache`); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.warn(`Failed to flush RBF cache event queues after reconnecting to Redis: ${e instanceof Error ? e.message : e}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async $getBlocks(): Promise<BlockExtended[]> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       const json = await this.client.get('blocks'); |       const json = await this.client.get('blocks'); | ||||||
|       return JSON.parse(json); |       return JSON.parse(json); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @ -163,8 +280,14 @@ class RedisCache { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $getBlockSummaries(): Promise<BlockSummary[]> { |   async $getBlockSummaries(): Promise<BlockSummary[]> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       const json = await this.client.get('block-summaries'); |       const json = await this.client.get('block-summaries'); | ||||||
|       return JSON.parse(json); |       return JSON.parse(json); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @ -174,10 +297,16 @@ class RedisCache { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> { |   async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return {}; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       logger.warn(`Failed to retrieve mempool from Redis cache: Redis is not connected`); | ||||||
|  |       return {}; | ||||||
|  |     } | ||||||
|     const start = Date.now(); |     const start = Date.now(); | ||||||
|     const mempool = {}; |     const mempool = {}; | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*'); |       const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*'); | ||||||
|       for (const tx of mempoolList) { |       for (const tx of mempoolList) { | ||||||
|         mempool[tx.key] = tx.value; |         mempool[tx.key] = tx.value; | ||||||
| @ -191,8 +320,14 @@ class RedisCache { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $getRbfEntries(type: string): Promise<any[]> { |   async $getRbfEntries(type: string): Promise<any[]> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |     if (!this.connected) { | ||||||
|  |       logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: Redis is not connected`); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await this.$ensureConnected(); |  | ||||||
|       const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`); |       const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`); | ||||||
|       return rbfEntries; |       return rbfEntries; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @ -201,7 +336,10 @@ class RedisCache { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $loadCache() { |   async $loadCache(): Promise<void> { | ||||||
|  |     if (!config.REDIS.ENABLED) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     logger.info('Restoring mempool and blocks data from Redis cache'); |     logger.info('Restoring mempool and blocks data from Redis cache'); | ||||||
|     // Load block data
 |     // Load block data
 | ||||||
|     const loadedBlocks = await this.$getBlocks(); |     const loadedBlocks = await this.$getBlocks(); | ||||||
| @ -226,7 +364,7 @@ class RedisCache { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) { |   private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }): void { | ||||||
|     for (const tx of Object.values(mempool)) { |     for (const tx of Object.values(mempool)) { | ||||||
|       for (const vin of tx.vin) { |       for (const vin of tx.vin) { | ||||||
|         if (vin.scriptsig) { |         if (vin.scriptsig) { | ||||||
|  | |||||||
| @ -185,7 +185,8 @@ class Indexer { | |||||||
|       await blocks.$generateCPFPDatabase(); |       await blocks.$generateCPFPDatabase(); | ||||||
|       await blocks.$generateAuditStats(); |       await blocks.$generateAuditStats(); | ||||||
|       await auditReplicator.$sync(); |       await auditReplicator.$sync(); | ||||||
|       await blocks.$classifyBlocks(); |       // do not wait for classify blocks to finish
 | ||||||
|  |       blocks.$classifyBlocks(); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       this.indexerRunning = false; |       this.indexerRunning = false; | ||||||
|       logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e)); |       logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  | |||||||
| @ -416,7 +416,7 @@ | |||||||
|       This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br> |       This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br> | ||||||
|     </p> |     </p> | ||||||
|     <p> |     <p> | ||||||
|       This program incorporates software and other components licensed from third parties. See the full list of <a href="https://mempool.space/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects. |       This program incorporates software and other components licensed from third parties. See the full list of <a href="/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects. | ||||||
|     </p> |     </p> | ||||||
|     <div class="title"> |     <div class="title"> | ||||||
|       Trademark Notice<br> |       Trademark Notice<br> | ||||||
| @ -429,10 +429,6 @@ | |||||||
|     </p> |     </p> | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|   <div class="footer-links"> |  | ||||||
|     <a href="/3rdpartylicenses.txt">Third-party Licenses</a> |  | ||||||
|   </div> |  | ||||||
| 
 |  | ||||||
|   <br> |   <br> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -129,7 +129,7 @@ | |||||||
|               </tr> |               </tr> | ||||||
|               <tr class="info"> |               <tr class="info"> | ||||||
|                 <td class="info"> |                 <td class="info"> | ||||||
|                   <i><small>mempool.space fee</small></i> |                   <i><small>Accelerator Service Fee</small></i> | ||||||
|                 </td> |                 </td> | ||||||
|                 <td class="amt"> |                 <td class="amt"> | ||||||
|                   +{{ estimate.mempoolBaseFee | number }} |                   +{{ estimate.mempoolBaseFee | number }} | ||||||
| @ -141,7 +141,7 @@ | |||||||
|               </tr> |               </tr> | ||||||
|               <tr class="info group-last"> |               <tr class="info group-last"> | ||||||
|                 <td class="info"> |                 <td class="info"> | ||||||
|                   <i><small>Transaction vsize fee</small></i> |                   <i><small>Transaction Size Surcharge</small></i> | ||||||
|                 </td> |                 </td> | ||||||
|                 <td class="amt"> |                 <td class="amt"> | ||||||
|                   +{{ estimate.vsizeFee | number }} |                   +{{ estimate.vsizeFee | number }} | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ | |||||||
|   text-align: left; |   text-align: left; | ||||||
|   min-width: 320px; |   min-width: 320px; | ||||||
|   pointer-events: none; |   pointer-events: none; | ||||||
|  |   z-index: 11; | ||||||
| 
 | 
 | ||||||
|   &.clickable { |   &.clickable { | ||||||
|     pointer-events: all; |     pointer-events: all; | ||||||
|  | |||||||
| @ -46,15 +46,13 @@ | |||||||
|         <div class="item" *ngIf="showHalving"> |         <div class="item" *ngIf="showHalving"> | ||||||
|           <h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5> |           <h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5> | ||||||
|           <div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom"> |           <div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom"> | ||||||
|             <ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container> |             <span>{{ timeUntilHalving | date }}</span> | ||||||
|             <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> |  | ||||||
|             <ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template> |  | ||||||
|             <div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime"> |             <div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime"> | ||||||
|               <app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time> |               <app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time> | ||||||
|             </div> |             </div> | ||||||
|             <ng-template #approxTime> |             <ng-template #approxTime> | ||||||
|               <div class="symbol"> |               <div class="symbol"> | ||||||
|                 <span>{{ timeUntilHalving | date }}</span> |                 <app-time kind="until" [time]="timeUntilHalving" [fastRender]="false" [fixedRender]="true" [precision]="0" [numUnits]="2" [units]="['year', 'day', 'hour', 'minute']"></app-time> | ||||||
|               </div> |               </div> | ||||||
|             </ng-template> |             </ng-template> | ||||||
|           </div> |           </div> | ||||||
|  | |||||||
| @ -163,7 +163,7 @@ export class PoolRankingComponent implements OnInit { | |||||||
|             const i = pool.blockCount.toString(); |             const i = pool.blockCount.toString(); | ||||||
|             if (this.miningWindowPreference === '24h') { |             if (this.miningWindowPreference === '24h') { | ||||||
|               return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` + |               return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` + | ||||||
|                 pool.lastEstimatedHashrate.toString() + ' PH/s' + |                 pool.lastEstimatedHashrate.toString() + ' ' + miningStats.miningUnits.hashrateUnit + | ||||||
|                 `<br>` + $localize`${ i }:INTERPOLATION: blocks`; |                 `<br>` + $localize`${ i }:INTERPOLATION: blocks`; | ||||||
|             } else { |             } else { | ||||||
|               return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` + |               return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` + | ||||||
| @ -201,7 +201,7 @@ export class PoolRankingComponent implements OnInit { | |||||||
|           const i = totalBlockOther.toString(); |           const i = totalBlockOther.toString(); | ||||||
|           if (this.miningWindowPreference === '24h') { |           if (this.miningWindowPreference === '24h') { | ||||||
|             return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + |             return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + | ||||||
|               totalEstimatedHashrateOther.toString() + ' PH/s' + |               totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit + | ||||||
|               `<br>` + $localize`${ i }:INTERPOLATION: blocks`; |               `<br>` + $localize`${ i }:INTERPOLATION: blocks`; | ||||||
|           } else { |           } else { | ||||||
|             return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + |             return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi | |||||||
| import { Price, PriceService } from '../../services/price.service'; | import { Price, PriceService } from '../../services/price.service'; | ||||||
| import { isFeatureActive } from '../../bitcoin.utils'; | import { isFeatureActive } from '../../bitcoin.utils'; | ||||||
| import { ServicesApiServices } from '../../services/services-api.service'; | import { ServicesApiServices } from '../../services/services-api.service'; | ||||||
|  | import { EnterpriseService } from '../../services/enterprise.service'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-transaction', |   selector: 'app-transaction', | ||||||
| @ -116,12 +117,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     private servicesApiService: ServicesApiServices, |     private servicesApiService: ServicesApiServices, | ||||||
|     private seoService: SeoService, |     private seoService: SeoService, | ||||||
|     private priceService: PriceService, |     private priceService: PriceService, | ||||||
|     private storageService: StorageService |     private storageService: StorageService, | ||||||
|  |     private enterpriseService: EnterpriseService, | ||||||
|   ) {} |   ) {} | ||||||
| 
 | 
 | ||||||
|   ngOnInit() { |   ngOnInit() { | ||||||
|     this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; |     this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||||
| 
 | 
 | ||||||
|  |     this.enterpriseService.page(); | ||||||
|  | 
 | ||||||
|     this.websocketService.want(['blocks', 'mempool-blocks']); |     this.websocketService.want(['blocks', 'mempool-blocks']); | ||||||
|     this.stateService.networkChanged$.subscribe( |     this.stateService.networkChanged$.subscribe( | ||||||
|       (network) => { |       (network) => { | ||||||
| @ -527,6 +531,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     if (!this.txId) { |     if (!this.txId) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     this.enterpriseService.goal(8); | ||||||
|     this.showAccelerationSummary = true && this.acceleratorAvailable; |     this.showAccelerationSummary = true && this.acceleratorAvailable; | ||||||
|     this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; |     this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; | ||||||
|     return false; |     return false; | ||||||
|  | |||||||
| @ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges { | |||||||
|             node.public_key, |             node.public_key, | ||||||
|             node.alias, |             node.alias, | ||||||
|             node.capacity, |             node.capacity, | ||||||
|             node.active_channel_count, |             node.channels, | ||||||
|             node.country, |             node.country, | ||||||
|             node.iso_code, |             node.iso_code, | ||||||
|           ]); |           ]); | ||||||
|  | |||||||
| @ -64,8 +64,8 @@ | |||||||
|         <th class="channels text-right" i18n="lightning.channels">Channels</th> |         <th class="channels text-right" i18n="lightning.channels">Channels</th> | ||||||
|         <th class="city text-right" i18n="lightning.location">Location</th> |         <th class="city text-right" i18n="lightning.location">Location</th> | ||||||
|       </thead> |       </thead> | ||||||
|       <tbody *ngIf="nodes$ | async as countryNodes; else skeleton"> |       <tbody *ngIf="nodesPagination$ | async as countryNodes; else skeleton"> | ||||||
|         <tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey"> |         <tr *ngFor="let node of countryNodes; let i= index; trackBy: trackByPublicKey"> | ||||||
|           <td class="alias text-left text-truncate"> |           <td class="alias text-left text-truncate"> | ||||||
|             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> |             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> | ||||||
|           </td> |           </td> | ||||||
| @ -116,5 +116,10 @@ | |||||||
|       </ng-template> |       </ng-template> | ||||||
| 
 | 
 | ||||||
|     </table> |     </table> | ||||||
|  | 
 | ||||||
|  |     <ngb-pagination *ngIf="nodes$ | async as countryNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''" | ||||||
|  |         [collectionSize]="countryNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page" | ||||||
|  |         (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false"> | ||||||
|  |       </ngb-pagination> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -22,14 +22,14 @@ | |||||||
| 
 | 
 | ||||||
| .timestamp-first { | .timestamp-first { | ||||||
|   width: 20%; |   width: 20%; | ||||||
|   @media (max-width: 576px) { |   @media (max-width: 1060px) { | ||||||
|     display: none |     display: none | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .timestamp-update { | .timestamp-update { | ||||||
|   width: 16%; |   width: 16%; | ||||||
|   @media (max-width: 576px) { |   @media (max-width: 1060px) { | ||||||
|     display: none |     display: none | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -50,7 +50,7 @@ | |||||||
| 
 | 
 | ||||||
| .city { | .city { | ||||||
|   max-width: 150px; |   max-width: 150px; | ||||||
|   @media (max-width: 576px) { |   @media (max-width: 675px) { | ||||||
|     display: none |     display: none | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -1,6 +1,6 @@ | |||||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||||
| import { ActivatedRoute } from '@angular/router'; | import { ActivatedRoute } from '@angular/router'; | ||||||
| import { map, Observable, share } from 'rxjs'; | import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs'; | ||||||
| import { ApiService } from '../../services/api.service'; | import { ApiService } from '../../services/api.service'; | ||||||
| import { SeoService } from '../../services/seo.service'; | import { SeoService } from '../../services/seo.service'; | ||||||
| import { getFlagEmoji } from '../../shared/common.utils'; | import { getFlagEmoji } from '../../shared/common.utils'; | ||||||
| @ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation | |||||||
| export class NodesPerCountry implements OnInit { | export class NodesPerCountry implements OnInit { | ||||||
|   nodes$: Observable<any>; |   nodes$: Observable<any>; | ||||||
|   country: {name: string, flag: string}; |   country: {name: string, flag: string}; | ||||||
|  |   nodesPagination$: Observable<any>; | ||||||
|  |   startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0); | ||||||
|  |   page = 1; | ||||||
|  |   pageSize = 15; | ||||||
|  |   maxSize = window.innerWidth <= 767.98 ? 3 : 5; | ||||||
|  |   isLoading = true; | ||||||
| 
 | 
 | ||||||
|   skeletonLines: number[] = []; |   skeletonLines: number[] = []; | ||||||
| 
 | 
 | ||||||
| @ -23,7 +29,7 @@ export class NodesPerCountry implements OnInit { | |||||||
|     private seoService: SeoService, |     private seoService: SeoService, | ||||||
|     private route: ActivatedRoute, |     private route: ActivatedRoute, | ||||||
|   ) { |   ) { | ||||||
|     for (let i = 0; i < 20; ++i) { |     for (let i = 0; i < this.pageSize; ++i) { | ||||||
|       this.skeletonLines.push(i); |       this.skeletonLines.push(i); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -31,6 +37,7 @@ export class NodesPerCountry implements OnInit { | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country) |     this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country) | ||||||
|       .pipe( |       .pipe( | ||||||
|  |         tap(() => this.isLoading = true), | ||||||
|         map(response => { |         map(response => { | ||||||
|           this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`); |           this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`); | ||||||
|           this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`); |           this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`); | ||||||
| @ -87,11 +94,21 @@ export class NodesPerCountry implements OnInit { | |||||||
|             ispCount: Object.keys(isps).length |             ispCount: Object.keys(isps).length | ||||||
|           }; |           }; | ||||||
|         }), |         }), | ||||||
|  |         tap(() => this.isLoading = false), | ||||||
|         share() |         share() | ||||||
|       ); |       ); | ||||||
|  | 
 | ||||||
|  |     this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe( | ||||||
|  |       map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize)) | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   trackByPublicKey(index: number, node: any): string { |   trackByPublicKey(index: number, node: any): string { | ||||||
|     return node.public_key; |     return node.public_key; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   pageChange(page: number): void { | ||||||
|  |     this.startingIndexSubject.next((page - 1) * this.pageSize); | ||||||
|  |     this.page = page; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -61,8 +61,8 @@ | |||||||
|         <th class="channels text-right" i18n="lightning.channels">Channels</th> |         <th class="channels text-right" i18n="lightning.channels">Channels</th> | ||||||
|         <th class="city text-right" i18n="lightning.location">Location</th> |         <th class="city text-right" i18n="lightning.location">Location</th> | ||||||
|       </thead> |       </thead> | ||||||
|       <tbody *ngIf="nodes$ | async as ispNodes; else skeleton"> |       <tbody *ngIf="nodesPagination$ | async as ispNodes; else skeleton"> | ||||||
|         <tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey"> |         <tr *ngFor="let node of ispNodes; let i= index; trackBy: trackByPublicKey"> | ||||||
|           <td class="alias text-left text-truncate"> |           <td class="alias text-left text-truncate"> | ||||||
|             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> |             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> | ||||||
|           </td> |           </td> | ||||||
| @ -113,5 +113,10 @@ | |||||||
|       </ng-template> |       </ng-template> | ||||||
|    |    | ||||||
|     </table> |     </table> | ||||||
|  | 
 | ||||||
|  |     <ngb-pagination *ngIf="nodes$ | async as ispNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''" | ||||||
|  |         [collectionSize]="ispNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page" | ||||||
|  |         (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false"> | ||||||
|  |       </ngb-pagination> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ | |||||||
| .timestamp-first { | .timestamp-first { | ||||||
|   width: 20%; |   width: 20%; | ||||||
| 
 | 
 | ||||||
|   @media (max-width: 576px) { |   @media (max-width: 1060px) { | ||||||
|     display: none |     display: none | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -32,7 +32,7 @@ | |||||||
| .timestamp-update { | .timestamp-update { | ||||||
|   width: 16%; |   width: 16%; | ||||||
| 
 | 
 | ||||||
|   @media (max-width: 576px) { |   @media (max-width: 1060px) { | ||||||
|     display: none |     display: none | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -56,7 +56,7 @@ | |||||||
| .city { | .city { | ||||||
|   max-width: 150px; |   max-width: 150px; | ||||||
| 
 | 
 | ||||||
|   @media (max-width: 576px) { |   @media (max-width: 675px) { | ||||||
|     display: none |     display: none | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||||
| import { ActivatedRoute } from '@angular/router'; | import { ActivatedRoute } from '@angular/router'; | ||||||
| import { map, Observable, share } from 'rxjs'; | import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs'; | ||||||
| import { ApiService } from '../../services/api.service'; | import { ApiService } from '../../services/api.service'; | ||||||
| import { SeoService } from '../../services/seo.service'; | import { SeoService } from '../../services/seo.service'; | ||||||
| import { getFlagEmoji } from '../../shared/common.utils'; | import { getFlagEmoji } from '../../shared/common.utils'; | ||||||
| @ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation | |||||||
| export class NodesPerISP implements OnInit { | export class NodesPerISP implements OnInit { | ||||||
|   nodes$: Observable<any>; |   nodes$: Observable<any>; | ||||||
|   isp: {name: string, id: number}; |   isp: {name: string, id: number}; | ||||||
|  |   nodesPagination$: Observable<any>; | ||||||
|  |   startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0); | ||||||
|  |   page = 1; | ||||||
|  |   pageSize = 15; | ||||||
|  |   maxSize = window.innerWidth <= 767.98 ? 3 : 5; | ||||||
|  |   isLoading = true; | ||||||
| 
 | 
 | ||||||
|   skeletonLines: number[] = []; |   skeletonLines: number[] = []; | ||||||
| 
 | 
 | ||||||
| @ -23,7 +29,7 @@ export class NodesPerISP implements OnInit { | |||||||
|     private seoService: SeoService, |     private seoService: SeoService, | ||||||
|     private route: ActivatedRoute, |     private route: ActivatedRoute, | ||||||
|   ) { |   ) { | ||||||
|     for (let i = 0; i < 20; ++i) { |     for (let i = 0; i < this.pageSize; ++i) { | ||||||
|       this.skeletonLines.push(i); |       this.skeletonLines.push(i); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -31,6 +37,7 @@ export class NodesPerISP implements OnInit { | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp) |     this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp) | ||||||
|       .pipe( |       .pipe( | ||||||
|  |         tap(() => this.isLoading = true), | ||||||
|         map(response => { |         map(response => { | ||||||
|           this.isp = { |           this.isp = { | ||||||
|             name: response.isp, |             name: response.isp, | ||||||
| @ -77,11 +84,21 @@ export class NodesPerISP implements OnInit { | |||||||
|             topCountry: topCountry, |             topCountry: topCountry, | ||||||
|           }; |           }; | ||||||
|         }), |         }), | ||||||
|  |         tap(() => this.isLoading = false), | ||||||
|         share() |         share() | ||||||
|       ); |       ); | ||||||
|  | 
 | ||||||
|  |     this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe( | ||||||
|  |       map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize)) | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   trackByPublicKey(index: number, node: any): string { |   trackByPublicKey(index: number, node: any): string { | ||||||
|     return node.public_key; |     return node.public_key; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   pageChange(page: number): void { | ||||||
|  |     this.startingIndexSubject.next((page - 1) * this.pageSize); | ||||||
|  |     this.page = page; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -139,6 +139,14 @@ export class EnterpriseService { | |||||||
|     this.getMatomo()?.trackGoal(id); |     this.getMatomo()?.trackGoal(id); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   page() { | ||||||
|  |     const matomo = this.getMatomo(); | ||||||
|  |     if (matomo) { | ||||||
|  |       matomo.setCustomUrl(this.getCustomUrl()); | ||||||
|  |       matomo.trackPageView(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private getCustomUrl(): string { |   private getCustomUrl(): string { | ||||||
|     let url = window.location.origin + '/'; |     let url = window.location.origin + '/'; | ||||||
|     let route = this.activatedRoute; |     let route = this.activatedRoute; | ||||||
|  | |||||||
| @ -77,6 +77,7 @@ | |||||||
|           <p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p> |           <p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p> | ||||||
|           <p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p> |           <p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p> | ||||||
|           <p><a [routerLink]="['/trademark-policy']" i18n="shared.trademark-policy|Trademark Policy">Trademark Policy</a></p> |           <p><a [routerLink]="['/trademark-policy']" i18n="shared.trademark-policy|Trademark Policy">Trademark Policy</a></p> | ||||||
|  |           <p><a [routerLink]="['/3rdpartylicenses.txt']" i18n="shared.trademark-policy|Third-party Licenses">Third-party Licenses</a></p> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="row social-links"> |     <div class="row social-links"> | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ discover=1 | |||||||
| par=16 | par=16 | ||||||
| dbcache=8192 | dbcache=8192 | ||||||
| maxmempool=4096 | maxmempool=4096 | ||||||
| mempoolexpiry=999999 |  | ||||||
| mempoolfullrbf=1 | mempoolfullrbf=1 | ||||||
| maxconnections=100 | maxconnections=100 | ||||||
| onion=127.0.0.1:9050 | onion=127.0.0.1:9050 | ||||||
| @ -20,6 +19,7 @@ whitelist=2401:b140::/32 | |||||||
| #uacomment=@wiz | #uacomment=@wiz | ||||||
| 
 | 
 | ||||||
| [main] | [main] | ||||||
|  | mempoolexpiry=999999 | ||||||
| rpcbind=127.0.0.1:8332 | rpcbind=127.0.0.1:8332 | ||||||
| rpcbind=[::1]:8332 | rpcbind=[::1]:8332 | ||||||
| bind=0.0.0.0:8333 | bind=0.0.0.0:8333 | ||||||
|  | |||||||
| @ -20,5 +20,10 @@ for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do | |||||||
|     kill $pid |     kill $pid | ||||||
| done | done | ||||||
| 
 | 
 | ||||||
|  | # kill nginx cache heater scripts | ||||||
|  | for pid in `ps uaxww|grep heater|grep zsh|awk '{print $2}'`;do | ||||||
|  |     kill $pid | ||||||
|  | done | ||||||
|  | 
 | ||||||
| # always exit successfully despite above errors | # always exit successfully despite above errors | ||||||
| exit 0 | exit 0 | ||||||
|  | |||||||
| @ -251,7 +251,8 @@ class Server { | |||||||
| 
 | 
 | ||||||
|       if (!img) { |       if (!img) { | ||||||
|         // send local fallback image file
 |         // send local fallback image file
 | ||||||
|         res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackFile)); |         res.set('Cache-control', 'no-cache'); | ||||||
|  |         res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackImg)); | ||||||
|       } else { |       } else { | ||||||
|         res.contentType('image/png'); |         res.contentType('image/png'); | ||||||
|         res.send(img); |         res.send(img); | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								unfurler/src/resources
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								unfurler/src/resources
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | |||||||
|  | ../../frontend/src/resources | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 94 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 289 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 1.8 MiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 96 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 289 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 607 KiB | 
| @ -2,7 +2,6 @@ interface Match { | |||||||
|   render: boolean; |   render: boolean; | ||||||
|   title: string; |   title: string; | ||||||
|   fallbackImg: string; |   fallbackImg: string; | ||||||
|   fallbackFile: string; |  | ||||||
|   staticImg?: string; |   staticImg?: string; | ||||||
|   networkMode: string; |   networkMode: string; | ||||||
| } | } | ||||||
| @ -32,7 +31,6 @@ const routes = { | |||||||
|   lightning: { |   lightning: { | ||||||
|     title: "Lightning", |     title: "Lightning", | ||||||
|     fallbackImg: '/resources/previews/lightning.png', |     fallbackImg: '/resources/previews/lightning.png', | ||||||
|     fallbackFile: '/resources/img/lightning.png', |  | ||||||
|     routes: { |     routes: { | ||||||
|       node: { |       node: { | ||||||
|         render: true, |         render: true, | ||||||
| @ -71,7 +69,6 @@ const routes = { | |||||||
|   mining: { |   mining: { | ||||||
|     title: "Mining", |     title: "Mining", | ||||||
|     fallbackImg: '/resources/previews/mining.png', |     fallbackImg: '/resources/previews/mining.png', | ||||||
|     fallbackFile: '/resources/img/mining.png', |  | ||||||
|     routes: { |     routes: { | ||||||
|       pool: { |       pool: { | ||||||
|         render: true, |         render: true, | ||||||
| @ -87,14 +84,12 @@ const routes = { | |||||||
| const networks = { | const networks = { | ||||||
|   bitcoin: { |   bitcoin: { | ||||||
|     fallbackImg: '/resources/previews/dashboard.png', |     fallbackImg: '/resources/previews/dashboard.png', | ||||||
|     fallbackFile: '/resources/img/dashboard.png', |  | ||||||
|     routes: { |     routes: { | ||||||
|       ...routes // all routes supported
 |       ...routes // all routes supported
 | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   liquid: { |   liquid: { | ||||||
|     fallbackImg: '/resources/liquid/liquid-network-preview.png', |     fallbackImg: '/resources/liquid/liquid-network-preview.png', | ||||||
|     fallbackFile: '/resources/img/liquid', |  | ||||||
|     routes: { // only block, address & tx routes supported
 |     routes: { // only block, address & tx routes supported
 | ||||||
|       block: routes.block, |       block: routes.block, | ||||||
|       address: routes.address, |       address: routes.address, | ||||||
| @ -103,7 +98,6 @@ const networks = { | |||||||
|   }, |   }, | ||||||
|   bisq: { |   bisq: { | ||||||
|     fallbackImg: '/resources/bisq/bisq-markets-preview.png', |     fallbackImg: '/resources/bisq/bisq-markets-preview.png', | ||||||
|     fallbackFile: '/resources/img/bisq.png', |  | ||||||
|     routes: {} // no routes supported
 |     routes: {} // no routes supported
 | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| @ -113,7 +107,6 @@ export function matchRoute(network: string, path: string): Match { | |||||||
|     render: false, |     render: false, | ||||||
|     title: '', |     title: '', | ||||||
|     fallbackImg: '', |     fallbackImg: '', | ||||||
|     fallbackFile: '', |  | ||||||
|     networkMode: 'mainnet' |     networkMode: 'mainnet' | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -128,7 +121,6 @@ export function matchRoute(network: string, path: string): Match { | |||||||
| 
 | 
 | ||||||
|   let route = networks[network] || networks.bitcoin; |   let route = networks[network] || networks.bitcoin; | ||||||
|   match.fallbackImg = route.fallbackImg; |   match.fallbackImg = route.fallbackImg; | ||||||
|   match.fallbackFile = route.fallbackFile; |  | ||||||
| 
 | 
 | ||||||
|   // traverse the route tree until we run out of route or tree, or hit a renderable match
 |   // traverse the route tree until we run out of route or tree, or hit a renderable match
 | ||||||
|   while (!route.render && route.routes && parts.length && route.routes[parts[0]]) { |   while (!route.render && route.routes && parts.length && route.routes[parts[0]]) { | ||||||
| @ -136,7 +128,6 @@ export function matchRoute(network: string, path: string): Match { | |||||||
|     parts.shift(); |     parts.shift(); | ||||||
|     if (route.fallbackImg) { |     if (route.fallbackImg) { | ||||||
|       match.fallbackImg = route.fallbackImg; |       match.fallbackImg = route.fallbackImg; | ||||||
|       match.fallbackFile = route.fallbackFile; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user