Merge branch 'master' into simon/connectivity-ranking-title-fix
This commit is contained in:
		
						commit
						b52edd5049
					
				@ -29,7 +29,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": false,
 | 
			
		||||
    "RUST_GBT": false,
 | 
			
		||||
    "CPFP_INDEXING": false,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 6
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 6,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -7570,7 +7570,7 @@
 | 
			
		||||
      "name": "gbt",
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@napi-rs/cli": "^2.16.1"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,9 @@
 | 
			
		||||
    "RUST_GBT": false,
 | 
			
		||||
    "CPFP_INDEXING": true,
 | 
			
		||||
    "MAX_BLOCKS_BULK_QUERY": 999,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 999
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 999,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
@ -120,4 +122,4 @@
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "__CLIGHTNING_SOCKET__"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,8 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        CPFP_INDEXING: false,
 | 
			
		||||
        MAX_BLOCKS_BULK_QUERY: 0,
 | 
			
		||||
        DISK_CACHE_BLOCK_INTERVAL: 6,
 | 
			
		||||
        MAX_PUSH_TX_SIZE_WEIGHT: 400000,
 | 
			
		||||
        ALLOW_UNREACHABLE: true,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
 | 
			
		||||
 | 
			
		||||
@ -723,12 +723,7 @@ class BitcoinRoutes {
 | 
			
		||||
  private async $postTransaction(req: Request, res: Response) {
 | 
			
		||||
    res.setHeader('content-type', 'text/plain');
 | 
			
		||||
    try {
 | 
			
		||||
      let rawTx;
 | 
			
		||||
      if (typeof req.body === 'object') {
 | 
			
		||||
        rawTx = Object.keys(req.body)[0];
 | 
			
		||||
      } else {
 | 
			
		||||
        rawTx = req.body;
 | 
			
		||||
      }
 | 
			
		||||
      const rawTx = Common.getTransactionFromRequest(req, false);
 | 
			
		||||
      const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
 | 
			
		||||
      res.send(txIdResult);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
@ -739,12 +734,8 @@ class BitcoinRoutes {
 | 
			
		||||
 | 
			
		||||
  private async $postTransactionForm(req: Request, res: Response) {
 | 
			
		||||
    res.setHeader('content-type', 'text/plain');
 | 
			
		||||
    const matches = /tx=([a-z0-9]+)/.exec(req.body);
 | 
			
		||||
    let txHex = '';
 | 
			
		||||
    if (matches && matches[1]) {
 | 
			
		||||
      txHex = matches[1];
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const txHex = Common.getTransactionFromRequest(req, true);
 | 
			
		||||
      const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
 | 
			
		||||
      res.send(txIdResult);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmen
 | 
			
		||||
import PricesRepository from '../repositories/PricesRepository';
 | 
			
		||||
import priceUpdater from '../tasks/price-updater';
 | 
			
		||||
import chainTips from './chain-tips';
 | 
			
		||||
import websocketHandler from './websocket-handler';
 | 
			
		||||
 | 
			
		||||
class Blocks {
 | 
			
		||||
  private blocks: BlockExtended[] = [];
 | 
			
		||||
@ -686,6 +687,8 @@ class Blocks {
 | 
			
		||||
            this.updateTimerProgress(timer, `reindexed difficulty adjustments`);
 | 
			
		||||
            logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
 | 
			
		||||
            indexer.reindex();
 | 
			
		||||
 | 
			
		||||
            websocketHandler.handleReorg();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import * as bitcoinjs from 'bitcoinjs-lib';
 | 
			
		||||
import { Request } from 'express';
 | 
			
		||||
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
 | 
			
		||||
@ -86,19 +88,19 @@ export class Common {
 | 
			
		||||
        const match = spendMap.get(`${vin.txid}:${vin.vout}`);
 | 
			
		||||
        if (match && match.txid !== tx.txid) {
 | 
			
		||||
          replaced.add(match);
 | 
			
		||||
          // remove this tx from the spendMap
 | 
			
		||||
          // prevents the same tx being replaced more than once
 | 
			
		||||
          for (const replacedVin of match.vin) {
 | 
			
		||||
            const key = `${replacedVin.txid}:${replacedVin.vout}`;
 | 
			
		||||
            spendMap.delete(key);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        const key = `${vin.txid}:${vin.vout}`;
 | 
			
		||||
        spendMap.delete(key);
 | 
			
		||||
      }
 | 
			
		||||
      if (replaced.size) {
 | 
			
		||||
        matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
 | 
			
		||||
      }
 | 
			
		||||
      // remove this tx from the spendMap
 | 
			
		||||
      // prevents the same tx being replaced more than once
 | 
			
		||||
      for (const vin of tx.vin) {
 | 
			
		||||
        const key = `${vin.txid}:${vin.vout}`;
 | 
			
		||||
        if (spendMap.get(key)?.txid === tx.txid) {
 | 
			
		||||
          spendMap.delete(key);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return matches;
 | 
			
		||||
  }
 | 
			
		||||
@ -511,6 +513,115 @@ export class Common {
 | 
			
		||||
  static getNthPercentile(n: number, sortedDistribution: any[]): any {
 | 
			
		||||
    return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getTransactionFromRequest(req: Request, form: boolean): string {
 | 
			
		||||
    let rawTx: any = typeof req.body === 'object' && form
 | 
			
		||||
      ? Object.values(req.body)[0] as any
 | 
			
		||||
      : req.body;
 | 
			
		||||
    if (typeof rawTx !== 'string') {
 | 
			
		||||
      throw Object.assign(new Error('Non-string request body'), { code: -1 });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Support both upper and lower case hex
 | 
			
		||||
    // Support both txHash= Form and direct API POST
 | 
			
		||||
    const reg = form ? /^txHash=((?:[a-fA-F0-9]{2})+)$/ : /^((?:[a-fA-F0-9]{2})+)$/;
 | 
			
		||||
    const matches = reg.exec(rawTx);
 | 
			
		||||
    if (!matches || !matches[1]) {
 | 
			
		||||
      throw Object.assign(new Error('Non-hex request body'), { 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
 | 
			
		||||
 | 
			
		||||
    // We assume txhex to be valid hex (output of getTransactionFromRequest above)
 | 
			
		||||
 | 
			
		||||
    // Check 1: Valid transaction parse
 | 
			
		||||
    let tx: bitcoinjs.Transaction;
 | 
			
		||||
    try {
 | 
			
		||||
      tx = bitcoinjs.Transaction.fromHex(txhex);
 | 
			
		||||
    } catch(e) {
 | 
			
		||||
      throw Object.assign(new Error('Invalid transaction (could not parse)'), { code: -4 });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check 2: Simple size check
 | 
			
		||||
    if (tx.weight() > config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT) {
 | 
			
		||||
      throw Object.assign(new Error(`Transaction too large (max ${config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT} weight units)`), { code: -3 });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check 3: Check unreachable script in taproot (if not allowed)
 | 
			
		||||
    if (!config.MEMPOOL.ALLOW_UNREACHABLE) {
 | 
			
		||||
      tx.ins.forEach(input => {
 | 
			
		||||
        const witness = input.witness;
 | 
			
		||||
        // See BIP 341: Script validation rules
 | 
			
		||||
        const hasAnnex = witness.length >= 2 &&
 | 
			
		||||
          witness[witness.length - 1][0] === 0x50;
 | 
			
		||||
        const scriptSpendMinLength = hasAnnex ? 3 : 2;
 | 
			
		||||
        const maybeScriptSpend = witness.length >= scriptSpendMinLength;
 | 
			
		||||
 | 
			
		||||
        if (maybeScriptSpend) {
 | 
			
		||||
          const controlBlock = witness[witness.length - scriptSpendMinLength + 1];
 | 
			
		||||
          if (controlBlock.length === 0 || !this.isValidLeafVersion(controlBlock[0])) {
 | 
			
		||||
            // Skip this input, it's not taproot
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          // Definitely taproot. Get script
 | 
			
		||||
          const script = witness[witness.length - scriptSpendMinLength];
 | 
			
		||||
          const decompiled = bitcoinjs.script.decompile(script);
 | 
			
		||||
          if (!decompiled || decompiled.length < 2) {
 | 
			
		||||
            // Skip this input
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          // Iterate up to second last (will look ahead 1 item)
 | 
			
		||||
          for (let i = 0; i < decompiled.length - 1; i++) {
 | 
			
		||||
            const first = decompiled[i];
 | 
			
		||||
            const second = decompiled[i + 1];
 | 
			
		||||
            if (
 | 
			
		||||
              first === bitcoinjs.opcodes.OP_FALSE &&
 | 
			
		||||
              second === bitcoinjs.opcodes.OP_IF
 | 
			
		||||
            ) {
 | 
			
		||||
              throw Object.assign(new Error('Unreachable taproot scripts not allowed'), { code: -5 });
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Pass through the input string untouched
 | 
			
		||||
    return txhex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static isValidLeafVersion(leafVersion: number): boolean {
 | 
			
		||||
    // See Note 7 in BIP341
 | 
			
		||||
    // https://github.com/bitcoin/bips/blob/66a1a8151021913047934ebab3f8883f2f8ca75b/bip-0341.mediawiki#cite_note-7
 | 
			
		||||
    // "What constraints are there on the leaf version?"
 | 
			
		||||
 | 
			
		||||
    // Must be an integer between 0 and 255
 | 
			
		||||
    // Since we're parsing a byte
 | 
			
		||||
    if (Math.floor(leafVersion) !== leafVersion || leafVersion < 0 || leafVersion > 255) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    // "the leaf version cannot be odd"
 | 
			
		||||
    if ((leafVersion & 0x01) === 1) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    // "The values that comply to this rule are
 | 
			
		||||
    // the 32 even values between 0xc0 and 0xfe
 | 
			
		||||
    if (leafVersion >= 0xc0 && leafVersion <= 0xfe) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    // and also 0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe."
 | 
			
		||||
    if ([0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe].includes(leafVersion)) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    // Otherwise, invalid
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ class RbfCache {
 | 
			
		||||
          if (tree) {
 | 
			
		||||
            tree.interval = newTime - tree?.time;
 | 
			
		||||
            replacedTrees.push(tree);
 | 
			
		||||
            fullRbf = fullRbf || tree.fullRbf;
 | 
			
		||||
            fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
 | 
			
		||||
@ -333,6 +333,40 @@ class WebsocketHandler {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleReorg(): void {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const da = difficultyAdjustment.getDifficultyAdjustment();
 | 
			
		||||
 | 
			
		||||
    // update init data
 | 
			
		||||
    this.updateSocketDataFields({
 | 
			
		||||
      'blocks': blocks.getBlocks(),
 | 
			
		||||
      'da': da?.previousTime ? da : undefined,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach((client) => {
 | 
			
		||||
      if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const response = {};
 | 
			
		||||
 | 
			
		||||
      if (client['want-blocks']) {
 | 
			
		||||
        response['blocks'] = this.socketData['blocks'];
 | 
			
		||||
      }
 | 
			
		||||
      if (client['want-stats']) {
 | 
			
		||||
        response['da'] = this.socketData['da'];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Object.keys(response).length) {
 | 
			
		||||
        const serializedResponse = this.serializeResponse(response);
 | 
			
		||||
        client.send(serializedResponse);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,8 @@ interface IConfig {
 | 
			
		||||
    CPFP_INDEXING: boolean;
 | 
			
		||||
    MAX_BLOCKS_BULK_QUERY: number;
 | 
			
		||||
    DISK_CACHE_BLOCK_INTERVAL: number;
 | 
			
		||||
    MAX_PUSH_TX_SIZE_WEIGHT: number;
 | 
			
		||||
    ALLOW_UNREACHABLE: boolean;
 | 
			
		||||
  };
 | 
			
		||||
  ESPLORA: {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
@ -165,6 +167,8 @@ const defaults: IConfig = {
 | 
			
		||||
    'CPFP_INDEXING': false,
 | 
			
		||||
    'MAX_BLOCKS_BULK_QUERY': 0,
 | 
			
		||||
    'DISK_CACHE_BLOCK_INTERVAL': 6,
 | 
			
		||||
    'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
 | 
			
		||||
    'ALLOW_UNREACHABLE': true,
 | 
			
		||||
  },
 | 
			
		||||
  'ESPLORA': {
 | 
			
		||||
    'REST_API_URL': 'http://127.0.0.1:3000',
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import logger from './logger';
 | 
			
		||||
import bitcoinClient from './api/bitcoin/bitcoin-client';
 | 
			
		||||
import priceUpdater from './tasks/price-updater';
 | 
			
		||||
import PricesRepository from './repositories/PricesRepository';
 | 
			
		||||
import config from './config';
 | 
			
		||||
 | 
			
		||||
export interface CoreIndex {
 | 
			
		||||
  name: string;
 | 
			
		||||
@ -72,7 +73,7 @@ class Indexer {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (task === 'blocksPrices' && !this.tasksRunning.includes(task)) {
 | 
			
		||||
    if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
      this.tasksRunning.push(task);
 | 
			
		||||
      const lastestPriceId = await PricesRepository.$getLatestPriceId();
 | 
			
		||||
      if (priceUpdater.historyInserted === false || lastestPriceId === null) {
 | 
			
		||||
 | 
			
		||||
@ -401,7 +401,7 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get average block health for all blocks for a single pool
 | 
			
		||||
   */
 | 
			
		||||
  public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> {
 | 
			
		||||
  public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number | null> {
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
    const query = `
 | 
			
		||||
      SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
 | 
			
		||||
@ -413,8 +413,8 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await DB.query(query, params);
 | 
			
		||||
      if (!rows[0] || !rows[0].avg_match_rate) {
 | 
			
		||||
        return 0;
 | 
			
		||||
      if (!rows[0] || rows[0].avg_match_rate == null) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      return Math.round(rows[0].avg_match_rate * 100) / 100;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
 | 
			
		||||
@ -269,7 +269,11 @@ class NetworkSyncService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $scanForClosedChannels(): Promise<void> {
 | 
			
		||||
    if (this.closedChannelsScanBlock === blocks.getCurrentBlockHeight()) {
 | 
			
		||||
    let currentBlockHeight = blocks.getCurrentBlockHeight();
 | 
			
		||||
    if (config.MEMPOOL.ENABLED === false) { // https://github.com/mempool/mempool/issues/3582
 | 
			
		||||
      currentBlockHeight = await bitcoinApi.$getBlockHeightTip();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.closedChannelsScanBlock === currentBlockHeight) {
 | 
			
		||||
      logger.debug(`We've already scan closed channels for this block, skipping.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -305,7 +309,7 @@ class NetworkSyncService {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.closedChannelsScanBlock = blocks.getCurrentBlockHeight();
 | 
			
		||||
      this.closedChannelsScanBlock = currentBlockHeight;
 | 
			
		||||
      logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,8 @@
 | 
			
		||||
    "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
 | 
			
		||||
    "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__,
 | 
			
		||||
    "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
 | 
			
		||||
    "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
 | 
			
		||||
  },
 | 
			
		||||
@ -126,4 +128,4 @@
 | 
			
		||||
    "GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
 | 
			
		||||
    "GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,9 @@ __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
 | 
			
		||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
 | 
			
		||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
 | 
			
		||||
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
 | 
			
		||||
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
 | 
			
		||||
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# CORE_RPC
 | 
			
		||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
 | 
			
		||||
@ -161,6 +164,8 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" me
 | 
			
		||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
@ -112,7 +112,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.error = error;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block)));
 | 
			
		||||
    this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
 | 
			
		||||
 | 
			
		||||
    this.stateService.bsqPrice$
 | 
			
		||||
      .subscribe((bsqPrice) => {
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
 | 
			
		||||
    this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
    <span i18n="shared.address">Address</span>
 | 
			
		||||
  </app-preview-title>
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-md">
 | 
			
		||||
    <div class="col-md table-col">
 | 
			
		||||
      <div class="row d-flex justify-content-between">
 | 
			
		||||
        <div class="title-wrapper">
 | 
			
		||||
          <h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,11 @@
 | 
			
		||||
  margin-right: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-col {
 | 
			
		||||
  max-width: calc(100% - 470px);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
  font-size: 32px;
 | 
			
		||||
  margin-top: 48px;
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
    <span i18n="shared.block-title">Block</span>
 | 
			
		||||
  </app-preview-title>
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-sm">
 | 
			
		||||
    <div class="col-sm table-col">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="block-titles">
 | 
			
		||||
          <h1 class="title">
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,11 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-col {
 | 
			
		||||
  max-width: calc(100% - 470px);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chart-container {
 | 
			
		||||
  flex-grow: 0;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
import { PriceService, Price } from '../../services/price.service';
 | 
			
		||||
import { CacheService } from '../../services/cache.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block',
 | 
			
		||||
@ -72,6 +73,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  auditSubscription: Subscription;
 | 
			
		||||
  keyNavigationSubscription: Subscription;
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  cacheBlocksSubscription: Subscription;
 | 
			
		||||
  networkChangedSubscription: Subscription;
 | 
			
		||||
  queryParamsSubscription: Subscription;
 | 
			
		||||
  nextBlockSubscription: Subscription = undefined;
 | 
			
		||||
@ -99,6 +101,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private priceService: PriceService,
 | 
			
		||||
    private cacheService: CacheService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = detectWebGL();
 | 
			
		||||
  }
 | 
			
		||||
@ -128,19 +131,27 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
 | 
			
		||||
      this.loadedCacheBlock(block);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
        this.latestBlock = block;
 | 
			
		||||
        this.latestBlocks.unshift(block);
 | 
			
		||||
        this.latestBlocks = this.latestBlocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.latestBlock = blocks[0];
 | 
			
		||||
        this.latestBlocks = blocks;
 | 
			
		||||
        this.setNextAndPreviousBlockLink();
 | 
			
		||||
 | 
			
		||||
        if (block.id === this.blockHash) {
 | 
			
		||||
          this.block = block;
 | 
			
		||||
          block.extras.minFee = this.getMinBlockFee(block);
 | 
			
		||||
          block.extras.maxFee = this.getMaxBlockFee(block);
 | 
			
		||||
          if (block?.extras?.reward != undefined) {
 | 
			
		||||
            this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
			
		||||
        for (const block of blocks) {
 | 
			
		||||
          if (block.id === this.blockHash) {
 | 
			
		||||
            this.block = block;
 | 
			
		||||
            block.extras.minFee = this.getMinBlockFee(block);
 | 
			
		||||
            block.extras.maxFee = this.getMaxBlockFee(block);
 | 
			
		||||
            if (block?.extras?.reward != undefined) {
 | 
			
		||||
              this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
			
		||||
            }
 | 
			
		||||
          } else if (block.height === this.block?.height) {
 | 
			
		||||
            this.block.stale = true;
 | 
			
		||||
            this.block.canonical = block.id;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
@ -254,6 +265,13 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        this.transactionsError = null;
 | 
			
		||||
        this.isLoadingOverview = true;
 | 
			
		||||
        this.overviewError = null;
 | 
			
		||||
 | 
			
		||||
        const cachedBlock = this.cacheService.getCachedBlock(block.height);
 | 
			
		||||
        if (!cachedBlock) {
 | 
			
		||||
          this.cacheService.loadBlock(block.height);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.loadedCacheBlock(cachedBlock);
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
 | 
			
		||||
      shareReplay(1)
 | 
			
		||||
@ -459,6 +477,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.auditSubscription?.unsubscribe();
 | 
			
		||||
    this.keyNavigationSubscription?.unsubscribe();
 | 
			
		||||
    this.blocksSubscription?.unsubscribe();
 | 
			
		||||
    this.cacheBlocksSubscription?.unsubscribe();
 | 
			
		||||
    this.networkChangedSubscription?.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription?.unsubscribe();
 | 
			
		||||
    this.timeLtrSubscription?.unsubscribe();
 | 
			
		||||
@ -679,4 +698,11 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadedCacheBlock(block: BlockExtended): void {
 | 
			
		||||
    if (this.block && block.height === this.block.height && block.id !== this.block.id) {
 | 
			
		||||
      this.block.stale = true;
 | 
			
		||||
      this.block.canonical = block.id;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -36,11 +36,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
 | 
			
		||||
  markHeight: number;
 | 
			
		||||
  chainTip: number;
 | 
			
		||||
  pendingMarkBlock: { animate: boolean, newBlockFromLeft: boolean };
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  blockPageSubscription: Subscription;
 | 
			
		||||
  networkSubscription: Subscription;
 | 
			
		||||
  tabHiddenSubscription: Subscription;
 | 
			
		||||
  markBlockSubscription: Subscription;
 | 
			
		||||
  txConfirmedSubscription: Subscription;
 | 
			
		||||
  loadingBlocks$: Observable<boolean>;
 | 
			
		||||
  blockStyles = [];
 | 
			
		||||
  emptyBlockStyles = [];
 | 
			
		||||
@ -82,7 +84,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.chainTip = this.stateService.latestBlockHeight;
 | 
			
		||||
    this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
 | 
			
		||||
 | 
			
		||||
    if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
 | 
			
		||||
@ -104,31 +105,22 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
 | 
			
		||||
    if (!this.static) {
 | 
			
		||||
      this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
        .subscribe(([block, txConfirmed]) => {
 | 
			
		||||
          if (this.blocks.some((b) => b.height === block.height)) {
 | 
			
		||||
        .subscribe((blocks) => {
 | 
			
		||||
          if (!blocks?.length) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          const latestHeight = blocks[0].height;
 | 
			
		||||
          const animate = this.chainTip != null && latestHeight > this.chainTip;
 | 
			
		||||
 | 
			
		||||
          if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
 | 
			
		||||
            this.blocks = [];
 | 
			
		||||
            this.blocksFilled = false;
 | 
			
		||||
          for (const block of blocks) {
 | 
			
		||||
            block.extras.minFee = this.getMinBlockFee(block);
 | 
			
		||||
            block.extras.maxFee = this.getMaxBlockFee(block);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          block.extras.minFee = this.getMinBlockFee(block);
 | 
			
		||||
          block.extras.maxFee = this.getMaxBlockFee(block);
 | 
			
		||||
 | 
			
		||||
          this.blocks.unshift(block);
 | 
			
		||||
          this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
 | 
			
		||||
 | 
			
		||||
          if (txConfirmed && block.height > this.chainTip) {
 | 
			
		||||
            this.markHeight = block.height;
 | 
			
		||||
            this.moveArrowToPosition(true, true);
 | 
			
		||||
          } else {
 | 
			
		||||
            this.moveArrowToPosition(true, false);
 | 
			
		||||
          }
 | 
			
		||||
          this.blocks = blocks;
 | 
			
		||||
 | 
			
		||||
          this.blockStyles = [];
 | 
			
		||||
          if (this.blocksFilled && block.height > this.chainTip) {
 | 
			
		||||
          if (animate) {
 | 
			
		||||
            this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              this.blockStyles = [];
 | 
			
		||||
@ -139,13 +131,23 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
            this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (this.blocks.length === this.dynamicBlocksAmount) {
 | 
			
		||||
            this.blocksFilled = true;
 | 
			
		||||
          }
 | 
			
		||||
          this.chainTip = latestHeight;
 | 
			
		||||
 | 
			
		||||
          this.chainTip = Math.max(this.chainTip, block.height);
 | 
			
		||||
          if (this.pendingMarkBlock) {
 | 
			
		||||
            this.moveArrowToPosition(this.pendingMarkBlock.animate, this.pendingMarkBlock.newBlockFromLeft);
 | 
			
		||||
            this.pendingMarkBlock = null;
 | 
			
		||||
          }
 | 
			
		||||
          this.cd.markForCheck();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txid, block]) => {
 | 
			
		||||
        if (txid) {
 | 
			
		||||
          this.markHeight = block.height;
 | 
			
		||||
          this.moveArrowToPosition(true, true);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.moveArrowToPosition(true, false);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
 | 
			
		||||
        if (block.height <= this.height && block.height > this.height - this.count) {
 | 
			
		||||
@ -164,9 +166,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (this.static) {
 | 
			
		||||
        this.updateStaticBlocks();
 | 
			
		||||
      }
 | 
			
		||||
    if (this.static) {
 | 
			
		||||
      this.updateStaticBlocks();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
@ -190,6 +192,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    if (this.blockPageSubscription) {
 | 
			
		||||
      this.blockPageSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.txConfirmedSubscription) {
 | 
			
		||||
      this.txConfirmedSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    this.networkSubscription.unsubscribe();
 | 
			
		||||
    this.tabHiddenSubscription.unsubscribe();
 | 
			
		||||
    this.markBlockSubscription.unsubscribe();
 | 
			
		||||
@ -202,6 +207,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
      this.arrowVisible = false;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.chainTip == null) {
 | 
			
		||||
      this.pendingMarkBlock = { animate, newBlockFromLeft };
 | 
			
		||||
    }
 | 
			
		||||
    const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
 | 
			
		||||
    if (blockindex > -1) {
 | 
			
		||||
      if (!animate) {
 | 
			
		||||
 | 
			
		||||
@ -82,12 +82,12 @@ export class BlocksList implements OnInit {
 | 
			
		||||
      ),
 | 
			
		||||
      this.stateService.blocks$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((block) => {
 | 
			
		||||
            if (block[0].height <= this.lastBlockHeight) {
 | 
			
		||||
          switchMap((blocks) => {
 | 
			
		||||
            if (blocks[0].height <= this.lastBlockHeight) {
 | 
			
		||||
              return [null]; // Return an empty stream so the last pipe is not executed
 | 
			
		||||
            }
 | 
			
		||||
            this.lastBlockHeight = block[0].height;
 | 
			
		||||
            return [block];
 | 
			
		||||
            this.lastBlockHeight = blocks[0].height;
 | 
			
		||||
            return blocks;
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,3 @@
 | 
			
		||||
<span [style]="change >= 0 ? 'color: #42B747' : 'color: #B74242'">
 | 
			
		||||
  {{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
 | 
			
		||||
  ‎{{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
 | 
			
		||||
</span>
 | 
			
		||||
 | 
			
		||||
@ -39,13 +39,10 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
      })
 | 
			
		||||
    ).subscribe();
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
        if (block) {
 | 
			
		||||
          this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
 | 
			
		||||
          // using block-reported times, so ensure they are sorted chronologically
 | 
			
		||||
          this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
 | 
			
		||||
          this.updateSegments();
 | 
			
		||||
        }
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
 | 
			
		||||
        this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
 | 
			
		||||
        this.updateSegments();
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -60,14 +60,11 @@ export class ClockComponent implements OnInit {
 | 
			
		||||
    this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
 | 
			
		||||
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
        if (block) {
 | 
			
		||||
          this.blocks.unshift(block);
 | 
			
		||||
          this.blocks = this.blocks.slice(0, 16);
 | 
			
		||||
          if (this.blocks[this.blockIndex]) {
 | 
			
		||||
            this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
 | 
			
		||||
            this.cd.markForCheck();
 | 
			
		||||
          }
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.blocks = blocks.slice(0, 16);
 | 
			
		||||
        if (this.blocks[this.blockIndex]) {
 | 
			
		||||
          this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
 | 
			
		||||
          this.cd.markForCheck();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -38,11 +38,12 @@ export class DifficultyMiningComponent implements OnInit {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
 | 
			
		||||
    this.difficultyEpoch$ = combineLatest([
 | 
			
		||||
      this.stateService.blocks$.pipe(map(([block]) => block)),
 | 
			
		||||
      this.stateService.blocks$,
 | 
			
		||||
      this.stateService.difficultyAdjustment$,
 | 
			
		||||
    ])
 | 
			
		||||
    .pipe(
 | 
			
		||||
      map(([block, da]) => {
 | 
			
		||||
      map(([blocks, da]) => {
 | 
			
		||||
        const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
 | 
			
		||||
        let colorAdjustments = '#ffffff66';
 | 
			
		||||
        if (da.difficultyChange > 0) {
 | 
			
		||||
          colorAdjustments = '#3bcc49';
 | 
			
		||||
@ -63,7 +64,7 @@ export class DifficultyMiningComponent implements OnInit {
 | 
			
		||||
          colorPreviousAdjustments = '#ffffff66';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const blocksUntilHalving = 210000 - (block.height % 210000);
 | 
			
		||||
        const blocksUntilHalving = 210000 - (maxHeight % 210000);
 | 
			
		||||
        const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
 | 
			
		||||
 | 
			
		||||
        const data = {
 | 
			
		||||
 | 
			
		||||
@ -67,11 +67,12 @@ export class DifficultyComponent implements OnInit {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
 | 
			
		||||
    this.difficultyEpoch$ = combineLatest([
 | 
			
		||||
      this.stateService.blocks$.pipe(map(([block]) => block)),
 | 
			
		||||
      this.stateService.blocks$,
 | 
			
		||||
      this.stateService.difficultyAdjustment$,
 | 
			
		||||
    ])
 | 
			
		||||
    .pipe(
 | 
			
		||||
      map(([block, da]) => {
 | 
			
		||||
      map(([blocks, da]) => {
 | 
			
		||||
        const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
 | 
			
		||||
        let colorAdjustments = '#ffffff66';
 | 
			
		||||
        if (da.difficultyChange > 0) {
 | 
			
		||||
          colorAdjustments = '#3bcc49';
 | 
			
		||||
@ -92,7 +93,7 @@ export class DifficultyComponent implements OnInit {
 | 
			
		||||
          colorPreviousAdjustments = '#ffffff66';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const blocksUntilHalving = 210000 - (block.height % 210000);
 | 
			
		||||
        const blocksUntilHalving = 210000 - (maxHeight % 210000);
 | 
			
		||||
        const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
 | 
			
		||||
        const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
 | 
			
		||||
        const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
 | 
			
		||||
 | 
			
		||||
@ -109,6 +109,14 @@ export class HashrateChartComponent implements OnInit {
 | 
			
		||||
        tap((response: any) => {
 | 
			
		||||
          const data = response.body;
 | 
			
		||||
 | 
			
		||||
          // always include the latest difficulty
 | 
			
		||||
          if (data.difficulty.length && data.difficulty[data.difficulty.length - 1].difficulty !== data.currentDifficulty) {
 | 
			
		||||
            data.difficulty.push({
 | 
			
		||||
              timestamp: Date.now() / 1000,
 | 
			
		||||
              difficulty: data.currentDifficulty
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // We generate duplicated data point so the tooltip works nicely
 | 
			
		||||
          const diffFixed = [];
 | 
			
		||||
          let diffIndex = 1;
 | 
			
		||||
@ -137,6 +145,14 @@ export class HashrateChartComponent implements OnInit {
 | 
			
		||||
            ++diffIndex;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          while (diffIndex <= data.difficulty.length) {
 | 
			
		||||
            diffFixed.push({
 | 
			
		||||
              timestamp: data.difficulty[diffIndex - 1].time,
 | 
			
		||||
              difficulty: data.difficulty[diffIndex - 1].difficulty
 | 
			
		||||
            });
 | 
			
		||||
            diffIndex++;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          let maResolution = 15;
 | 
			
		||||
          const hashrateMa = [];
 | 
			
		||||
          for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
 | 
			
		||||
 | 
			
		||||
@ -124,7 +124,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    )
 | 
			
		||||
    .pipe(
 | 
			
		||||
      switchMap(() => combineLatest([
 | 
			
		||||
        this.stateService.blocks$.pipe(map(([block]) => block)),
 | 
			
		||||
        this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
 | 
			
		||||
        this.stateService.mempoolBlocks$
 | 
			
		||||
          .pipe(
 | 
			
		||||
            map((mempoolBlocks) => {
 | 
			
		||||
@ -186,8 +186,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    this.blockSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
    this.blockSubscription = this.stateService.blocks$.pipe(map((blocks) => blocks[0]))
 | 
			
		||||
      .subscribe((block) => {
 | 
			
		||||
        if (!block) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        if (this.chainTip === -1) {
 | 
			
		||||
          this.animateEntry = block.height === this.stateService.latestBlockHeight;
 | 
			
		||||
        } else {
 | 
			
		||||
@ -221,8 +224,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
          this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.stateService.blocks$
 | 
			
		||||
            .pipe(take(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT))
 | 
			
		||||
            .subscribe(([block]) => {
 | 
			
		||||
            .pipe(map((blocks) => blocks[0]))
 | 
			
		||||
            .subscribe((block) => {
 | 
			
		||||
              if (this.stateService.latestBlockHeight === block.height) {
 | 
			
		||||
                this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
 | 
			
		||||
              }
 | 
			
		||||
@ -297,7 +300,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    while (blocks.length > blocksAmount) {
 | 
			
		||||
      const block = blocks.pop();
 | 
			
		||||
      if (!this.count) {
 | 
			
		||||
        const lastBlock = blocks[blocks.length - 1];
 | 
			
		||||
        const lastBlock = blocks[0];
 | 
			
		||||
        lastBlock.blockSize += block.blockSize;
 | 
			
		||||
        lastBlock.blockVSize += block.blockVSize;
 | 
			
		||||
        lastBlock.nTx += block.nTx;
 | 
			
		||||
@ -308,7 +311,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (blocks.length) {
 | 
			
		||||
      blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
 | 
			
		||||
      blocks[0].isStack = blocks[0].blockVSize > this.stateService.blockVSize;
 | 
			
		||||
    }
 | 
			
		||||
    return blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  auditAvailable = false;
 | 
			
		||||
 | 
			
		||||
  loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
 | 
			
		||||
  loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[0]?.height);
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
@ -68,7 +68,7 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
          return this.apiService.getPoolStats$(slug);
 | 
			
		||||
        }),
 | 
			
		||||
        tap(() => {
 | 
			
		||||
          this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
 | 
			
		||||
          this.loadMoreSubject.next(this.blocks[0]?.height);
 | 
			
		||||
        }),
 | 
			
		||||
        map((poolStats) => {
 | 
			
		||||
          this.seoService.setTitle(poolStats.pool.name);
 | 
			
		||||
@ -91,7 +91,7 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
          if (this.slug === undefined) {
 | 
			
		||||
            return [];
 | 
			
		||||
          }
 | 
			
		||||
          return this.apiService.getPoolBlocks$(this.slug, this.blocks[this.blocks.length - 1]?.height);
 | 
			
		||||
          return this.apiService.getPoolBlocks$(this.slug, this.blocks[0]?.height);
 | 
			
		||||
        }),
 | 
			
		||||
        tap((newBlocks) => {
 | 
			
		||||
          this.blocks = this.blocks.concat(newBlocks);
 | 
			
		||||
@ -237,7 +237,7 @@ export class PoolComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadMore() {
 | 
			
		||||
    this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
 | 
			
		||||
    this.loadMoreSubject.next(this.blocks[0]?.height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByBlock(index: number, block: BlockExtended) {
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td class="td-width" i18n="transaction.status|Transaction Status">Status</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <span *ngIf="rbfInfo.tx.fullRbf" class="badge badge-info" i18n="rbfInfo-features.tag.full-rbf|Full RBF">Full RBF</span>
 | 
			
		||||
          <span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span>
 | 
			
		||||
          <ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template>
 | 
			
		||||
          <span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
 | 
			
		||||
import { RbfInfo } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { RbfTree } from '../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-rbf-timeline-tooltip',
 | 
			
		||||
@ -7,7 +7,7 @@ import { RbfInfo } from '../../interfaces/node-api.interface';
 | 
			
		||||
  styleUrls: ['./rbf-timeline-tooltip.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class RbfTimelineTooltipComponent implements OnChanges {
 | 
			
		||||
  @Input() rbfInfo: RbfInfo | void;
 | 
			
		||||
  @Input() rbfInfo: RbfTree | null;
 | 
			
		||||
  @Input() cursorPosition: { x: number, y: number };
 | 
			
		||||
 | 
			
		||||
  tooltipPosition = null;
 | 
			
		||||
 | 
			
		||||
@ -15,14 +15,15 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nodes">
 | 
			
		||||
        <ng-container *ngFor="let cell of timeline; let i = index;">
 | 
			
		||||
          <ng-container *ngIf="cell.replacement; else nonNode">
 | 
			
		||||
          <ng-container *ngIf="cell.replacement?.tx; else nonNode">
 | 
			
		||||
            <div class="node"
 | 
			
		||||
              [id]="'node-'+cell.replacement.tx.txid"
 | 
			
		||||
              [class.selected]="txid === cell.replacement.tx.txid"
 | 
			
		||||
              [class.mined]="cell.replacement.tx.mined"
 | 
			
		||||
              [class.first-node]="cell.first"
 | 
			
		||||
            >
 | 
			
		||||
              <div class="track"></div>
 | 
			
		||||
              <div class="track left" [class.fullrbf]="cell.replacement?.tx?.fullRbf"></div>
 | 
			
		||||
              <div class="track right" [class.fullrbf]="cell.fullRbf"></div>
 | 
			
		||||
              <a class="shape-border"
 | 
			
		||||
                [class.rbf]="cell.replacement.tx.rbf"
 | 
			
		||||
                [routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]"
 | 
			
		||||
@ -36,14 +37,14 @@
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-template #nonNode>
 | 
			
		||||
            <ng-container [ngSwitch]="cell.connector">
 | 
			
		||||
              <div class="connector" *ngSwitchCase="'pipe'"><div class="pipe"></div></div>
 | 
			
		||||
              <div class="connector" *ngSwitchCase="'corner'"><div class="corner"></div></div>
 | 
			
		||||
              <div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
 | 
			
		||||
              <div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
 | 
			
		||||
              <div class="node-spacer" *ngSwitchDefault></div>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <ng-container *ngIf="i < timeline.length - 1">
 | 
			
		||||
            <div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
 | 
			
		||||
              <div class="track"></div>
 | 
			
		||||
              <div class="track" [class.fullrbf]="cell.fullRbf"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
 | 
			
		||||
@ -83,15 +83,26 @@
 | 
			
		||||
      transform: translateY(-50%);
 | 
			
		||||
      background: #105fb0;
 | 
			
		||||
      border-radius: 5px;
 | 
			
		||||
 | 
			
		||||
      &.left {
 | 
			
		||||
        right: 50%;
 | 
			
		||||
      }
 | 
			
		||||
      &.right {
 | 
			
		||||
        left: 50%;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.fullrbf {
 | 
			
		||||
        background: #1bd8f4;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.first-node {
 | 
			
		||||
      .track {
 | 
			
		||||
        left: 50%;
 | 
			
		||||
      .track.left {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      .track {
 | 
			
		||||
        right: 50%;
 | 
			
		||||
      .track.right {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -177,11 +188,17 @@
 | 
			
		||||
        height: 108px;
 | 
			
		||||
        bottom: 50%;
 | 
			
		||||
        border-right: solid 10px #105fb0;
 | 
			
		||||
        &.fullrbf {
 | 
			
		||||
          border-right: solid 10px #1bd8f4;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .corner {
 | 
			
		||||
        border-bottom: solid 10px #105fb0;
 | 
			
		||||
        border-bottom-right-radius: 10px;
 | 
			
		||||
        &.fullrbf {
 | 
			
		||||
          border-bottom: solid 10px #1bd8f4;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,20 @@
 | 
			
		||||
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { RbfInfo, RbfTree } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { RbfTree, RbfTransaction } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
 | 
			
		||||
type Connector = 'pipe' | 'corner';
 | 
			
		||||
 | 
			
		||||
interface TimelineCell {
 | 
			
		||||
  replacement?: RbfInfo,
 | 
			
		||||
  replacement?: RbfTree,
 | 
			
		||||
  connector?: Connector,
 | 
			
		||||
  first?: boolean,
 | 
			
		||||
  fullRbf?: boolean,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isTimelineCell(val: RbfTree | TimelineCell): boolean {
 | 
			
		||||
  return !val || !('tx' in val);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -22,7 +27,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() txid: string;
 | 
			
		||||
  rows: TimelineCell[][] = [];
 | 
			
		||||
 | 
			
		||||
  hoverInfo: RbfInfo | void = null;
 | 
			
		||||
  hoverInfo: RbfTree | null = null;
 | 
			
		||||
  tooltipPosition = null;
 | 
			
		||||
 | 
			
		||||
  dir: 'rtl' | 'ltr' = 'ltr';
 | 
			
		||||
@ -53,13 +58,27 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
  buildTimelines(tree: RbfTree): TimelineCell[][] {
 | 
			
		||||
    if (!tree) return [];
 | 
			
		||||
 | 
			
		||||
    this.flagFullRbf(tree);
 | 
			
		||||
    const split = this.splitTimelines(tree);
 | 
			
		||||
    const timelines = this.prepareTimelines(split);
 | 
			
		||||
    return this.connectTimelines(timelines);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // sets the fullRbf flag on each transaction in the tree
 | 
			
		||||
  flagFullRbf(tree: RbfTree): void {
 | 
			
		||||
    let fullRbf = false;
 | 
			
		||||
    for (const replaced of tree.replaces) {
 | 
			
		||||
      if (!replaced.tx.rbf) {
 | 
			
		||||
        fullRbf = true;
 | 
			
		||||
      }
 | 
			
		||||
      replaced.replacedBy = tree.tx;
 | 
			
		||||
      this.flagFullRbf(replaced);
 | 
			
		||||
    }
 | 
			
		||||
    tree.tx.fullRbf = fullRbf;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // splits a tree into N leaf-to-root paths
 | 
			
		||||
  splitTimelines(tree: RbfTree, tail: RbfInfo[] = []): RbfInfo[][] {
 | 
			
		||||
  splitTimelines(tree: RbfTree, tail: RbfTree[] = []): RbfTree[][] {
 | 
			
		||||
    const replacements = [...tail, tree];
 | 
			
		||||
    if (tree.replaces.length) {
 | 
			
		||||
      return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements)));
 | 
			
		||||
@ -70,7 +89,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
 | 
			
		||||
  // merges separate leaf-to-root paths into a coherent forking timeline
 | 
			
		||||
  // represented as a 2D array of Rbf events
 | 
			
		||||
  prepareTimelines(lines: RbfInfo[][]): RbfInfo[][] {
 | 
			
		||||
  prepareTimelines(lines: RbfTree[][]): (RbfTree | TimelineCell)[][] {
 | 
			
		||||
    lines.sort((a, b) => b.length - a.length);
 | 
			
		||||
 | 
			
		||||
    const rows = lines.map(() => []);
 | 
			
		||||
@ -85,7 +104,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
      let emptyCount = 0;
 | 
			
		||||
      const nextGroups = [];
 | 
			
		||||
      for (const group of lineGroups) {
 | 
			
		||||
        const toMerge: { [txid: string]: RbfInfo[][] } = {};
 | 
			
		||||
        const toMerge: { [txid: string]: RbfTree[][] } = {};
 | 
			
		||||
        let emptyInGroup = 0;
 | 
			
		||||
        let first = true;
 | 
			
		||||
        for (const line of group) {
 | 
			
		||||
@ -97,7 +116,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
          } else {
 | 
			
		||||
            // substitute duplicates with empty cells
 | 
			
		||||
            // (we'll fill these in with connecting lines later)
 | 
			
		||||
            rows[index].unshift(null);
 | 
			
		||||
            rows[index].unshift({ connector: true, replacement: head });
 | 
			
		||||
          }
 | 
			
		||||
          // group the tails of the remaining lines for the next iteration
 | 
			
		||||
          if (line.length) {
 | 
			
		||||
@ -127,7 +146,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements
 | 
			
		||||
  connectTimelines(timelines: RbfInfo[][]): TimelineCell[][] {
 | 
			
		||||
  connectTimelines(timelines: (RbfTree | TimelineCell)[][]): TimelineCell[][] {
 | 
			
		||||
    const rows: TimelineCell[][] = [];
 | 
			
		||||
    timelines.forEach((lines, row) => {
 | 
			
		||||
      rows.push([]);
 | 
			
		||||
@ -135,11 +154,12 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
      let finished = false;
 | 
			
		||||
      lines.forEach((replacement, column) => {
 | 
			
		||||
        const cell: TimelineCell = {};
 | 
			
		||||
        if (replacement) {
 | 
			
		||||
          cell.replacement = replacement;
 | 
			
		||||
        if (!isTimelineCell(replacement)) {
 | 
			
		||||
          cell.replacement = replacement as RbfTree;
 | 
			
		||||
          cell.fullRbf = (replacement as RbfTree).replacedBy?.fullRbf;
 | 
			
		||||
        }
 | 
			
		||||
        rows[row].push(cell);
 | 
			
		||||
        if (replacement) {
 | 
			
		||||
        if (!isTimelineCell(replacement)) {
 | 
			
		||||
          if (!started) {
 | 
			
		||||
            cell.first = true;
 | 
			
		||||
            started = true;
 | 
			
		||||
@ -153,11 +173,13 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
                matched = true;
 | 
			
		||||
              } else if (i === row) {
 | 
			
		||||
                rows[i][column] = {
 | 
			
		||||
                  connector: 'corner'
 | 
			
		||||
                  connector: 'corner',
 | 
			
		||||
                  fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
 | 
			
		||||
                };
 | 
			
		||||
              } else if (nextCell.connector !== 'corner') {
 | 
			
		||||
                rows[i][column] = {
 | 
			
		||||
                  connector: 'pipe'
 | 
			
		||||
                  connector: 'pipe',
 | 
			
		||||
                  fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
 | 
			
		||||
                };
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -29,11 +29,12 @@ export class RewardStatsComponent implements OnInit {
 | 
			
		||||
      // Or when we receive a newer block, newer than the latest reward stats api call
 | 
			
		||||
      this.stateService.blocks$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((block) => {
 | 
			
		||||
            if (block[0].height <= this.lastBlockHeight) {
 | 
			
		||||
          switchMap((blocks) => {
 | 
			
		||||
            const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
 | 
			
		||||
            if (maxHeight <= this.lastBlockHeight) {
 | 
			
		||||
              return []; // Return an empty stream so the last pipe is not executed
 | 
			
		||||
            }
 | 
			
		||||
            this.lastBlockHeight = block[0].height;
 | 
			
		||||
            this.lastBlockHeight = maxHeight;
 | 
			
		||||
            return this.apiService.getRewardStats$();
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Inpu
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { MarkBlockState, StateService } from '../../services/state.service';
 | 
			
		||||
import { specialBlocks } from '../../app.constants';
 | 
			
		||||
import { BlockExtended } from '../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-start',
 | 
			
		||||
@ -55,8 +56,8 @@ export class StartComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
 | 
			
		||||
    this.blockCounterSubscription = this.stateService.blocks$.subscribe(() => {
 | 
			
		||||
      this.blockCount++;
 | 
			
		||||
    this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
 | 
			
		||||
      this.blockCount = blocks.length;
 | 
			
		||||
      this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
 | 
			
		||||
      this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
 | 
			
		||||
      if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
 | 
			
		||||
@ -110,9 +111,12 @@ export class StartComponent implements OnInit, OnDestroy {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.stateService.blocks$
 | 
			
		||||
      .subscribe((blocks: any) => {
 | 
			
		||||
      .subscribe((blocks: BlockExtended[]) => {
 | 
			
		||||
        this.countdown = 0;
 | 
			
		||||
        const block = blocks[0];
 | 
			
		||||
        if (!block) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const sb in specialBlocks) {
 | 
			
		||||
          if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {
 | 
			
		||||
 | 
			
		||||
@ -306,7 +306,7 @@
 | 
			
		||||
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="isLoadingTx && !error">
 | 
			
		||||
  <ng-template [ngIf]="(isLoadingTx && !error) || loadingCachedTx">
 | 
			
		||||
 | 
			
		||||
    <div class="box">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
@ -451,7 +451,7 @@
 | 
			
		||||
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="error">
 | 
			
		||||
  <ng-template [ngIf]="error && !loadingCachedTx">
 | 
			
		||||
 | 
			
		||||
    <div class="text-center" *ngIf="waitingForTransaction; else errorTemplate">
 | 
			
		||||
      <h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ import {
 | 
			
		||||
  tap
 | 
			
		||||
} from 'rxjs/operators';
 | 
			
		||||
import { Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { of, merge, Subscription, Observable, Subject, timer, from, throwError } from 'rxjs';
 | 
			
		||||
import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { CacheService } from '../../services/cache.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
@ -39,6 +39,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  isLoadingTx = true;
 | 
			
		||||
  error: any = undefined;
 | 
			
		||||
  errorUnblinded: any = undefined;
 | 
			
		||||
  loadingCachedTx = false;
 | 
			
		||||
  waitingForTransaction = false;
 | 
			
		||||
  latestBlock: BlockExtended;
 | 
			
		||||
  transactionTime = -1;
 | 
			
		||||
@ -49,10 +50,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  txReplacedSubscription: Subscription;
 | 
			
		||||
  txRbfInfoSubscription: Subscription;
 | 
			
		||||
  mempoolPositionSubscription: Subscription;
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  queryParamsSubscription: Subscription;
 | 
			
		||||
  urlFragmentSubscription: Subscription;
 | 
			
		||||
  mempoolBlocksSubscription: Subscription;
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  fragmentParams: URLSearchParams;
 | 
			
		||||
  rbfTransaction: undefined | Transaction;
 | 
			
		||||
  replaced: boolean = false;
 | 
			
		||||
@ -131,6 +132,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
      this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$.subscribe((blocks) => {
 | 
			
		||||
      this.latestBlock = blocks[0];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.fetchCpfpSubscription = this.fetchCpfp$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((txId) =>
 | 
			
		||||
@ -199,6 +204,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.fetchCachedTxSubscription = this.fetchCachedTx$
 | 
			
		||||
    .pipe(
 | 
			
		||||
      tap(() => {
 | 
			
		||||
        this.loadingCachedTx = true;
 | 
			
		||||
      }),
 | 
			
		||||
      switchMap((txId) =>
 | 
			
		||||
        this.apiService
 | 
			
		||||
          .getRbfCachedTx$(txId)
 | 
			
		||||
@ -207,6 +215,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
        return of(null);
 | 
			
		||||
      })
 | 
			
		||||
    ).subscribe((tx) => {
 | 
			
		||||
      this.loadingCachedTx = false;
 | 
			
		||||
      if (!tx) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
@ -338,6 +347,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
          this.tx.feePerVsize = tx.fee / (tx.weight / 4);
 | 
			
		||||
          this.isLoadingTx = false;
 | 
			
		||||
          this.error = undefined;
 | 
			
		||||
          this.loadingCachedTx = false;
 | 
			
		||||
          this.waitingForTransaction = false;
 | 
			
		||||
          this.websocketService.startTrackTransaction(tx.txid);
 | 
			
		||||
          this.graphExpanded = false;
 | 
			
		||||
@ -391,9 +401,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
 | 
			
		||||
      this.latestBlock = block;
 | 
			
		||||
 | 
			
		||||
    this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
 | 
			
		||||
      if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
 | 
			
		||||
        this.tx.status = {
 | 
			
		||||
          confirmed: true,
 | 
			
		||||
@ -409,6 +417,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
 | 
			
		||||
      if (!this.tx) {
 | 
			
		||||
        this.error = new Error();
 | 
			
		||||
        this.loadingCachedTx = false;
 | 
			
		||||
        this.waitingForTransaction = false;
 | 
			
		||||
      }
 | 
			
		||||
      this.rbfTransaction = rbfTransaction;
 | 
			
		||||
@ -593,13 +602,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.fetchCachedTxSubscription.unsubscribe();
 | 
			
		||||
    this.txReplacedSubscription.unsubscribe();
 | 
			
		||||
    this.txRbfInfoSubscription.unsubscribe();
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription.unsubscribe();
 | 
			
		||||
    this.flowPrefSubscription.unsubscribe();
 | 
			
		||||
    this.urlFragmentSubscription.unsubscribe();
 | 
			
		||||
    this.mempoolBlocksSubscription.unsubscribe();
 | 
			
		||||
    this.mempoolPositionSubscription.unsubscribe();
 | 
			
		||||
    this.mempoolBlocksSubscription.unsubscribe();
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.leaveTransaction();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
 | 
			
		||||
    this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
 | 
			
		||||
    this.stateService.networkChanged$.subscribe((network) => this.network = network);
 | 
			
		||||
 | 
			
		||||
    if (this.network === 'liquid' || this.network === 'liquidtestnet') {
 | 
			
		||||
 | 
			
		||||
@ -132,26 +132,19 @@ export class DashboardComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = this.stateService.blocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        tap(([block]) => {
 | 
			
		||||
          this.latestBlockHeight = block.height;
 | 
			
		||||
        tap((blocks) => {
 | 
			
		||||
          this.latestBlockHeight = blocks[0].height;
 | 
			
		||||
        }),
 | 
			
		||||
        scan((acc, [block]) => {
 | 
			
		||||
          if (acc.find((b) => b.height == block.height)) {
 | 
			
		||||
            return acc;
 | 
			
		||||
          }
 | 
			
		||||
          acc.unshift(block);
 | 
			
		||||
          acc = acc.slice(0, 6);
 | 
			
		||||
 | 
			
		||||
        switchMap((blocks) => {
 | 
			
		||||
          if (this.stateService.env.MINING_DASHBOARD === true) {
 | 
			
		||||
            for (const block of acc) {
 | 
			
		||||
            for (const block of blocks) {
 | 
			
		||||
              // @ts-ignore: Need to add an extra field for the template
 | 
			
		||||
              block.extras.pool.logo = `/resources/mining-pools/` +
 | 
			
		||||
                block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return acc;
 | 
			
		||||
        }, []),
 | 
			
		||||
          return of(blocks.slice(0, 6));
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.transactions$ = this.stateService.transactions$
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,10 @@
 | 
			
		||||
</span>
 | 
			
		||||
 | 
			
		||||
<ng-template #noblockconversion>
 | 
			
		||||
  <span [class]="colorClass" *ngIf="(conversions$ | async) as conversions">
 | 
			
		||||
  <span [class]="colorClass" *ngIf="(conversions$ | async) as conversions; else noconversion">
 | 
			
		||||
    {{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}
 | 
			
		||||
  </span>
 | 
			
		||||
  <ng-template #noconversion>
 | 
			
		||||
    <span> </span>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -39,6 +39,7 @@ export interface RbfTree extends RbfInfo {
 | 
			
		||||
  mined?: boolean;
 | 
			
		||||
  fullRbf: boolean;
 | 
			
		||||
  replaces: RbfTree[];
 | 
			
		||||
  replacedBy?: RbfTransaction;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DifficultyAdjustment {
 | 
			
		||||
@ -176,9 +177,10 @@ export interface TransactionStripped {
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface RbfTransaction extends TransactionStripped {
 | 
			
		||||
export interface RbfTransaction extends TransactionStripped {
 | 
			
		||||
  rbf?: boolean;
 | 
			
		||||
  mined?: boolean,
 | 
			
		||||
  fullRbf?: boolean,
 | 
			
		||||
}
 | 
			
		||||
export interface MempoolPosition {
 | 
			
		||||
  block: number,
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-md">
 | 
			
		||||
    <div class="col-md table-col">
 | 
			
		||||
      <a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a>
 | 
			
		||||
      <table class="table table-borderless table-striped">
 | 
			
		||||
        <tbody>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,8 @@
 | 
			
		||||
.table-col {
 | 
			
		||||
  max-width: calc(100% - 470px);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
  font-size: 32px;
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,8 @@
 | 
			
		||||
.table-col {
 | 
			
		||||
  max-width: calc(100% - 470px);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
  margin-top: 6px;
 | 
			
		||||
  font-size: 32px;
 | 
			
		||||
@ -18,10 +23,6 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-col {
 | 
			
		||||
  max-width: calc(100% - 470px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-col {
 | 
			
		||||
  flex-grow: 0;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ export class CacheService {
 | 
			
		||||
  txCache: { [txid: string]: Transaction } = {};
 | 
			
		||||
 | 
			
		||||
  network: string;
 | 
			
		||||
  blockHashCache: { [hash: string]: BlockExtended } = {};
 | 
			
		||||
  blockCache: { [height: number]: BlockExtended } = {};
 | 
			
		||||
  blockLoading: { [height: number]: boolean } = {};
 | 
			
		||||
  copiesInBlockQueue: { [height: number]: number } = {};
 | 
			
		||||
@ -27,8 +28,10 @@ export class CacheService {
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.stateService.blocks$.subscribe(([block]) => {
 | 
			
		||||
      this.addBlockToCache(block);
 | 
			
		||||
    this.stateService.blocks$.subscribe((blocks) => {
 | 
			
		||||
      for (const block of blocks) {
 | 
			
		||||
        this.addBlockToCache(block);
 | 
			
		||||
      }
 | 
			
		||||
      this.clearBlocks();
 | 
			
		||||
    });
 | 
			
		||||
    this.stateService.chainTip$.subscribe((height) => {
 | 
			
		||||
@ -56,8 +59,11 @@ export class CacheService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addBlockToCache(block: BlockExtended) {
 | 
			
		||||
    this.blockCache[block.height] = block;
 | 
			
		||||
    this.bumpBlockPriority(block.height);
 | 
			
		||||
    if (!this.blockHashCache[block.id]) {
 | 
			
		||||
      this.blockHashCache[block.id] = block;
 | 
			
		||||
      this.blockCache[block.height] = block;
 | 
			
		||||
      this.bumpBlockPriority(block.height);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async loadBlock(height) {
 | 
			
		||||
@ -105,7 +111,9 @@ export class CacheService {
 | 
			
		||||
      } else if ((this.tip - height) < KEEP_RECENT_BLOCKS) {
 | 
			
		||||
        this.bumpBlockPriority(height);
 | 
			
		||||
      } else {
 | 
			
		||||
        const block = this.blockCache[height];
 | 
			
		||||
        delete this.blockCache[height];
 | 
			
		||||
        delete this.blockHashCache[block.id];
 | 
			
		||||
        delete this.copiesInBlockQueue[height];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -113,6 +121,7 @@ export class CacheService {
 | 
			
		||||
 | 
			
		||||
  // remove all blocks from the cache
 | 
			
		||||
  resetBlockCache() {
 | 
			
		||||
    this.blockHashCache = {};
 | 
			
		||||
    this.blockCache = {};
 | 
			
		||||
    this.blockLoading = {};
 | 
			
		||||
    this.copiesInBlockQueue = {};
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
 | 
			
		||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
 | 
			
		||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Router, NavigationStart } from '@angular/router';
 | 
			
		||||
import { isPlatformBrowser } from '@angular/common';
 | 
			
		||||
import { map, scan, shareReplay, tap } from 'rxjs/operators';
 | 
			
		||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
 | 
			
		||||
import { StorageService } from './storage.service';
 | 
			
		||||
 | 
			
		||||
export interface MarkBlockState {
 | 
			
		||||
@ -90,10 +90,12 @@ export class StateService {
 | 
			
		||||
  blockVSize: number;
 | 
			
		||||
  env: Env;
 | 
			
		||||
  latestBlockHeight = -1;
 | 
			
		||||
  blocks: BlockExtended[] = [];
 | 
			
		||||
 | 
			
		||||
  networkChanged$ = new ReplaySubject<string>(1);
 | 
			
		||||
  lightningChanged$ = new ReplaySubject<boolean>(1);
 | 
			
		||||
  blocks$: ReplaySubject<[BlockExtended, string]>;
 | 
			
		||||
  blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
 | 
			
		||||
  blocks$: Observable<BlockExtended[]>;
 | 
			
		||||
  transactions$ = new ReplaySubject<TransactionStripped>(6);
 | 
			
		||||
  conversions$ = new ReplaySubject<any>(1);
 | 
			
		||||
  bsqPrice$ = new ReplaySubject<number>(1);
 | 
			
		||||
@ -102,6 +104,7 @@ export class StateService {
 | 
			
		||||
  mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
 | 
			
		||||
  mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
 | 
			
		||||
  liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
 | 
			
		||||
  txConfirmed$ = new Subject<[string, BlockExtended]>();
 | 
			
		||||
  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
			
		||||
  txRbfInfo$ = new Subject<RbfTree>();
 | 
			
		||||
  rbfLatest$ = new Subject<RbfTree[]>();
 | 
			
		||||
@ -167,8 +170,6 @@ export class StateService {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
 | 
			
		||||
 | 
			
		||||
    this.liveMempoolBlockTransactions$ = merge(
 | 
			
		||||
      this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
 | 
			
		||||
      this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
 | 
			
		||||
@ -198,8 +199,15 @@ export class StateService {
 | 
			
		||||
      this.networkChanged$.next(this.env.BASE_MODULE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.networkChanged$.subscribe((network) => {
 | 
			
		||||
      this.transactions$ = new ReplaySubject<TransactionStripped>(6);
 | 
			
		||||
      this.blocksSubject$.next([]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
 | 
			
		||||
 | 
			
		||||
    this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));
 | 
			
		||||
 | 
			
		||||
    const savedTimePreference = this.storageService.getValue('time-preference-ltr');
 | 
			
		||||
    const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
 | 
			
		||||
    // default time direction is right-to-left, unless locale is a RTL language
 | 
			
		||||
@ -336,4 +344,15 @@ export class StateService {
 | 
			
		||||
      this.chainTip$.next(height);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resetBlocks(blocks: BlockExtended[]): void {
 | 
			
		||||
    this.blocks = blocks.reverse();
 | 
			
		||||
    this.blocksSubject$.next(blocks);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addBlock(block: BlockExtended): void {
 | 
			
		||||
    this.blocks.unshift(block);
 | 
			
		||||
    this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
 | 
			
		||||
    this.blocksSubject$.next(this.blocks);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,13 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
 | 
			
		||||
import { WebsocketResponse, IBackendInfo } from '../interfaces/websocket.interface';
 | 
			
		||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
			
		||||
import { StateService } from './state.service';
 | 
			
		||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { ApiService } from './api.service';
 | 
			
		||||
import { take } from 'rxjs/operators';
 | 
			
		||||
import { TransferState, makeStateKey } from '@angular/platform-browser';
 | 
			
		||||
import { BlockExtended } from '../interfaces/node-api.interface';
 | 
			
		||||
import { CacheService } from './cache.service';
 | 
			
		||||
 | 
			
		||||
const OFFLINE_RETRY_AFTER_MS = 2000;
 | 
			
		||||
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
 | 
			
		||||
@ -40,6 +40,7 @@ export class WebsocketService {
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private transferState: TransferState,
 | 
			
		||||
    private cacheService: CacheService,
 | 
			
		||||
  ) {
 | 
			
		||||
    if (!this.stateService.isBrowser) {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
@ -239,13 +240,8 @@ export class WebsocketService {
 | 
			
		||||
 | 
			
		||||
    if (response.blocks && response.blocks.length) {
 | 
			
		||||
      const blocks = response.blocks;
 | 
			
		||||
      let maxHeight = 0;
 | 
			
		||||
      blocks.forEach((block: BlockExtended) => {
 | 
			
		||||
        if (block.height > this.stateService.latestBlockHeight) {
 | 
			
		||||
          maxHeight = Math.max(maxHeight, block.height);
 | 
			
		||||
          this.stateService.blocks$.next([block, '']);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      this.stateService.resetBlocks(blocks);
 | 
			
		||||
      const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight);
 | 
			
		||||
      this.stateService.updateChainTip(maxHeight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -260,7 +256,8 @@ export class WebsocketService {
 | 
			
		||||
    if (response.block) {
 | 
			
		||||
      if (response.block.height === this.stateService.latestBlockHeight + 1) {
 | 
			
		||||
        this.stateService.updateChainTip(response.block.height);
 | 
			
		||||
        this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
 | 
			
		||||
        this.stateService.addBlock(response.block);
 | 
			
		||||
        this.stateService.txConfirmed$.next([response.txConfirmed, response.block]);
 | 
			
		||||
      } else if (response.block.height > this.stateService.latestBlockHeight + 1) {
 | 
			
		||||
        reinitBlocks = true;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -993,6 +993,10 @@ th {
 | 
			
		||||
      margin-right: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn-audit {
 | 
			
		||||
    margin-left: .5em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scriptmessage {
 | 
			
		||||
 | 
			
		||||
@ -1240,8 +1240,8 @@ if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then
 | 
			
		||||
        FreeBSD)
 | 
			
		||||
            echo "[*] Patching Bitcoin Electrs code for FreeBSD"
 | 
			
		||||
            osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
 | 
			
		||||
            osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
 | 
			
		||||
            osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
 | 
			
		||||
            #osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
 | 
			
		||||
            #osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
 | 
			
		||||
        ;;
 | 
			
		||||
        Debian)
 | 
			
		||||
        ;;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8993,
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": true,
 | 
			
		||||
    "RUST_GBT": true,
 | 
			
		||||
    "USE_SECOND_NODE_FOR_MINFEE": true,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG" : {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "NETWORK": "signet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8991,
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": true,
 | 
			
		||||
    "RUST_GBT": true,
 | 
			
		||||
    "POLL_RATE_MS": 1000,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG" : {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "NETWORK": "testnet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8992,
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,9 @@
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": true,
 | 
			
		||||
    "RUST_GBT": true,
 | 
			
		||||
    "POLL_RATE_MS": 1000,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 1,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG" : {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
 | 
			
		||||
@ -109,7 +109,10 @@ class Server {
 | 
			
		||||
        page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false)
 | 
			
		||||
      ])
 | 
			
		||||
      if (success === true) {
 | 
			
		||||
        const screenshot = await page.screenshot();
 | 
			
		||||
        const screenshot = await page.screenshot({
 | 
			
		||||
          captureBeyondViewport: false,
 | 
			
		||||
          clip: { width: 1200, height: 600, x: 0, y: 0, scale: 1 },
 | 
			
		||||
        });
 | 
			
		||||
        return screenshot;
 | 
			
		||||
      } else if (success === false) {
 | 
			
		||||
        logger.warn(`failed to render ${path} for ${action} due to client-side error, e.g. requested an invalid txid`);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user