Merge branch 'master' into natsoni/add-block-fee-graph
This commit is contained in:
		
						commit
						53a493c233
					
				
							
								
								
									
										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 }}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -23,7 +23,7 @@
 | 
			
		||||
        "rust-gbt": "file:./rust-gbt",
 | 
			
		||||
        "socks-proxy-agent": "~7.0.0",
 | 
			
		||||
        "typescript": "~4.9.3",
 | 
			
		||||
        "ws": "~8.16.0"
 | 
			
		||||
        "ws": "~8.17.0"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@babel/code-frame": "^7.18.6",
 | 
			
		||||
@ -7690,9 +7690,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ws": {
 | 
			
		||||
      "version": "8.16.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
 | 
			
		||||
      "version": "8.17.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
 | 
			
		||||
      "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=10.0.0"
 | 
			
		||||
      },
 | 
			
		||||
@ -13424,9 +13424,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "ws": {
 | 
			
		||||
      "version": "8.16.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
 | 
			
		||||
      "version": "8.17.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
 | 
			
		||||
      "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
 | 
			
		||||
      "requires": {}
 | 
			
		||||
    },
 | 
			
		||||
    "y18n": {
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@
 | 
			
		||||
    "redis": "^4.6.6",
 | 
			
		||||
    "socks-proxy-agent": "~7.0.0",
 | 
			
		||||
    "typescript": "~4.9.3",
 | 
			
		||||
    "ws": "~8.16.0"
 | 
			
		||||
    "ws": "~8.17.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/code-frame": "^7.18.6",
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -54,9 +54,11 @@ class ChannelsRoutes {
 | 
			
		||||
 | 
			
		||||
      if (index < -1) {
 | 
			
		||||
        res.status(400).send('Invalid index');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (['open', 'active', 'closed'].includes(status) === false) {
 | 
			
		||||
        res.status(400).send('Invalid status');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -63,6 +63,7 @@ src/resources/pools.json
 | 
			
		||||
src/resources/mining-pools/*
 | 
			
		||||
src/resources/**/*.mp4
 | 
			
		||||
src/resources/**/*.vtt
 | 
			
		||||
src/resources/customize.js
 | 
			
		||||
 | 
			
		||||
# environment config
 | 
			
		||||
mempool-frontend-config.json
 | 
			
		||||
 | 
			
		||||
@ -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,29 +1,37 @@
 | 
			
		||||
{
 | 
			
		||||
  "theme": "contrast",
 | 
			
		||||
  "theme": "bukele",
 | 
			
		||||
  "enterprise": "onbtc",
 | 
			
		||||
  "branding": {
 | 
			
		||||
    "name": "onbtc",
 | 
			
		||||
    "title": "Oficina Nacional del Bitcoin",
 | 
			
		||||
    "header_img": "/resources/onbtc.svg",
 | 
			
		||||
    "img": "/resources/elsalvador.svg",
 | 
			
		||||
    "title": "Bitcoin Office",
 | 
			
		||||
    "site_id": 19,
 | 
			
		||||
    "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": "bitcoinofficesv"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "component": "address",
 | 
			
		||||
        "mobileOrder": 2,
 | 
			
		||||
        "props": {
 | 
			
		||||
          "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
 | 
			
		||||
          "period": "1m"
 | 
			
		||||
@ -34,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,
 | 
			
		||||
 | 
			
		||||
@ -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[];
 | 
			
		||||
@ -174,6 +176,10 @@ export class AddressComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        this.transactions = this.tempTransactions;
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
 | 
			
		||||
        if (!this.showBalancePeriod()) {
 | 
			
		||||
          this.setBalancePeriod('all');
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      (error) => {
 | 
			
		||||
        console.log(error);
 | 
			
		||||
@ -296,6 +302,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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
@ -283,8 +285,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 +301,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 +371,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;
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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,65 @@
 | 
			
		||||
import { Component, Input, ChangeDetectionStrategy, SecurityContext } 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 {
 | 
			
		||||
  @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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setIframeSrc(): void {
 | 
			
		||||
    this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL,
 | 
			
		||||
      'https://syndication.twitter.com/srv/timeline-profile/screen-name/bitcoinofficesv?creatorScreenName=mempool'
 | 
			
		||||
      + '&dnt=true'
 | 
			
		||||
      + '&embedId=twitter-widget-0'
 | 
			
		||||
      + '&features=eyJ0ZndfdGltZWxpbmVfbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%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 {
 | 
			
		||||
    console.log('ready!');
 | 
			
		||||
    this.loading = false;
 | 
			
		||||
    this.error = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onFailed(): void {
 | 
			
		||||
    console.log('failed!')
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
@ -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: {
 | 
			
		||||
 | 
			
		||||
@ -133,7 +133,7 @@
 | 
			
		||||
          <p>{{electrsPort}}</p>
 | 
			
		||||
          <p class="subtitle">SSL</p>
 | 
			
		||||
          <p>Enabled</p>
 | 
			
		||||
          <p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='https://mempool.space/enterprise'>sponsors</a> only—whitelisting is required.</p>
 | 
			
		||||
          <p class="note" *ngIf="network.val !== 'signet' && network.val !== 'testnet4'">Electrum RPC interface for <a href="/signet/docs/api/electrs">Bitcoin Signet</a> and <a href="/testnet4/docs/api/electrs">Bitcoin Testnet4</a> is publicly available. Electrum RPC interface for all other networks is available to <a href='https://mempool.space/enterprise'>sponsors</a> only—whitelisting is required.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -102,6 +102,8 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
 | 
			
		||||
          this.electrsPort = 50002; break;
 | 
			
		||||
        case "testnet":
 | 
			
		||||
          this.electrsPort = 60002; break;
 | 
			
		||||
        case "testnet4":
 | 
			
		||||
          this.electrsPort = 40002; break;
 | 
			
		||||
        case "signet":
 | 
			
		||||
          this.electrsPort = 60602; break;
 | 
			
		||||
        case "liquid":
 | 
			
		||||
@ -170,6 +172,9 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    if (network === 'testnet') {
 | 
			
		||||
      curlResponse = code.codeSampleTestnet.curl;
 | 
			
		||||
    }
 | 
			
		||||
    if (network === 'testnet4') {
 | 
			
		||||
      curlResponse = code.codeSampleTestnet.curl;
 | 
			
		||||
    }
 | 
			
		||||
    if (network === 'signet') {
 | 
			
		||||
      curlResponse = code.codeSampleSignet.curl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -111,7 +111,10 @@ export class CodeTemplateComponent implements OnInit {
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'testnet') {
 | 
			
		||||
      codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'testnet4') {
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'signet') {
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
 | 
			
		||||
@ -144,7 +147,10 @@ init();`;
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'testnet') {
 | 
			
		||||
      codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'testnet4') {
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'signet') {
 | 
			
		||||
        codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
 | 
			
		||||
@ -212,6 +218,9 @@ yarn add @mempool/liquid.js`;
 | 
			
		||||
      if (this.network === 'testnet') {
 | 
			
		||||
        return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleTestnet);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'testnet4') {
 | 
			
		||||
        return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleTestnet);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.network === 'signet') {
 | 
			
		||||
        return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleSignet);
 | 
			
		||||
      }
 | 
			
		||||
@ -234,6 +243,9 @@ yarn add @mempool/liquid.js`;
 | 
			
		||||
    if (this.network === 'testnet') {
 | 
			
		||||
      return code.codeSampleTestnet.response;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.network === 'testnet4') {
 | 
			
		||||
      return code.codeSampleTestnet.response;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.network === 'signet') {
 | 
			
		||||
      return code.codeSampleSignet.response;
 | 
			
		||||
    }
 | 
			
		||||
@ -247,7 +259,7 @@ yarn add @mempool/liquid.js`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  wrapPythonTemplate(code: any) {
 | 
			
		||||
    return ( ( this.network === 'testnet' || this.network === 'signet' ) ? ( code.codeTemplate.python.replace( "wss://mempool.space/api/v1/ws", "wss://mempool.space/" + this.network + "/api/v1/ws" ) ) : code.codeTemplate.python );
 | 
			
		||||
    return ( ( this.network === 'testnet' || this.network === 'testnet4' || this.network === 'signet' ) ? ( code.codeTemplate.python.replace( "wss://mempool.space/api/v1/ws", "wss://mempool.space/" + this.network + "/api/v1/ws" ) ) : code.codeTemplate.python );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  replaceJSPlaceholder(text: string, code: any) {
 | 
			
		||||
 | 
			
		||||
@ -423,4 +423,17 @@ export interface AccelerationInfo {
 | 
			
		||||
  effective_fee: number,
 | 
			
		||||
  boost_rate: number,
 | 
			
		||||
  boost_cost: 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,7 +1,7 @@
 | 
			
		||||
import { SafeResourceUrl } from '@angular/platform-browser';
 | 
			
		||||
import { ILoadingIndicators } from '../services/state.service';
 | 
			
		||||
import { Transaction } from './electrs.interface';
 | 
			
		||||
import { BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
 | 
			
		||||
import { Acceleration, BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface WebsocketResponse {
 | 
			
		||||
  backend?: 'esplora' | 'electrum' | 'none';
 | 
			
		||||
@ -35,6 +35,7 @@ export interface WebsocketResponse {
 | 
			
		||||
  'track-mempool-block'?: number;
 | 
			
		||||
  'track-rbf'?: string;
 | 
			
		||||
  'track-rbf-summary'?: boolean;
 | 
			
		||||
  'track-accelerations'?: boolean;
 | 
			
		||||
  'watch-mempool'?: boolean;
 | 
			
		||||
  'refresh-blocks'?: boolean;
 | 
			
		||||
}
 | 
			
		||||
@ -75,6 +76,16 @@ export interface MempoolBlockDelta {
 | 
			
		||||
  removed: string[];
 | 
			
		||||
  changed: { txid: string, rate: number, flags: number, acc: boolean }[];
 | 
			
		||||
}
 | 
			
		||||
export interface MempoolBlockState {
 | 
			
		||||
  transactions: TransactionStripped[];
 | 
			
		||||
}
 | 
			
		||||
export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState;
 | 
			
		||||
export function isMempoolState(update: MempoolBlockUpdate): update is MempoolBlockState {
 | 
			
		||||
  return update['transactions'] !== undefined;
 | 
			
		||||
}
 | 
			
		||||
export function isMempoolDelta(update: MempoolBlockUpdate): update is MempoolBlockDelta {
 | 
			
		||||
  return update['transactions'] === undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlockDeltaCompressed {
 | 
			
		||||
  added: TransactionCompressed[];
 | 
			
		||||
@ -82,6 +93,12 @@ export interface MempoolBlockDeltaCompressed {
 | 
			
		||||
  changed: MempoolDeltaChange[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AccelerationDelta {
 | 
			
		||||
  added: Acceleration[];
 | 
			
		||||
  removed: string[];
 | 
			
		||||
  reset?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolInfo {
 | 
			
		||||
  loaded: boolean;                 //  (boolean) True if the mempool is fully loaded
 | 
			
		||||
  size: number;                    //  (numeric) Current tx count
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,8 @@
 | 
			
		||||
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  color: var(--fg);
 | 
			
		||||
  opacity: var(--opacity);
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
 | 
			
		||||
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