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/')" | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node: ["18", "20"] | ||||
|         node: ["20", "21"] | ||||
|         flavor: ["dev", "prod"] | ||||
|       fail-fast: false | ||||
|     runs-on: "ubuntu-latest" | ||||
| @ -160,7 +160,7 @@ jobs: | ||||
|     if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node: ["18", "20"] | ||||
|         node: ["20", "21"] | ||||
|         flavor: ["dev", "prod"] | ||||
|       fail-fast: false | ||||
|     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" \ | ||||
|           --platform linux/amd64,linux/arm64 \ | ||||
|           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ | ||||
|           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ | ||||
|           --output "type=registry" ./${{ matrix.service }}/ \ | ||||
|           --build-arg commitHash=$SHORT_SHA | ||||
|  | ||||
| @ -40,6 +40,7 @@ class Blocks { | ||||
|   private quarterEpochBlockTime: number | null = null; | ||||
|   private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; | ||||
|   private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = []; | ||||
|   private classifyingBlocks: boolean = false; | ||||
| 
 | ||||
|   private mainLoopTimeout: number = 120000; | ||||
| 
 | ||||
| @ -568,6 +569,11 @@ class Blocks { | ||||
|    * [INDEXING] Index transaction classification flags for Goggles | ||||
|    */ | ||||
|   public async $classifyBlocks(): Promise<void> { | ||||
|     if (this.classifyingBlocks) { | ||||
|       return; | ||||
|     } | ||||
|     this.classifyingBlocks = true; | ||||
| 
 | ||||
|     // classification requires an esplora backend
 | ||||
|     if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') { | ||||
|       return; | ||||
| @ -679,6 +685,8 @@ class Blocks { | ||||
|         indexedThisRun = 0; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     this.classifyingBlocks = false; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -19,45 +19,90 @@ class RedisCache { | ||||
|   private client; | ||||
|   private connected = false; | ||||
|   private schemaVersion = 1; | ||||
|   private redisConfig: any; | ||||
| 
 | ||||
|   private pauseFlush: boolean = false; | ||||
|   private cacheQueue: MempoolTransactionExtended[] = []; | ||||
|   private removeQueue: string[] = []; | ||||
|   private rbfCacheQueue: { type: string, txid: string, value: any }[] = []; | ||||
|   private rbfRemoveQueue: { type: string, txid: string }[] = []; | ||||
|   private txFlushLimit: number = 10000; | ||||
| 
 | ||||
|   constructor() { | ||||
|     if (config.REDIS.ENABLED) { | ||||
|       const redisConfig = { | ||||
|       this.redisConfig = { | ||||
|         socket: { | ||||
|           path: config.REDIS.UNIX_SOCKET_PATH | ||||
|         }, | ||||
|         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(); | ||||
|       setInterval(() => { this.$ensureConnected(); }, 10000); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $ensureConnected(): Promise<void> { | ||||
|   private async $ensureConnected(): Promise<boolean> { | ||||
|     if (!this.connected && config.REDIS.ENABLED) { | ||||
|       return this.client.connect().then(async () => { | ||||
|         this.connected = true; | ||||
|         logger.info(`Redis client connected`); | ||||
|         const version = await this.client.get('schema_version'); | ||||
|         if (version !== this.schemaVersion) { | ||||
|           // schema changed
 | ||||
|           // perform migrations or flush DB if necessary
 | ||||
|           logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`); | ||||
|           await this.client.set('schema_version', this.schemaVersion); | ||||
|         } | ||||
|       }); | ||||
|       try { | ||||
|         this.client = createClient(this.redisConfig); | ||||
|         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'); | ||||
|             this.connected = true; | ||||
|             if (version !== this.schemaVersion) { | ||||
|               // schema changed
 | ||||
|               // perform migrations or flush DB if necessary
 | ||||
|               logger.info(`Redis schema version changed from ${version} to ${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 { | ||||
|       await this.$ensureConnected(); | ||||
|       await this.client.set('blocks', JSON.stringify(blocks)); | ||||
|       logger.debug(`Saved latest blocks to Redis cache`); | ||||
|     } 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 { | ||||
|       await this.$ensureConnected(); | ||||
|       await this.client.set('block-summaries', JSON.stringify(summaries)); | ||||
|       logger.debug(`Saved latest block summaries to Redis cache`); | ||||
|     } 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); | ||||
|     if (this.cacheQueue.length >= this.txFlushLimit) { | ||||
|       await this.$flushTransactions(); | ||||
|       if (!this.pauseFlush) { | ||||
|         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`); | ||||
|   async $flushTransactions(): Promise<void> { | ||||
|     if (!config.REDIS.ENABLED) { | ||||
|       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; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> { | ||||
|     if (!newTransactions.length) { | ||||
|       return true; | ||||
|     } | ||||
|     this.pauseFlush = false; | ||||
| 
 | ||||
|     const toAdd = this.cacheQueue.slice(0, this.txFlushLimit); | ||||
|     try { | ||||
|       await this.$ensureConnected(); | ||||
|       const msetData = newTransactions.map(tx => { | ||||
|       const msetData = toAdd.map(tx => { | ||||
|         const minified: any = { ...tx }; | ||||
|         delete minified.hex; | ||||
|         for (const vin of minified.vin) { | ||||
| @ -112,30 +168,53 @@ class RedisCache { | ||||
|         return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)]; | ||||
|       }); | ||||
|       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) { | ||||
|       logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`); | ||||
|       return false; | ||||
|       logger.warn(`Failed to add ${toAdd.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`); | ||||
|       this.pauseFlush = true; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async $removeTransactions(transactions: string[]) { | ||||
|     try { | ||||
|       await this.$ensureConnected(); | ||||
|   async $removeTransactions(transactions: string[]): Promise<void> { | ||||
|     if (!config.REDIS.ENABLED) { | ||||
|       return; | ||||
|     } | ||||
|     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(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`); | ||||
|       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) { | ||||
|           logger.warn(`Failed to remove ${slice.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`); | ||||
|           failed = failed.concat(slice); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`); | ||||
|       // 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> { | ||||
|     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 { | ||||
|       await this.$ensureConnected(); | ||||
|       await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value)); | ||||
|     } catch (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> { | ||||
|     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 { | ||||
|       await this.$ensureConnected(); | ||||
|       await this.client.unlink(`rbf:${type}:${txid}`); | ||||
|     } catch (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 { | ||||
|       await this.$ensureConnected(); | ||||
|       const json = await this.client.get('blocks'); | ||||
|       return JSON.parse(json); | ||||
|     } catch (e) { | ||||
| @ -163,8 +280,14 @@ class RedisCache { | ||||
|   } | ||||
| 
 | ||||
|   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 { | ||||
|       await this.$ensureConnected(); | ||||
|       const json = await this.client.get('block-summaries'); | ||||
|       return JSON.parse(json); | ||||
|     } catch (e) { | ||||
| @ -174,10 +297,16 @@ class RedisCache { | ||||
|   } | ||||
| 
 | ||||
|   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 mempool = {}; | ||||
|     try { | ||||
|       await this.$ensureConnected(); | ||||
|       const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*'); | ||||
|       for (const tx of mempoolList) { | ||||
|         mempool[tx.key] = tx.value; | ||||
| @ -191,8 +320,14 @@ class RedisCache { | ||||
|   } | ||||
| 
 | ||||
|   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 { | ||||
|       await this.$ensureConnected(); | ||||
|       const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`); | ||||
|       return rbfEntries; | ||||
|     } 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'); | ||||
|     // Load block data
 | ||||
|     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 vin of tx.vin) { | ||||
|         if (vin.scriptsig) { | ||||
|  | ||||
| @ -185,7 +185,8 @@ class Indexer { | ||||
|       await blocks.$generateCPFPDatabase(); | ||||
|       await blocks.$generateAuditStats(); | ||||
|       await auditReplicator.$sync(); | ||||
|       await blocks.$classifyBlocks(); | ||||
|       // do not wait for classify blocks to finish
 | ||||
|       blocks.$classifyBlocks(); | ||||
|     } catch (e) { | ||||
|       this.indexerRunning = false; | ||||
|       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> | ||||
|     </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> | ||||
|     <div class="title"> | ||||
|       Trademark Notice<br> | ||||
| @ -429,10 +429,6 @@ | ||||
|     </p> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="footer-links"> | ||||
|     <a href="/3rdpartylicenses.txt">Third-party Licenses</a> | ||||
|   </div> | ||||
| 
 | ||||
|   <br> | ||||
| </div> | ||||
| 
 | ||||
|  | ||||
| @ -129,7 +129,7 @@ | ||||
|               </tr> | ||||
|               <tr class="info"> | ||||
|                 <td class="info"> | ||||
|                   <i><small>mempool.space fee</small></i> | ||||
|                   <i><small>Accelerator Service Fee</small></i> | ||||
|                 </td> | ||||
|                 <td class="amt"> | ||||
|                   +{{ estimate.mempoolBaseFee | number }} | ||||
| @ -141,7 +141,7 @@ | ||||
|               </tr> | ||||
|               <tr class="info group-last"> | ||||
|                 <td class="info"> | ||||
|                   <i><small>Transaction vsize fee</small></i> | ||||
|                   <i><small>Transaction Size Surcharge</small></i> | ||||
|                 </td> | ||||
|                 <td class="amt"> | ||||
|                   +{{ estimate.vsizeFee | number }} | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
|   text-align: left; | ||||
|   min-width: 320px; | ||||
|   pointer-events: none; | ||||
|   z-index: 11; | ||||
| 
 | ||||
|   &.clickable { | ||||
|     pointer-events: all; | ||||
|  | ||||
| @ -46,15 +46,13 @@ | ||||
|         <div class="item" *ngIf="showHalving"> | ||||
|           <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"> | ||||
|             <ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container> | ||||
|             <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> | ||||
|             <span>{{ timeUntilHalving | date }}</span> | ||||
|             <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> | ||||
|             </div> | ||||
|             <ng-template #approxTime> | ||||
|               <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> | ||||
|             </ng-template> | ||||
|           </div> | ||||
|  | ||||
| @ -163,7 +163,7 @@ export class PoolRankingComponent implements OnInit { | ||||
|             const i = pool.blockCount.toString(); | ||||
|             if (this.miningWindowPreference === '24h') { | ||||
|               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`; | ||||
|             } else { | ||||
|               return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` + | ||||
| @ -201,7 +201,7 @@ export class PoolRankingComponent implements OnInit { | ||||
|           const i = totalBlockOther.toString(); | ||||
|           if (this.miningWindowPreference === '24h') { | ||||
|             return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + | ||||
|               totalEstimatedHashrateOther.toString() + ' PH/s' + | ||||
|               totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit + | ||||
|               `<br>` + $localize`${ i }:INTERPOLATION: blocks`; | ||||
|           } else { | ||||
|             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 { isFeatureActive } from '../../bitcoin.utils'; | ||||
| import { ServicesApiServices } from '../../services/services-api.service'; | ||||
| import { EnterpriseService } from '../../services/enterprise.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-transaction', | ||||
| @ -116,12 +117,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     private servicesApiService: ServicesApiServices, | ||||
|     private seoService: SeoService, | ||||
|     private priceService: PriceService, | ||||
|     private storageService: StorageService | ||||
|     private storageService: StorageService, | ||||
|     private enterpriseService: EnterpriseService, | ||||
|   ) {} | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     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.stateService.networkChanged$.subscribe( | ||||
|       (network) => { | ||||
| @ -527,6 +531,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     if (!this.txId) { | ||||
|       return; | ||||
|     } | ||||
|     this.enterpriseService.goal(8); | ||||
|     this.showAccelerationSummary = true && this.acceleratorAvailable; | ||||
|     this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; | ||||
|     return false; | ||||
|  | ||||
| @ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges { | ||||
|             node.public_key, | ||||
|             node.alias, | ||||
|             node.capacity, | ||||
|             node.active_channel_count, | ||||
|             node.channels, | ||||
|             node.country, | ||||
|             node.iso_code, | ||||
|           ]); | ||||
|  | ||||
| @ -64,8 +64,8 @@ | ||||
|         <th class="channels text-right" i18n="lightning.channels">Channels</th> | ||||
|         <th class="city text-right" i18n="lightning.location">Location</th> | ||||
|       </thead> | ||||
|       <tbody *ngIf="nodes$ | async as countryNodes; else skeleton"> | ||||
|         <tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey"> | ||||
|       <tbody *ngIf="nodesPagination$ | async as countryNodes; else skeleton"> | ||||
|         <tr *ngFor="let node of countryNodes; let i= index; trackBy: trackByPublicKey"> | ||||
|           <td class="alias text-left text-truncate"> | ||||
|             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> | ||||
|           </td> | ||||
| @ -116,5 +116,10 @@ | ||||
|       </ng-template> | ||||
| 
 | ||||
|     </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> | ||||
|  | ||||
| @ -22,14 +22,14 @@ | ||||
| 
 | ||||
| .timestamp-first { | ||||
|   width: 20%; | ||||
|   @media (max-width: 576px) { | ||||
|   @media (max-width: 1060px) { | ||||
|     display: none | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .timestamp-update { | ||||
|   width: 16%; | ||||
|   @media (max-width: 576px) { | ||||
|   @media (max-width: 1060px) { | ||||
|     display: none | ||||
|   } | ||||
| } | ||||
| @ -50,7 +50,7 @@ | ||||
| 
 | ||||
| .city { | ||||
|   max-width: 150px; | ||||
|   @media (max-width: 576px) { | ||||
|   @media (max-width: 675px) { | ||||
|     display: none | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| 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 { SeoService } from '../../services/seo.service'; | ||||
| import { getFlagEmoji } from '../../shared/common.utils'; | ||||
| @ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation | ||||
| export class NodesPerCountry implements OnInit { | ||||
|   nodes$: Observable<any>; | ||||
|   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[] = []; | ||||
| 
 | ||||
| @ -23,7 +29,7 @@ export class NodesPerCountry implements OnInit { | ||||
|     private seoService: SeoService, | ||||
|     private route: ActivatedRoute, | ||||
|   ) { | ||||
|     for (let i = 0; i < 20; ++i) { | ||||
|     for (let i = 0; i < this.pageSize; ++i) { | ||||
|       this.skeletonLines.push(i); | ||||
|     } | ||||
|   } | ||||
| @ -31,6 +37,7 @@ export class NodesPerCountry implements OnInit { | ||||
|   ngOnInit(): void { | ||||
|     this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country) | ||||
|       .pipe( | ||||
|         tap(() => this.isLoading = true), | ||||
|         map(response => { | ||||
|           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.`); | ||||
| @ -87,11 +94,21 @@ export class NodesPerCountry implements OnInit { | ||||
|             ispCount: Object.keys(isps).length | ||||
|           }; | ||||
|         }), | ||||
|         tap(() => this.isLoading = false), | ||||
|         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 { | ||||
|     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="city text-right" i18n="lightning.location">Location</th> | ||||
|       </thead> | ||||
|       <tbody *ngIf="nodes$ | async as ispNodes; else skeleton"> | ||||
|         <tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey"> | ||||
|       <tbody *ngIf="nodesPagination$ | async as ispNodes; else skeleton"> | ||||
|         <tr *ngFor="let node of ispNodes; let i= index; trackBy: trackByPublicKey"> | ||||
|           <td class="alias text-left text-truncate"> | ||||
|             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> | ||||
|           </td> | ||||
| @ -113,5 +113,10 @@ | ||||
|       </ng-template> | ||||
|    | ||||
|     </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> | ||||
|  | ||||
| @ -24,7 +24,7 @@ | ||||
| .timestamp-first { | ||||
|   width: 20%; | ||||
| 
 | ||||
|   @media (max-width: 576px) { | ||||
|   @media (max-width: 1060px) { | ||||
|     display: none | ||||
|   } | ||||
| } | ||||
| @ -32,7 +32,7 @@ | ||||
| .timestamp-update { | ||||
|   width: 16%; | ||||
| 
 | ||||
|   @media (max-width: 576px) { | ||||
|   @media (max-width: 1060px) { | ||||
|     display: none | ||||
|   } | ||||
| } | ||||
| @ -56,7 +56,7 @@ | ||||
| .city { | ||||
|   max-width: 150px; | ||||
| 
 | ||||
|   @media (max-width: 576px) { | ||||
|   @media (max-width: 675px) { | ||||
|     display: none | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||
| 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 { SeoService } from '../../services/seo.service'; | ||||
| import { getFlagEmoji } from '../../shared/common.utils'; | ||||
| @ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation | ||||
| export class NodesPerISP implements OnInit { | ||||
|   nodes$: Observable<any>; | ||||
|   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[] = []; | ||||
| 
 | ||||
| @ -23,7 +29,7 @@ export class NodesPerISP implements OnInit { | ||||
|     private seoService: SeoService, | ||||
|     private route: ActivatedRoute, | ||||
|   ) { | ||||
|     for (let i = 0; i < 20; ++i) { | ||||
|     for (let i = 0; i < this.pageSize; ++i) { | ||||
|       this.skeletonLines.push(i); | ||||
|     } | ||||
|   } | ||||
| @ -31,6 +37,7 @@ export class NodesPerISP implements OnInit { | ||||
|   ngOnInit(): void { | ||||
|     this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp) | ||||
|       .pipe( | ||||
|         tap(() => this.isLoading = true), | ||||
|         map(response => { | ||||
|           this.isp = { | ||||
|             name: response.isp, | ||||
| @ -77,11 +84,21 @@ export class NodesPerISP implements OnInit { | ||||
|             topCountry: topCountry, | ||||
|           }; | ||||
|         }), | ||||
|         tap(() => this.isLoading = false), | ||||
|         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 { | ||||
|     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); | ||||
|   } | ||||
| 
 | ||||
|   page() { | ||||
|     const matomo = this.getMatomo(); | ||||
|     if (matomo) { | ||||
|       matomo.setCustomUrl(this.getCustomUrl()); | ||||
|       matomo.trackPageView(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private getCustomUrl(): string { | ||||
|     let url = window.location.origin + '/'; | ||||
|     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]="['/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]="['/3rdpartylicenses.txt']" i18n="shared.trademark-policy|Third-party Licenses">Third-party Licenses</a></p> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="row social-links"> | ||||
|  | ||||
| @ -7,7 +7,6 @@ discover=1 | ||||
| par=16 | ||||
| dbcache=8192 | ||||
| maxmempool=4096 | ||||
| mempoolexpiry=999999 | ||||
| mempoolfullrbf=1 | ||||
| maxconnections=100 | ||||
| onion=127.0.0.1:9050 | ||||
| @ -20,6 +19,7 @@ whitelist=2401:b140::/32 | ||||
| #uacomment=@wiz | ||||
| 
 | ||||
| [main] | ||||
| mempoolexpiry=999999 | ||||
| rpcbind=127.0.0.1:8332 | ||||
| rpcbind=[::1]:8332 | ||||
| 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 | ||||
| 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 | ||||
| exit 0 | ||||
|  | ||||
| @ -251,7 +251,8 @@ class Server { | ||||
| 
 | ||||
|       if (!img) { | ||||
|         // 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 { | ||||
|         res.contentType('image/png'); | ||||
|         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; | ||||
|   title: string; | ||||
|   fallbackImg: string; | ||||
|   fallbackFile: string; | ||||
|   staticImg?: string; | ||||
|   networkMode: string; | ||||
| } | ||||
| @ -32,7 +31,6 @@ const routes = { | ||||
|   lightning: { | ||||
|     title: "Lightning", | ||||
|     fallbackImg: '/resources/previews/lightning.png', | ||||
|     fallbackFile: '/resources/img/lightning.png', | ||||
|     routes: { | ||||
|       node: { | ||||
|         render: true, | ||||
| @ -71,7 +69,6 @@ const routes = { | ||||
|   mining: { | ||||
|     title: "Mining", | ||||
|     fallbackImg: '/resources/previews/mining.png', | ||||
|     fallbackFile: '/resources/img/mining.png', | ||||
|     routes: { | ||||
|       pool: { | ||||
|         render: true, | ||||
| @ -87,14 +84,12 @@ const routes = { | ||||
| const networks = { | ||||
|   bitcoin: { | ||||
|     fallbackImg: '/resources/previews/dashboard.png', | ||||
|     fallbackFile: '/resources/img/dashboard.png', | ||||
|     routes: { | ||||
|       ...routes // all routes supported
 | ||||
|     } | ||||
|   }, | ||||
|   liquid: { | ||||
|     fallbackImg: '/resources/liquid/liquid-network-preview.png', | ||||
|     fallbackFile: '/resources/img/liquid', | ||||
|     routes: { // only block, address & tx routes supported
 | ||||
|       block: routes.block, | ||||
|       address: routes.address, | ||||
| @ -103,7 +98,6 @@ const networks = { | ||||
|   }, | ||||
|   bisq: { | ||||
|     fallbackImg: '/resources/bisq/bisq-markets-preview.png', | ||||
|     fallbackFile: '/resources/img/bisq.png', | ||||
|     routes: {} // no routes supported
 | ||||
|   } | ||||
| }; | ||||
| @ -113,7 +107,6 @@ export function matchRoute(network: string, path: string): Match { | ||||
|     render: false, | ||||
|     title: '', | ||||
|     fallbackImg: '', | ||||
|     fallbackFile: '', | ||||
|     networkMode: 'mainnet' | ||||
|   } | ||||
| 
 | ||||
| @ -128,7 +121,6 @@ export function matchRoute(network: string, path: string): Match { | ||||
| 
 | ||||
|   let route = networks[network] || networks.bitcoin; | ||||
|   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
 | ||||
|   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(); | ||||
|     if (route.fallbackImg) { | ||||
|       match.fallbackImg = route.fallbackImg; | ||||
|       match.fallbackFile = route.fallbackFile; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user