Merge branch 'master' into mononaut/fix-loading-block-title
This commit is contained in:
		
						commit
						acd633530f
					
				@ -82,7 +82,8 @@
 | 
			
		||||
    "BACKEND": "lnd",
 | 
			
		||||
    "STATS_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30,
 | 
			
		||||
    "FORENSICS_INTERVAL": 43200
 | 
			
		||||
  },
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "TLS_CERT_PATH": "tls.cert",
 | 
			
		||||
 | 
			
		||||
@ -98,7 +98,8 @@
 | 
			
		||||
    "TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
 | 
			
		||||
    "STATS_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30,
 | 
			
		||||
    "FORENSICS_INTERVAL": 43200
 | 
			
		||||
  },
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "TLS_CERT_PATH": "",
 | 
			
		||||
 | 
			
		||||
@ -10,9 +10,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
 | 
			
		||||
 | 
			
		||||
class Audit {
 | 
			
		||||
  auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
 | 
			
		||||
   : { censored: string[], added: string[], score: number } {
 | 
			
		||||
   : { censored: string[], added: string[], fresh: string[], score: number } {
 | 
			
		||||
    if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
 | 
			
		||||
      return { censored: [], added: [], score: 0 };
 | 
			
		||||
      return { censored: [], added: [], fresh: [], score: 0 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const matches: string[] = []; // present in both mined block and template
 | 
			
		||||
@ -83,7 +83,17 @@ class Audit {
 | 
			
		||||
      } else {
 | 
			
		||||
        if (!isDisplaced[tx.txid]) {
 | 
			
		||||
          added.push(tx.txid);
 | 
			
		||||
        } else {
 | 
			
		||||
        }
 | 
			
		||||
        let blockIndex = -1;
 | 
			
		||||
        let index = -1;
 | 
			
		||||
        projectedBlocks.forEach((block, bi) => {
 | 
			
		||||
          const i = block.transactionIds.indexOf(tx.txid);
 | 
			
		||||
          if (i >= 0) {
 | 
			
		||||
            blockIndex = bi;
 | 
			
		||||
            index = i;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        overflowWeight += tx.weight;
 | 
			
		||||
      }
 | 
			
		||||
      totalWeight += tx.weight;
 | 
			
		||||
@ -119,48 +129,10 @@ class Audit {
 | 
			
		||||
    return {
 | 
			
		||||
      censored: Object.keys(isCensored),
 | 
			
		||||
      added,
 | 
			
		||||
      fresh,
 | 
			
		||||
      score
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditScores(fromHeight?: number, limit: number = 15): Promise<AuditScore[]> {
 | 
			
		||||
    let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
 | 
			
		||||
    const returnScores: AuditScore[] = [];
 | 
			
		||||
 | 
			
		||||
    if (currentHeight < 0) {
 | 
			
		||||
      return returnScores;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < limit && currentHeight >= 0; i++) {
 | 
			
		||||
      const block = blocks.getBlocks().find((b) => b.height === currentHeight);
 | 
			
		||||
      if (block?.extras?.matchRate != null) {
 | 
			
		||||
        returnScores.push({
 | 
			
		||||
          hash: block.id,
 | 
			
		||||
          matchRate: block.extras.matchRate
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        let currentHash;
 | 
			
		||||
        if (!currentHash && Common.indexingEnabled()) {
 | 
			
		||||
          const dbBlock = await blocksRepository.$getBlockByHeight(currentHeight);
 | 
			
		||||
          if (dbBlock && dbBlock['id']) {
 | 
			
		||||
            currentHash = dbBlock['id'];
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (!currentHash) {
 | 
			
		||||
          currentHash = await bitcoinApi.$getBlockHash(currentHeight);
 | 
			
		||||
        }
 | 
			
		||||
        if (currentHash) {
 | 
			
		||||
          const auditScore = await blocksAuditsRepository.$getBlockAuditScore(currentHash);
 | 
			
		||||
          returnScores.push({
 | 
			
		||||
            hash: currentHash,
 | 
			
		||||
            matchRate: auditScore?.matchRate
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      currentHeight--;
 | 
			
		||||
    }
 | 
			
		||||
    return returnScores;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Audit();
 | 
			
		||||
@ -89,6 +89,7 @@ class BitcoinRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
 | 
			
		||||
      .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
 | 
			
		||||
      ;
 | 
			
		||||
 | 
			
		||||
@ -324,6 +325,16 @@ class BitcoinRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getStrippedBlockTransactions(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getBlock(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const block = await blocks.$getBlock(req.params.hash);
 | 
			
		||||
@ -356,9 +367,9 @@ class BitcoinRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getStrippedBlockTransactions(req: Request, res: Response) {
 | 
			
		||||
  private async getBlockAuditSummary(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
 | 
			
		||||
      const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
 | 
			
		||||
@ -590,7 +590,7 @@ class Blocks {
 | 
			
		||||
    if (skipMemoryCache === false) {
 | 
			
		||||
      // Check the memory cache
 | 
			
		||||
      const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
 | 
			
		||||
      if (cachedSummary) {
 | 
			
		||||
      if (cachedSummary?.transactions?.length) {
 | 
			
		||||
        return cachedSummary.transactions;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -598,7 +598,7 @@ class Blocks {
 | 
			
		||||
    // Check if it's indexed in db
 | 
			
		||||
    if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
 | 
			
		||||
      const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
 | 
			
		||||
      if (indexedSummary !== undefined) {
 | 
			
		||||
      if (indexedSummary !== undefined && indexedSummary?.transactions?.length) {
 | 
			
		||||
        return indexedSummary.transactions;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -651,6 +651,19 @@ class Blocks {
 | 
			
		||||
    return returnBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditSummary(hash: string): Promise<any> {
 | 
			
		||||
    let summary = await BlocksAuditsRepository.$getBlockAudit(hash);
 | 
			
		||||
 | 
			
		||||
    // fallback to non-audited transaction summary
 | 
			
		||||
    if (!summary?.transactions?.length) {
 | 
			
		||||
      const strippedTransactions = await this.$getStrippedBlockTransactions(hash);
 | 
			
		||||
      summary = {
 | 
			
		||||
        transactions: strippedTransactions
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return summary;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getLastDifficultyAdjustmentTime(): number {
 | 
			
		||||
    return this.lastDifficultyAdjustmentTime;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import logger from '../logger';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 44;
 | 
			
		||||
  private static currentVersion = 45;
 | 
			
		||||
  private queryTimeout = 900_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -365,6 +365,10 @@ class DatabaseMigration {
 | 
			
		||||
      await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
 | 
			
		||||
      await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 45 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -283,9 +283,12 @@ class MiningRoutes {
 | 
			
		||||
 | 
			
		||||
  private async $getBlockAuditScores(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
 | 
			
		||||
      let height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
 | 
			
		||||
      if (height == null) {
 | 
			
		||||
        height = await BlocksRepository.$mostRecentBlockHeight();
 | 
			
		||||
      }
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(await audits.$getBlockAuditScores(height, 15));
 | 
			
		||||
      res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -428,7 +428,7 @@ class WebsocketHandler {
 | 
			
		||||
    if (Common.indexingEnabled() && memPool.isInSync()) {
 | 
			
		||||
      const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
 | 
			
		||||
 | 
			
		||||
      const { censored, added, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
 | 
			
		||||
      const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
 | 
			
		||||
      matchRate = Math.round(score * 100 * 100) / 100;
 | 
			
		||||
 | 
			
		||||
      const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
 | 
			
		||||
@ -454,6 +454,7 @@ class WebsocketHandler {
 | 
			
		||||
        hash: block.id,
 | 
			
		||||
        addedTxs: added,
 | 
			
		||||
        missingTxs: censored,
 | 
			
		||||
        freshTxs: fresh,
 | 
			
		||||
        matchRate: matchRate,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,7 @@ interface IConfig {
 | 
			
		||||
    STATS_REFRESH_INTERVAL: number;
 | 
			
		||||
    GRAPH_REFRESH_INTERVAL: number;
 | 
			
		||||
    LOGGER_UPDATE_INTERVAL: number;
 | 
			
		||||
    FORENSICS_INTERVAL: number;
 | 
			
		||||
  };
 | 
			
		||||
  LND: {
 | 
			
		||||
    TLS_CERT_PATH: string;
 | 
			
		||||
@ -199,6 +200,7 @@ const defaults: IConfig = {
 | 
			
		||||
    'STATS_REFRESH_INTERVAL': 600,
 | 
			
		||||
    'GRAPH_REFRESH_INTERVAL': 600,
 | 
			
		||||
    'LOGGER_UPDATE_INTERVAL': 30,
 | 
			
		||||
    'FORENSICS_INTERVAL': 43200,
 | 
			
		||||
  },
 | 
			
		||||
  'LND': {
 | 
			
		||||
    'TLS_CERT_PATH': '',
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ import bisqRoutes from './api/bisq/bisq.routes';
 | 
			
		||||
import liquidRoutes from './api/liquid/liquid.routes';
 | 
			
		||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
 | 
			
		||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
 | 
			
		||||
import forensicsService from './tasks/lightning/forensics.service';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -192,6 +193,7 @@ class Server {
 | 
			
		||||
    try {
 | 
			
		||||
      await fundingTxFetcher.$init();
 | 
			
		||||
      await networkSyncService.$startService();
 | 
			
		||||
      await forensicsService.$startService();
 | 
			
		||||
      await lightningStatsUpdater.$startService();
 | 
			
		||||
    } catch(e) {
 | 
			
		||||
      logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ export interface BlockAudit {
 | 
			
		||||
  height: number,
 | 
			
		||||
  hash: string,
 | 
			
		||||
  missingTxs: string[],
 | 
			
		||||
  freshTxs: string[],
 | 
			
		||||
  addedTxs: string[],
 | 
			
		||||
  matchRate: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import blocks from '../api/blocks';
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
@ -5,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
class BlocksAuditRepositories {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate)
 | 
			
		||||
        VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
 | 
			
		||||
          JSON.stringify(audit.addedTxs), audit.matchRate]);
 | 
			
		||||
      await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate)
 | 
			
		||||
        VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
 | 
			
		||||
          JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
 | 
			
		||||
        logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
 | 
			
		||||
@ -51,7 +52,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
 | 
			
		||||
        blocks.weight, blocks.tx_count,
 | 
			
		||||
        transactions, template, missing_txs as missingTxs, added_txs as addedTxs, match_rate as matchRate
 | 
			
		||||
        transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        JOIN blocks ON blocks.hash = blocks_audits.hash
 | 
			
		||||
        JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
 | 
			
		||||
@ -61,11 +62,15 @@ class BlocksAuditRepositories {
 | 
			
		||||
      if (rows.length) {
 | 
			
		||||
        rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
 | 
			
		||||
        rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
 | 
			
		||||
        rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
 | 
			
		||||
        rows[0].transactions = JSON.parse(rows[0].transactions);
 | 
			
		||||
        rows[0].template = JSON.parse(rows[0].template);
 | 
			
		||||
 | 
			
		||||
        if (rows[0].transactions.length) {
 | 
			
		||||
          return rows[0];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
            
 | 
			
		||||
      return rows[0];
 | 
			
		||||
      return null;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
@ -85,6 +90,20 @@ class BlocksAuditRepositories {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT hash, match_rate as matchRate
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE blocks_audits.height BETWEEN ? AND ?
 | 
			
		||||
      `, [minHeight, maxHeight]);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new BlocksAuditRepositories();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										225
									
								
								backend/src/tasks/lightning/forensics.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								backend/src/tasks/lightning/forensics.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,225 @@
 | 
			
		||||
import DB from '../../database';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import channelsApi from '../../api/explorer/channels.api';
 | 
			
		||||
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
 | 
			
		||||
import { Common } from '../../api/common';
 | 
			
		||||
 | 
			
		||||
const throttleDelay = 20; //ms
 | 
			
		||||
 | 
			
		||||
class ForensicsService {
 | 
			
		||||
  loggerTimer = 0;
 | 
			
		||||
  closedChannelsScanBlock = 0;
 | 
			
		||||
  txCache: { [txid: string]: IEsploraApi.Transaction } = {};
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  public async $startService(): Promise<void> {
 | 
			
		||||
    logger.info('Starting lightning network forensics service');
 | 
			
		||||
 | 
			
		||||
    this.loggerTimer = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
    await this.$runTasks();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $runTasks(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      logger.info(`Running forensics scans`);
 | 
			
		||||
 | 
			
		||||
      if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
        await this.$runClosedChannelsForensics(false);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('ForensicsService.$runTasks() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.FORENSICS_INTERVAL);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    1. Mutually closed
 | 
			
		||||
    2. Forced closed
 | 
			
		||||
    3. Forced closed with penalty
 | 
			
		||||
 | 
			
		||||
    ┌────────────────────────────────────┐       ┌────────────────────────────┐
 | 
			
		||||
    │ outputs contain revocation script? ├──yes──► force close w/ penalty = 3 │
 | 
			
		||||
    └──────────────┬─────────────────────┘       └────────────────────────────┘
 | 
			
		||||
                   no
 | 
			
		||||
    ┌──────────────▼──────────────────────────┐
 | 
			
		||||
    │ outputs contain other lightning script? ├──┐
 | 
			
		||||
    └──────────────┬──────────────────────────┘  │
 | 
			
		||||
                   no                           yes
 | 
			
		||||
    ┌──────────────▼─────────────┐               │
 | 
			
		||||
    │ sequence starts with 0x80  │      ┌────────▼────────┐
 | 
			
		||||
    │           and              ├──────► force close = 2 │
 | 
			
		||||
    │ locktime starts with 0x20? │      └─────────────────┘
 | 
			
		||||
    └──────────────┬─────────────┘
 | 
			
		||||
                   no
 | 
			
		||||
         ┌─────────▼────────┐
 | 
			
		||||
         │ mutual close = 1 │
 | 
			
		||||
         └──────────────────┘
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  public async $runClosedChannelsForensics(onlyNewChannels: boolean = false): Promise<void> {
 | 
			
		||||
    if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let progress = 0;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      logger.info(`Started running closed channel forensics...`);
 | 
			
		||||
      let channels;
 | 
			
		||||
      if (onlyNewChannels) {
 | 
			
		||||
        channels = await channelsApi.$getClosedChannelsWithoutReason();
 | 
			
		||||
      } else {
 | 
			
		||||
        channels = await channelsApi.$getUnresolvedClosedChannels();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        let reason = 0;
 | 
			
		||||
        let resolvedForceClose = false;
 | 
			
		||||
        // Only Esplora backend can retrieve spent transaction outputs
 | 
			
		||||
        const cached: string[] = [];
 | 
			
		||||
        try {
 | 
			
		||||
          let outspends: IEsploraApi.Outspend[] | undefined;
 | 
			
		||||
          try {
 | 
			
		||||
            outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
 | 
			
		||||
            await Common.sleep$(throttleDelay);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
            continue;
 | 
			
		||||
          }
 | 
			
		||||
          const lightningScriptReasons: number[] = [];
 | 
			
		||||
          for (const outspend of outspends) {
 | 
			
		||||
            if (outspend.spent && outspend.txid) {
 | 
			
		||||
              let spendingTx: IEsploraApi.Transaction | undefined = this.txCache[outspend.txid];
 | 
			
		||||
              if (!spendingTx) {
 | 
			
		||||
                try {
 | 
			
		||||
                  spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
 | 
			
		||||
                  await Common.sleep$(throttleDelay);
 | 
			
		||||
                  this.txCache[outspend.txid] = spendingTx;
 | 
			
		||||
                } catch (e) {
 | 
			
		||||
                  logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
                  continue;
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              cached.push(spendingTx.txid);
 | 
			
		||||
              const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
 | 
			
		||||
              lightningScriptReasons.push(lightningScript);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
 | 
			
		||||
          if (filteredReasons.length) {
 | 
			
		||||
            if (filteredReasons.some((r) => r === 2 || r === 4)) {
 | 
			
		||||
              reason = 3;
 | 
			
		||||
            } else {
 | 
			
		||||
              reason = 2;
 | 
			
		||||
              resolvedForceClose = true;
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            /*
 | 
			
		||||
              We can detect a commitment transaction (force close) by reading Sequence and Locktime
 | 
			
		||||
              https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
 | 
			
		||||
            */
 | 
			
		||||
            let closingTx: IEsploraApi.Transaction | undefined = this.txCache[channel.closing_transaction_id];
 | 
			
		||||
            if (!closingTx) {
 | 
			
		||||
              try {
 | 
			
		||||
                closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
 | 
			
		||||
                await Common.sleep$(throttleDelay);
 | 
			
		||||
                this.txCache[channel.closing_transaction_id] = closingTx;
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
                continue;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            cached.push(closingTx.txid);
 | 
			
		||||
            const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
 | 
			
		||||
            const locktimeHex: string = closingTx.locktime.toString(16);
 | 
			
		||||
            if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
 | 
			
		||||
              reason = 2; // Here we can't be sure if it's a penalty or not
 | 
			
		||||
            } else {
 | 
			
		||||
              reason = 1;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (reason) {
 | 
			
		||||
            logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
 | 
			
		||||
            await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
 | 
			
		||||
            if (reason === 2 && resolvedForceClose) {
 | 
			
		||||
              await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
 | 
			
		||||
            }
 | 
			
		||||
            if (reason !== 2 || resolvedForceClose) {
 | 
			
		||||
              cached.forEach(txid => {
 | 
			
		||||
                delete this.txCache[txid];
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ++progress;
 | 
			
		||||
        const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
 | 
			
		||||
        if (elapsedSeconds > 10) {
 | 
			
		||||
          logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
 | 
			
		||||
          this.loggerTimer = new Date().getTime() / 1000;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      logger.info(`Closed channels forensics scan complete.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private findLightningScript(vin: IEsploraApi.Vin): number {
 | 
			
		||||
    const topElement = vin.witness[vin.witness.length - 2];
 | 
			
		||||
      if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
 | 
			
		||||
        if (topElement === '01') {
 | 
			
		||||
          // top element is '01' to get in the revocation path
 | 
			
		||||
          // 'Revoked Lightning Force Close';
 | 
			
		||||
          // Penalty force closed
 | 
			
		||||
          return 2;
 | 
			
		||||
        } else {
 | 
			
		||||
          // top element is '', this is a delayed to_local output
 | 
			
		||||
          // 'Lightning Force Close';
 | 
			
		||||
          return 3;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (
 | 
			
		||||
        /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) ||
 | 
			
		||||
        /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm)
 | 
			
		||||
      ) {
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
 | 
			
		||||
        if (topElement.length === 66) {
 | 
			
		||||
          // top element is a public key
 | 
			
		||||
          // 'Revoked Lightning HTLC'; Penalty force closed
 | 
			
		||||
          return 4;
 | 
			
		||||
        } else if (topElement) {
 | 
			
		||||
          // top element is a preimage
 | 
			
		||||
          // 'Lightning HTLC';
 | 
			
		||||
          return 5;
 | 
			
		||||
        } else {
 | 
			
		||||
          // top element is '' to get in the expiry of the script
 | 
			
		||||
          // 'Expired Lightning HTLC';
 | 
			
		||||
          return 6;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(vin.inner_witnessscript_asm)) {
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
 | 
			
		||||
        if (topElement) {
 | 
			
		||||
          // top element is a signature
 | 
			
		||||
          // 'Lightning Anchor';
 | 
			
		||||
          return 7;
 | 
			
		||||
        } else {
 | 
			
		||||
          // top element is '', it has been swept after 16 blocks
 | 
			
		||||
          // 'Swept Lightning Anchor';
 | 
			
		||||
          return 8;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new ForensicsService();
 | 
			
		||||
@ -14,6 +14,7 @@ import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
 | 
			
		||||
import { Common } from '../../api/common';
 | 
			
		||||
import blocks from '../../api/blocks';
 | 
			
		||||
import NodeRecordsRepository from '../../repositories/NodeRecordsRepository';
 | 
			
		||||
import forensicsService from './forensics.service';
 | 
			
		||||
 | 
			
		||||
class NetworkSyncService {
 | 
			
		||||
  loggerTimer = 0;
 | 
			
		||||
@ -46,8 +47,10 @@ class NetworkSyncService {
 | 
			
		||||
      await this.$lookUpCreationDateFromChain();
 | 
			
		||||
      await this.$updateNodeFirstSeen();
 | 
			
		||||
      await this.$scanForClosedChannels();
 | 
			
		||||
      
 | 
			
		||||
      if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
        await this.$runClosedChannelsForensics();
 | 
			
		||||
        // run forensics on new channels only
 | 
			
		||||
        await forensicsService.$runClosedChannelsForensics(true);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@ -301,174 +304,6 @@ class NetworkSyncService {
 | 
			
		||||
      logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    1. Mutually closed
 | 
			
		||||
    2. Forced closed
 | 
			
		||||
    3. Forced closed with penalty
 | 
			
		||||
 | 
			
		||||
    ┌────────────────────────────────────┐       ┌────────────────────────────┐
 | 
			
		||||
    │ outputs contain revocation script? ├──yes──► force close w/ penalty = 3 │
 | 
			
		||||
    └──────────────┬─────────────────────┘       └────────────────────────────┘
 | 
			
		||||
                   no
 | 
			
		||||
    ┌──────────────▼──────────────────────────┐
 | 
			
		||||
    │ outputs contain other lightning script? ├──┐
 | 
			
		||||
    └──────────────┬──────────────────────────┘  │
 | 
			
		||||
                   no                           yes
 | 
			
		||||
    ┌──────────────▼─────────────┐               │
 | 
			
		||||
    │ sequence starts with 0x80  │      ┌────────▼────────┐
 | 
			
		||||
    │           and              ├──────► force close = 2 │
 | 
			
		||||
    │ locktime starts with 0x20? │      └─────────────────┘
 | 
			
		||||
    └──────────────┬─────────────┘
 | 
			
		||||
                   no
 | 
			
		||||
         ┌─────────▼────────┐
 | 
			
		||||
         │ mutual close = 1 │
 | 
			
		||||
         └──────────────────┘
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  private async $runClosedChannelsForensics(skipUnresolved: boolean = false): Promise<void> {
 | 
			
		||||
    if (!config.ESPLORA.REST_API_URL) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let progress = 0;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      logger.info(`Started running closed channel forensics...`);
 | 
			
		||||
      let channels;
 | 
			
		||||
      const closedChannels = await channelsApi.$getClosedChannelsWithoutReason();
 | 
			
		||||
      if (skipUnresolved) {
 | 
			
		||||
        channels = closedChannels;
 | 
			
		||||
      } else {
 | 
			
		||||
        const unresolvedChannels = await channelsApi.$getUnresolvedClosedChannels();
 | 
			
		||||
        channels = [...closedChannels, ...unresolvedChannels];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        let reason = 0;
 | 
			
		||||
        let resolvedForceClose = false;
 | 
			
		||||
        // Only Esplora backend can retrieve spent transaction outputs
 | 
			
		||||
        try {
 | 
			
		||||
          let outspends: IEsploraApi.Outspend[] | undefined;
 | 
			
		||||
          try {
 | 
			
		||||
            outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
            continue;
 | 
			
		||||
          }
 | 
			
		||||
          const lightningScriptReasons: number[] = [];
 | 
			
		||||
          for (const outspend of outspends) {
 | 
			
		||||
            if (outspend.spent && outspend.txid) {
 | 
			
		||||
              let spendingTx: IEsploraApi.Transaction | undefined;
 | 
			
		||||
              try {
 | 
			
		||||
                spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
                continue;
 | 
			
		||||
              }
 | 
			
		||||
              const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
 | 
			
		||||
              lightningScriptReasons.push(lightningScript);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
 | 
			
		||||
          if (filteredReasons.length) {
 | 
			
		||||
            if (filteredReasons.some((r) => r === 2 || r === 4)) {
 | 
			
		||||
              reason = 3;
 | 
			
		||||
            } else {
 | 
			
		||||
              reason = 2;
 | 
			
		||||
              resolvedForceClose = true;
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            /*
 | 
			
		||||
              We can detect a commitment transaction (force close) by reading Sequence and Locktime
 | 
			
		||||
              https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
 | 
			
		||||
            */
 | 
			
		||||
            let closingTx: IEsploraApi.Transaction | undefined;
 | 
			
		||||
            try {
 | 
			
		||||
              closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
              continue;
 | 
			
		||||
            }
 | 
			
		||||
            const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
 | 
			
		||||
            const locktimeHex: string = closingTx.locktime.toString(16);
 | 
			
		||||
            if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
 | 
			
		||||
              reason = 2; // Here we can't be sure if it's a penalty or not
 | 
			
		||||
            } else {
 | 
			
		||||
              reason = 1;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (reason) {
 | 
			
		||||
            logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
 | 
			
		||||
            await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
 | 
			
		||||
            if (reason === 2 && resolvedForceClose) {
 | 
			
		||||
              await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ++progress;
 | 
			
		||||
        const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
 | 
			
		||||
        if (elapsedSeconds > 10) {
 | 
			
		||||
          logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
 | 
			
		||||
          this.loggerTimer = new Date().getTime() / 1000;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      logger.info(`Closed channels forensics scan complete.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private findLightningScript(vin: IEsploraApi.Vin): number {
 | 
			
		||||
    const topElement = vin.witness[vin.witness.length - 2];
 | 
			
		||||
      if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
 | 
			
		||||
        if (topElement === '01') {
 | 
			
		||||
          // top element is '01' to get in the revocation path
 | 
			
		||||
          // 'Revoked Lightning Force Close';
 | 
			
		||||
          // Penalty force closed
 | 
			
		||||
          return 2;
 | 
			
		||||
        } else {
 | 
			
		||||
          // top element is '', this is a delayed to_local output
 | 
			
		||||
          // 'Lightning Force Close';
 | 
			
		||||
          return 3;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (
 | 
			
		||||
        /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) ||
 | 
			
		||||
        /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm)
 | 
			
		||||
      ) {
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
 | 
			
		||||
        if (topElement.length === 66) {
 | 
			
		||||
          // top element is a public key
 | 
			
		||||
          // 'Revoked Lightning HTLC'; Penalty force closed
 | 
			
		||||
          return 4;
 | 
			
		||||
        } else if (topElement) {
 | 
			
		||||
          // top element is a preimage
 | 
			
		||||
          // 'Lightning HTLC';
 | 
			
		||||
          return 5;
 | 
			
		||||
        } else {
 | 
			
		||||
          // top element is '' to get in the expiry of the script
 | 
			
		||||
          // 'Expired Lightning HTLC';
 | 
			
		||||
          return 6;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(vin.inner_witnessscript_asm)) {
 | 
			
		||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
 | 
			
		||||
        if (topElement) {
 | 
			
		||||
          // top element is a signature
 | 
			
		||||
          // 'Lightning Anchor';
 | 
			
		||||
          return 7;
 | 
			
		||||
        } else {
 | 
			
		||||
          // top element is '', it has been swept after 16 blocks
 | 
			
		||||
          // 'Swept Lightning Anchor';
 | 
			
		||||
          return 8;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new NetworkSyncService();
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
 | 
			
		||||
import { StartComponent } from './components/start/start.component';
 | 
			
		||||
import { TransactionComponent } from './components/transaction/transaction.component';
 | 
			
		||||
import { BlockComponent } from './components/block/block.component';
 | 
			
		||||
import { BlockAuditComponent } from './components/block-audit/block-audit.component';
 | 
			
		||||
import { AddressComponent } from './components/address/address.component';
 | 
			
		||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
			
		||||
import { AboutComponent } from './components/about/about.component';
 | 
			
		||||
@ -103,16 +102,6 @@ let routes: Routes = [
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'block-audit',
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':id',
 | 
			
		||||
                component: BlockAuditComponent,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'docs',
 | 
			
		||||
            loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
 | 
			
		||||
@ -219,16 +208,6 @@ let routes: Routes = [
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'block-audit',
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':id',
 | 
			
		||||
                component: BlockAuditComponent,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'docs',
 | 
			
		||||
            loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
 | 
			
		||||
@ -331,16 +310,6 @@ let routes: Routes = [
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'block-audit',
 | 
			
		||||
        data: { networkSpecific: true },
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: ':id',
 | 
			
		||||
            component: BlockAuditComponent
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'docs',
 | 
			
		||||
        loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import { map } from 'rxjs/operators';
 | 
			
		||||
import { moveDec } from '../../bitcoin.utils';
 | 
			
		||||
import { AssetsService } from '../../services/assets.service';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { environment } from '../../../environments/environment';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-asset-circulation',
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import { AudioService } from '../../services/audio.service';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { of, merge, Subscription, combineLatest } from 'rxjs';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { environment } from '../../../environments/environment';
 | 
			
		||||
import { AssetsService } from '../../services/assets.service';
 | 
			
		||||
import { moveDec } from '../../bitcoin.utils';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import { AssetsService } from '../../../services/assets.service';
 | 
			
		||||
import { SeoService } from '../../../services/seo.service';
 | 
			
		||||
import { StateService } from '../../../services/state.service';
 | 
			
		||||
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { environment } from '../../../../environments/environment';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-assets-nav',
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
 | 
			
		||||
import { AssetsService } from '../../services/assets.service';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { environment } from '../../../environments/environment';
 | 
			
		||||
import { FormGroup } from '@angular/forms';
 | 
			
		||||
import { filter, map, switchMap, take } from 'rxjs/operators';
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
@ -1,172 +0,0 @@
 | 
			
		||||
<div class="container-xl" (window:resize)="onResize($event)">
 | 
			
		||||
 | 
			
		||||
  <div class="title-block" id="block">
 | 
			
		||||
    <h1>
 | 
			
		||||
      <span class="next-previous-blocks">
 | 
			
		||||
        <span i18n="shared.block-audit-title">Block Audit</span>
 | 
			
		||||
         
 | 
			
		||||
        <a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
 | 
			
		||||
         
 | 
			
		||||
      </span>
 | 
			
		||||
    </h1>
 | 
			
		||||
 | 
			
		||||
    <div class="grow"></div>
 | 
			
		||||
 | 
			
		||||
    <button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">✕</button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="!error && !isLoading">
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    <!-- OVERVIEW -->
 | 
			
		||||
    <div class="box mb-3">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <!-- LEFT COLUMN -->
 | 
			
		||||
        <div class="col-sm">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="td-width" i18n="block.hash">Hash</td>
 | 
			
		||||
                <td><a [routerLink]="['/block/' | relativeUrl, blockHash]" title="{{ blockHash }}">{{ blockHash | shortenString : 13 }}</a>
 | 
			
		||||
                  <app-clipboard class="d-none d-sm-inline-block" [text]="blockHash"></app-clipboard>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="blockAudit.timestamp">Timestamp</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  ‎{{ blockAudit.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
                  <div class="lg-inline">
 | 
			
		||||
                    <i class="symbol">(<app-time-since [time]="blockAudit.timestamp" [fastRender]="true">
 | 
			
		||||
                      </app-time-since>)</i>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="blockAudit.size">Size</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.weight">Weight</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (blockAudit.weight | wuBytes: 2)"></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- RIGHT COLUMN -->
 | 
			
		||||
        <div class="col-sm" *ngIf="blockAudit">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="td-width" i18n="shared.transaction-count">Transactions</td>
 | 
			
		||||
                <td>{{ blockAudit.tx_count }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.health">Block health</td>
 | 
			
		||||
                <td>{{ blockAudit.matchRate }}%</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.missing-txs">Removed txs</td>
 | 
			
		||||
                <td>{{ blockAudit.missingTxs.length }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.added-txs">Added txs</td>
 | 
			
		||||
                <td>{{ blockAudit.addedTxs.length }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div> <!-- row -->
 | 
			
		||||
    </div> <!-- box -->
 | 
			
		||||
 | 
			
		||||
    <!-- ADDED vs MISSING button -->
 | 
			
		||||
    <div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
 | 
			
		||||
      <a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
 | 
			
		||||
        fragment="projected" (click)="changeMode('projected')">Projected</a>
 | 
			
		||||
      <a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
 | 
			
		||||
        fragment="actual" (click)="changeMode('actual')">Actual</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="!error && isLoading">
 | 
			
		||||
    <!-- OVERVIEW -->
 | 
			
		||||
    <div class="box mb-3">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <!-- LEFT COLUMN -->
 | 
			
		||||
        <div class="col-sm">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- RIGHT COLUMN -->
 | 
			
		||||
        <div class="col-sm">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
              <tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div> <!-- row -->
 | 
			
		||||
    </div> <!-- box -->
 | 
			
		||||
 | 
			
		||||
    <!-- ADDED vs MISSING button -->
 | 
			
		||||
    <div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
 | 
			
		||||
      <a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
 | 
			
		||||
        fragment="projected" (click)="changeMode('projected')">Projected</a>
 | 
			
		||||
      <a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
 | 
			
		||||
        fragment="actual" (click)="changeMode('actual')">Actual</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="error">
 | 
			
		||||
    <div *ngIf="error && error.status === 404; else generalError" class="text-center">
 | 
			
		||||
      <br>
 | 
			
		||||
      <b i18n="error.audit-unavailable">audit unavailable</b>
 | 
			
		||||
      <br><br>
 | 
			
		||||
      <i>{{ error.error }}</i>
 | 
			
		||||
      <br>
 | 
			
		||||
      <br>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ng-template #generalError>
 | 
			
		||||
      <div class="text-center">
 | 
			
		||||
        <br>
 | 
			
		||||
        <span i18n="error.general-loading-data">Error loading data.</span>
 | 
			
		||||
        <br><br>
 | 
			
		||||
        <i>{{ error }}</i>
 | 
			
		||||
        <br>
 | 
			
		||||
        <br>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <!-- VISUALIZATIONS -->
 | 
			
		||||
  <div class="box" *ngIf="!error">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <!-- MISSING TX RENDERING -->
 | 
			
		||||
      <div class="col-sm" *ngIf="webGlEnabled">
 | 
			
		||||
        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
 | 
			
		||||
        <app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
 | 
			
		||||
          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- ADDED TX RENDERING -->
 | 
			
		||||
      <div class="col-sm" *ngIf="webGlEnabled && !isMobile">
 | 
			
		||||
        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
 | 
			
		||||
        <app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
 | 
			
		||||
          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div> <!-- row -->
 | 
			
		||||
  </div> <!-- box -->
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
.title-block {
 | 
			
		||||
  border-top: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
  tr td {
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      text-align: right;
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        text-align: left;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-tx-title {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  @media (min-width: 550px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  h2 {
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    @media (min-width: 550px) {
 | 
			
		||||
      padding-bottom: 0px;
 | 
			
		||||
      align-self: end;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.menu-button {
 | 
			
		||||
  @media (min-width: 768px) {
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-subtitle {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
@ -1,229 +0,0 @@
 | 
			
		||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
			
		||||
import { Subscription, combineLatest, of } from 'rxjs';
 | 
			
		||||
import { map, switchMap, startWith, catchError, filter } from 'rxjs/operators';
 | 
			
		||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-audit',
 | 
			
		||||
  templateUrl: './block-audit.component.html',
 | 
			
		||||
  styleUrls: ['./block-audit.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  blockAudit: BlockAudit = undefined;
 | 
			
		||||
  transactions: string[];
 | 
			
		||||
  auditSubscription: Subscription;
 | 
			
		||||
  urlFragmentSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  paginationMaxSize: number;
 | 
			
		||||
  page = 1;
 | 
			
		||||
  itemsPerPage: number;
 | 
			
		||||
 | 
			
		||||
  mode: 'projected' | 'actual' = 'projected';
 | 
			
		||||
  error: any;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
  isMobile = window.innerWidth <= 767.98;
 | 
			
		||||
  hoverTx: string;
 | 
			
		||||
 | 
			
		||||
  childChangeSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  blockHash: string;
 | 
			
		||||
  numMissing: number = 0;
 | 
			
		||||
  numUnexpected: number = 0;
 | 
			
		||||
 | 
			
		||||
  @ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
 | 
			
		||||
  @ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = detectWebGL();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.childChangeSubscription.unsubscribe();
 | 
			
		||||
    this.urlFragmentSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
 | 
			
		||||
    this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
 | 
			
		||||
 | 
			
		||||
    this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
 | 
			
		||||
      if (fragment === 'actual') {
 | 
			
		||||
        this.mode = 'actual';
 | 
			
		||||
      } else {
 | 
			
		||||
        this.mode = 'projected'
 | 
			
		||||
      }
 | 
			
		||||
      this.setupBlockGraphs();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.auditSubscription = this.route.paramMap.pipe(
 | 
			
		||||
      switchMap((params: ParamMap) => {
 | 
			
		||||
        const blockHash = params.get('id') || null;
 | 
			
		||||
        if (!blockHash) {
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let isBlockHeight = false;
 | 
			
		||||
        if (/^[0-9]+$/.test(blockHash)) {
 | 
			
		||||
          isBlockHeight = true;
 | 
			
		||||
        } else {
 | 
			
		||||
          this.blockHash = blockHash;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isBlockHeight) {
 | 
			
		||||
          return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
 | 
			
		||||
            .pipe(
 | 
			
		||||
              switchMap((hash: string) => {
 | 
			
		||||
                if (hash) {
 | 
			
		||||
                  this.blockHash = hash;
 | 
			
		||||
                  return this.apiService.getBlockAudit$(this.blockHash)
 | 
			
		||||
                } else {
 | 
			
		||||
                  return null;
 | 
			
		||||
                }
 | 
			
		||||
              }),
 | 
			
		||||
              catchError((err) => {
 | 
			
		||||
                this.error = err;
 | 
			
		||||
                return of(null);
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        return this.apiService.getBlockAudit$(this.blockHash)
 | 
			
		||||
      }),
 | 
			
		||||
      filter((response) => response != null),
 | 
			
		||||
      map((response) => {
 | 
			
		||||
        const blockAudit = response.body;
 | 
			
		||||
        const inTemplate = {};
 | 
			
		||||
        const inBlock = {};
 | 
			
		||||
        const isAdded = {};
 | 
			
		||||
        const isCensored = {};
 | 
			
		||||
        const isMissing = {};
 | 
			
		||||
        const isSelected = {};
 | 
			
		||||
        this.numMissing = 0;
 | 
			
		||||
        this.numUnexpected = 0;
 | 
			
		||||
        for (const tx of blockAudit.template) {
 | 
			
		||||
          inTemplate[tx.txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
        for (const tx of blockAudit.transactions) {
 | 
			
		||||
          inBlock[tx.txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
        for (const txid of blockAudit.addedTxs) {
 | 
			
		||||
          isAdded[txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
        for (const txid of blockAudit.missingTxs) {
 | 
			
		||||
          isCensored[txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
        // set transaction statuses
 | 
			
		||||
        for (const tx of blockAudit.template) {
 | 
			
		||||
          if (isCensored[tx.txid]) {
 | 
			
		||||
            tx.status = 'censored';
 | 
			
		||||
          } else if (inBlock[tx.txid]) {
 | 
			
		||||
            tx.status = 'found';
 | 
			
		||||
          } else {
 | 
			
		||||
            tx.status = 'missing';
 | 
			
		||||
            isMissing[tx.txid] = true;
 | 
			
		||||
            this.numMissing++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        for (const [index, tx] of blockAudit.transactions.entries()) {
 | 
			
		||||
          if (index === 0) {
 | 
			
		||||
            tx.status = null;
 | 
			
		||||
          } else if (isAdded[tx.txid]) {
 | 
			
		||||
            tx.status = 'added';
 | 
			
		||||
          } else if (inTemplate[tx.txid]) {
 | 
			
		||||
            tx.status = 'found';
 | 
			
		||||
          } else {
 | 
			
		||||
            tx.status = 'selected';
 | 
			
		||||
            isSelected[tx.txid] = true;
 | 
			
		||||
            this.numUnexpected++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        for (const tx of blockAudit.transactions) {
 | 
			
		||||
          inBlock[tx.txid] = true;
 | 
			
		||||
        }
 | 
			
		||||
        return blockAudit;
 | 
			
		||||
      }),
 | 
			
		||||
      catchError((err) => {
 | 
			
		||||
        console.log(err);
 | 
			
		||||
        this.error = err;
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        return of(null);
 | 
			
		||||
      }),
 | 
			
		||||
    ).subscribe((blockAudit) => {
 | 
			
		||||
      this.blockAudit = blockAudit;
 | 
			
		||||
      this.setupBlockGraphs();
 | 
			
		||||
      this.isLoading = false;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {
 | 
			
		||||
    this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
 | 
			
		||||
      this.setupBlockGraphs();
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setupBlockGraphs() {
 | 
			
		||||
    if (this.blockAudit) {
 | 
			
		||||
      this.blockGraphProjected.forEach(graph => {
 | 
			
		||||
        graph.destroy();
 | 
			
		||||
        if (this.isMobile && this.mode === 'actual') {
 | 
			
		||||
          graph.setup(this.blockAudit.transactions);
 | 
			
		||||
        } else {
 | 
			
		||||
          graph.setup(this.blockAudit.template);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      this.blockGraphActual.forEach(graph => {
 | 
			
		||||
        graph.destroy();
 | 
			
		||||
        graph.setup(this.blockAudit.transactions);
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onResize(event: any) {
 | 
			
		||||
    const isMobile = event.target.innerWidth <= 767.98;
 | 
			
		||||
    const changed = isMobile !== this.isMobile;
 | 
			
		||||
    this.isMobile = isMobile;
 | 
			
		||||
    this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
 | 
			
		||||
 | 
			
		||||
    if (changed) {
 | 
			
		||||
      this.changeMode(this.mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeMode(mode: 'projected' | 'actual') {
 | 
			
		||||
    this.router.navigate([], { fragment: mode });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxClick(event: TransactionStripped): void {
 | 
			
		||||
    const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
 | 
			
		||||
    this.router.navigate([url]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxHover(txid: string): void {
 | 
			
		||||
    if (txid && txid.length) {
 | 
			
		||||
      this.hoverTx = txid;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hoverTx = null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
<div class="block-overview-graph">
 | 
			
		||||
  <canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
 | 
			
		||||
  <div class="loader-wrapper" [class.hidden]="!isLoading || disableSpinner">
 | 
			
		||||
    <div class="spinner-border ml-3 loading" role="status"></div>
 | 
			
		||||
  <div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
 | 
			
		||||
    <div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
 | 
			
		||||
    <div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <app-block-overview-tooltip
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  @Input() flip = true;
 | 
			
		||||
  @Input() disableSpinner = false;
 | 
			
		||||
  @Input() mirrorTxid: string | void;
 | 
			
		||||
  @Input() unavailable: boolean = false;
 | 
			
		||||
  @Output() txClickEvent = new EventEmitter<TransactionStripped>();
 | 
			
		||||
  @Output() txHoverEvent = new EventEmitter<string>();
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
@ -3,12 +3,13 @@ import { FastVertexArray } from './fast-vertex-array';
 | 
			
		||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
 | 
			
		||||
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
 | 
			
		||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
 | 
			
		||||
import BlockScene from './block-scene';
 | 
			
		||||
 | 
			
		||||
const hoverTransitionTime = 300;
 | 
			
		||||
const defaultHoverColor = hexToColor('1bd8f4');
 | 
			
		||||
 | 
			
		||||
const feeColors = mempoolFeeColors.map(hexToColor);
 | 
			
		||||
const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
 | 
			
		||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
 | 
			
		||||
const auditColors = {
 | 
			
		||||
  censored: hexToColor('f344df'),
 | 
			
		||||
  missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
 | 
			
		||||
@ -34,7 +35,8 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  feerate: number;
 | 
			
		||||
  status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
 | 
			
		||||
  status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
 | 
			
		||||
  initialised: boolean;
 | 
			
		||||
  vertexArray: FastVertexArray;
 | 
			
		||||
@ -48,6 +50,7 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
  dirty: boolean;
 | 
			
		||||
 | 
			
		||||
  constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
 | 
			
		||||
    this.context = tx.context;
 | 
			
		||||
    this.txid = tx.txid;
 | 
			
		||||
    this.fee = tx.fee;
 | 
			
		||||
    this.vsize = tx.vsize;
 | 
			
		||||
@ -159,12 +162,18 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
        return auditColors.censored;
 | 
			
		||||
      case 'missing':
 | 
			
		||||
        return auditColors.missing;
 | 
			
		||||
      case 'fresh':
 | 
			
		||||
        return auditColors.missing;
 | 
			
		||||
      case 'added':
 | 
			
		||||
        return auditColors.added;
 | 
			
		||||
      case 'selected':
 | 
			
		||||
        return auditColors.selected;
 | 
			
		||||
      case 'found':
 | 
			
		||||
        return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
 | 
			
		||||
        if (this.context === 'projected') {
 | 
			
		||||
          return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
 | 
			
		||||
        } else {
 | 
			
		||||
          return feeLevelColor;
 | 
			
		||||
        }
 | 
			
		||||
      default:
 | 
			
		||||
        return feeLevelColor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -37,9 +37,10 @@
 | 
			
		||||
        <ng-container [ngSwitch]="tx?.status">
 | 
			
		||||
          <td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
 | 
			
		||||
          <td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
 | 
			
		||||
          <td *ngSwitchCase="'missing'" i18n="transaction.audit.omitted">omitted</td>
 | 
			
		||||
          <td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
 | 
			
		||||
          <td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
 | 
			
		||||
          <td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
 | 
			
		||||
          <td *ngSwitchCase="'selected'" i18n="transaction.audit.extra">extra</td>
 | 
			
		||||
          <td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,19 @@
 | 
			
		||||
                <td i18n="block.weight">Weight</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <ng-template [ngIf]="webGlEnabled">
 | 
			
		||||
              <tr *ngIf="auditEnabled">
 | 
			
		||||
                <td i18n="block.health">Block health</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <span *ngIf="blockAudit?.matchRate != null">{{ blockAudit.matchRate }}%</span>
 | 
			
		||||
                  <span *ngIf="blockAudit?.matchRate === null" i18n="unknown">Unknown</span>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <ng-container *ngIf="!indexingAvailable && webGlEnabled">
 | 
			
		||||
                <tr *ngIf="isMobile && auditEnabled"></tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td i18n="mempool-block.fee-span">Fee span</td>
 | 
			
		||||
                  <td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr *ngIf="block?.extras?.medianFee != undefined">
 | 
			
		||||
                  <td class="td-width" i18n="block.median-fee">Median fee</td>
 | 
			
		||||
                  <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
 | 
			
		||||
@ -98,26 +110,19 @@
 | 
			
		||||
                <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
                  <td i18n="block.miner">Miner</td>
 | 
			
		||||
                  <td *ngIf="stateService.env.MINING_DASHBOARD">
 | 
			
		||||
                    <a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
 | 
			
		||||
                    <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
 | 
			
		||||
                      [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
 | 
			
		||||
                      {{ block.extras.pool.name }}
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </td>
 | 
			
		||||
                  <td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
 | 
			
		||||
                    <span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
 | 
			
		||||
                    <span placement="bottom" class="badge"
 | 
			
		||||
                      [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
 | 
			
		||||
                      {{ block.extras.pool.name }}
 | 
			
		||||
                  </span>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr *ngIf="indexingAvailable">
 | 
			
		||||
                  <td i18n="block.health">Block health</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
 | 
			
		||||
                    <span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
@ -138,7 +143,11 @@
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <ng-template [ngIf]="webGlEnabled">
 | 
			
		||||
              <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
                <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <ng-container *ngIf="!indexingAvailable && webGlEnabled">
 | 
			
		||||
                <tr *ngIf="isMobile && !auditEnabled"></tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
@ -148,17 +157,25 @@
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
                <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
      <div class="col-sm" *ngIf="!webGlEnabled">
 | 
			
		||||
        <table class="table table-borderless table-striped" *ngIf="!isLoadingBlock">
 | 
			
		||||
      <div class="col-sm">
 | 
			
		||||
        <table class="table table-borderless table-striped" *ngIf="!isLoadingBlock && (indexingAvailable || !webGlEnabled)">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <tr *ngIf="isMobile && auditEnabled"></tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="mempool-block.fee-span">Fee span</td>
 | 
			
		||||
              <td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="block?.extras?.medianFee != undefined">
 | 
			
		||||
              <td class="td-width" i18n="block.median-fee">Median fee</td>
 | 
			
		||||
              <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
 | 
			
		||||
@ -216,8 +233,9 @@
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
        <table class="table table-borderless table-striped" *ngIf="isLoadingBlock">
 | 
			
		||||
        <table class="table table-borderless table-striped" *ngIf="isLoadingBlock && (indexingAvailable || !webGlEnabled)">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <tr *ngIf="isMobile && !auditEnabled"></tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
@ -230,22 +248,54 @@
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
              <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-sm chart-container" *ngIf="webGlEnabled">
 | 
			
		||||
        <app-block-overview-graph
 | 
			
		||||
          #blockGraph
 | 
			
		||||
          [isLoading]="isLoadingOverview"
 | 
			
		||||
          [resolution]="75"
 | 
			
		||||
          [blockLimit]="stateService.blockVSize"
 | 
			
		||||
          [orientation]="'top'"
 | 
			
		||||
          [flip]="false"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)"
 | 
			
		||||
        ></app-block-overview-graph>
 | 
			
		||||
        <div class="col-sm chart-container" *ngIf="webGlEnabled && !indexingAvailable">
 | 
			
		||||
          <app-block-overview-graph
 | 
			
		||||
            #blockGraphActual
 | 
			
		||||
            [isLoading]="isLoadingOverview"
 | 
			
		||||
            [resolution]="75"
 | 
			
		||||
            [blockLimit]="stateService.blockVSize"
 | 
			
		||||
            [orientation]="'top'"
 | 
			
		||||
            [flip]="false"
 | 
			
		||||
            (txClickEvent)="onTxClick($event)"
 | 
			
		||||
          ></app-block-overview-graph>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <span id="overview"></span>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <!-- VISUALIZATIONS -->
 | 
			
		||||
  <div class="box" *ngIf="!error && webGlEnabled && indexingAvailable">
 | 
			
		||||
    <div class="nav nav-tabs" *ngIf="isMobile && auditEnabled">
 | 
			
		||||
      <a class="nav-link" [class.active]="mode === 'projected'" i18n="block.projected"
 | 
			
		||||
        fragment="projected" (click)="changeMode('projected')">Projected</a>
 | 
			
		||||
      <a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
 | 
			
		||||
        fragment="actual" (click)="changeMode('actual')">Actual</a>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <div class="col-sm">
 | 
			
		||||
        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
 | 
			
		||||
        <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
 | 
			
		||||
          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !auditEnabled"></app-block-overview-graph> 
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-sm" *ngIf="!isMobile">
 | 
			
		||||
        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
 | 
			
		||||
        <app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
 | 
			
		||||
          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !auditEnabled"></app-block-overview-graph>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
  <ng-template [ngIf]="!isLoadingBlock && !error">
 | 
			
		||||
    <div [hidden]="!showDetails" id="details">
 | 
			
		||||
      <br>
 | 
			
		||||
@ -273,6 +323,7 @@
 | 
			
		||||
          <div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
            <table class="table table-borderless table-striped">
 | 
			
		||||
              <tbody>
 | 
			
		||||
                <tr *ngIf="isMobile"></tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td class="td-width" i18n="block.difficulty">Difficulty</td>
 | 
			
		||||
                  <td>{{ block.difficulty }}</td>
 | 
			
		||||
 | 
			
		||||
@ -171,3 +171,35 @@ h1 {
 | 
			
		||||
    margin: auto;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.menu-button {
 | 
			
		||||
  @media (min-width: 768px) {
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-subtitle {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-tabs {
 | 
			
		||||
  border-color: white;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-tabs .nav-link {
 | 
			
		||||
  background: inherit;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
  border-bottom: none;
 | 
			
		||||
  border-color: transparent;
 | 
			
		||||
  margin-bottom: -1px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  &.active {
 | 
			
		||||
    background: #24273e;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.active, &:hover {
 | 
			
		||||
    border-color: white;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +1,15 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
 | 
			
		||||
import { Location } from '@angular/common';
 | 
			
		||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
 | 
			
		||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
 | 
			
		||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } from 'rxjs';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
 | 
			
		||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
@ -17,11 +17,20 @@ import { detectWebGL } from '../../shared/graphs.utils';
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block',
 | 
			
		||||
  templateUrl: './block.component.html',
 | 
			
		||||
  styleUrls: ['./block.component.scss']
 | 
			
		||||
  styleUrls: ['./block.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  network = '';
 | 
			
		||||
  block: BlockExtended;
 | 
			
		||||
  blockAudit: BlockAudit = undefined;
 | 
			
		||||
  blockHeight: number;
 | 
			
		||||
  lastBlockHeight: number;
 | 
			
		||||
  nextBlockHeight: number;
 | 
			
		||||
@ -48,9 +57,16 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  overviewError: any = null;
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
  indexingAvailable = false;
 | 
			
		||||
  auditEnabled = true;
 | 
			
		||||
  isMobile = window.innerWidth <= 767.98;
 | 
			
		||||
  hoverTx: string;
 | 
			
		||||
  numMissing: number = 0;
 | 
			
		||||
  numUnexpected: number = 0;
 | 
			
		||||
  mode: 'projected' | 'actual' = 'projected';
 | 
			
		||||
 | 
			
		||||
  transactionSubscription: Subscription;
 | 
			
		||||
  overviewSubscription: Subscription;
 | 
			
		||||
  auditSubscription: Subscription;
 | 
			
		||||
  keyNavigationSubscription: Subscription;
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  networkChangedSubscription: Subscription;
 | 
			
		||||
@ -60,10 +76,10 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  nextBlockTxListSubscription: Subscription = undefined;
 | 
			
		||||
  timeLtrSubscription: Subscription;
 | 
			
		||||
  timeLtr: boolean;
 | 
			
		||||
  fetchAuditScore$ = new Subject<string>();
 | 
			
		||||
  fetchAuditScoreSubscription: Subscription;
 | 
			
		||||
  childChangeSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
			
		||||
  @ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
 | 
			
		||||
  @ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
@ -89,8 +105,8 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.timeLtr = !!ltr;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
 | 
			
		||||
      this.stateService.env.MINING_DASHBOARD === true);
 | 
			
		||||
    this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true);
 | 
			
		||||
    this.auditEnabled = this.indexingAvailable;
 | 
			
		||||
 | 
			
		||||
    this.txsLoadingStatus$ = this.route.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
@ -107,30 +123,12 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        if (block.id === this.blockHash) {
 | 
			
		||||
          this.block = block;
 | 
			
		||||
          if (this.block.id && this.block?.extras?.matchRate == null) {
 | 
			
		||||
            this.fetchAuditScore$.next(this.block.id);
 | 
			
		||||
          }
 | 
			
		||||
          if (block?.extras?.reward != undefined) {
 | 
			
		||||
            this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    if (this.indexingAvailable) {
 | 
			
		||||
      this.fetchAuditScoreSubscription = this.fetchAuditScore$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((hash) => this.apiService.getBlockAuditScore$(hash)),
 | 
			
		||||
          catchError(() => EMPTY),
 | 
			
		||||
        )
 | 
			
		||||
        .subscribe((score) => {
 | 
			
		||||
          if (score && score.hash === this.block.id) {
 | 
			
		||||
            this.block.extras.matchRate = score.matchRate || null;
 | 
			
		||||
          } else {
 | 
			
		||||
            this.block.extras.matchRate = null;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const block$ = this.route.paramMap.pipe(
 | 
			
		||||
      switchMap((params: ParamMap) => {
 | 
			
		||||
        const blockHash: string = params.get('id') || '';
 | 
			
		||||
@ -212,7 +210,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
 | 
			
		||||
            this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
 | 
			
		||||
            this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe();
 | 
			
		||||
            this.apiService.getBlockAudit$(block.previousblockhash);
 | 
			
		||||
          }, 100);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -229,9 +227,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
          this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
			
		||||
        }
 | 
			
		||||
        this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
 | 
			
		||||
        if (this.block.id && this.block?.extras?.matchRate == null) {
 | 
			
		||||
          this.fetchAuditScore$.next(this.block.id);
 | 
			
		||||
        }
 | 
			
		||||
        this.isLoadingTransactions = true;
 | 
			
		||||
        this.transactions = null;
 | 
			
		||||
        this.transactionsError = null;
 | 
			
		||||
@ -263,40 +258,126 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.isLoadingOverview = false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.overviewSubscription = block$.pipe(
 | 
			
		||||
      startWith(null),
 | 
			
		||||
      pairwise(),
 | 
			
		||||
      switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
 | 
			
		||||
        .pipe(
 | 
			
		||||
          catchError((err) => {
 | 
			
		||||
            this.overviewError = err;
 | 
			
		||||
            return of([]);
 | 
			
		||||
          }),
 | 
			
		||||
          switchMap((transactions) => {
 | 
			
		||||
            if (prevBlock) {
 | 
			
		||||
              return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
 | 
			
		||||
            } else {
 | 
			
		||||
              return of({ transactions, direction: 'down' });
 | 
			
		||||
    if (!this.indexingAvailable) {
 | 
			
		||||
      this.overviewSubscription = block$.pipe(
 | 
			
		||||
        startWith(null),
 | 
			
		||||
        pairwise(),
 | 
			
		||||
        switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
 | 
			
		||||
          .pipe(
 | 
			
		||||
            catchError((err) => {
 | 
			
		||||
              this.overviewError = err;
 | 
			
		||||
              return of([]);
 | 
			
		||||
            }),
 | 
			
		||||
            switchMap((transactions) => {
 | 
			
		||||
              if (prevBlock) {
 | 
			
		||||
                return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
 | 
			
		||||
              } else {
 | 
			
		||||
                return of({ transactions, direction: 'down' });
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
 | 
			
		||||
        this.strippedTransactions = transactions;
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
        this.setupBlockGraphs();
 | 
			
		||||
      },
 | 
			
		||||
      (error) => {
 | 
			
		||||
        this.error = error;
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.indexingAvailable) {
 | 
			
		||||
      this.auditSubscription = block$.pipe(
 | 
			
		||||
        startWith(null),
 | 
			
		||||
        pairwise(),
 | 
			
		||||
        switchMap(([prevBlock, block]) => this.apiService.getBlockAudit$(block.id)
 | 
			
		||||
          .pipe(
 | 
			
		||||
            catchError((err) => {
 | 
			
		||||
              this.overviewError = err;
 | 
			
		||||
              return of([]);
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
        filter((response) => response != null),
 | 
			
		||||
        map((response) => {
 | 
			
		||||
          const blockAudit = response.body;
 | 
			
		||||
          const inTemplate = {};
 | 
			
		||||
          const inBlock = {};
 | 
			
		||||
          const isAdded = {};
 | 
			
		||||
          const isCensored = {};
 | 
			
		||||
          const isMissing = {};
 | 
			
		||||
          const isSelected = {};
 | 
			
		||||
          const isFresh = {};
 | 
			
		||||
          this.numMissing = 0;
 | 
			
		||||
          this.numUnexpected = 0;
 | 
			
		||||
 | 
			
		||||
          if (blockAudit?.template) {
 | 
			
		||||
            for (const tx of blockAudit.template) {
 | 
			
		||||
              inTemplate[tx.txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ),
 | 
			
		||||
    )
 | 
			
		||||
    .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
 | 
			
		||||
      this.strippedTransactions = transactions;
 | 
			
		||||
      this.isLoadingOverview = false;
 | 
			
		||||
      if (this.blockGraph) {
 | 
			
		||||
        this.blockGraph.destroy();
 | 
			
		||||
        this.blockGraph.setup(this.strippedTransactions);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    (error) => {
 | 
			
		||||
      this.error = error;
 | 
			
		||||
      this.isLoadingOverview = false;
 | 
			
		||||
      if (this.blockGraph) {
 | 
			
		||||
        this.blockGraph.destroy();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
            for (const tx of blockAudit.transactions) {
 | 
			
		||||
              inBlock[tx.txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            for (const txid of blockAudit.addedTxs) {
 | 
			
		||||
              isAdded[txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            for (const txid of blockAudit.missingTxs) {
 | 
			
		||||
              isCensored[txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            for (const txid of blockAudit.freshTxs || []) {
 | 
			
		||||
              isFresh[txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            // set transaction statuses
 | 
			
		||||
            for (const tx of blockAudit.template) {
 | 
			
		||||
              tx.context = 'projected';
 | 
			
		||||
              if (isCensored[tx.txid]) {
 | 
			
		||||
                tx.status = 'censored';
 | 
			
		||||
              } else if (inBlock[tx.txid]) {
 | 
			
		||||
                tx.status = 'found';
 | 
			
		||||
              } else {
 | 
			
		||||
                tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
 | 
			
		||||
                isMissing[tx.txid] = true;
 | 
			
		||||
                this.numMissing++;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            for (const [index, tx] of blockAudit.transactions.entries()) {
 | 
			
		||||
              tx.context = 'actual';
 | 
			
		||||
              if (index === 0) {
 | 
			
		||||
                tx.status = null;
 | 
			
		||||
              } else if (isAdded[tx.txid]) {
 | 
			
		||||
                tx.status = 'added';
 | 
			
		||||
              } else if (inTemplate[tx.txid]) {
 | 
			
		||||
                tx.status = 'found';
 | 
			
		||||
              } else {
 | 
			
		||||
                tx.status = 'selected';
 | 
			
		||||
                isSelected[tx.txid] = true;
 | 
			
		||||
                this.numUnexpected++;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            for (const tx of blockAudit.transactions) {
 | 
			
		||||
              inBlock[tx.txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            this.auditEnabled = true;
 | 
			
		||||
          } else {
 | 
			
		||||
            this.auditEnabled = false;
 | 
			
		||||
          }
 | 
			
		||||
          return blockAudit;
 | 
			
		||||
        }),
 | 
			
		||||
        catchError((err) => {
 | 
			
		||||
          console.log(err);
 | 
			
		||||
          this.error = err;
 | 
			
		||||
          this.isLoadingOverview = false;
 | 
			
		||||
          return of(null);
 | 
			
		||||
        }),
 | 
			
		||||
      ).subscribe((blockAudit) => {
 | 
			
		||||
        this.blockAudit = blockAudit;
 | 
			
		||||
        this.setupBlockGraphs();
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.networkChangedSubscription = this.stateService.networkChanged$
 | 
			
		||||
      .subscribe((network) => this.network = network);
 | 
			
		||||
@ -307,6 +388,12 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      } else {
 | 
			
		||||
        this.showDetails = false;
 | 
			
		||||
      }
 | 
			
		||||
      if (params.view === 'projected') {
 | 
			
		||||
        this.mode = 'projected';
 | 
			
		||||
      } else {
 | 
			
		||||
        this.mode = 'actual';
 | 
			
		||||
      }
 | 
			
		||||
      this.setupBlockGraphs();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
 | 
			
		||||
@ -325,17 +412,24 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
 | 
			
		||||
      this.setupBlockGraphs();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.stateService.markBlock$.next({});
 | 
			
		||||
    this.transactionSubscription.unsubscribe();
 | 
			
		||||
    this.overviewSubscription.unsubscribe();
 | 
			
		||||
    this.overviewSubscription?.unsubscribe();
 | 
			
		||||
    this.auditSubscription?.unsubscribe();
 | 
			
		||||
    this.keyNavigationSubscription.unsubscribe();
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.networkChangedSubscription.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription.unsubscribe();
 | 
			
		||||
    this.timeLtrSubscription.unsubscribe();
 | 
			
		||||
    this.fetchAuditScoreSubscription?.unsubscribe();
 | 
			
		||||
    this.unsubscribeNextBlockSubscriptions();
 | 
			
		||||
    this.childChangeSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  unsubscribeNextBlockSubscriptions() {
 | 
			
		||||
@ -382,7 +476,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.showDetails = false;
 | 
			
		||||
      this.router.navigate([], {
 | 
			
		||||
        relativeTo: this.route,
 | 
			
		||||
        queryParams: { showDetails: false },
 | 
			
		||||
        queryParams: { showDetails: false, view: this.mode },
 | 
			
		||||
        queryParamsHandling: 'merge',
 | 
			
		||||
        fragment: 'block'
 | 
			
		||||
      });
 | 
			
		||||
@ -390,7 +484,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.showDetails = true;
 | 
			
		||||
      this.router.navigate([], {
 | 
			
		||||
        relativeTo: this.route,
 | 
			
		||||
        queryParams: { showDetails: true },
 | 
			
		||||
        queryParams: { showDetails: true, view: this.mode },
 | 
			
		||||
        queryParamsHandling: 'merge',
 | 
			
		||||
        fragment: 'details'
 | 
			
		||||
      });
 | 
			
		||||
@ -409,10 +503,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onResize(event: any) {
 | 
			
		||||
    this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  navigateToPreviousBlock() {
 | 
			
		||||
    if (!this.block) {
 | 
			
		||||
      return;
 | 
			
		||||
@ -443,8 +533,53 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setupBlockGraphs(): void {
 | 
			
		||||
    if (this.blockAudit || this.strippedTransactions) {
 | 
			
		||||
      this.blockGraphProjected.forEach(graph => {
 | 
			
		||||
        graph.destroy();
 | 
			
		||||
        if (this.isMobile && this.mode === 'actual') {
 | 
			
		||||
          graph.setup(this.blockAudit?.transactions || this.strippedTransactions ||  []);
 | 
			
		||||
        } else {
 | 
			
		||||
          graph.setup(this.blockAudit?.template || []);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      this.blockGraphActual.forEach(graph => {
 | 
			
		||||
        graph.destroy();
 | 
			
		||||
        graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onResize(event: any): void {
 | 
			
		||||
    const isMobile = event.target.innerWidth <= 767.98;
 | 
			
		||||
    const changed = isMobile !== this.isMobile;
 | 
			
		||||
    this.isMobile = isMobile;
 | 
			
		||||
    this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
 | 
			
		||||
 | 
			
		||||
    if (changed) {
 | 
			
		||||
      this.changeMode(this.mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeMode(mode: 'projected' | 'actual'): void {
 | 
			
		||||
    this.router.navigate([], {
 | 
			
		||||
      relativeTo: this.route,
 | 
			
		||||
      queryParams: { showDetails: this.showDetails, view: mode },
 | 
			
		||||
      queryParamsHandling: 'merge',
 | 
			
		||||
      fragment: 'overview'
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxClick(event: TransactionStripped): void {
 | 
			
		||||
    const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
 | 
			
		||||
    this.router.navigate([url]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxHover(txid: string): void {
 | 
			
		||||
    if (txid && txid.length) {
 | 
			
		||||
      this.hoverTx = txid;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hoverTx = null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -46,14 +46,13 @@
 | 
			
		||||
            <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
            <a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null">
 | 
			
		||||
            <a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block/' | relativeUrl, block.id] : null">
 | 
			
		||||
              <div class="progress progress-health">
 | 
			
		||||
                <div class="progress-bar progress-bar-health" role="progressbar"
 | 
			
		||||
                  [ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
 | 
			
		||||
                <div class="progress-text">
 | 
			
		||||
                  <span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span>
 | 
			
		||||
                  <span *ngIf="auditScores[block.id] === undefined" class="skeleton-loader"></span>
 | 
			
		||||
                  <span *ngIf="auditScores[block.id] === null">~</span>
 | 
			
		||||
                  <span *ngIf="auditScores[block.id] == null">~</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs';
 | 
			
		||||
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { environment } from '../../../environments/environment';
 | 
			
		||||
import { AssetsService } from '../../services/assets.service';
 | 
			
		||||
import { filter, map, tap, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended } from '../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@ -141,7 +141,7 @@ export interface TransactionStripped {
 | 
			
		||||
  fee: number;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
 | 
			
		||||
  status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RewardStats {
 | 
			
		||||
 | 
			
		||||
@ -70,7 +70,8 @@ export interface TransactionStripped {
 | 
			
		||||
  fee: number;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
 | 
			
		||||
  status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IBackendInfo {
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
 | 
			
		||||
import { ILiquidityAd, parseLiquidityAdHex } from './liquidity-ad';
 | 
			
		||||
import { haversineDistance, kmToMiles } from 'src/app/shared/common.utils';
 | 
			
		||||
import { haversineDistance, kmToMiles } from '../../../app/shared/common.utils';
 | 
			
		||||
 | 
			
		||||
interface CustomRecord {
 | 
			
		||||
  type: string;
 | 
			
		||||
 | 
			
		||||
@ -230,7 +230,7 @@ export class ApiService {
 | 
			
		||||
 | 
			
		||||
  getBlockAudit$(hash: string) : Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/` + hash, { observe: 'response' }
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`, { observe: 'response' }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { map, shareReplay, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { StateService } from './state.service';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { environment } from '../../../src/environments/environment';
 | 
			
		||||
import { AssetExtended } from '../interfaces/electrs.interface';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,6 @@ import { StartComponent } from '../components/start/start.component';
 | 
			
		||||
import { TransactionComponent } from '../components/transaction/transaction.component';
 | 
			
		||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
 | 
			
		||||
import { BlockComponent } from '../components/block/block.component';
 | 
			
		||||
import { BlockAuditComponent } from '../components/block-audit/block-audit.component';
 | 
			
		||||
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
 | 
			
		||||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
 | 
			
		||||
import { AddressComponent } from '../components/address/address.component';
 | 
			
		||||
@ -120,7 +119,6 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    StartComponent,
 | 
			
		||||
    TransactionComponent,
 | 
			
		||||
    BlockComponent,
 | 
			
		||||
    BlockAuditComponent,
 | 
			
		||||
    BlockOverviewGraphComponent,
 | 
			
		||||
    BlockOverviewTooltipComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
@ -223,7 +221,6 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    StartComponent,
 | 
			
		||||
    TransactionComponent,
 | 
			
		||||
    BlockComponent,
 | 
			
		||||
    BlockAuditComponent,
 | 
			
		||||
    BlockOverviewGraphComponent,
 | 
			
		||||
    BlockOverviewTooltipComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user