Merge branch 'master' into natsoni/fix-unnecessary-load-more
This commit is contained in:
		
						commit
						d02625eb0d
					
				
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -35,7 +35,7 @@ jobs: | ||||
|       - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain | ||||
|         # Latest version available on this commit is 1.71.1 | ||||
|         # Commit date is Aug 3, 2023 | ||||
|         uses: dtolnay/rust-toolchain@bb45937a053e097f8591208d8e74c90db1873d07 | ||||
|         uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a | ||||
|         with: | ||||
|           toolchain: ${{ steps.gettoolchain.outputs.toolchain }} | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { IBitcoinApi } from './bitcoin-api.interface'; | ||||
| import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; | ||||
| import { IEsploraApi } from './esplora-api.interface'; | ||||
| 
 | ||||
| export interface AbstractBitcoinApi { | ||||
| @ -22,6 +22,7 @@ export interface AbstractBitcoinApi { | ||||
|   $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>; | ||||
|   $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; | ||||
|   $sendRawTransaction(rawTransaction: string): Promise<string>; | ||||
|   $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>; | ||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; | ||||
|   $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; | ||||
|   $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; | ||||
|  | ||||
| @ -205,3 +205,16 @@ export namespace IBitcoinApi { | ||||
|     "utxo_size_inc": number; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export interface TestMempoolAcceptResult { | ||||
|   txid: string, | ||||
|   wtxid: string, | ||||
|   allowed?: boolean, | ||||
|   vsize?: number, | ||||
|   fees?: { | ||||
|     base: number, | ||||
|     "effective-feerate": number, | ||||
|     "effective-includes": string[], | ||||
|   }, | ||||
|   ['reject-reason']?: string, | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import * as bitcoinjs from 'bitcoinjs-lib'; | ||||
| import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; | ||||
| import { IBitcoinApi } from './bitcoin-api.interface'; | ||||
| import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; | ||||
| import { IEsploraApi } from './esplora-api.interface'; | ||||
| import blocks from '../blocks'; | ||||
| import mempool from '../mempool'; | ||||
| @ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi { | ||||
|     return this.bitcoindClient.sendRawTransaction(rawTransaction); | ||||
|   } | ||||
| 
 | ||||
|   async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> { | ||||
|     if (rawTransactions.length) { | ||||
|       return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined); | ||||
|     } else { | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { | ||||
|     const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); | ||||
|     return { | ||||
|  | ||||
| @ -55,6 +55,7 @@ class BitcoinRoutes { | ||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) | ||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) | ||||
|           .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) | ||||
|           .post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions) | ||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) | ||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) | ||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) | ||||
| @ -749,6 +750,19 @@ class BitcoinRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $testTransactions(req: Request, res: Response) { | ||||
|     try { | ||||
|       const rawTxs = Common.getTransactionsFromRequest(req); | ||||
|       const maxfeerate = parseFloat(req.query.maxfeerate as string); | ||||
|       const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); | ||||
|       res.send(result); | ||||
|     } catch (e: any) { | ||||
|       res.setHeader('content-type', 'text/plain'); | ||||
|       res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) | ||||
|         : (e.message || 'Error')); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default new BitcoinRoutes(); | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact | ||||
| import { IEsploraApi } from './esplora-api.interface'; | ||||
| import logger from '../../logger'; | ||||
| import { Common } from '../common'; | ||||
| import { TestMempoolAcceptResult } from './bitcoin-api.interface'; | ||||
| 
 | ||||
| interface FailoverHost { | ||||
|   host: string, | ||||
| @ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi { | ||||
|     throw new Error('Method not implemented.'); | ||||
|   } | ||||
| 
 | ||||
|   $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> { | ||||
|     throw new Error('Method not implemented.'); | ||||
|   } | ||||
| 
 | ||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { | ||||
|     return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); | ||||
|   } | ||||
|  | ||||
| @ -839,8 +839,11 @@ class Blocks { | ||||
|       } else { | ||||
|         this.currentBlockHeight++; | ||||
|         logger.debug(`New block found (#${this.currentBlockHeight})!`); | ||||
|         this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`); | ||||
|         await chainTips.updateOrphanedBlocks(); | ||||
|         // skip updating the orphan block cache if we've fallen behind the chain tip
 | ||||
|         if (this.currentBlockHeight >= blockHeightTip - 2) { | ||||
|           this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`); | ||||
|           await chainTips.updateOrphanedBlocks(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`); | ||||
|  | ||||
| @ -12,32 +12,68 @@ export interface OrphanedBlock { | ||||
|   height: number; | ||||
|   hash: string; | ||||
|   status: 'valid-fork' | 'valid-headers' | 'headers-only'; | ||||
|   prevhash: string; | ||||
| } | ||||
| 
 | ||||
| class ChainTips { | ||||
|   private chainTips: ChainTip[] = []; | ||||
|   private orphanedBlocks: OrphanedBlock[] = []; | ||||
|   private orphanedBlocks: { [hash: string]: OrphanedBlock } = {}; | ||||
|   private blockCache: { [hash: string]: OrphanedBlock } = {}; | ||||
|   private orphansByHeight: { [height: number]: OrphanedBlock[] } = {}; | ||||
| 
 | ||||
|   public async updateOrphanedBlocks(): Promise<void> { | ||||
|     try { | ||||
|       this.chainTips = await bitcoinClient.getChainTips(); | ||||
|       this.orphanedBlocks = []; | ||||
| 
 | ||||
|       const start = Date.now(); | ||||
|       const breakAt = start + 10000; | ||||
|       let newOrphans = 0; | ||||
|       this.orphanedBlocks = {}; | ||||
| 
 | ||||
|       for (const chain of this.chainTips) { | ||||
|         if (chain.status === 'valid-fork' || chain.status === 'valid-headers') { | ||||
|           let block = await bitcoinClient.getBlock(chain.hash); | ||||
|           while (block && block.confirmations === -1) { | ||||
|             this.orphanedBlocks.push({ | ||||
|               height: block.height, | ||||
|               hash: block.hash, | ||||
|               status: chain.status | ||||
|             }); | ||||
|             block = await bitcoinClient.getBlock(block.previousblockhash); | ||||
|           const orphans: OrphanedBlock[] = []; | ||||
|           let hash = chain.hash; | ||||
|           do { | ||||
|             let orphan = this.blockCache[hash]; | ||||
|             if (!orphan) { | ||||
|               const block = await bitcoinClient.getBlock(hash); | ||||
|               if (block && block.confirmations === -1) { | ||||
|                 newOrphans++; | ||||
|                 orphan = { | ||||
|                   height: block.height, | ||||
|                   hash: block.hash, | ||||
|                   status: chain.status, | ||||
|                   prevhash: block.previousblockhash, | ||||
|                 }; | ||||
|                 this.blockCache[hash] = orphan; | ||||
|               } | ||||
|             } | ||||
|             if (orphan) { | ||||
|               orphans.push(orphan); | ||||
|             } | ||||
|             hash = orphan?.prevhash; | ||||
|           } while (hash && (Date.now() < breakAt)); | ||||
|           for (const orphan of orphans) { | ||||
|             this.orphanedBlocks[orphan.hash] = orphan; | ||||
|           } | ||||
|         } | ||||
|         if (Date.now() >= breakAt) { | ||||
|           logger.debug(`Breaking orphaned blocks updater after 10s, will continue next block`); | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       logger.debug(`Updated orphaned blocks cache. Found ${this.orphanedBlocks.length} orphaned blocks`); | ||||
|       this.orphansByHeight = {}; | ||||
|       const allOrphans = Object.values(this.orphanedBlocks); | ||||
|       for (const orphan of allOrphans) { | ||||
|         if (!this.orphansByHeight[orphan.height]) { | ||||
|           this.orphansByHeight[orphan.height] = []; | ||||
|         } | ||||
|         this.orphansByHeight[orphan.height].push(orphan); | ||||
|       } | ||||
| 
 | ||||
|       logger.debug(`Updated orphaned blocks cache. Fetched ${newOrphans} new orphaned blocks. Total ${allOrphans.length}`); | ||||
|     } catch (e) { | ||||
|       logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`); | ||||
|     } | ||||
| @ -48,13 +84,7 @@ class ChainTips { | ||||
|       return []; | ||||
|     } | ||||
| 
 | ||||
|     const orphans: OrphanedBlock[] = []; | ||||
|     for (const block of this.orphanedBlocks) { | ||||
|       if (block.height === height) { | ||||
|         orphans.push(block); | ||||
|       } | ||||
|     } | ||||
|     return orphans; | ||||
|     return this.orphansByHeight[height] || []; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -946,6 +946,33 @@ export class Common { | ||||
|     return this.validateTransactionHex(matches[1].toLowerCase()); | ||||
|   } | ||||
| 
 | ||||
|   static getTransactionsFromRequest(req: Request, limit: number = 25): string[] { | ||||
|     if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) { | ||||
|       throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 }); | ||||
|     } | ||||
| 
 | ||||
|     if (limit && req.body.length > limit) { | ||||
|       throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 }); | ||||
|     } | ||||
| 
 | ||||
|     const txs = req.body; | ||||
| 
 | ||||
|     return txs.map(rawTx => { | ||||
|       // Support both upper and lower case hex
 | ||||
|       // Support both txHash= Form and direct API POST
 | ||||
|       const reg = /^((?:[a-fA-F0-9]{2})+)$/; | ||||
|       const matches = reg.exec(rawTx); | ||||
|       if (!matches || !matches[1]) { | ||||
|         throw Object.assign(new Error('Invalid hex string'), { code: -2 }); | ||||
|       } | ||||
| 
 | ||||
|       // Guaranteed to be a hex string of multiple of 2
 | ||||
|       // Guaranteed to be lower case
 | ||||
|       // Guaranteed to pass validation (see function below)
 | ||||
|       return this.validateTransactionHex(matches[1].toLowerCase()); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private static validateTransactionHex(txhex: string): string { | ||||
|     // Do not mutate txhex
 | ||||
| 
 | ||||
|  | ||||
| @ -666,7 +666,9 @@ class NodesApi { | ||||
|         node.last_update = null; | ||||
|       } | ||||
|    | ||||
|       const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? ''; | ||||
|       const uniqueAddr = [...new Set(node.addresses?.map(a => a.addr))]; | ||||
|       const formattedSockets = (uniqueAddr.join(',')) ?? ''; | ||||
| 
 | ||||
|       const query = `INSERT INTO nodes(
 | ||||
|           public_key, | ||||
|           first_seen, | ||||
| @ -695,13 +697,13 @@ class NodesApi { | ||||
|         node.alias, | ||||
|         this.aliasToSearchText(node.alias), | ||||
|         node.color, | ||||
|         sockets, | ||||
|         formattedSockets, | ||||
|         JSON.stringify(node.features), | ||||
|         node.last_update, | ||||
|         node.alias, | ||||
|         this.aliasToSearchText(node.alias), | ||||
|         node.color, | ||||
|         sockets, | ||||
|         formattedSockets, | ||||
|         JSON.stringify(node.features), | ||||
|       ]); | ||||
|     } catch (e) { | ||||
|  | ||||
| @ -404,6 +404,10 @@ class Mempool { | ||||
| 
 | ||||
|       const newAccelerationMap: { [txid: string]: Acceleration } = {}; | ||||
|       for (const acceleration of newAccelerations) { | ||||
|         // skip transactions we don't know about
 | ||||
|         if (!this.mempoolCache[acceleration.txid]) { | ||||
|           continue; | ||||
|         } | ||||
|         newAccelerationMap[acceleration.txid] = acceleration; | ||||
|         if (this.accelerations[acceleration.txid] == null) { | ||||
|           // new acceleration
 | ||||
|  | ||||
| @ -3,6 +3,7 @@ import * as WebSocket from 'ws'; | ||||
| import { | ||||
|   BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, | ||||
|   OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, | ||||
|   MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids | ||||
| } from '../mempool.interfaces'; | ||||
| import blocks from './blocks'; | ||||
| import memPool from './mempool'; | ||||
| @ -346,6 +347,17 @@ class WebsocketHandler { | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (parsedMessage && parsedMessage['track-accelerations'] != null) { | ||||
|             if (parsedMessage['track-accelerations']) { | ||||
|               client['track-accelerations'] = true; | ||||
|               response['accelerations'] = JSON.stringify({ | ||||
|                 accelerations: Object.values(memPool.getAccelerations()), | ||||
|               }); | ||||
|             } else { | ||||
|               client['track-accelerations'] = false; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (parsedMessage.action === 'init') { | ||||
|             if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) { | ||||
|               this.updateSocketData(); | ||||
| @ -364,6 +376,18 @@ class WebsocketHandler { | ||||
|             client['track-donation'] = parsedMessage['track-donation']; | ||||
|           } | ||||
| 
 | ||||
|           if (parsedMessage['track-mempool-txids'] === true) { | ||||
|             client['track-mempool-txids'] = true; | ||||
|           } else if (parsedMessage['track-mempool-txids'] === false) { | ||||
|             delete client['track-mempool-txids']; | ||||
|           } | ||||
| 
 | ||||
|           if (parsedMessage['track-mempool'] === true) { | ||||
|             client['track-mempool'] = true; | ||||
|           } else if (parsedMessage['track-mempool'] === false) { | ||||
|             delete client['track-mempool']; | ||||
|           } | ||||
| 
 | ||||
|           if (Object.keys(response).length) { | ||||
|             client.send(this.serializeResponse(response)); | ||||
|           } | ||||
| @ -524,6 +548,7 @@ class WebsocketHandler { | ||||
|     const vBytesPerSecond = memPool.getVBytesPerSecond(); | ||||
|     const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); | ||||
|     const da = difficultyAdjustment.getDifficultyAdjustment(); | ||||
|     const accelerations = memPool.getAccelerations(); | ||||
|     memPool.handleRbfTransactions(rbfTransactions); | ||||
|     const rbfChanges = rbfCache.getRbfChanges(); | ||||
|     let rbfReplacements; | ||||
| @ -545,6 +570,33 @@ class WebsocketHandler { | ||||
| 
 | ||||
|     const latestTransactions = memPool.getLatestTransactions(); | ||||
| 
 | ||||
|     if (memPool.isInSync()) { | ||||
|       this.mempoolSequence++; | ||||
|     } | ||||
| 
 | ||||
|     const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; | ||||
|     for (const tx of newTransactions) { | ||||
|       if (rbfTransactions[tx.txid]) { | ||||
|         for (const replaced of rbfTransactions[tx.txid]) { | ||||
|           replacedTransactions.push({ replaced: replaced.txid, by: tx }); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     const mempoolDeltaTxids: MempoolDeltaTxids = { | ||||
|       sequence: this.mempoolSequence, | ||||
|       added: newTransactions.map(tx => tx.txid), | ||||
|       removed: deletedTransactions.map(tx => tx.txid), | ||||
|       mined: [], | ||||
|       replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), | ||||
|     }; | ||||
|     const mempoolDelta: MempoolDelta = { | ||||
|       sequence: this.mempoolSequence, | ||||
|       added: newTransactions, | ||||
|       removed: deletedTransactions.map(tx => tx.txid), | ||||
|       mined: [], | ||||
|       replaced: replacedTransactions, | ||||
|     }; | ||||
| 
 | ||||
|     // update init data
 | ||||
|     const socketDataFields = { | ||||
|       'mempoolInfo': mempoolInfo, | ||||
| @ -604,9 +656,11 @@ class WebsocketHandler { | ||||
|     const addressCache = this.makeAddressCache(newTransactions); | ||||
|     const removedAddressCache = this.makeAddressCache(deletedTransactions); | ||||
| 
 | ||||
|     if (memPool.isInSync()) { | ||||
|       this.mempoolSequence++; | ||||
|     } | ||||
|     // pre-compute acceleration delta
 | ||||
|     const accelerationUpdate = { | ||||
|       added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), | ||||
|       removed: accelerationDelta.filter(txid => !accelerations[txid]), | ||||
|     }; | ||||
| 
 | ||||
|     // TODO - Fix indentation after PR is merged
 | ||||
|     for (const server of this.webSocketServers) { | ||||
| @ -847,6 +901,18 @@ class WebsocketHandler { | ||||
|         response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary); | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-mempool-txids']) { | ||||
|         response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids); | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-mempool']) { | ||||
|         response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) { | ||||
|         response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate); | ||||
|       } | ||||
| 
 | ||||
|       if (Object.keys(response).length) { | ||||
|         client.send(this.serializeResponse(response)); | ||||
|       } | ||||
| @ -992,6 +1058,31 @@ class WebsocketHandler { | ||||
| 
 | ||||
|     const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||
| 
 | ||||
|     if (memPool.isInSync()) { | ||||
|       this.mempoolSequence++; | ||||
|     } | ||||
| 
 | ||||
|     const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; | ||||
|     for (const txid of Object.keys(rbfTransactions)) { | ||||
|       for (const replaced of rbfTransactions[txid].replaced) { | ||||
|         replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy }); | ||||
|       } | ||||
|     } | ||||
|     const mempoolDeltaTxids: MempoolDeltaTxids = { | ||||
|       sequence: this.mempoolSequence, | ||||
|       added: [], | ||||
|       removed: [], | ||||
|       mined: transactions.map(tx => tx.txid), | ||||
|       replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), | ||||
|     }; | ||||
|     const mempoolDelta: MempoolDelta = { | ||||
|       sequence: this.mempoolSequence, | ||||
|       added: [], | ||||
|       removed: [], | ||||
|       mined: transactions.map(tx => tx.txid), | ||||
|       replaced: replacedTransactions, | ||||
|     }; | ||||
| 
 | ||||
|     const responseCache = { ...this.socketData }; | ||||
|     function getCachedResponse(key, data): string { | ||||
|       if (!responseCache[key]) { | ||||
| @ -1000,10 +1091,6 @@ class WebsocketHandler { | ||||
|       return responseCache[key]; | ||||
|     } | ||||
| 
 | ||||
|     if (memPool.isInSync()) { | ||||
|       this.mempoolSequence++; | ||||
|     } | ||||
| 
 | ||||
|     // TODO - Fix indentation after PR is merged
 | ||||
|     for (const server of this.webSocketServers) { | ||||
|     server.clients.forEach((client) => { | ||||
| @ -1185,6 +1272,14 @@ class WebsocketHandler { | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-mempool-txids']) { | ||||
|         response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids); | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-mempool']) { | ||||
|         response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); | ||||
|       } | ||||
| 
 | ||||
|       if (Object.keys(response).length) { | ||||
|         client.send(this.serializeResponse(response)); | ||||
|       } | ||||
|  | ||||
| @ -131,6 +131,7 @@ class Server { | ||||
|       }) | ||||
|       .use(express.urlencoded({ extended: true })) | ||||
|       .use(express.text({ type: ['text/plain', 'application/base64'] })) | ||||
|       .use(express.json()) | ||||
|       ; | ||||
| 
 | ||||
|     if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) { | ||||
|  | ||||
| @ -71,6 +71,22 @@ export interface MempoolBlockDelta { | ||||
|   changed: MempoolDeltaChange[]; | ||||
| } | ||||
| 
 | ||||
| export interface MempoolDeltaTxids { | ||||
|   sequence: number, | ||||
|   added: string[]; | ||||
|   removed: string[]; | ||||
|   mined: string[]; | ||||
|   replaced: { replaced: string, by: string }[]; | ||||
| } | ||||
| 
 | ||||
| export interface MempoolDelta { | ||||
|   sequence: number, | ||||
|   added: MempoolTransactionExtended[]; | ||||
|   removed: string[]; | ||||
|   mined: string[]; | ||||
|   replaced: { replaced: string, by: TransactionExtended }[]; | ||||
| } | ||||
| 
 | ||||
| interface VinStrippedToScriptsig { | ||||
|   scriptsig: string; | ||||
| } | ||||
|  | ||||
| @ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional | ||||
| 
 | ||||
| RUN npm run build | ||||
| 
 | ||||
| FROM nginx:1.25.4-alpine | ||||
| FROM nginx:1.26.0-alpine | ||||
| 
 | ||||
| WORKDIR /patch | ||||
| 
 | ||||
|  | ||||
| @ -181,6 +181,11 @@ | ||||
|                 "bundleName": "wiz", | ||||
|                 "inject": false | ||||
|               }, | ||||
|               { | ||||
|                 "input": "src/theme-bukele.scss", | ||||
|                 "bundleName": "bukele", | ||||
|                 "inject": false | ||||
|               }, | ||||
|               "node_modules/@fortawesome/fontawesome-svg-core/styles.css" | ||||
|             ], | ||||
|             "vendorChunk": true, | ||||
|  | ||||
| @ -1,30 +1,37 @@ | ||||
| { | ||||
|   "theme": "contrast", | ||||
|   "theme": "bukele", | ||||
|   "enterprise": "onbtc", | ||||
|   "branding": { | ||||
|     "name": "onbtc", | ||||
|     "title": "Oficina Nacional del Bitcoin", | ||||
|     "title": "Bitcoin Office", | ||||
|     "site_id": 19, | ||||
|     "header_img": "/resources/onbtc.svg", | ||||
|     "img": "/resources/elsalvador.svg", | ||||
|     "header_img": "/resources/onbtclogo.svg", | ||||
|     "footer_img": "/resources/onbtclogo.svg", | ||||
|     "rounded_corner": true | ||||
|   }, | ||||
|   "dashboard": { | ||||
|     "widgets": [ | ||||
|       { | ||||
|         "component": "fees" | ||||
|         "component": "fees", | ||||
|         "mobileOrder": 4 | ||||
|       }, | ||||
|       { | ||||
|         "component": "balance", | ||||
|         "mobileOrder": 1, | ||||
|         "props": { | ||||
|           "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         "component": "goggles" | ||||
|         "component": "twitter", | ||||
|         "mobileOrder": 5, | ||||
|         "props": { | ||||
|           "handle": "nayibbukele" | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         "component": "address", | ||||
|         "mobileOrder": 2, | ||||
|         "props": { | ||||
|           "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo", | ||||
|           "period": "1m" | ||||
| @ -35,6 +42,7 @@ | ||||
|       }, | ||||
|       { | ||||
|         "component": "addressTransactions", | ||||
|         "mobileOrder": 3, | ||||
|         "props": { | ||||
|           "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" | ||||
|         } | ||||
|  | ||||
| @ -10,6 +10,7 @@ let settings = []; | ||||
| let configContent = {}; | ||||
| let gitCommitHash = ''; | ||||
| let packetJsonVersion = ''; | ||||
| let customConfig; | ||||
| 
 | ||||
| try { | ||||
|   const rawConfig = fs.readFileSync(CONFIG_FILE_NAME); | ||||
| @ -23,7 +24,13 @@ try { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html'; | ||||
| if (configContent && configContent.CUSTOMIZATION) { | ||||
|   customConfig = readConfig(configContent.CUSTOMIZATION); | ||||
| } | ||||
| 
 | ||||
| const baseModuleName = configContent.BASE_MODULE || 'mempool'; | ||||
| const customBuildName = (customConfig && configContent.enterprise) ? ('.' + configContent.enterprise) : ''; | ||||
| const indexFilePath = 'src/index.' + baseModuleName + customBuildName + '.html'; | ||||
| 
 | ||||
| try { | ||||
|   fs.copyFileSync(indexFilePath, 'src/index.html'); | ||||
| @ -111,20 +118,14 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate); | ||||
| const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME); | ||||
| 
 | ||||
| let customConfigJs = ''; | ||||
| if (configContent && configContent.CUSTOMIZATION) { | ||||
|   const customConfig = readConfig(configContent.CUSTOMIZATION); | ||||
|   if (customConfig) { | ||||
|     console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`); | ||||
|     customConfigJs = `(function (window) {
 | ||||
|       window.__env = window.__env || {}; | ||||
|       window.__env.customize = ${customConfig}; | ||||
|       }((typeof global !== 'undefined') ? global : this)); | ||||
|     `;
 | ||||
|   } else { | ||||
|     throw new Error('Failed to load customization file'); | ||||
|   } | ||||
| if (customConfig) { | ||||
|   console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`); | ||||
|   customConfigJs = `(function (window) {
 | ||||
|     window.__env = window.__env || {}; | ||||
|     window.__env.customize = ${customConfig}; | ||||
|     }((typeof global !== 'undefined') ? global : this)); | ||||
|   `;
 | ||||
| } | ||||
| 
 | ||||
| writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs); | ||||
| 
 | ||||
| if (currentConfig && currentConfig === newConfig) { | ||||
|  | ||||
							
								
								
									
										398
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										398
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -32,10 +32,10 @@ | ||||
|         "bootstrap": "~4.6.2", | ||||
|         "browserify": "^17.0.0", | ||||
|         "clipboard": "^2.0.11", | ||||
|         "cypress": "^13.8.0", | ||||
|         "cypress": "^13.9.0", | ||||
|         "domino": "^2.1.6", | ||||
|         "echarts": "~5.5.0", | ||||
|         "esbuild": "^0.20.2", | ||||
|         "esbuild": "^0.21.1", | ||||
|         "lightweight-charts": "~3.8.0", | ||||
|         "ngx-echarts": "~17.1.0", | ||||
|         "ngx-infinite-scroll": "^17.0.0", | ||||
| @ -63,7 +63,7 @@ | ||||
|       "optionalDependencies": { | ||||
|         "@cypress/schematic": "^2.5.0", | ||||
|         "@types/cypress": "^1.1.3", | ||||
|         "cypress": "^13.8.0", | ||||
|         "cypress": "^13.9.0", | ||||
|         "cypress-fail-on-console-error": "~5.1.0", | ||||
|         "cypress-wait-until": "^2.0.1", | ||||
|         "mock-socket": "~9.3.1", | ||||
| @ -3197,9 +3197,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/aix-ppc64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", | ||||
|       "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz", | ||||
|       "integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==", | ||||
|       "cpu": [ | ||||
|         "ppc64" | ||||
|       ], | ||||
| @ -3212,9 +3212,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/android-arm": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", | ||||
|       "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz", | ||||
|       "integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==", | ||||
|       "cpu": [ | ||||
|         "arm" | ||||
|       ], | ||||
| @ -3227,9 +3227,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/android-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -3242,9 +3242,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/android-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -3257,9 +3257,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/darwin-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -3272,9 +3272,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/darwin-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -3287,9 +3287,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/freebsd-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -3302,9 +3302,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/freebsd-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -3317,9 +3317,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-arm": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", | ||||
|       "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz", | ||||
|       "integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==", | ||||
|       "cpu": [ | ||||
|         "arm" | ||||
|       ], | ||||
| @ -3332,9 +3332,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -3347,9 +3347,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-ia32": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", | ||||
|       "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz", | ||||
|       "integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==", | ||||
|       "cpu": [ | ||||
|         "ia32" | ||||
|       ], | ||||
| @ -3362,9 +3362,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-loong64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", | ||||
|       "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz", | ||||
|       "integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==", | ||||
|       "cpu": [ | ||||
|         "loong64" | ||||
|       ], | ||||
| @ -3377,9 +3377,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-mips64el": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", | ||||
|       "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz", | ||||
|       "integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==", | ||||
|       "cpu": [ | ||||
|         "mips64el" | ||||
|       ], | ||||
| @ -3392,9 +3392,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-ppc64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", | ||||
|       "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz", | ||||
|       "integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==", | ||||
|       "cpu": [ | ||||
|         "ppc64" | ||||
|       ], | ||||
| @ -3407,9 +3407,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-riscv64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", | ||||
|       "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz", | ||||
|       "integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==", | ||||
|       "cpu": [ | ||||
|         "riscv64" | ||||
|       ], | ||||
| @ -3422,9 +3422,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-s390x": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", | ||||
|       "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz", | ||||
|       "integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==", | ||||
|       "cpu": [ | ||||
|         "s390x" | ||||
|       ], | ||||
| @ -3437,9 +3437,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/linux-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -3452,9 +3452,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/netbsd-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -3467,9 +3467,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/openbsd-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -3482,9 +3482,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/sunos-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -3497,9 +3497,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/win32-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -3512,9 +3512,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/win32-ia32": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", | ||||
|       "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz", | ||||
|       "integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==", | ||||
|       "cpu": [ | ||||
|         "ia32" | ||||
|       ], | ||||
| @ -3527,9 +3527,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@esbuild/win32-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -8029,9 +8029,9 @@ | ||||
|       "peer": true | ||||
|     }, | ||||
|     "node_modules/cypress": { | ||||
|       "version": "13.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.0.tgz", | ||||
|       "integrity": "sha512-Qau//mtrwEGOU9cn2YjavECKyDUwBh8J2tit+y9s1wsv6C3BX+rlv6I9afmQnL8PmEEzJ6be7nppMHacFzZkTw==", | ||||
|       "version": "13.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz", | ||||
|       "integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==", | ||||
|       "hasInstallScript": true, | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
| @ -9197,9 +9197,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/esbuild": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", | ||||
|       "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz", | ||||
|       "integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==", | ||||
|       "hasInstallScript": true, | ||||
|       "bin": { | ||||
|         "esbuild": "bin/esbuild" | ||||
| @ -9208,29 +9208,29 @@ | ||||
|         "node": ">=12" | ||||
|       }, | ||||
|       "optionalDependencies": { | ||||
|         "@esbuild/aix-ppc64": "0.20.2", | ||||
|         "@esbuild/android-arm": "0.20.2", | ||||
|         "@esbuild/android-arm64": "0.20.2", | ||||
|         "@esbuild/android-x64": "0.20.2", | ||||
|         "@esbuild/darwin-arm64": "0.20.2", | ||||
|         "@esbuild/darwin-x64": "0.20.2", | ||||
|         "@esbuild/freebsd-arm64": "0.20.2", | ||||
|         "@esbuild/freebsd-x64": "0.20.2", | ||||
|         "@esbuild/linux-arm": "0.20.2", | ||||
|         "@esbuild/linux-arm64": "0.20.2", | ||||
|         "@esbuild/linux-ia32": "0.20.2", | ||||
|         "@esbuild/linux-loong64": "0.20.2", | ||||
|         "@esbuild/linux-mips64el": "0.20.2", | ||||
|         "@esbuild/linux-ppc64": "0.20.2", | ||||
|         "@esbuild/linux-riscv64": "0.20.2", | ||||
|         "@esbuild/linux-s390x": "0.20.2", | ||||
|         "@esbuild/linux-x64": "0.20.2", | ||||
|         "@esbuild/netbsd-x64": "0.20.2", | ||||
|         "@esbuild/openbsd-x64": "0.20.2", | ||||
|         "@esbuild/sunos-x64": "0.20.2", | ||||
|         "@esbuild/win32-arm64": "0.20.2", | ||||
|         "@esbuild/win32-ia32": "0.20.2", | ||||
|         "@esbuild/win32-x64": "0.20.2" | ||||
|         "@esbuild/aix-ppc64": "0.21.1", | ||||
|         "@esbuild/android-arm": "0.21.1", | ||||
|         "@esbuild/android-arm64": "0.21.1", | ||||
|         "@esbuild/android-x64": "0.21.1", | ||||
|         "@esbuild/darwin-arm64": "0.21.1", | ||||
|         "@esbuild/darwin-x64": "0.21.1", | ||||
|         "@esbuild/freebsd-arm64": "0.21.1", | ||||
|         "@esbuild/freebsd-x64": "0.21.1", | ||||
|         "@esbuild/linux-arm": "0.21.1", | ||||
|         "@esbuild/linux-arm64": "0.21.1", | ||||
|         "@esbuild/linux-ia32": "0.21.1", | ||||
|         "@esbuild/linux-loong64": "0.21.1", | ||||
|         "@esbuild/linux-mips64el": "0.21.1", | ||||
|         "@esbuild/linux-ppc64": "0.21.1", | ||||
|         "@esbuild/linux-riscv64": "0.21.1", | ||||
|         "@esbuild/linux-s390x": "0.21.1", | ||||
|         "@esbuild/linux-x64": "0.21.1", | ||||
|         "@esbuild/netbsd-x64": "0.21.1", | ||||
|         "@esbuild/openbsd-x64": "0.21.1", | ||||
|         "@esbuild/sunos-x64": "0.21.1", | ||||
|         "@esbuild/win32-arm64": "0.21.1", | ||||
|         "@esbuild/win32-ia32": "0.21.1", | ||||
|         "@esbuild/win32-x64": "0.21.1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/esbuild-wasm": { | ||||
| @ -20563,141 +20563,141 @@ | ||||
|       "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" | ||||
|     }, | ||||
|     "@esbuild/aix-ppc64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", | ||||
|       "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz", | ||||
|       "integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/android-arm": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", | ||||
|       "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz", | ||||
|       "integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/android-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/android-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/darwin-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/darwin-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/freebsd-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/freebsd-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-arm": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", | ||||
|       "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz", | ||||
|       "integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-ia32": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", | ||||
|       "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz", | ||||
|       "integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-loong64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", | ||||
|       "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz", | ||||
|       "integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-mips64el": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", | ||||
|       "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz", | ||||
|       "integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-ppc64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", | ||||
|       "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz", | ||||
|       "integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-riscv64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", | ||||
|       "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz", | ||||
|       "integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-s390x": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", | ||||
|       "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz", | ||||
|       "integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/linux-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/netbsd-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/openbsd-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/sunos-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/win32-arm64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", | ||||
|       "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz", | ||||
|       "integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/win32-ia32": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", | ||||
|       "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz", | ||||
|       "integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@esbuild/win32-x64": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", | ||||
|       "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz", | ||||
|       "integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@eslint-community/eslint-utils": { | ||||
| @ -24112,9 +24112,9 @@ | ||||
|       "peer": true | ||||
|     }, | ||||
|     "cypress": { | ||||
|       "version": "13.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.0.tgz", | ||||
|       "integrity": "sha512-Qau//mtrwEGOU9cn2YjavECKyDUwBh8J2tit+y9s1wsv6C3BX+rlv6I9afmQnL8PmEEzJ6be7nppMHacFzZkTw==", | ||||
|       "version": "13.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz", | ||||
|       "integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==", | ||||
|       "optional": true, | ||||
|       "requires": { | ||||
|         "@cypress/request": "^3.0.0", | ||||
| @ -25032,33 +25032,33 @@ | ||||
|       } | ||||
|     }, | ||||
|     "esbuild": { | ||||
|       "version": "0.20.2", | ||||
|       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", | ||||
|       "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", | ||||
|       "version": "0.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz", | ||||
|       "integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==", | ||||
|       "requires": { | ||||
|         "@esbuild/aix-ppc64": "0.20.2", | ||||
|         "@esbuild/android-arm": "0.20.2", | ||||
|         "@esbuild/android-arm64": "0.20.2", | ||||
|         "@esbuild/android-x64": "0.20.2", | ||||
|         "@esbuild/darwin-arm64": "0.20.2", | ||||
|         "@esbuild/darwin-x64": "0.20.2", | ||||
|         "@esbuild/freebsd-arm64": "0.20.2", | ||||
|         "@esbuild/freebsd-x64": "0.20.2", | ||||
|         "@esbuild/linux-arm": "0.20.2", | ||||
|         "@esbuild/linux-arm64": "0.20.2", | ||||
|         "@esbuild/linux-ia32": "0.20.2", | ||||
|         "@esbuild/linux-loong64": "0.20.2", | ||||
|         "@esbuild/linux-mips64el": "0.20.2", | ||||
|         "@esbuild/linux-ppc64": "0.20.2", | ||||
|         "@esbuild/linux-riscv64": "0.20.2", | ||||
|         "@esbuild/linux-s390x": "0.20.2", | ||||
|         "@esbuild/linux-x64": "0.20.2", | ||||
|         "@esbuild/netbsd-x64": "0.20.2", | ||||
|         "@esbuild/openbsd-x64": "0.20.2", | ||||
|         "@esbuild/sunos-x64": "0.20.2", | ||||
|         "@esbuild/win32-arm64": "0.20.2", | ||||
|         "@esbuild/win32-ia32": "0.20.2", | ||||
|         "@esbuild/win32-x64": "0.20.2" | ||||
|         "@esbuild/aix-ppc64": "0.21.1", | ||||
|         "@esbuild/android-arm": "0.21.1", | ||||
|         "@esbuild/android-arm64": "0.21.1", | ||||
|         "@esbuild/android-x64": "0.21.1", | ||||
|         "@esbuild/darwin-arm64": "0.21.1", | ||||
|         "@esbuild/darwin-x64": "0.21.1", | ||||
|         "@esbuild/freebsd-arm64": "0.21.1", | ||||
|         "@esbuild/freebsd-x64": "0.21.1", | ||||
|         "@esbuild/linux-arm": "0.21.1", | ||||
|         "@esbuild/linux-arm64": "0.21.1", | ||||
|         "@esbuild/linux-ia32": "0.21.1", | ||||
|         "@esbuild/linux-loong64": "0.21.1", | ||||
|         "@esbuild/linux-mips64el": "0.21.1", | ||||
|         "@esbuild/linux-ppc64": "0.21.1", | ||||
|         "@esbuild/linux-riscv64": "0.21.1", | ||||
|         "@esbuild/linux-s390x": "0.21.1", | ||||
|         "@esbuild/linux-x64": "0.21.1", | ||||
|         "@esbuild/netbsd-x64": "0.21.1", | ||||
|         "@esbuild/openbsd-x64": "0.21.1", | ||||
|         "@esbuild/sunos-x64": "0.21.1", | ||||
|         "@esbuild/win32-arm64": "0.21.1", | ||||
|         "@esbuild/win32-ia32": "0.21.1", | ||||
|         "@esbuild/win32-x64": "0.21.1" | ||||
|       } | ||||
|     }, | ||||
|     "esbuild-wasm": { | ||||
|  | ||||
| @ -92,7 +92,7 @@ | ||||
|     "ngx-infinite-scroll": "^17.0.0", | ||||
|     "qrcode": "1.5.1", | ||||
|     "rxjs": "~7.8.1", | ||||
|     "esbuild": "^0.20.2", | ||||
|     "esbuild": "^0.21.1", | ||||
|     "tinyify": "^4.0.0", | ||||
|     "tlite": "^0.1.9", | ||||
|     "tslib": "~2.6.0", | ||||
| @ -115,7 +115,7 @@ | ||||
|   "optionalDependencies": { | ||||
|     "@cypress/schematic": "^2.5.0", | ||||
|     "@types/cypress": "^1.1.3", | ||||
|     "cypress": "^13.8.0", | ||||
|     "cypress": "^13.9.0", | ||||
|     "cypress-fail-on-console-error": "~5.1.0", | ||||
|     "cypress-wait-until": "^2.0.1", | ||||
|     "mock-socket": "~9.3.1", | ||||
|  | ||||
| @ -53,6 +53,44 @@ let routes: Routes = [ | ||||
|       }, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     path: 'testnet4', | ||||
|     children: [ | ||||
|       { | ||||
|         path: '', | ||||
|         pathMatch: 'full', | ||||
|         loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||
|         data: { preload: true }, | ||||
|       }, | ||||
|       { | ||||
|         path: '', | ||||
|         loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), | ||||
|         data: { preload: true }, | ||||
|       }, | ||||
|       { | ||||
|         path: 'wallet', | ||||
|         children: [], | ||||
|         component: AddressGroupComponent, | ||||
|         data: { | ||||
|           networkSpecific: true, | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         path: 'status', | ||||
|         data: { networks: ['bitcoin', 'liquid'] }, | ||||
|         component: StatusViewComponent | ||||
|       }, | ||||
|       { | ||||
|         path: '', | ||||
|         loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||
|         data: { preload: true }, | ||||
|       }, | ||||
|       { | ||||
|         path: '**', | ||||
|         redirectTo: '/testnet4' | ||||
|       }, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     path: 'signet', | ||||
|     children: [ | ||||
| @ -130,6 +168,10 @@ let routes: Routes = [ | ||||
|         path: 'testnet', | ||||
|         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) | ||||
|       }, | ||||
|       { | ||||
|         path: 'testnet4', | ||||
|         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) | ||||
|       }, | ||||
|       { | ||||
|         path: 'signet', | ||||
|         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) | ||||
|  | ||||
| @ -189,22 +189,22 @@ export const specialBlocks = { | ||||
|   '0': { | ||||
|     labelEvent: 'Genesis', | ||||
|     labelEventCompleted: 'The Genesis of Bitcoin', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '210000': { | ||||
|     labelEvent: 'Bitcoin\'s 1st Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '420000': { | ||||
|     labelEvent: 'Bitcoin\'s 2nd Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '630000': { | ||||
|     labelEvent: 'Bitcoin\'s 3rd Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '709632': { | ||||
|     labelEvent: 'Taproot 🌱 activation', | ||||
| @ -214,62 +214,62 @@ export const specialBlocks = { | ||||
|   '840000': { | ||||
|     labelEvent: 'Bitcoin\'s 4th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '1050000': { | ||||
|     labelEvent: 'Bitcoin\'s 5th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '1260000': { | ||||
|     labelEvent: 'Bitcoin\'s 6th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '1470000': { | ||||
|     labelEvent: 'Bitcoin\'s 7th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '1680000': { | ||||
|     labelEvent: 'Bitcoin\'s 8th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '1890000': { | ||||
|     labelEvent: 'Bitcoin\'s 9th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '2100000': { | ||||
|     labelEvent: 'Bitcoin\'s 10th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '2310000': { | ||||
|     labelEvent: 'Bitcoin\'s 11th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '2520000': { | ||||
|     labelEvent: 'Bitcoin\'s 12th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '2730000': { | ||||
|     labelEvent: 'Bitcoin\'s 13th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '2940000': { | ||||
|     labelEvent: 'Bitcoin\'s 14th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   }, | ||||
|   '3150000': { | ||||
|     labelEvent: 'Bitcoin\'s 15th Halving', | ||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block', | ||||
|     networks: ['mainnet', 'testnet'], | ||||
|     networks: ['mainnet', 'testnet', 'testnet4'], | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
|  | ||||
| @ -266,6 +266,11 @@ const featureActivation = { | ||||
|     segwit: 872730, | ||||
|     taproot: 2032291, | ||||
|   }, | ||||
|   testnet4: { | ||||
|     rbf: 0, | ||||
|     segwit: 0, | ||||
|     taproot: 0, | ||||
|   }, | ||||
|   signet: { | ||||
|     rbf: 0, | ||||
|     segwit: 0, | ||||
|  | ||||
| @ -343,8 +343,8 @@ | ||||
|       <a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance"> | ||||
|         <img class="copa" src="/resources/profile/copa.png" /> | ||||
|       </a> | ||||
|       <a href="https://bisq.network/" title="Bisq Network"> | ||||
|         <img class="bisq" src="/resources/profile/bisq.svg" /> | ||||
|       <a href="https://bitcoin.gob.sv" title="Oficina Nacional del Bitcoin"> | ||||
|         <img class="sv" src="/resources/profile/onbtc-full.svg" /> | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
| @ -129,8 +129,9 @@ | ||||
|       position: relative; | ||||
|       width: 300px; | ||||
|     } | ||||
|     .bisq { | ||||
|       top: 3px; | ||||
|     .sv { | ||||
|       height: 85px; | ||||
|       width: auto; | ||||
|       position: relative; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; | ||||
| import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; | ||||
| import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core'; | ||||
| import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; | ||||
| import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| @ -11,7 +11,7 @@ import { ServicesApiServices } from '../../../services/services-api.service'; | ||||
|   styleUrls: ['./accelerations-list.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AccelerationsListComponent implements OnInit { | ||||
| export class AccelerationsListComponent implements OnInit, OnDestroy { | ||||
|   @Input() widget: boolean = false; | ||||
|   @Input() pending: boolean = false; | ||||
|   @Input() accelerations$: Observable<Acceleration[]>; | ||||
| @ -44,7 +44,10 @@ export class AccelerationsListComponent implements OnInit { | ||||
|      | ||||
|     this.accelerationList$ = this.pageSubject.pipe( | ||||
|       switchMap((page) => { | ||||
|         const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page })); | ||||
|         const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page })); | ||||
|         if (!this.accelerations$ && this.pending) { | ||||
|           this.websocketService.ensureTrackAccelerations(); | ||||
|         } | ||||
|         return accelerationObservable$.pipe( | ||||
|           switchMap(response => { | ||||
|             let accelerations = response; | ||||
| @ -85,4 +88,8 @@ export class AccelerationsListComponent implements OnInit { | ||||
|   trackByBlock(index: number, block: BlockExtended): number { | ||||
|     return block.height; | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.websocketService.stopTrackAccelerations(); | ||||
|   } | ||||
| } | ||||
| @ -60,7 +60,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core'; | ||||
| import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; | ||||
| import { SeoService } from '../../../services/seo.service'; | ||||
| import { OpenGraphService } from '../../../services/opengraph.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs'; | ||||
| import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs'; | ||||
| import { Color } from '../../block-overview-graph/sprite-types'; | ||||
| import { hexToColor } from '../../block-overview-graph/utils'; | ||||
| import TxView from '../../block-overview-graph/tx-view'; | ||||
| @ -28,7 +28,7 @@ interface AccelerationBlock extends BlockExtended { | ||||
|   styleUrls: ['./accelerator-dashboard.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AcceleratorDashboardComponent implements OnInit { | ||||
| export class AcceleratorDashboardComponent implements OnInit, OnDestroy { | ||||
|   blocks$: Observable<AccelerationBlock[]>; | ||||
|   accelerations$: Observable<Acceleration[]>; | ||||
|   pendingAccelerations$: Observable<Acceleration[]>; | ||||
| @ -39,6 +39,8 @@ export class AcceleratorDashboardComponent implements OnInit { | ||||
|   firstLoad = true; | ||||
|   timespan: '3d' | '1w' | '1m' = '1w'; | ||||
| 
 | ||||
|   accelerationDeltaSubscription: Subscription; | ||||
| 
 | ||||
|   graphHeight: number = 300; | ||||
|   theme: ThemeService; | ||||
| 
 | ||||
| @ -59,27 +61,28 @@ export class AcceleratorDashboardComponent implements OnInit { | ||||
|   ngOnInit(): void { | ||||
|     this.onResize(); | ||||
|     this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); | ||||
|     this.websocketService.startTrackAccelerations(); | ||||
| 
 | ||||
|     this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe( | ||||
|       startWith(true), | ||||
|       switchMap(() => { | ||||
|         return this.serviceApiServices.getAccelerations$().pipe( | ||||
|           catchError(() => { | ||||
|             return of([]); | ||||
|           }), | ||||
|         ); | ||||
|       }), | ||||
|       tap(accelerations => { | ||||
|         if (!this.firstLoad && accelerations.some(acc => !this.seen.has(acc.txid))) { | ||||
|           this.audioService.playSound('bright-harmony'); | ||||
|         } | ||||
|         for(const acc of accelerations) { | ||||
|           this.seen.add(acc.txid); | ||||
|         } | ||||
|         this.firstLoad = false; | ||||
|       }), | ||||
|     this.pendingAccelerations$ = this.stateService.liveAccelerations$.pipe( | ||||
|       share(), | ||||
|     ); | ||||
|     this.accelerationDeltaSubscription = this.stateService.accelerations$.subscribe((delta) => { | ||||
|       if (!delta.reset) { | ||||
|         let hasNewAcceleration = false; | ||||
|         for (const acc of delta.added) { | ||||
|           if (!this.seen.has(acc.txid)) { | ||||
|             hasNewAcceleration = true; | ||||
|           } | ||||
|           this.seen.add(acc.txid); | ||||
|         } | ||||
|         for (const txid of delta.removed) { | ||||
|           this.seen.delete(txid); | ||||
|         } | ||||
|         if (hasNewAcceleration) { | ||||
|           this.audioService.playSound('bright-harmony'); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     this.accelerations$ = this.stateService.chainTip$.pipe( | ||||
|       distinctUntilChanged(), | ||||
| @ -145,7 +148,7 @@ export class AcceleratorDashboardComponent implements OnInit { | ||||
|     } else { | ||||
|       const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
 | ||||
|       const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; | ||||
|       return this.theme.theme === 'contrast' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1]; | ||||
|       return this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1]; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -154,6 +157,11 @@ export class AcceleratorDashboardComponent implements OnInit { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.accelerationDeltaSubscription.unsubscribe(); | ||||
|     this.websocketService.stopTrackAccelerations(); | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     if (window.innerWidth >= 992) { | ||||
|  | ||||
| @ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core | ||||
| import { Observable, of } from 'rxjs'; | ||||
| import { switchMap } from 'rxjs/operators'; | ||||
| import { Acceleration } from '../../../interfaces/node-api.interface'; | ||||
| import { ServicesApiServices } from '../../../services/services-api.service'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-pending-stats', | ||||
| @ -15,11 +16,12 @@ export class PendingStatsComponent implements OnInit { | ||||
|   public accelerationStats$: Observable<any>; | ||||
| 
 | ||||
|   constructor( | ||||
|     private servicesApiService: ServicesApiServices, | ||||
|     private stateService: StateService, | ||||
|     private websocketService: WebsocketService, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe( | ||||
|     this.accelerationStats$ = (this.accelerations$ || this.stateService.liveAccelerations$).pipe( | ||||
|       switchMap(accelerations => { | ||||
|         let totalAccelerations = 0; | ||||
|         let totalFeeDelta = 0; | ||||
|  | ||||
| @ -1,12 +1,6 @@ | ||||
| <app-indexing-progress *ngIf="!widget"></app-indexing-progress> | ||||
| 
 | ||||
| <div [class.full-container]="!widget"> | ||||
|   <div *ngIf="!widget" class="card-header mb-0 mb-md-2"> | ||||
|     <div class="d-flex d-md-block align-items-baseline"> | ||||
|       <span i18n="address.balance-history">Balance History</span> | ||||
|     </div>   | ||||
|   </div> | ||||
| 
 | ||||
|   <ng-container *ngIf="!error"> | ||||
|     <div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||
|       (chartInit)="onChartInit($event)"> | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
| @ -45,23 +46,8 @@ | ||||
|   display: flex; | ||||
|   flex: 1; | ||||
|   width: 100%; | ||||
|   padding-bottom: 20px; | ||||
|   padding-bottom: 10px; | ||||
|   padding-right: 10px; | ||||
|   @media (max-width: 992px) { | ||||
|     padding-bottom: 25px; | ||||
|   } | ||||
|   @media (max-width: 829px) { | ||||
|     padding-bottom: 50px; | ||||
|   } | ||||
|   @media (max-width: 767px) { | ||||
|     padding-bottom: 25px; | ||||
|   } | ||||
|   @media (max-width: 629px) { | ||||
|     padding-bottom: 55px; | ||||
|   } | ||||
|   @media (max-width: 567px) { | ||||
|     padding-bottom: 55px; | ||||
|   } | ||||
| } | ||||
| .chart-widget { | ||||
|   width: 100%; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; | ||||
| import { echarts, EChartsOption } from '../../graphs/echarts'; | ||||
| import { Observable, of } from 'rxjs'; | ||||
| import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; | ||||
| import { catchError } from 'rxjs/operators'; | ||||
| import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| @ -32,7 +32,7 @@ const periodSeconds = { | ||||
|   `],
 | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AddressGraphComponent implements OnChanges { | ||||
| export class AddressGraphComponent implements OnChanges, OnDestroy { | ||||
|   @Input() address: string; | ||||
|   @Input() isPubkey: boolean = false; | ||||
|   @Input() stats: ChainStats; | ||||
| @ -46,6 +46,9 @@ export class AddressGraphComponent implements OnChanges { | ||||
|   data: any[] = []; | ||||
|   hoverData: any[] = []; | ||||
| 
 | ||||
|   subscription: Subscription; | ||||
|   redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false); | ||||
| 
 | ||||
|   chartOptions: EChartsOption = {}; | ||||
|   chartInitOptions = { | ||||
|     renderer: 'svg', | ||||
| @ -70,24 +73,38 @@ export class AddressGraphComponent implements OnChanges { | ||||
|     if (!this.address || !this.stats) { | ||||
|       return; | ||||
|     } | ||||
|     (this.addressSummary$ || (this.isPubkey | ||||
|       ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') | ||||
|       : this.electrsApiService.getAddressSummary$(this.address)).pipe( | ||||
|       catchError(e => { | ||||
|         this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`; | ||||
|         return of(null); | ||||
|       }), | ||||
|     )).subscribe(addressSummary => { | ||||
|       if (addressSummary) { | ||||
|         this.error = null; | ||||
|         this.prepareChartOptions(addressSummary); | ||||
|     if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { | ||||
|       if (this.subscription) { | ||||
|         this.subscription.unsubscribe(); | ||||
|       } | ||||
|       this.isLoading = false; | ||||
|       this.cd.markForCheck(); | ||||
|     }); | ||||
|       this.subscription = combineLatest([ | ||||
|         this.redraw$, | ||||
|         (this.addressSummary$ || (this.isPubkey | ||||
|           ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') | ||||
|           : this.electrsApiService.getAddressSummary$(this.address)).pipe( | ||||
|           catchError(e => { | ||||
|             this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`; | ||||
|             return of(null); | ||||
|           }), | ||||
|         )) | ||||
|       ]).subscribe(([redraw, addressSummary]) => { | ||||
|         if (addressSummary) { | ||||
|           this.error = null; | ||||
|           this.prepareChartOptions(addressSummary); | ||||
|         } | ||||
|         this.isLoading = false; | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
|     } else { | ||||
|       // re-trigger subscription
 | ||||
|       this.redraw$.next(true); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   prepareChartOptions(summary): void { | ||||
|     if (!summary || !this.stats) { | ||||
|       return; | ||||
|     } | ||||
|     let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); | ||||
|     this.data = summary.map(d => { | ||||
|       const balance = total; | ||||
| @ -104,8 +121,8 @@ export class AddressGraphComponent implements OnChanges { | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] || d.value[1])), 0); | ||||
|     const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] || d.value[1])), maxValue); | ||||
|     const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); | ||||
|     const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue); | ||||
| 
 | ||||
|     this.chartOptions = { | ||||
|       color: [ | ||||
| @ -230,6 +247,12 @@ export class AddressGraphComponent implements OnChanges { | ||||
|     this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     if (this.subscription) { | ||||
|       this.subscription.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   isMobile() { | ||||
|     return (window.innerWidth <= 767.98); | ||||
|   } | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| } | ||||
| 
 | ||||
| .qr-wrapper { | ||||
|   background-color: var(--fg); | ||||
|   background-color: #fff; | ||||
|   padding: 10px; | ||||
|   padding-bottom: 5px; | ||||
|   display: inline-block; | ||||
|  | ||||
| @ -53,10 +53,20 @@ | ||||
| 
 | ||||
|     <ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2"> | ||||
|       <br> | ||||
|       <div class="title-tx"> | ||||
|         <h2 class="text-left" i18n="address.balance-history">Balance History</h2> | ||||
|       </div> | ||||
|       <div class="box"> | ||||
|         <div class="widget-toggler" *ngIf="showBalancePeriod()"> | ||||
|           <a href="" (click)="setBalancePeriod('all')" class="toggler-option" | ||||
|             [ngClass]="{'inactive': balancePeriod === 'all'}"><small i18n="all">all</small></a> | ||||
|           <span style="color: var(--transparent-fg); font-size: 8px"> | </span> | ||||
|           <a href="" (click)="setBalancePeriod('1m')" class="toggler-option" | ||||
|             [ngClass]="{'inactive': balancePeriod === '1m'}"><small i18n="recent">recent</small></a> | ||||
|         </div> | ||||
|         <div class="row"> | ||||
|           <div class="col-md"> | ||||
|             <app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" /> | ||||
|             <app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| .qr-wrapper { | ||||
|   background-color: var(--fg); | ||||
|   background-color: #fff; | ||||
|   padding: 10px; | ||||
|   padding-bottom: 5px; | ||||
|   display: inline-block; | ||||
| @ -109,3 +109,19 @@ h1 { | ||||
|     flex-grow: 0.5; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .widget-toggler { | ||||
|   font-size: 12px; | ||||
|   position: absolute; | ||||
|   top: -20px; | ||||
|   right: 3px; | ||||
|   text-align: right; | ||||
| } | ||||
| 
 | ||||
| .toggler-option { | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .inactive { | ||||
|   color: var(--transparent-fg); | ||||
| } | ||||
| @ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|   txCount = 0; | ||||
|   received = 0; | ||||
|   sent = 0; | ||||
|   now = Date.now() / 1000; | ||||
|   balancePeriod: 'all' | '1m' = 'all'; | ||||
| 
 | ||||
|   private tempTransactions: Transaction[]; | ||||
|   private timeTxIndexes: number[]; | ||||
| @ -175,6 +177,10 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|         this.transactions = this.tempTransactions; | ||||
|         if (this.transactions.length === this.txCount) this.fullyLoaded = true; | ||||
|         this.isLoadingTransactions = false; | ||||
| 
 | ||||
|         if (!this.showBalancePeriod()) { | ||||
|           this.setBalancePeriod('all'); | ||||
|         } | ||||
|       }, | ||||
|       (error) => { | ||||
|         console.log(error); | ||||
| @ -297,6 +303,18 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|     this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count; | ||||
|   } | ||||
| 
 | ||||
|   setBalancePeriod(period: 'all' | '1m'): boolean { | ||||
|     this.balancePeriod = period; | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   showBalancePeriod(): boolean { | ||||
|     return this.transactions?.length && ( | ||||
|       !this.transactions[0].status?.confirmed | ||||
|       || this.transactions[0].status.block_time > (this.now - (60 * 60 * 24 * 30)) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.mainSubscription.unsubscribe(); | ||||
|     this.mempoolTxSubscription.unsubscribe(); | ||||
|  | ||||
| @ -43,5 +43,6 @@ | ||||
|   <ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'testnet'">t</ng-template> | ||||
|   <ng-template [ngIf]="network === 'testnet4'">t</ng-template> | ||||
|   <ng-template [ngIf]="network === 'signet'">s</ng-template> | ||||
| </ng-template> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| .qr-wrapper { | ||||
|   background-color: var(--fg); | ||||
|   background-color: #fff; | ||||
|   padding: 10px; | ||||
|   padding-bottom: 5px; | ||||
|   display: inline-block; | ||||
|  | ||||
| @ -57,8 +57,9 @@ export class BalanceWidgetComponent implements OnInit, OnChanges { | ||||
|   calculateStats(summary: AddressTxSummary[]): void { | ||||
|     let weekTotal = 0; | ||||
|     let monthTotal = 0; | ||||
|     const weekAgo = (Date.now() / 1000) - (60 * 60 * 24 * 7); | ||||
|     const monthAgo = (Date.now() / 1000) - (60 * 60 * 24 * 30); | ||||
| 
 | ||||
|     const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000; | ||||
|     const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000; | ||||
|     for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) { | ||||
|       monthTotal += summary[i].value; | ||||
|       if (summary[i].time >= weekAgo) { | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -81,6 +81,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|   tooltipPosition: Position; | ||||
| 
 | ||||
|   readyNextFrame = false; | ||||
|   lastUpdate: number = 0; | ||||
|   pendingUpdate: { | ||||
|     count: number, | ||||
|     add: { [txid: string]: TransactionStripped }, | ||||
|     remove: { [txid: string]: string }, | ||||
|     change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } }, | ||||
|     direction?: string, | ||||
|   } = { | ||||
|     count: 0, | ||||
|     add: {}, | ||||
|     remove: {}, | ||||
|     change: {}, | ||||
|     direction: 'left', | ||||
|   }; | ||||
| 
 | ||||
|   searchText: string; | ||||
|   searchSubscription: Subscription; | ||||
| @ -176,6 +190,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|   destroy(): void { | ||||
|     if (this.scene) { | ||||
|       this.scene.destroy(); | ||||
|       this.clearUpdateQueue(); | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
| @ -188,6 +203,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|     } | ||||
|     this.filtersAvailable = filtersAvailable; | ||||
|     if (this.scene) { | ||||
|       this.clearUpdateQueue(); | ||||
|       this.scene.setup(transactions); | ||||
|       this.readyNextFrame = true; | ||||
|       this.start(); | ||||
| @ -197,6 +213,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
| 
 | ||||
|   enter(transactions: TransactionStripped[], direction: string): void { | ||||
|     if (this.scene) { | ||||
|       this.clearUpdateQueue(); | ||||
|       this.scene.enter(transactions, direction); | ||||
|       this.start(); | ||||
|       this.updateSearchHighlight(); | ||||
| @ -205,6 +222,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
| 
 | ||||
|   exit(direction: string): void { | ||||
|     if (this.scene) { | ||||
|       this.clearUpdateQueue(); | ||||
|       this.scene.exit(direction); | ||||
|       this.start(); | ||||
|       this.updateSearchHighlight(); | ||||
| @ -213,13 +231,67 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
| 
 | ||||
|   replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void { | ||||
|     if (this.scene) { | ||||
|       this.clearUpdateQueue(); | ||||
|       this.scene.replace(transactions || [], direction, sort, startTime); | ||||
|       this.start(); | ||||
|       this.updateSearchHighlight(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // collates deferred updates into a set of consistent pending changes
 | ||||
|   queueUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void { | ||||
|     for (const tx of add) { | ||||
|       this.pendingUpdate.add[tx.txid] = tx; | ||||
|       delete this.pendingUpdate.remove[tx.txid]; | ||||
|       delete this.pendingUpdate.change[tx.txid]; | ||||
|     } | ||||
|     for (const txid of remove) { | ||||
|       delete this.pendingUpdate.add[txid]; | ||||
|       this.pendingUpdate.remove[txid] = txid; | ||||
|       delete this.pendingUpdate.change[txid]; | ||||
|     } | ||||
|     for (const tx of change) { | ||||
|       if (this.pendingUpdate.add[tx.txid]) { | ||||
|         this.pendingUpdate.add[tx.txid].rate = tx.rate; | ||||
|         this.pendingUpdate.add[tx.txid].acc = tx.acc; | ||||
|       } else { | ||||
|         this.pendingUpdate.change[tx.txid] = tx; | ||||
|       } | ||||
|     } | ||||
|     this.pendingUpdate.direction = direction; | ||||
|     this.pendingUpdate.count++; | ||||
|   } | ||||
| 
 | ||||
|   deferredUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void { | ||||
|     this.queueUpdate(add, remove, change, direction); | ||||
|     this.applyQueuedUpdates(); | ||||
|   } | ||||
| 
 | ||||
|   applyQueuedUpdates(): void { | ||||
|     if (this.pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) { | ||||
|       this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), this.pendingUpdate.direction); | ||||
|       this.clearUpdateQueue(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   clearUpdateQueue(): void { | ||||
|     this.pendingUpdate = { | ||||
|       count: 0, | ||||
|       add: {}, | ||||
|       remove: {}, | ||||
|       change: {}, | ||||
|     }; | ||||
|     this.lastUpdate = performance.now(); | ||||
|   } | ||||
| 
 | ||||
|   update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { | ||||
|     // merge any pending changes into this update
 | ||||
|     this.queueUpdate(add, remove, change); | ||||
|     this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), direction, resetLayout); | ||||
|     this.clearUpdateQueue(); | ||||
|   } | ||||
| 
 | ||||
|   applyUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { | ||||
|     if (this.scene) { | ||||
|       add = add.filter(tx => !this.scene.txs[tx.txid]); | ||||
|       remove = remove.filter(txid => this.scene.txs[txid]); | ||||
| @ -230,6 +302,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|       } | ||||
|       this.scene.update(add, remove, change, direction, resetLayout); | ||||
|       this.start(); | ||||
|       this.lastUpdate = performance.now(); | ||||
|       this.updateSearchHighlight(); | ||||
|     } | ||||
|   } | ||||
| @ -370,6 +443,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|     if (!now) { | ||||
|       now = performance.now(); | ||||
|     } | ||||
|     this.applyQueuedUpdates(); | ||||
|     // skip re-render if there's no change to the scene
 | ||||
|     if (this.scene && this.gl) { | ||||
|       /* SET UP SHADER UNIFORMS */ | ||||
| @ -577,13 +651,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|   getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) { | ||||
|     return (tx: TxView) => { | ||||
|       if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) { | ||||
|         if (this.themeService.theme !== 'contrast') { | ||||
|         if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') { | ||||
|           return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)); | ||||
|         } else { | ||||
|           return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)); | ||||
|         } | ||||
|       } else { | ||||
|         if (this.themeService.theme !== 'contrast') { | ||||
|         if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') { | ||||
|           return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction( | ||||
|             tx, | ||||
|             defaultColors.unmatchedfee, | ||||
|  | ||||
| @ -13,7 +13,7 @@ export default class BlockScene { | ||||
|   theme: ThemeService; | ||||
|   orientation: string; | ||||
|   flip: boolean; | ||||
|   animationDuration: number = 900; | ||||
|   animationDuration: number = 1000; | ||||
|   configAnimationOffset: number | null; | ||||
|   animationOffset: number; | ||||
|   highlightingEnabled: boolean; | ||||
| @ -69,7 +69,7 @@ export default class BlockScene { | ||||
|   } | ||||
| 
 | ||||
|   setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { | ||||
|     this.theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction; | ||||
|     this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction; | ||||
|     this.updateAllColors(); | ||||
|   } | ||||
| 
 | ||||
| @ -179,7 +179,7 @@ export default class BlockScene { | ||||
|       removed.forEach(tx => { | ||||
|         tx.destroy(); | ||||
|       }); | ||||
|     }, 1000); | ||||
|     }, (startTime - performance.now()) + this.animationDuration + 1000); | ||||
| 
 | ||||
|     if (resetLayout) { | ||||
|       add.forEach(tx => { | ||||
| @ -239,14 +239,14 @@ export default class BlockScene { | ||||
|       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||
|   ): void { | ||||
|     this.animationDuration = animationDuration || 1000; | ||||
|     this.animationDuration = animationDuration || this.animationDuration || 1000; | ||||
|     this.configAnimationOffset = animationOffset; | ||||
|     this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset; | ||||
|     this.orientation = orientation; | ||||
|     this.flip = flip; | ||||
|     this.vertexArray = vertexArray; | ||||
|     this.highlightingEnabled = highlighting; | ||||
|     theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction; | ||||
|     theme.theme === 'contrast' || theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction; | ||||
|     this.theme = theme; | ||||
| 
 | ||||
|     this.scene = { | ||||
|  | ||||
| @ -177,7 +177,7 @@ export function ageColorFunction( | ||||
|     return auditColors.accelerated; | ||||
|   } | ||||
| 
 | ||||
|   const color = theme !== 'contrast' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime); | ||||
|   const color = theme !== 'contrast' && theme !== 'bukele' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime); | ||||
| 
 | ||||
|   const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60))))))); | ||||
|   return { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| .block-overview-tooltip { | ||||
|   position: absolute; | ||||
|   background: rgba(#11131f, 0.95); | ||||
| 	background: color-mix(in srgb, var(--active-bg) 95%, transparent); | ||||
|   border-radius: 4px; | ||||
|   box-shadow: 1px 1px 10px rgba(0,0,0,0.5); | ||||
|   color: var(--tooltip-grey); | ||||
| @ -30,7 +30,7 @@ th, td { | ||||
| } | ||||
| 
 | ||||
| .badge.badge-accelerated { | ||||
|   background-color: var(--tertiary); | ||||
|   background-color: #653b9c; | ||||
|   box-shadow: #ad7de57f 0px 0px 12px -2px; | ||||
|   color: white; | ||||
|   animation: acceleratePulse 1s infinite; | ||||
| @ -71,7 +71,7 @@ th, td { | ||||
| } | ||||
| 
 | ||||
| @keyframes acceleratePulse { | ||||
|   0% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; } | ||||
|   0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; } | ||||
|   50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;} | ||||
|   100% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; } | ||||
|   100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; } | ||||
| } | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -136,7 +136,12 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { | ||||
|                   return of(transactions); | ||||
|                 }) | ||||
|               ), | ||||
|             this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) : of([]) | ||||
|             this.stateService.env.ACCELERATOR === true && block.height > 819500 | ||||
|               ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) | ||||
|                 .pipe(catchError(() => { | ||||
|                   return of([]); | ||||
|                 })) | ||||
|               : of([]) | ||||
|           ]); | ||||
|         } | ||||
|       ), | ||||
|  | ||||
| @ -345,7 +345,12 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|                 return of(null); | ||||
|               }) | ||||
|             ), | ||||
|           this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) : of([]) | ||||
|           this.stateService.env.ACCELERATOR === true && block.height > 819500 | ||||
|             ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) | ||||
|               .pipe(catchError(() => { | ||||
|                 return of([]); | ||||
|               })) | ||||
|             : of([]) | ||||
|         ]); | ||||
|       }) | ||||
|     ) | ||||
|  | ||||
| @ -63,7 +63,7 @@ | ||||
| .fee-span { | ||||
|   font-size: 11px; | ||||
|   margin-bottom: 5px; | ||||
|   color: #fff000; | ||||
|   color: var(--yellow); | ||||
| } | ||||
| 
 | ||||
| .transaction-count { | ||||
| @ -130,7 +130,7 @@ | ||||
|   height: 0; | ||||
|   border-left: calc(var(--block-size) * 0.3) solid transparent; | ||||
|   border-right: calc(var(--block-size) * 0.3) solid transparent; | ||||
|   border-bottom: calc(var(--block-size) * 0.3) solid #FFF; | ||||
|   border-bottom: calc(var(--block-size) * 0.3) solid var(--fg); | ||||
| } | ||||
| 
 | ||||
| .flashing { | ||||
|  | ||||
| @ -70,6 +70,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     liquid: ['var(--liquid)', 'var(--testnet-alt)'], | ||||
|     'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'], | ||||
|     testnet: ['var(--testnet)', 'var(--testnet-alt)'], | ||||
|     testnet4: ['var(--testnet)', 'var(--testnet-alt)'], | ||||
|     signet: ['var(--signet)', 'var(--signet-alt)'], | ||||
|   }; | ||||
| 
 | ||||
| @ -349,7 +350,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     return { | ||||
|       left: addLeft + this.blockOffset * index + 'px', | ||||
|       background: `repeating-linear-gradient(
 | ||||
|         #2d3348, | ||||
|         var(--secondary), | ||||
|         var(--secondary) ${greenBackgroundHeight}%, | ||||
|         ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, | ||||
|         ${this.gradientColors[this.network][1]} 100% | ||||
| @ -361,7 +362,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   convertStyleForLoadingBlock(style) { | ||||
|     return { | ||||
|       ...style, | ||||
|       background: "#2d3348", | ||||
|       background: "var(--secondary)", | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
| @ -370,7 +371,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
| 
 | ||||
|     return { | ||||
|       left: addLeft + (this.blockOffset * index) + 'px', | ||||
|       background: "#2d3348", | ||||
|       background: "var(--secondary)", | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -54,7 +54,7 @@ | ||||
| } | ||||
| 
 | ||||
| .time-toggle { | ||||
|   color: white; | ||||
|   color: var(--fg); | ||||
|   font-size: 0.8rem; | ||||
|   position: absolute; | ||||
|   bottom: -1.8em; | ||||
| @ -68,7 +68,7 @@ | ||||
| } | ||||
| 
 | ||||
| .block-display-toggle { | ||||
|   color: white; | ||||
|   color: var(--fg); | ||||
|   font-size: 0.8rem; | ||||
|   position: absolute; | ||||
|   bottom: 15.8em; | ||||
|  | ||||
| @ -55,7 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { | ||||
|     firstValueFrom(this.stateService.chainTip$).then(() => { | ||||
|       this.loadingTip = false; | ||||
|     }); | ||||
|     this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'size'; | ||||
|     this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'fees'; | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|  | ||||
| @ -32,11 +32,12 @@ export class ClockComponent implements OnInit { | ||||
|   limitHeight: number; | ||||
| 
 | ||||
|   gradientColors = { | ||||
|     '': ['#9339f4', '#105fb0'], | ||||
|     liquid: ['#116761', '#183550'], | ||||
|     'liquidtestnet': ['#494a4a', '#272e46'], | ||||
|     testnet: ['#1d486f', '#183550'], | ||||
|     signet: ['#6f1d5d', '#471850'], | ||||
|     '': ['var(--mainnet-alt)', 'var(--primary)'], | ||||
|     liquid: ['var(--liquid)', 'var(--testnet-alt)'], | ||||
|     'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'], | ||||
|     testnet: ['var(--testnet)', 'var(--testnet-alt)'], | ||||
|     testnet4: ['var(--testnet)', 'var(--testnet-alt)'], | ||||
|     signet: ['var(--signet)', 'var(--signet-alt)'], | ||||
|   }; | ||||
| 
 | ||||
|   constructor( | ||||
| @ -99,8 +100,8 @@ export class ClockComponent implements OnInit { | ||||
| 
 | ||||
|     return { | ||||
|       background: `repeating-linear-gradient(
 | ||||
|         #2d3348, | ||||
|         #2d3348 ${greenBackgroundHeight}%, | ||||
|         var(--secondary), | ||||
|         var(--secondary) ${greenBackgroundHeight}%, | ||||
|         ${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%, | ||||
|         ${this.gradientColors[''][1]} 100% | ||||
|       )`,
 | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
|     @for (widget of widgets; track widget.component) { | ||||
|       @switch (widget.component) { | ||||
|         @case ('fees') { | ||||
|           <div class="col card-wrapper"> | ||||
|           <div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div> | ||||
|             <div class="card"> | ||||
|               <div class="card-body less-padding"> | ||||
| @ -14,12 +14,12 @@ | ||||
|           </div> | ||||
|         } | ||||
|         @case ('difficulty') { | ||||
|           <div class="col"> | ||||
|           <div class="col" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <app-difficulty></app-difficulty> | ||||
|           </div> | ||||
|         } | ||||
|         @case ('goggles') { | ||||
|           <div class="col"> | ||||
|           <div class="col" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card graph-card"> | ||||
|               <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> | ||||
|                 <a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]"> | ||||
| @ -48,7 +48,7 @@ | ||||
|           </div> | ||||
|         } | ||||
|         @case ('incoming') { | ||||
|           <div class="col"> | ||||
|           <div class="col" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card graph-card"> | ||||
|               <div class="card-body"> | ||||
|                 <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> | ||||
| @ -93,7 +93,7 @@ | ||||
|           </ng-template> | ||||
|         } | ||||
|         @case ('replacements') { | ||||
|           <div class="col" style="max-height: 410px"> | ||||
|           <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card"> | ||||
|               <div class="card-body"> | ||||
|                 <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]"> | ||||
| @ -140,7 +140,7 @@ | ||||
|           </ng-template> | ||||
|         } | ||||
|         @case ('blocks') { | ||||
|           <div class="col" style="max-height: 410px"> | ||||
|           <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card"> | ||||
|               <div class="card-body"> | ||||
|                 <a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]"> | ||||
| @ -184,7 +184,7 @@ | ||||
|           </ng-template> | ||||
|         } | ||||
|         @case ('transactions') { | ||||
|           <div class="col" style="max-height: 410px"> | ||||
|           <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card"> | ||||
|               <div class="card-body"> | ||||
|                 <h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5> | ||||
| @ -224,13 +224,13 @@ | ||||
|           </ng-template> | ||||
|         } | ||||
|         @case ('balance') { | ||||
|           <div class="col card-wrapper"> | ||||
|           <div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="main-title" i18n="dashboard.treasury">Treasury</div> | ||||
|             <app-balance-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-balance-widget> | ||||
|           </div> | ||||
|         } | ||||
|         @case ('address') { | ||||
|           <div class="col" style="max-height: 410px"> | ||||
|           <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card graph-card"> | ||||
|               <div class="card-body"> | ||||
|                 <a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]"> | ||||
| @ -238,13 +238,13 @@ | ||||
|                   <span> </span> | ||||
|                   <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon> | ||||
|                 </a> | ||||
|                 <app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address?.chain_stats" [widget]="true" [height]="graphHeight"></app-address-graph> | ||||
|                 <app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [height]="graphHeight"></app-address-graph> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         } | ||||
|         @case ('addressTransactions') { | ||||
|           <div class="col" style="max-height: 410px"> | ||||
|           <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card"> | ||||
|               <div class="card-body"> | ||||
|                 <a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]"> | ||||
| @ -257,6 +257,22 @@ | ||||
|             </div> | ||||
|           </div> | ||||
|         } | ||||
|         @case ('twitter') { | ||||
|           <div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8"> | ||||
|             <div class="card graph-card"> | ||||
|               <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2 d-flex flex-column"> | ||||
|                 <a class="title-link" [href]="'https://x.com/' + widget.props?.handle" target="_blank"> | ||||
|                   <h5 class="card-title d-inline" i18n="dashboard.x-timeline">X Timeline</h5> | ||||
|                   <span> </span> | ||||
|                   <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon> | ||||
|                 </a> | ||||
|                 @defer { | ||||
|                   <app-twitter-widget [handle]="widget.props?.handle" style="flex-grow: 1"></app-twitter-widget> | ||||
|                 } | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   </div> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; | ||||
| import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; | ||||
| import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs'; | ||||
| import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; | ||||
| import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface'; | ||||
| @ -57,6 +57,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni | ||||
|   incomingGraphHeight: number = 300; | ||||
|   graphHeight: number = 300; | ||||
|   webGlEnabled = true; | ||||
|   isMobile: boolean = window.innerWidth <= 767.98; | ||||
| 
 | ||||
|   widgets; | ||||
| 
 | ||||
| @ -85,6 +86,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private websocketService: WebsocketService, | ||||
|     private seoService: SeoService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|     @Inject(PLATFORM_ID) private platformId: Object, | ||||
|   ) { | ||||
|     this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); | ||||
| @ -230,8 +232,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni | ||||
|             this.stateService.live2Chart$ | ||||
|               .pipe( | ||||
|                 scan((acc, stats) => { | ||||
|                   const now = Date.now() / 1000; | ||||
|                   const start = now - (2 * 60 * 60); | ||||
|                   acc.unshift(stats); | ||||
|                   acc = acc.slice(0, 120); | ||||
|                   acc = acc.filter(p => p.added >= start); | ||||
|                   return acc; | ||||
|                 }, (mempoolStats || [])) | ||||
|               ), | ||||
| @ -283,8 +287,8 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni | ||||
| 
 | ||||
|   startAddressSubscription(): void { | ||||
|     if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) { | ||||
|       const address = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address; | ||||
|       const addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) ? address.toLowerCase() : address; | ||||
|       let addressString = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address; | ||||
|       addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(addressString)) ? addressString.toLowerCase() : addressString; | ||||
|        | ||||
|       this.addressSubscription = ( | ||||
|         addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/) | ||||
| @ -299,6 +303,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni | ||||
|         ).subscribe((address: Address) => { | ||||
|           this.websocketService.startTrackAddress(address.address); | ||||
|           this.address = address; | ||||
|           this.cd.markForCheck(); | ||||
|         }); | ||||
| 
 | ||||
|       this.addressSummary$ = ( | ||||
| @ -368,5 +373,6 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni | ||||
|       this.goggleResolution = 86; | ||||
|       this.graphHeight = 310; | ||||
|     } | ||||
|     this.isMobile = window.innerWidth <= 767.98; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -119,7 +119,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| .difficulty-tooltip { | ||||
|   position: fixed; | ||||
|   background: rgba(#11131f, 0.95); | ||||
|   background: color-mix(in srgb, var(--active-bg) 95%, transparent); | ||||
|   border-radius: 4px; | ||||
|   box-shadow: 1px 1px 10px rgba(0,0,0,0.5); | ||||
|   color: #b1b1b1; | ||||
|   color: var(--tooltip-grey); | ||||
|   padding: 10px 15px; | ||||
|   text-align: left; | ||||
|   pointer-events: none; | ||||
|  | ||||
| @ -15,8 +15,8 @@ | ||||
|           <svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none"> | ||||
|             <defs> | ||||
|               <linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse"> | ||||
|                 <stop offset="0%" stop-color="#105fb0" /> | ||||
|                 <stop offset="100%" stop-color="#9339f4" /> | ||||
|                 <stop offset="0%" stop-color="var(--primary)" /> | ||||
|                 <stop offset="100%" stop-color="var(--mainnet-alt)" /> | ||||
|               </linearGradient> | ||||
|               <linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse"> | ||||
|                 <stop offset="0%" stop-color="#2486eb" /> | ||||
|  | ||||
| @ -128,7 +128,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
| @ -223,7 +224,7 @@ | ||||
|     height: 100%; | ||||
|   } | ||||
|   .background { | ||||
|     background: linear-gradient(to right, var(--primary), #9339f4); | ||||
|     background: linear-gradient(to right, var(--primary), var(--mainnet-alt)); | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|   } | ||||
|  | ||||
| @ -79,7 +79,7 @@ | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   transition: background-color 1s; | ||||
|   color: var(--color-fg); | ||||
|   color: #fff; | ||||
|   &.priority { | ||||
|     @media (767px < width < 992px), (width < 576px) { | ||||
|       width: 100%; | ||||
|  | ||||
| @ -16,8 +16,8 @@ export class FeesBoxComponent implements OnInit, OnDestroy { | ||||
|   isLoading$: Observable<boolean>; | ||||
|   recommendedFees$: Observable<Recommendedfees>; | ||||
|   themeSubscription: Subscription; | ||||
|   gradient = 'linear-gradient(to right, #2e324e, #2e324e)'; | ||||
|   noPriority = '#2e324e'; | ||||
|   gradient = 'linear-gradient(to right, var(--skeleton-bg), var(--skeleton-bg))'; | ||||
|   noPriority = 'var(--skeleton-bg)'; | ||||
|   fees: Recommendedfees; | ||||
| 
 | ||||
|   constructor( | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -11,7 +11,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -66,7 +66,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On | ||||
|     if (!this.data) { | ||||
|       return; | ||||
|     } | ||||
|     this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference'); | ||||
|     this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h'; | ||||
|     const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8)); | ||||
|     this.MA = this.calculateMA(this.data.series[0], windowSize); | ||||
|     if (this.outlierCappingEnabled === true) { | ||||
| @ -216,22 +216,19 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On | ||||
|           type: 'line', | ||||
|         }, | ||||
|         formatter: (params: any) => { | ||||
|           const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue); | ||||
|           const bestItem = params.reduce((best, item) => { | ||||
|             return (item.seriesName === 'data' && (!best || best.value[1] < item.value[1])) ? item : best; | ||||
|           }, null); | ||||
|           const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, bestItem.axisValue); | ||||
|           const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`; | ||||
|           let itemFormatted = '<div class="title">' + axisValueLabel + '</div>'; | ||||
|           params.map((item: any, index: number) => { | ||||
| 
 | ||||
|             //Do no include MA in tooltip legend!
 | ||||
|             if (item.seriesName !== 'MA') { | ||||
|               if (index < 26) { | ||||
|                 itemFormatted += `<div class="item">
 | ||||
|                   <div class="indicator-container">${colorSpan(item.color)}</div> | ||||
|           if (bestItem) { | ||||
|             itemFormatted += `<div class="item">
 | ||||
|                   <div class="indicator-container">${colorSpan(bestItem.color)}</div> | ||||
|                   <div class="grow"></div> | ||||
|                   <div class="value">${formatNumber(item.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div> | ||||
|                   <div class="value">${formatNumber(bestItem.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div> | ||||
|                 </div>`;
 | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|           } | ||||
|           return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}" 
 | ||||
|                   style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
 | ||||
|         } | ||||
|  | ||||
| @ -51,7 +51,8 @@ | ||||
|     <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}"> | ||||
|       <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a> | ||||
|       <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a> | ||||
|       <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a> | ||||
|       <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a> | ||||
|       <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a> | ||||
|       <h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6> | ||||
|       <a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a> | ||||
|       <a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a> | ||||
|  | ||||
| @ -4,6 +4,7 @@ | ||||
|   top: 0; | ||||
|   width: 100%; | ||||
|   z-index: 100; | ||||
|   background-color: var(--bg); | ||||
| } | ||||
| 
 | ||||
| li.nav-item.active { | ||||
| @ -17,7 +18,7 @@ fa-icon { | ||||
| .navbar { | ||||
|   z-index: 100; | ||||
|   min-height: 64px; | ||||
|   background-color: var(--bg); | ||||
|   background-color: var(--nav-bg); | ||||
| } | ||||
| 
 | ||||
| li.nav-item { | ||||
| @ -48,7 +49,7 @@ li.nav-item { | ||||
| } | ||||
| 
 | ||||
| .navbar-nav { | ||||
|   background: var(--navbar-bg); | ||||
|   background: var(--nav-bg); | ||||
|   bottom: 0; | ||||
|   box-shadow: 0px 0px 15px 0px #000; | ||||
|   flex-direction: row; | ||||
| @ -169,4 +170,8 @@ nav { | ||||
|     margin-left: 5px; | ||||
|     margin-right: 0px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .beta-network { | ||||
|   font-size: 8px; | ||||
| } | ||||
| @ -6,7 +6,7 @@ | ||||
|         <img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> | ||||
|       } | ||||
|       @if (enterpriseInfo?.header_img) { | ||||
|         <img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> | ||||
|         <img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="60px" class="mr-3"> | ||||
|       } @else { | ||||
|         <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images> | ||||
|         <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images> | ||||
| @ -15,7 +15,8 @@ | ||||
| 
 | ||||
|     <div [ngSwitch]="network.val"> | ||||
|       <span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span> | ||||
|       <span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span> | ||||
|       <span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet3</span> | ||||
|       <span *ngSwitchCase="'testnet4'" class="network testnet"><app-svg-images name="testnet4" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet4</span> | ||||
|       <span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span> | ||||
|       <span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span> | ||||
|       <span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span> | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
|   max-width: 1200px; | ||||
|   max-height: 600px; | ||||
|   padding-top: 80px; | ||||
|   background: var(--nav-bg); | ||||
| 
 | ||||
|   header { | ||||
|     position: absolute; | ||||
| @ -18,7 +19,7 @@ | ||||
|     flex-direction: row; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     background: var(--stat-box-bg); | ||||
|     background: var(--nav-bg); | ||||
|     text-align: start; | ||||
|     font-size: 1.8em; | ||||
|   } | ||||
|  | ||||
| @ -17,16 +17,16 @@ | ||||
| 
 | ||||
|   <!-- Large screen --> | ||||
|   <a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> | ||||
|     <ng-template [ngIf]="subdomain && enterpriseInfo"> | ||||
|       <div class="subdomain_container"> | ||||
|         <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> | ||||
|       </div> | ||||
|       <div class="vertical-line"></div> | ||||
|     </ng-template> | ||||
|     <ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> | ||||
|       @if (enterpriseInfo?.header_img) { | ||||
|         <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> | ||||
|         <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="48px" class="mr-3"> | ||||
|       } @else { | ||||
|         <ng-template [ngIf]="subdomain && enterpriseInfo"> | ||||
|           <div class="subdomain_container"> | ||||
|             <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> | ||||
|           </div> | ||||
|           <div class="vertical-line"></div> | ||||
|         </ng-template> | ||||
|         <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images> | ||||
|         <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> | ||||
|       } | ||||
| @ -38,34 +38,39 @@ | ||||
|   </a> | ||||
|   <!-- Mobile --> | ||||
|   <a class="navbar-brand d-flex d-md-none justify-content-center" [ngClass]="{'dual-logos': subdomain, 'mr-0': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> | ||||
|     <ng-template [ngIf]="subdomain && enterpriseInfo"> | ||||
|       <div class="subdomain_container"> | ||||
|         <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> | ||||
|       </div> | ||||
|       <div class="vertical-line"></div> | ||||
|     </ng-template> | ||||
|     <ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> | ||||
|       @if (enterpriseInfo?.header_img) { | ||||
|         <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> | ||||
|       } @else { | ||||
|         <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images> | ||||
|         <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> | ||||
|       } | ||||
|         <div class="connection-badge"> | ||||
|         <div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div> | ||||
|         <div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div> | ||||
|       </div> | ||||
|     </ng-container> | ||||
|     @if (enterpriseInfo?.header_img) { | ||||
|       <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px"> | ||||
|     } @else { | ||||
|       <ng-template [ngIf]="subdomain && enterpriseInfo"> | ||||
|         <div class="subdomain_container"> | ||||
|           <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> | ||||
|         </div> | ||||
|         <div class="vertical-line"></div> | ||||
|       </ng-template> | ||||
|       <ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> | ||||
|         @if (enterpriseInfo?.header_img) { | ||||
|           <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> | ||||
|         } @else { | ||||
|           <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images> | ||||
|           <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> | ||||
|         } | ||||
|           <div class="connection-badge"> | ||||
|           <div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div> | ||||
|           <div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div> | ||||
|         </div> | ||||
|       </ng-container> | ||||
|     } | ||||
|   </a> | ||||
| 
 | ||||
|   <div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED"> | ||||
|   <div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED"> | ||||
|     <button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true"> | ||||
|       <app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images> | ||||
|     </button> | ||||
|     <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}"> | ||||
|       <a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a> | ||||
|       <a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a> | ||||
|       <a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a> | ||||
|       <a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a> | ||||
|       <a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a> | ||||
|       <h6 *ngIf="env.LIQUID_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6> | ||||
|       <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a> | ||||
|       <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage  + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a> | ||||
| @ -87,7 +92,7 @@ | ||||
|       <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD"> | ||||
|         <a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a> | ||||
|       </li> | ||||
|       <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-lightning" *ngIf="stateService.env.LIGHTNING"> | ||||
|       <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-lightning" *ngIf="stateService.env.LIGHTNING && lightningNetworks.includes(stateService.network)"> | ||||
|         <a class="nav-link" [routerLink]="['/lightning' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" i18n-title="master-page.lightning" title="Lightning Explorer"></fa-icon> | ||||
|         </a> | ||||
|       </li> | ||||
| @ -114,7 +119,7 @@ | ||||
|   <div class="empty-sidenav"><!-- empty sidenav needed to push footer down the screen --></div> | ||||
| 
 | ||||
|   <div class="flex-grow-1 d-flex flex-column"> | ||||
|     <app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert> | ||||
|     <app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'testnet4' || network.val === 'signet'"></app-testnet-alert> | ||||
| 
 | ||||
|     <main style="min-width: 375px; max-width: 100vw"> | ||||
|       <router-outlet></router-outlet> | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
|   position: -webkit-sticky; | ||||
|   top: 0; | ||||
|   width: 100%; | ||||
|   background-color: var(--bg); | ||||
|   z-index: 100; | ||||
| } | ||||
| 
 | ||||
| @ -18,7 +19,7 @@ fa-icon { | ||||
|   z-index: 100; | ||||
|   min-height: 64px; | ||||
|   width: 100%; | ||||
|   background-color: var(--bg); | ||||
|   background-color: var(--nav-bg); | ||||
| } | ||||
| 
 | ||||
| li.nav-item { | ||||
| @ -59,7 +60,7 @@ li.nav-item { | ||||
| } | ||||
| 
 | ||||
| .navbar-nav { | ||||
|   background: var(--navbar-bg); | ||||
|   background: var(--nav-bg); | ||||
|   bottom: 0; | ||||
|   box-shadow: 0px 0px 15px 0px #000; | ||||
|   flex-direction: row; | ||||
| @ -243,6 +244,10 @@ nav { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .beta-network { | ||||
|   font-size: 8px; | ||||
| } | ||||
| 
 | ||||
| .current-network-svg { | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|  | ||||
| @ -27,6 +27,7 @@ export class MasterPageComponent implements OnInit, OnDestroy { | ||||
|   subdomain = ''; | ||||
|   networkPaths: { [network: string]: string }; | ||||
|   networkPaths$: Observable<Record<string, string>>; | ||||
|   lightningNetworks = ['', 'mainnet', 'bitcoin', 'testnet', 'signet']; | ||||
|   footerVisible = true; | ||||
|   user: any = undefined; | ||||
|   servicesEnabled = false; | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter, | ||||
| import { Component, ViewChild, Input, Output, EventEmitter, | ||||
|   OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { MempoolBlockDelta } from '../../interfaces/websocket.interface'; | ||||
| import { MempoolBlockDelta, isMempoolDelta } from '../../interfaces/websocket.interface'; | ||||
| import { TransactionStripped } from '../../interfaces/node-api.interface'; | ||||
| import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; | ||||
| import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs'; | ||||
| import { switchMap, filter, concatMap, map } from 'rxjs/operators'; | ||||
| import { Subscription, BehaviorSubject } from 'rxjs'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||
| import { Router } from '@angular/router'; | ||||
| @ -39,10 +38,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang | ||||
|   poolDirection: string = 'left'; | ||||
| 
 | ||||
|   blockSub: Subscription; | ||||
|   rateLimit = 1000; | ||||
|   private lastEventTime = Date.now() - this.rateLimit; | ||||
|   private subId = 0; | ||||
| 
 | ||||
|   firstLoad: boolean = true; | ||||
| 
 | ||||
|   constructor( | ||||
| @ -62,39 +57,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang | ||||
|   } | ||||
| 
 | ||||
|   ngAfterViewInit(): void { | ||||
|     this.blockSub = merge( | ||||
|       this.stateService.mempoolBlockTransactions$, | ||||
|       this.stateService.mempoolBlockDelta$, | ||||
|     ).pipe( | ||||
|       concatMap(update => { | ||||
|         const now = Date.now(); | ||||
|         const timeSinceLastEvent = now - this.lastEventTime; | ||||
|         this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit); | ||||
| 
 | ||||
|         const subId = this.subId; | ||||
| 
 | ||||
|         // If time since last event is less than X seconds, delay this event
 | ||||
|         if (timeSinceLastEvent < this.rateLimit) { | ||||
|           return timer(this.rateLimit - timeSinceLastEvent).pipe( | ||||
|             // Emit the event after the timer
 | ||||
|             map(() => ({ update, subId })) | ||||
|           ); | ||||
|         } else { | ||||
|           // If enough time has passed, emit the event immediately
 | ||||
|           return of({ update, subId }); | ||||
|         } | ||||
|       }) | ||||
|     ).subscribe(({ update, subId }) => { | ||||
|       // discard stale updates after a block transition
 | ||||
|       if (subId !== this.subId) { | ||||
|         return; | ||||
|       } | ||||
|     this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => { | ||||
|       // process update
 | ||||
|       if (update['added']) { | ||||
|       if (isMempoolDelta(update)) { | ||||
|         // delta
 | ||||
|         this.updateBlock(update as MempoolBlockDelta); | ||||
|         this.updateBlock(update); | ||||
|       } else { | ||||
|         const transactionsStripped = update as TransactionStripped[]; | ||||
|         const transactionsStripped = update.transactions; | ||||
|         // new transactions
 | ||||
|         if (this.firstLoad) { | ||||
|           this.replaceBlock(transactionsStripped); | ||||
| @ -137,7 +106,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang | ||||
| 
 | ||||
|   ngOnChanges(changes): void { | ||||
|     if (changes.index) { | ||||
|       this.subId++; | ||||
|       this.firstLoad = true; | ||||
|       if (this.blockGraph) { | ||||
|         this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); | ||||
| @ -173,7 +141,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang | ||||
|       const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; | ||||
|       this.blockGraph.replace(delta.added, direction); | ||||
|     } else { | ||||
|       this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined); | ||||
|       if (blockMined) { | ||||
|         this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined); | ||||
|       } else { | ||||
|         this.blockGraph.deferredUpdate(delta.added, delta.removed, delta.changed || [], this.poolDirection); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     this.lastBlockHeight = this.stateService.latestBlockHeight; | ||||
|  | ||||
| @ -56,7 +56,7 @@ | ||||
| .fee-span { | ||||
|   font-size: 11px; | ||||
|   margin-bottom: 5px; | ||||
|   color: #fff000; | ||||
|   color: var(--yellow); | ||||
| } | ||||
| 
 | ||||
| .transaction-count { | ||||
| @ -119,7 +119,7 @@ | ||||
|   height: 0; | ||||
|   border-left: calc(var(--block-size) * 0.3) solid transparent; | ||||
|   border-right: calc(var(--block-size) * 0.3) solid transparent; | ||||
|   border-bottom: calc(var(--block-size) * 0.3) solid #FFF; | ||||
|   border-bottom: calc(var(--block-size) * 0.3) solid var(--fg); | ||||
| } | ||||
| 
 | ||||
| .blockLink { | ||||
|  | ||||
| @ -77,7 +77,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges { | ||||
|     } | ||||
|     this.isWidget = this.template === 'widget'; | ||||
|     this.showCount = !this.isWidget && !this.hideCount; | ||||
|     this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference'); | ||||
|     this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h'; | ||||
|     this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([])); | ||||
|     this.mountFeeChart(); | ||||
|   } | ||||
| @ -256,11 +256,17 @@ export class MempoolGraphComponent implements OnInit, OnChanges { | ||||
|           const itemFormatted = []; | ||||
|           let sum = 0; | ||||
|           let progressPercentageText = ''; | ||||
|           let countItem; | ||||
|           let items = this.inverted ? [...params].reverse() : params; | ||||
|           if (items[items.length - 1].seriesName === 'count') { | ||||
|             countItem = items.pop(); | ||||
|           } | ||||
|           const unfilteredItems = this.inverted ? [...params].reverse() : params; | ||||
|           const countItem = unfilteredItems.find(p => p.seriesName === 'count'); | ||||
|           const usedSeries = {}; | ||||
|           const items = unfilteredItems.filter(p => { | ||||
|             if (usedSeries[p.seriesName] || p.seriesName === 'count') { | ||||
|               return false; | ||||
|             } | ||||
|             usedSeries[p.seriesName] = true; | ||||
|             return true; | ||||
|           }); | ||||
| 
 | ||||
|           items.map((item: any, index: number) => { | ||||
|             sum += item.value[1]; | ||||
|             const progressPercentage = (item.value[1] / totalValue) * 100; | ||||
|  | ||||
| @ -63,7 +63,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| .rbf-tooltip { | ||||
|   position: fixed; | ||||
|   z-index: 3; | ||||
|   background: rgba(#11131f, 0.95); | ||||
|   background: color-mix(in srgb, var(--active-bg) 95%, transparent); | ||||
|   border-radius: 4px; | ||||
|   box-shadow: 1px 1px 10px rgba(0,0,0,0.5); | ||||
|   color: #b1b1b1; | ||||
|   color: var(--tooltip-grey); | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: space-between; | ||||
|  | ||||
| @ -159,7 +159,7 @@ | ||||
| 
 | ||||
|       &.selected { | ||||
|         .shape-border { | ||||
|           background: #9339f4; | ||||
|           background: var(--mainnet-alt); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
| @ -183,7 +183,7 @@ | ||||
|           width: calc(1em + 16px); | ||||
| 
 | ||||
|           .shape { | ||||
|             border: solid 4px #9339f4; | ||||
|             border: solid 4px var(--mainnet-alt); | ||||
|           } | ||||
| 
 | ||||
|           &:hover { | ||||
|  | ||||
| @ -179,7 +179,7 @@ export class SearchFormComponent implements OnInit { | ||||
|           const lightningResults = result[1]; | ||||
| 
 | ||||
|           // Do not show date and timestamp results for liquid
 | ||||
|           const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet'; | ||||
|           const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'testnet4' || this.network === 'signet'; | ||||
| 
 | ||||
|           const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight; | ||||
|           const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin; | ||||
|  | ||||
| @ -60,6 +60,9 @@ | ||||
|   <ng-container *ngSwitchCase="'testnet'"> | ||||
|     <ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component> | ||||
|   </ng-container> | ||||
|   <ng-container *ngSwitchCase="'testnet4'"> | ||||
|     <ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component> | ||||
|   </ng-container> | ||||
|   <ng-container *ngSwitchCase="'liquid'"> | ||||
|     <ng-component *ngTemplateOutlet="liquidLogo; context: {$implicit: '', width, height, viewBox, color1: '#2cccbf', color2: '#9ef2ed'}"></ng-component> | ||||
|   </ng-container> | ||||
|  | ||||
| @ -71,7 +71,9 @@ export class TelevisionComponent implements OnInit, OnDestroy { | ||||
|           mempoolStats = newStats; | ||||
|         } else if (['2h', '24h'].includes(this.fragment)) { | ||||
|           mempoolStats.unshift(newStats[0]); | ||||
|           mempoolStats = mempoolStats.slice(0, mempoolStats.length - 1); | ||||
|           const now = Date.now() / 1000; | ||||
|           const start = now - (this.fragment === '2h' ? (2 * 60 * 60) : (24 * 60 * 60) ); | ||||
|           mempoolStats = mempoolStats.filter(p => p.added >= start); | ||||
|         } | ||||
|         return mempoolStats; | ||||
|       }) | ||||
|  | ||||
| @ -11,7 +11,7 @@ | ||||
| 
 | ||||
|     <div class="text-left"> | ||||
| 
 | ||||
|         <p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p> | ||||
|         <p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p> | ||||
| 
 | ||||
|         <p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p> | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,53 @@ | ||||
| <div class="container-xl"> | ||||
|   <h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1> | ||||
| 
 | ||||
|   <form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate> | ||||
|     <label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label> | ||||
|     <div class="mb-3"> | ||||
|       <textarea formControlName="txs" class="form-control" rows="5" i18n-placeholder="transaction.test-transactions" placeholder="Comma-separated list of raw transactions"></textarea> | ||||
|     </div> | ||||
|     <label for="maxfeerate" i18n="test.tx.max-fee-rate">Maximum fee rate (sat/vB)</label> | ||||
|     <input type="number" class="form-control input-dark" formControlName="maxfeerate" id="maxfeerate" | ||||
|         [value]="10000" placeholder="10,000 s/vb" [class]="{invalid: invalidMaxfeerate}"> | ||||
|     <br> | ||||
|     <button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.test-transactions|Test Transactions">Test Transactions</button> | ||||
|     <p class="red-color d-inline">{{ error }}</p> | ||||
|   </form> | ||||
| 
 | ||||
|   <br> | ||||
| 
 | ||||
|   <div class="box" *ngIf="results?.length"> | ||||
|     <table class="accept-results table table-fixed table-borderless table-striped"> | ||||
|       <tbody> | ||||
|         <tr> | ||||
|           <th class="allowed" i18n="test-tx.is-allowed">Allowed?</th> | ||||
|           <th class="txid" i18n="dashboard.latest-transactions.txid">TXID</th> | ||||
|           <th class="rate" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</th> | ||||
|           <th class="reason" i18n="test-tx.rejection-reason">Rejection reason</th> | ||||
|         </tr> | ||||
|         <ng-container *ngFor="let result of results;"> | ||||
|           <tr> | ||||
|             <td class="allowed"> | ||||
|               <ng-container [ngSwitch]="result.allowed"> | ||||
|                 <span *ngSwitchCase="true">✅</span> | ||||
|                 <span *ngSwitchCase="false">❌</span> | ||||
|                 <span *ngSwitchDefault>-</span> | ||||
|               </ng-container> | ||||
|             </td> | ||||
|             <td class="txid"> | ||||
|               <app-truncate [text]="result.txid || '-'"></app-truncate> | ||||
|             </td> | ||||
|             <td class="rate"> | ||||
|               <app-fee-rate *ngIf="result.fees?.['effective-feerate'] != null" [fee]="result.fees?.['effective-feerate'] * 100000"></app-fee-rate> | ||||
|               <span *ngIf="result.fees?.['effective-feerate'] == null">-</span> | ||||
|             </td> | ||||
|             <td class="reason"> | ||||
|               {{ result['reject-reason'] || '-' }} | ||||
|             </td> | ||||
|           </tr> | ||||
|         </ng-container> | ||||
|       </tbody> | ||||
|     </table> | ||||
|   </div> | ||||
| 
 | ||||
| </div> | ||||
| @ -0,0 +1,34 @@ | ||||
| .accept-results { | ||||
|   td, th { | ||||
|     &.allowed { | ||||
|       width: 10%; | ||||
|       text-align: center; | ||||
|     } | ||||
|     &.txid { | ||||
|       width: 50%; | ||||
|     } | ||||
|     &.rate { | ||||
|       width: 20%; | ||||
|       text-align: right; | ||||
|       white-space: wrap; | ||||
|     } | ||||
|     &.reason { | ||||
|       width: 20%; | ||||
|       text-align: right; | ||||
|       white-space: wrap; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @media (max-width: 950px) { | ||||
|     table-layout: auto; | ||||
| 
 | ||||
|     td, th { | ||||
|       &.allowed { | ||||
|         width: 100px; | ||||
|       } | ||||
|       &.txid { | ||||
|         max-width: 200px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,86 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| import { OpenGraphService } from '../../services/opengraph.service'; | ||||
| import { TestMempoolAcceptResult } from '../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-test-transactions', | ||||
|   templateUrl: './test-transactions.component.html', | ||||
|   styleUrls: ['./test-transactions.component.scss'] | ||||
| }) | ||||
| export class TestTransactionsComponent implements OnInit { | ||||
|   testTxsForm: UntypedFormGroup; | ||||
|   error: string = ''; | ||||
|   results: TestMempoolAcceptResult[] = []; | ||||
|   isLoading = false; | ||||
|   invalidMaxfeerate = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     private formBuilder: UntypedFormBuilder, | ||||
|     private apiService: ApiService, | ||||
|     public stateService: StateService, | ||||
|     private seoService: SeoService, | ||||
|     private ogService: OpenGraphService, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.testTxsForm = this.formBuilder.group({ | ||||
|       txs: ['', Validators.required], | ||||
|       maxfeerate: ['', Validators.min(0)] | ||||
|     }); | ||||
| 
 | ||||
|     this.seoService.setTitle($localize`:@@meta.title.test-txs:Test Transactions`); | ||||
|     this.ogService.setManualOgImage('tx-push.jpg'); | ||||
|   } | ||||
| 
 | ||||
|   testTxs() { | ||||
|     let txs: string[] = []; | ||||
|     try { | ||||
|       txs = (this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()); | ||||
|       if (!txs?.length) { | ||||
|         this.error = 'At least one transaction is required'; | ||||
|         return; | ||||
|       } else if (txs.length > 25) { | ||||
|         this.error = 'Exceeded maximum of 25 transactions'; | ||||
|         return; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.error = e?.message; | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     let maxfeerate; | ||||
|     this.invalidMaxfeerate = false; | ||||
|     try { | ||||
|       const maxfeerateVal = this.testTxsForm.get('maxfeerate')?.value; | ||||
|       if (maxfeerateVal != null && maxfeerateVal !== '') { | ||||
|         maxfeerate = parseFloat(maxfeerateVal) / 100_000; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.invalidMaxfeerate = true; | ||||
|     } | ||||
| 
 | ||||
|     this.isLoading = true; | ||||
|     this.error = ''; | ||||
|     this.results = []; | ||||
|     this.apiService.testTransactions$(txs, maxfeerate === 0.1 ? null : maxfeerate) | ||||
|       .subscribe((result) => { | ||||
|         this.isLoading = false; | ||||
|         this.results = result || []; | ||||
|         this.testTxsForm.reset(); | ||||
|       }, | ||||
|       (error) => { | ||||
|         if (typeof error.error === 'string') { | ||||
|           const matchText = error.error.match('"message":"(.*?)"'); | ||||
|           this.error = matchText && matchText[1] || error.error; | ||||
|         } else if (error.message) { | ||||
|           this.error = error.message; | ||||
|         } | ||||
|         this.isLoading = false; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @ -11,7 +11,7 @@ import { Subscription } from 'rxjs'; | ||||
| }) | ||||
| export class ThemeSelectorComponent implements OnInit { | ||||
|   themeForm: UntypedFormGroup; | ||||
|   themes = ['default', 'contrast', 'wiz']; | ||||
|   themes = ['default', 'contrast', 'wiz', 'bukele']; | ||||
|   themeSubscription: Subscription; | ||||
| 
 | ||||
|   constructor( | ||||
|  | ||||
| @ -1,15 +1,34 @@ | ||||
| <div class="mobile-wrapper"> | ||||
|   <div class="mobile-container"> | ||||
|     <div class="panel"> | ||||
|       <div class="field nav-header"> | ||||
|           <app-svg-images name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images> | ||||
|           <div [ngSwitch]="network" class="network-label"> | ||||
|       <div class="nav-header"> | ||||
|         @if (enterpriseInfo?.header_img) { | ||||
|           <a class="d-flex" [routerLink]="['/' | relativeUrl]"> | ||||
|             <img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px"> | ||||
|           </a> | ||||
|         } @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) { | ||||
|           <a [routerLink]="['/' | relativeUrl]"> | ||||
|             <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> | ||||
|           </a> | ||||
|           <div class="vertical-line"></div> | ||||
|         } | ||||
|         @if (!enterpriseInfo?.header_img) { | ||||
|           <a [routerLink]="['/' | relativeUrl]"> | ||||
|             <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" style="width: 144px; height: 36px" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo"></app-svg-images> | ||||
|             <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images> | ||||
|           </a> | ||||
|         } | ||||
| 
 | ||||
|         @if (enterpriseInfo?.header_img || (!enterpriseInfo?.img && !enterpriseInfo?.imageMd5)) { | ||||
|           <div [ngSwitch]="network" class="network-label" [class.hide-name]="enterpriseInfo?.header_img"> | ||||
|             <span *ngSwitchCase="'signet'" class="network signet"><span class="name">Bitcoin Signet</span><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> | ||||
|             <span *ngSwitchCase="'testnet'" class="network testnet"><span class="name">Bitcoin Testnet</span><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> | ||||
|             <span *ngSwitchCase="'testnet'" class="network testnet"><span class="name">Bitcoin Testnet3</span><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> | ||||
|             <span *ngSwitchCase="'testnet4'" class="network testnet"><span class="name">Bitcoin Testnet4</span><app-svg-images name="testnet4" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> | ||||
|             <span *ngSwitchCase="'liquid'" class="network liquid"><span class="name">Liquid</span><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> | ||||
|             <span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><span class="name">Liquid Testnet</span><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> | ||||
|             <span *ngSwitchDefault class="network mainnet"><span class="name">Bitcoin</span><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> | ||||
|           </div> | ||||
|         } | ||||
|       </div> | ||||
|       <div class="field"> | ||||
|         <div class="label" i18n="shared.transaction">Transaction</div> | ||||
|  | ||||
| @ -40,7 +40,14 @@ | ||||
| } | ||||
| 
 | ||||
| .nav-header { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   flex-wrap: nowrap; | ||||
|   width: 100%; | ||||
|   max-width: 100%; | ||||
|   padding: 1em; | ||||
|   position: relative; | ||||
|   background: var(--nav-bg); | ||||
|   box-shadow: 0 -5px 15px #000; | ||||
|   z-index: 100; | ||||
|   align-items: center; | ||||
| @ -53,6 +60,40 @@ | ||||
|       flex-direction: row; | ||||
|       align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     &.hide-name .name { | ||||
|       display: none; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .subdomain_logo { | ||||
|     height: 35px; | ||||
|     overflow: clip; | ||||
|     max-width: 140px; | ||||
|     margin: auto; | ||||
|     align-self: center; | ||||
|     .rounded { | ||||
|       border-radius: 5px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .subdomain_container { | ||||
|     max-width: 140px; | ||||
|     text-align: center; | ||||
|     align-self: center; | ||||
|   } | ||||
| 
 | ||||
|   .vertical-line { | ||||
|     border-left: 1px solid #444; | ||||
|     height: 30px; | ||||
|     margin-left: 10px; | ||||
|     margin-right: 10px; | ||||
|     margin-top: 3px; | ||||
|   } | ||||
| 
 | ||||
|   .logo-holder { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -113,6 +113,10 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|   scrollIntoAccelPreview = false; | ||||
|   auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true; | ||||
| 
 | ||||
|   enterpriseInfo: any; | ||||
|   enterpriseInfo$: Subscription; | ||||
|   officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; | ||||
| 
 | ||||
|   constructor( | ||||
|     private route: ActivatedRoute, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
| @ -152,6 +156,10 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     this.enterpriseService.page(); | ||||
| 
 | ||||
|     this.enterpriseInfo$ = this.enterpriseService.info$.subscribe(info => { | ||||
|       this.enterpriseInfo = info; | ||||
|     }); | ||||
| 
 | ||||
|     this.websocketService.want(['blocks', 'mempool-blocks']); | ||||
|     this.stateService.networkChanged$.subscribe( | ||||
|       (network) => { | ||||
| @ -702,6 +710,7 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|     this.blocksSubscription.unsubscribe(); | ||||
|     this.miningSubscription?.unsubscribe(); | ||||
|     this.currencyChangeSubscription?.unsubscribe(); | ||||
|     this.enterpriseInfo$?.unsubscribe(); | ||||
|     this.leaveTransaction(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -103,7 +103,8 @@ td.amount.large { | ||||
| 	margin-top: 10px; | ||||
| } | ||||
| .assetBox { | ||||
| 	background-color: #653b9c90; | ||||
| 	background: color-mix(in srgb, var(--tertiary) 56%, transparent); | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| .details-container { | ||||
|  | ||||
| @ -0,0 +1,16 @@ | ||||
| @if (loading) { | ||||
|   <div class="spinner-wrapper"> | ||||
|     <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||
|   </div> | ||||
| } @else if (error) { | ||||
|   <div class="error-wrapper"> | ||||
|     <span>failed to load X timeline</span> | ||||
|   </div> | ||||
| } | ||||
| <iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" allowfullscreen="true" | ||||
|   title="Twitter Timeline" | ||||
|   [src]="iframeSrc" | ||||
|   style="position: static; visibility: visible; width: 100%; height: 100%; display: block; flex-grow: 1;" | ||||
|   (load)="onReady()" | ||||
| ></iframe> | ||||
| 
 | ||||
| @ -0,0 +1,10 @@ | ||||
| .spinner-wrapper, .error-wrapper { | ||||
|   position: absolute; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
| } | ||||
| @ -0,0 +1,71 @@ | ||||
| import { Component, Input, ChangeDetectionStrategy, SecurityContext, SimpleChanges, OnChanges } from '@angular/core'; | ||||
| import { LanguageService } from '../../services/language.service'; | ||||
| import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-twitter-widget', | ||||
|   templateUrl: './twitter-widget.component.html', | ||||
|   styleUrls: ['./twitter-widget.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class TwitterWidgetComponent implements OnChanges { | ||||
|   @Input() handle: string; | ||||
|   @Input() width = 300; | ||||
|   @Input() height = 400; | ||||
| 
 | ||||
|   loading: boolean = true; | ||||
|   error: boolean = false; | ||||
|   lang: string = 'en'; | ||||
| 
 | ||||
|   iframeSrc: SafeResourceUrl; | ||||
| 
 | ||||
|   constructor( | ||||
|     private languageService: LanguageService, | ||||
|     public sanitizer: DomSanitizer, | ||||
|   ) { | ||||
|     this.lang = this.languageService.getLanguage(); | ||||
|     this.setIframeSrc(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (changes.handle) { | ||||
|       this.setIframeSrc(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setIframeSrc(): void { | ||||
|     if (this.handle) { | ||||
|       this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, | ||||
|         `https://syndication.twitter.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool` | ||||
|         + '&dnt=true' | ||||
|         + '&embedId=twitter-widget-0' | ||||
|         + '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D' | ||||
|         + '&frame=false' | ||||
|         + '&hideBorder=true' | ||||
|         + '&hideFooter=false' | ||||
|         + '&hideHeader=true' | ||||
|         + '&hideScrollBar=false' | ||||
|         + `&lang=${this.lang}` | ||||
|         + '&maxHeight=500px' | ||||
|         + '&origin=https%3A%2F%2Fmempool.space%2F' | ||||
|         // + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9'
 | ||||
|         + '&showHeader=false' | ||||
|         + '&showReplies=false' | ||||
|         + '&siteScreenName=mempool' | ||||
|         + '&theme=dark' | ||||
|         + '&transparent=true' | ||||
|         + '&widgetsVersion=2615f7e52b7e0%3A1702314776716' | ||||
|       )); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onReady(): void { | ||||
|     this.loading = false; | ||||
|     this.error = false; | ||||
|   } | ||||
| 
 | ||||
|   onFailed(): void { | ||||
|     this.loading = false; | ||||
|     this.error = true; | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| .bowtie-graph-tooltip { | ||||
|   position: absolute; | ||||
|   background: rgba(#11131f, 0.95); | ||||
|   background: color-mix(in srgb, var(--active-bg) 95%, transparent); | ||||
|   border-radius: 4px; | ||||
|   box-shadow: 1px 1px 10px rgba(0,0,0,0.5); | ||||
|   color: var(--tooltip-grey); | ||||
|  | ||||
| @ -84,18 +84,19 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { | ||||
|   refreshOutspends$: ReplaySubject<string> = new ReplaySubject(); | ||||
| 
 | ||||
|   gradientColors = { | ||||
|     '': ['#9339f4', '#105fb0', '#9339f400'], | ||||
|     '': ['var(--mainnet-alt)', 'var(--primary)', 'color-mix(in srgb, var(--mainnet-alt) 1%, transparent)'], | ||||
|     // liquid: ['#116761', '#183550'],
 | ||||
|     liquid: ['#09a197', '#0f62af', '#09a19700'], | ||||
|     // 'liquidtestnet': ['#494a4a', '#272e46'],
 | ||||
|     'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'], | ||||
|     // testnet: ['#1d486f', '#183550'],
 | ||||
|     testnet: ['#4edf77', '#10a0af', '#4edf7700'], | ||||
|     testnet4: ['#4edf77', '#10a0af', '#4edf7700'], | ||||
|     // signet: ['#6f1d5d', '#471850'],
 | ||||
|     signet: ['#d24fc8', '#a84fd2', '#d24fc800'], | ||||
|   }; | ||||
| 
 | ||||
|   gradient: string[] = ['#105fb0', '#105fb0']; | ||||
|   gradient: string[] = ['var(--primary)', 'var(--primary)']; | ||||
| 
 | ||||
|   constructor( | ||||
|     private router: Router, | ||||
|  | ||||
| @ -301,7 +301,8 @@ | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
| @ -435,7 +436,8 @@ | ||||
| 
 | ||||
| .in-progress-message { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   color: var(--fg); | ||||
|   opacity: var(--opacity); | ||||
|   margin-top: 20px; | ||||
|   text-align: center; | ||||
|   padding-bottom: 3px; | ||||
|  | ||||
| @ -231,8 +231,10 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|             this.stateService.live2Chart$ | ||||
|               .pipe( | ||||
|                 scan((acc, stats) => { | ||||
|                   const now = Date.now() / 1000; | ||||
|                   const start = now - (2 * 60 * 60); | ||||
|                   acc.unshift(stats); | ||||
|                   acc = acc.slice(0, 120); | ||||
|                   acc = acc.filter(p => p.added >= start); | ||||
|                   return acc; | ||||
|                 }, (mempoolStats || [])) | ||||
|               ), | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const bitcoinNetworks = ["", "testnet", "signet"]; | ||||
| const bitcoinNetworks = ["", "testnet", "testnet4", "signet"]; | ||||
| const liquidNetworks = ["liquid", "liquidtestnet"]; | ||||
| const lightningNetworks = ["", "testnet", "signet"]; | ||||
| const miningTimeIntervals = "<code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>"; | ||||
| 
 | ||||
| const emptyCodeSample = { | ||||
| @ -6513,7 +6514,7 @@ export const restApiDocsData = [ | ||||
|     category: "lightning", | ||||
|     fragment: "lightning", | ||||
|     title: "Lightning", | ||||
|     showConditions: bitcoinNetworks | ||||
|     showConditions: lightningNetworks | ||||
|   }, | ||||
|   { | ||||
|     type: "endpoint", | ||||
| @ -6525,7 +6526,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns network-wide stats such as total number of channels and nodes, total capacity, and average/median fee figures.</p><p>Pass one of the following for <code>:interval</code>: <code>latest</code>, <code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/statistics/:interval", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -6621,7 +6622,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns Lightning nodes and channels that match a full-text, case-insensitive search <code>:query</code> across node aliases, node pubkeys, channel IDs, and short channel IDs.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/search?searchText=:query", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -6706,7 +6707,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of Lightning nodes running on clearnet in the requested <code>:country</code>, where <code>:country</code> is an ISO Alpha-2 country code.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/country/:country", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -6928,7 +6929,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns aggregate capacity and number of clearnet nodes per country. Capacity figures are in satoshis.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/countries", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -7072,7 +7073,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of nodes hosted by a specified <code>:isp</code>, where <code>:isp</code> is an ISP's ASN.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/isp/:isp", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -7191,7 +7192,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns aggregate capacity, number of nodes, and number of channels per ISP. Capacity figures are in satoshis.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/isp-ranking", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -7303,7 +7304,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns two lists of the top 100 nodes: one ordered by liquidity (aggregate channel capacity) and the other ordered by connectivity (number of open channels).</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/rankings", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -7426,7 +7427,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of the top 100 nodes by liquidity (aggregate channel capacity).</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/rankings/liquidity", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -7623,7 +7624,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of the top 100 nodes by connectivity (number of open channels).</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/rankings/connectivity", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -7819,7 +7820,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of the top 100 oldest nodes.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/rankings/age", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -8006,7 +8007,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns details about a node with the given <code>:pubKey</code>.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/:pubKey", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -8170,7 +8171,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns historical stats for a node with the given <code>:pubKey</code>.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/nodes/:pubKey/statistics", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -8268,7 +8269,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns info about a Lightning channel with the given <code>:channelId</code>.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/channels/:channelId", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -8433,7 +8434,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns channels that correspond to the given <code>:txid</code> (multiple transaction IDs can be specified).</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/channels/txids?txId[]=:txid", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -8634,7 +8635,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of a node's channels given its <code>:pubKey</code>. Ten channels are returned at a time. Use <code>:index</code> for paging. <code>:channelStatus</code> can be <code>open</code>, <code>active</code>, or <code>closed</code>.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/channels?public_key=:pubKey&status=:channelStatus", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -8770,7 +8771,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of channels with corresponding node geodata.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/channels-geo", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
| @ -8878,7 +8879,7 @@ export const restApiDocsData = [ | ||||
|       default: "<p>Returns a list of channels with corresponding geodata for a node with the given <code>:pubKey</code>.</p>" | ||||
|     }, | ||||
|     urlString: "/v1/lightning/channels-geo/:pubKey", | ||||
|     showConditions: bitcoinNetworks, | ||||
|     showConditions: lightningNetworks, | ||||
|     showJsExamples: showJsExamplesDefaultFalse, | ||||
|     codeExample: { | ||||
|       default: { | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user