Merge branch 'master' into add-nunchuk
This commit is contained in:
		
						commit
						d8d8a52445
					
				
							
								
								
									
										8
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							@ -68,24 +68,24 @@ jobs:
 | 
			
		||||
        run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
 | 
			
		||||
 | 
			
		||||
      - name: Checkout project
 | 
			
		||||
        uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
 | 
			
		||||
        uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0
 | 
			
		||||
 | 
			
		||||
      - name: Init repo for Dockerization
 | 
			
		||||
        run: docker/init.sh "$TAG"
 | 
			
		||||
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1
 | 
			
		||||
        uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0
 | 
			
		||||
        id: qemu
 | 
			
		||||
 | 
			
		||||
      - name: Setup Docker buildx action
 | 
			
		||||
        uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 # v1
 | 
			
		||||
        uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2.2.1
 | 
			
		||||
        id: buildx
 | 
			
		||||
 | 
			
		||||
      - name: Available platforms
 | 
			
		||||
        run: echo ${{ steps.buildx.outputs.platforms }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache Docker layers
 | 
			
		||||
        uses: actions/cache@661fd3eb7f2f20d8c7c84bc2b0509efd7a826628 # v2
 | 
			
		||||
        uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11
 | 
			
		||||
        id: cache
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/.buildx-cache
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -3,3 +3,5 @@ data
 | 
			
		||||
docker-compose.yml
 | 
			
		||||
backend/mempool-config.json
 | 
			
		||||
*.swp
 | 
			
		||||
frontend/src/resources/config.template.js
 | 
			
		||||
frontend/src/resources/config.js
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "electrum",
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "HTTP_PORT": 8999,
 | 
			
		||||
    "SPAWN_CLUSTER_PROCS": 0,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/",
 | 
			
		||||
@ -23,7 +24,8 @@
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "debug",
 | 
			
		||||
    "AUTOMATIC_BLOCK_REINDEXING": false,
 | 
			
		||||
    "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master"
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
 | 
			
		||||
    "ADVANCED_TRANSACTION_SELECTION": false
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "NETWORK": "__MEMPOOL_NETWORK__",
 | 
			
		||||
    "BACKEND": "__MEMPOOL_BACKEND__",
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "BLOCKS_SUMMARIES_INDEXING": true,
 | 
			
		||||
    "HTTP_PORT": 1,
 | 
			
		||||
    "SPAWN_CLUSTER_PROCS": 2,
 | 
			
		||||
@ -23,7 +25,8 @@
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
 | 
			
		||||
    "INDEXING_BLOCKS_AMOUNT": 14,
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
 | 
			
		||||
    "POOLS_JSON_URL": "__POOLS_JSON_URL__"
 | 
			
		||||
    "POOLS_JSON_URL": "__POOLS_JSON_URL__",
 | 
			
		||||
    "ADVANCED_TRANSACTION_SELECTION": "__ADVANCED_TRANSACTION_SELECTION__"
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
      const config = jest.requireActual('../config').default;
 | 
			
		||||
 | 
			
		||||
      expect(config.MEMPOOL).toStrictEqual({
 | 
			
		||||
        ENABLED: true,
 | 
			
		||||
        NETWORK: 'mainnet',
 | 
			
		||||
        BACKEND: 'none',
 | 
			
		||||
        BLOCKS_SUMMARIES_INDEXING: false,
 | 
			
		||||
@ -36,7 +37,8 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        USER_AGENT: 'mempool',
 | 
			
		||||
        STDOUT_LOG_MIN_PRIORITY: 'debug',
 | 
			
		||||
        POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
 | 
			
		||||
        POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'
 | 
			
		||||
        POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
 | 
			
		||||
        ADVANCED_TRANSACTION_SELECTION: false,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,10 @@
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import { TransactionExtended, MempoolBlockWithTransactions, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
import blocksRepository from '../repositories/BlocksRepository';
 | 
			
		||||
import blocksAuditsRepository from '../repositories/BlocksAuditsRepository';
 | 
			
		||||
import blocks from '../api/blocks';
 | 
			
		||||
 | 
			
		||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
 | 
			
		||||
 | 
			
		||||
@ -44,8 +49,6 @@ class Audit {
 | 
			
		||||
 | 
			
		||||
    displacedWeight += (4000 - transactions[0].weight);
 | 
			
		||||
 | 
			
		||||
    logger.warn(`${fresh.length} fresh, ${Object.keys(isCensored).length} possibly censored, ${displacedWeight} displaced weight`);
 | 
			
		||||
 | 
			
		||||
    // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
 | 
			
		||||
    // these displaced transactions should occupy the first N weight units of the next projected block
 | 
			
		||||
    let displacedWeightRemaining = displacedWeight;
 | 
			
		||||
@ -73,6 +76,7 @@ class Audit {
 | 
			
		||||
 | 
			
		||||
    // mark unexpected transactions in the mined block as 'added'
 | 
			
		||||
    let overflowWeight = 0;
 | 
			
		||||
    let totalWeight = 0;
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      if (inTemplate[tx.txid]) {
 | 
			
		||||
        matches.push(tx.txid);
 | 
			
		||||
@ -82,11 +86,13 @@ class Audit {
 | 
			
		||||
        }
 | 
			
		||||
        overflowWeight += tx.weight;
 | 
			
		||||
      }
 | 
			
		||||
      totalWeight += tx.weight;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // transactions missing from near the end of our template are probably not being censored
 | 
			
		||||
    let overflowWeightRemaining = overflowWeight;
 | 
			
		||||
    let lastOverflowRate = 1.00;
 | 
			
		||||
    let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
 | 
			
		||||
    let maxOverflowRate = 0;
 | 
			
		||||
    let rateThreshold = 0;
 | 
			
		||||
    index = projectedBlocks[0].transactionIds.length - 1;
 | 
			
		||||
    while (index >= 0) {
 | 
			
		||||
      const txid = projectedBlocks[0].transactionIds[index];
 | 
			
		||||
@ -94,8 +100,11 @@ class Audit {
 | 
			
		||||
        if (isCensored[txid]) {
 | 
			
		||||
          delete isCensored[txid];
 | 
			
		||||
        }
 | 
			
		||||
        lastOverflowRate = mempool[txid].effectiveFeePerVsize;
 | 
			
		||||
      } else if (Math.floor(mempool[txid].effectiveFeePerVsize * 100) <= Math.ceil(lastOverflowRate * 100)) { // tolerance of 0.01 sat/vb
 | 
			
		||||
        if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) {
 | 
			
		||||
          maxOverflowRate = mempool[txid].effectiveFeePerVsize;
 | 
			
		||||
          rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (mempool[txid].effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
 | 
			
		||||
        if (isCensored[txid]) {
 | 
			
		||||
          delete isCensored[txid];
 | 
			
		||||
        }
 | 
			
		||||
@ -113,6 +122,45 @@ class Audit {
 | 
			
		||||
      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();
 | 
			
		||||
@ -34,6 +34,7 @@ class Blocks {
 | 
			
		||||
  private lastDifficultyAdjustmentTime = 0;
 | 
			
		||||
  private previousDifficultyRetarget = 0;
 | 
			
		||||
  private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
			
		||||
  private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
@ -57,6 +58,10 @@ class Blocks {
 | 
			
		||||
    this.newBlockCallbacks.push(fn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setNewAsyncBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>) {
 | 
			
		||||
    this.newAsyncBlockCallbacks.push(fn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return the list of transaction for a block
 | 
			
		||||
   * @param blockHash
 | 
			
		||||
@ -130,7 +135,7 @@ class Blocks {
 | 
			
		||||
    const stripped = block.tx.map((tx) => {
 | 
			
		||||
      return {
 | 
			
		||||
        txid: tx.txid,
 | 
			
		||||
        vsize: tx.vsize,
 | 
			
		||||
        vsize: tx.weight / 4,
 | 
			
		||||
        fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
 | 
			
		||||
        value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000)
 | 
			
		||||
      };
 | 
			
		||||
@ -195,9 +200,9 @@ class Blocks {
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const auditSummary = await BlocksAuditsRepository.$getShortBlockAudit(block.id);
 | 
			
		||||
      if (auditSummary) {
 | 
			
		||||
        blockExtended.extras.matchRate = auditSummary.matchRate;
 | 
			
		||||
      const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
 | 
			
		||||
      if (auditScore != null) {
 | 
			
		||||
        blockExtended.extras.matchRate = auditScore.matchRate;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -444,6 +449,9 @@ class Blocks {
 | 
			
		||||
      const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
      const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
 | 
			
		||||
 | 
			
		||||
      // start async callbacks
 | 
			
		||||
      const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
 | 
			
		||||
 | 
			
		||||
      if (Common.indexingEnabled()) {
 | 
			
		||||
        if (!fastForwarded) {
 | 
			
		||||
          const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
 | 
			
		||||
@ -514,6 +522,9 @@ class Blocks {
 | 
			
		||||
      if (!memPool.hasPriority()) {
 | 
			
		||||
        diskCache.$saveCacheToDisk();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // wait for pending async callbacks to finish
 | 
			
		||||
      await Promise.all(callbackPromises);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,8 @@ import logger from '../logger';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 41;
 | 
			
		||||
  private queryTimeout = 120000;
 | 
			
		||||
  private static currentVersion = 44;
 | 
			
		||||
  private queryTimeout = 900_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
 | 
			
		||||
@ -352,6 +352,19 @@ class DatabaseMigration {
 | 
			
		||||
    if (databaseSchemaVersion < 41 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 42 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 43 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 44 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
 | 
			
		||||
      await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -787,6 +800,19 @@ class DatabaseMigration {
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateLNNodeRecordsTableQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS nodes_records (
 | 
			
		||||
      public_key varchar(66) NOT NULL,
 | 
			
		||||
      type int(10) unsigned NOT NULL,
 | 
			
		||||
      payload blob NOT NULL,
 | 
			
		||||
      UNIQUE KEY public_key_type (public_key, type),
 | 
			
		||||
      INDEX (public_key),
 | 
			
		||||
      FOREIGN KEY (public_key)
 | 
			
		||||
        REFERENCES nodes (public_key)
 | 
			
		||||
        ON DELETE CASCADE
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $truncateIndexedData(tables: string[]) {
 | 
			
		||||
    const allowedTables = ['blocks', 'hashrates', 'prices'];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -117,6 +117,17 @@ class ChannelsApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getUnresolvedClosedChannels(): Promise<any[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason = 2 AND closing_resolved = 0 AND closing_transaction_id != ''`;
 | 
			
		||||
      const [rows]: any = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getUnresolvedClosedChannels error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT * FROM channels WHERE created IS NULL`;
 | 
			
		||||
 | 
			
		||||
@ -105,6 +105,18 @@ class NodesApi {
 | 
			
		||||
        node.closed_channel_count = rows[0].closed_channel_count;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Custom records
 | 
			
		||||
      query = `
 | 
			
		||||
        SELECT type, payload
 | 
			
		||||
        FROM nodes_records
 | 
			
		||||
        WHERE public_key = ?
 | 
			
		||||
      `;
 | 
			
		||||
      [rows] = await DB.query(query, [public_key]);
 | 
			
		||||
      node.custom_records = {};
 | 
			
		||||
      for (const record of rows) {
 | 
			
		||||
        node.custom_records[record.type] = Buffer.from(record.payload, 'binary').toString('hex');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return node;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,15 @@ import { Common } from '../../common';
 | 
			
		||||
 * Convert a clightning "listnode" entry to a lnd node entry
 | 
			
		||||
 */
 | 
			
		||||
export function convertNode(clNode: any): ILightningApi.Node {
 | 
			
		||||
  let custom_records: { [type: number]: string } | undefined = undefined;
 | 
			
		||||
  if (clNode.option_will_fund) {
 | 
			
		||||
    try {
 | 
			
		||||
      custom_records = { '1': Buffer.from(clNode.option_will_fund.compact_lease || '', 'hex').toString('base64') };
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot decode option_will_fund compact_lease for ${clNode.nodeid}). Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      custom_records = undefined;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    alias: clNode.alias ?? '',
 | 
			
		||||
    color: `#${clNode.color ?? ''}`,
 | 
			
		||||
@ -23,6 +32,7 @@ export function convertNode(clNode: any): ILightningApi.Node {
 | 
			
		||||
      };
 | 
			
		||||
    }) ?? [],
 | 
			
		||||
    last_update: clNode?.last_timestamp ?? 0,
 | 
			
		||||
    custom_records
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,7 @@ export namespace ILightningApi {
 | 
			
		||||
    }[];
 | 
			
		||||
    color: string;
 | 
			
		||||
    features: { [key: number]: Feature };
 | 
			
		||||
    custom_records?: { [type: number]: string };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Info {
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,17 @@
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { MempoolBlock, TransactionExtended, AuditTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
 | 
			
		||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { PairingHeap } from '../utils/pairing-heap';
 | 
			
		||||
import { StaticPool } from 'node-worker-threads-pool';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
class MempoolBlocks {
 | 
			
		||||
  private mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
  private mempoolBlockDeltas: MempoolBlockDelta[] = [];
 | 
			
		||||
  private makeTemplatesPool = new StaticPool({
 | 
			
		||||
    size: 1,
 | 
			
		||||
    task: path.resolve(__dirname, './tx-selection-worker.js'),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
@ -72,16 +77,15 @@ class MempoolBlocks {
 | 
			
		||||
    const time = end - start;
 | 
			
		||||
    logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
 | 
			
		||||
 | 
			
		||||
    const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
 | 
			
		||||
    const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
 | 
			
		||||
    const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
 | 
			
		||||
 | 
			
		||||
    this.mempoolBlocks = blocks;
 | 
			
		||||
    this.mempoolBlockDeltas = deltas;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]):
 | 
			
		||||
    { blocks: MempoolBlockWithTransactions[], deltas: MempoolBlockDelta[] } {
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
 | 
			
		||||
    const mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
    const mempoolBlockDeltas: MempoolBlockDelta[] = [];
 | 
			
		||||
    let blockWeight = 0;
 | 
			
		||||
    let blockSize = 0;
 | 
			
		||||
    let transactions: TransactionExtended[] = [];
 | 
			
		||||
@ -102,7 +106,11 @@ class MempoolBlocks {
 | 
			
		||||
      mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate change from previous block states
 | 
			
		||||
    return mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
 | 
			
		||||
    const mempoolBlockDeltas: MempoolBlockDelta[] = [];
 | 
			
		||||
    for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
 | 
			
		||||
      let added: TransactionStripped[] = [];
 | 
			
		||||
      let removed: string[] = [];
 | 
			
		||||
@ -135,284 +143,25 @@ class MempoolBlocks {
 | 
			
		||||
        removed
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      blocks: mempoolBlocks,
 | 
			
		||||
      deltas: mempoolBlockDeltas
 | 
			
		||||
    };
 | 
			
		||||
    return mempoolBlockDeltas;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
  * Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
 | 
			
		||||
  * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 | 
			
		||||
  *
 | 
			
		||||
  * blockLimit: number of blocks to build in total.
 | 
			
		||||
  * weightLimit: maximum weight of transactions to consider using the selection algorithm.
 | 
			
		||||
  *              if weightLimit is significantly lower than the mempool size, results may start to diverge from getBlockTemplate 
 | 
			
		||||
  * condenseRest: whether to ignore excess transactions or append them to the final block.
 | 
			
		||||
  */
 | 
			
		||||
  public makeBlockTemplates(mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null = null, condenseRest = false): MempoolBlockWithTransactions[] {
 | 
			
		||||
    const start = Date.now();
 | 
			
		||||
    const auditPool: { [txid: string]: AuditTransaction } = {};
 | 
			
		||||
    const mempoolArray: AuditTransaction[] = [];
 | 
			
		||||
    const restOfArray: TransactionExtended[] = [];
 | 
			
		||||
    
 | 
			
		||||
    let weight = 0;
 | 
			
		||||
    const maxWeight = weightLimit ? Math.max(4_000_000 * blockLimit, weightLimit) : Infinity;
 | 
			
		||||
    // grab the top feerate txs up to maxWeight
 | 
			
		||||
    Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
 | 
			
		||||
      weight += tx.weight;
 | 
			
		||||
      if (weight >= maxWeight) {
 | 
			
		||||
        restOfArray.push(tx);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      // initializing everything up front helps V8 optimize property access later
 | 
			
		||||
      auditPool[tx.txid] = {
 | 
			
		||||
        txid: tx.txid,
 | 
			
		||||
        fee: tx.fee,
 | 
			
		||||
        size: tx.size,
 | 
			
		||||
        weight: tx.weight,
 | 
			
		||||
        feePerVsize: tx.feePerVsize,
 | 
			
		||||
        vin: tx.vin,
 | 
			
		||||
        relativesSet: false,
 | 
			
		||||
        ancestorMap: new Map<string, AuditTransaction>(),
 | 
			
		||||
        children: new Set<AuditTransaction>(),
 | 
			
		||||
        ancestorFee: 0,
 | 
			
		||||
        ancestorWeight: 0,
 | 
			
		||||
        score: 0,
 | 
			
		||||
        used: false,
 | 
			
		||||
        modified: false,
 | 
			
		||||
        modifiedNode: null,
 | 
			
		||||
      }
 | 
			
		||||
      mempoolArray.push(auditPool[tx.txid]);
 | 
			
		||||
    })
 | 
			
		||||
  public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null = null, condenseRest = false): Promise<void> {
 | 
			
		||||
    const { mempool, blocks } = await this.makeTemplatesPool.exec({ mempool: newMempool, blockLimit, weightLimit, condenseRest });
 | 
			
		||||
    const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
 | 
			
		||||
 | 
			
		||||
    // Build relatives graph & calculate ancestor scores
 | 
			
		||||
    for (const tx of mempoolArray) {
 | 
			
		||||
      if (!tx.relativesSet) {
 | 
			
		||||
        this.setRelatives(tx, auditPool);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sort by descending ancestor score
 | 
			
		||||
    mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
 | 
			
		||||
 | 
			
		||||
    // Build blocks by greedily choosing the highest feerate package
 | 
			
		||||
    // (i.e. the package rooted in the transaction with the best ancestor score)
 | 
			
		||||
    const blocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
    let blockWeight = 4000;
 | 
			
		||||
    let blockSize = 0;
 | 
			
		||||
    let transactions: AuditTransaction[] = [];
 | 
			
		||||
    const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
 | 
			
		||||
    let overflow: AuditTransaction[] = [];
 | 
			
		||||
    let failures = 0;
 | 
			
		||||
    let top = 0;
 | 
			
		||||
    while ((top < mempoolArray.length || !modified.isEmpty()) && (condenseRest || blocks.length < blockLimit)) {
 | 
			
		||||
      // skip invalid transactions
 | 
			
		||||
      while (top < mempoolArray.length && (mempoolArray[top].used || mempoolArray[top].modified)) {
 | 
			
		||||
        top++;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Select best next package
 | 
			
		||||
      let nextTx;
 | 
			
		||||
      const nextPoolTx = mempoolArray[top];
 | 
			
		||||
      const nextModifiedTx = modified.peek();
 | 
			
		||||
      if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
 | 
			
		||||
        nextTx = nextPoolTx;
 | 
			
		||||
        top++;
 | 
			
		||||
      } else {
 | 
			
		||||
        modified.pop();
 | 
			
		||||
        if (nextModifiedTx) {
 | 
			
		||||
          nextTx = nextModifiedTx;
 | 
			
		||||
          nextTx.modifiedNode = undefined;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (nextTx && !nextTx?.used) {
 | 
			
		||||
        // Check if the package fits into this block
 | 
			
		||||
        if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
 | 
			
		||||
          blockWeight += nextTx.ancestorWeight;
 | 
			
		||||
          const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
 | 
			
		||||
          // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
 | 
			
		||||
          const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
 | 
			
		||||
          const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
 | 
			
		||||
          sortedTxSet.forEach((ancestor, i, arr) => {
 | 
			
		||||
            const mempoolTx = mempool[ancestor.txid];
 | 
			
		||||
            if (ancestor && !ancestor?.used) {
 | 
			
		||||
              ancestor.used = true;
 | 
			
		||||
              // update original copy of this tx with effective fee rate & relatives data
 | 
			
		||||
              mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
 | 
			
		||||
              mempoolTx.ancestors = (Array.from(ancestor.ancestorMap?.values()) as AuditTransaction[]).map((a) => {
 | 
			
		||||
                return {
 | 
			
		||||
                  txid: a.txid,
 | 
			
		||||
                  fee: a.fee,
 | 
			
		||||
                  weight: a.weight,
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
              if (i < arr.length - 1) {
 | 
			
		||||
                mempoolTx.bestDescendant = {
 | 
			
		||||
                  txid: arr[arr.length - 1].txid,
 | 
			
		||||
                  fee: arr[arr.length - 1].fee,
 | 
			
		||||
                  weight: arr[arr.length - 1].weight,
 | 
			
		||||
                };
 | 
			
		||||
              }
 | 
			
		||||
              transactions.push(ancestor);
 | 
			
		||||
              blockSize += ancestor.size;
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // remove these as valid package ancestors for any descendants remaining in the mempool
 | 
			
		||||
          if (sortedTxSet.length) {
 | 
			
		||||
            sortedTxSet.forEach(tx => {
 | 
			
		||||
              this.updateDescendants(tx, auditPool, modified);
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          failures = 0;
 | 
			
		||||
        } else {
 | 
			
		||||
          // hold this package in an overflow list while we check for smaller options
 | 
			
		||||
          overflow.push(nextTx);
 | 
			
		||||
          failures++;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // this block is full
 | 
			
		||||
      const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000);
 | 
			
		||||
      if (exceededPackageTries && (!condenseRest || blocks.length < blockLimit - 1)) {
 | 
			
		||||
        // construct this block
 | 
			
		||||
        if (transactions.length) {
 | 
			
		||||
          blocks.push(this.dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
 | 
			
		||||
        }
 | 
			
		||||
        // reset for the next block
 | 
			
		||||
        transactions = [];
 | 
			
		||||
        blockSize = 0;
 | 
			
		||||
        blockWeight = 4000;
 | 
			
		||||
 | 
			
		||||
        // 'overflow' packages didn't fit in this block, but are valid candidates for the next
 | 
			
		||||
        for (const overflowTx of overflow.reverse()) {
 | 
			
		||||
          if (overflowTx.modified) {
 | 
			
		||||
            overflowTx.modifiedNode = modified.add(overflowTx);
 | 
			
		||||
          } else {
 | 
			
		||||
            top--;
 | 
			
		||||
            mempoolArray[top] = overflowTx;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        overflow = [];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (condenseRest) {
 | 
			
		||||
      // pack any leftover transactions into the last block
 | 
			
		||||
      for (const tx of overflow) {
 | 
			
		||||
        if (!tx || tx?.used) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
        blockWeight += tx.weight;
 | 
			
		||||
        blockSize += tx.size;
 | 
			
		||||
        transactions.push(tx);
 | 
			
		||||
        tx.used = true;
 | 
			
		||||
      }
 | 
			
		||||
      const blockTransactions = transactions.map(t => mempool[t.txid])
 | 
			
		||||
      restOfArray.forEach(tx => {
 | 
			
		||||
        blockWeight += tx.weight;
 | 
			
		||||
        blockSize += tx.size;
 | 
			
		||||
        blockTransactions.push(tx);
 | 
			
		||||
      });
 | 
			
		||||
      if (blockTransactions.length) {
 | 
			
		||||
        blocks.push(this.dataToMempoolBlocks(blockTransactions, blockSize, blockWeight, blocks.length));
 | 
			
		||||
      }
 | 
			
		||||
      transactions = [];
 | 
			
		||||
    } else if (transactions.length) {
 | 
			
		||||
      blocks.push(this.dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const end = Date.now();
 | 
			
		||||
    const time = end - start;
 | 
			
		||||
    logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
 | 
			
		||||
 | 
			
		||||
    return blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // traverse in-mempool ancestors
 | 
			
		||||
  // recursion unavoidable, but should be limited to depth < 25 by mempool policy
 | 
			
		||||
  public setRelatives(
 | 
			
		||||
    tx: AuditTransaction,
 | 
			
		||||
    mempool: { [txid: string]: AuditTransaction },
 | 
			
		||||
  ): void {
 | 
			
		||||
    for (const parent of tx.vin) {
 | 
			
		||||
      const parentTx = mempool[parent.txid];
 | 
			
		||||
      if (parentTx && !tx.ancestorMap!.has(parent.txid)) {
 | 
			
		||||
        tx.ancestorMap.set(parent.txid, parentTx);
 | 
			
		||||
        parentTx.children.add(tx);
 | 
			
		||||
        // visit each node only once
 | 
			
		||||
        if (!parentTx.relativesSet) {
 | 
			
		||||
          this.setRelatives(parentTx, mempool);
 | 
			
		||||
        }
 | 
			
		||||
        parentTx.ancestorMap.forEach((ancestor) => {
 | 
			
		||||
          tx.ancestorMap.set(ancestor.txid, ancestor);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    tx.ancestorFee = tx.fee || 0;
 | 
			
		||||
    tx.ancestorWeight = tx.weight || 0;
 | 
			
		||||
    tx.ancestorMap.forEach((ancestor) => {
 | 
			
		||||
      tx.ancestorFee += ancestor.fee;
 | 
			
		||||
      tx.ancestorWeight += ancestor.weight;
 | 
			
		||||
    });
 | 
			
		||||
    tx.score = tx.ancestorFee / (tx.ancestorWeight || 1);
 | 
			
		||||
    tx.relativesSet = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
 | 
			
		||||
  // avoids recursion to limit call stack depth
 | 
			
		||||
  private updateDescendants(
 | 
			
		||||
    rootTx: AuditTransaction,
 | 
			
		||||
    mempool: { [txid: string]: AuditTransaction },
 | 
			
		||||
    modified: PairingHeap<AuditTransaction>,
 | 
			
		||||
  ): void {
 | 
			
		||||
    const descendantSet: Set<AuditTransaction> = new Set();
 | 
			
		||||
    // stack of nodes left to visit
 | 
			
		||||
    const descendants: AuditTransaction[] = [];
 | 
			
		||||
    let descendantTx;
 | 
			
		||||
    let ancestorIndex;
 | 
			
		||||
    let tmpScore;
 | 
			
		||||
    rootTx.children.forEach(childTx => {
 | 
			
		||||
      if (!descendantSet.has(childTx)) {
 | 
			
		||||
        descendants.push(childTx);
 | 
			
		||||
        descendantSet.add(childTx);
 | 
			
		||||
    // copy CPFP info across to main thread's mempool
 | 
			
		||||
    Object.keys(newMempool).forEach((txid) => {
 | 
			
		||||
      if (newMempool[txid] && mempool[txid]) {
 | 
			
		||||
        newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize;
 | 
			
		||||
        newMempool[txid].ancestors = mempool[txid].ancestors;
 | 
			
		||||
        newMempool[txid].bestDescendant = mempool[txid].bestDescendant;
 | 
			
		||||
        newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    while (descendants.length) {
 | 
			
		||||
      descendantTx = descendants.pop();
 | 
			
		||||
      if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
 | 
			
		||||
        // remove tx as ancestor
 | 
			
		||||
        descendantTx.ancestorMap.delete(rootTx.txid);
 | 
			
		||||
        descendantTx.ancestorFee -= rootTx.fee;
 | 
			
		||||
        descendantTx.ancestorWeight -= rootTx.weight;
 | 
			
		||||
        tmpScore = descendantTx.score;
 | 
			
		||||
        descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorWeight;
 | 
			
		||||
 | 
			
		||||
        if (!descendantTx.modifiedNode) {
 | 
			
		||||
          descendantTx.modified = true;
 | 
			
		||||
          descendantTx.modifiedNode = modified.add(descendantTx);
 | 
			
		||||
        } else {
 | 
			
		||||
          // rebalance modified heap if score has changed
 | 
			
		||||
          if (descendantTx.score < tmpScore) {
 | 
			
		||||
            modified.decreasePriority(descendantTx.modifiedNode);
 | 
			
		||||
          } else if (descendantTx.score > tmpScore) {
 | 
			
		||||
            modified.increasePriority(descendantTx.modifiedNode);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // add this node's children to the stack
 | 
			
		||||
        descendantTx.children.forEach(childTx => {
 | 
			
		||||
          // visit each node only once
 | 
			
		||||
          if (!descendantSet.has(childTx)) {
 | 
			
		||||
            descendants.push(childTx);
 | 
			
		||||
            descendantSet.add(childTx);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.mempoolBlocks = blocks;
 | 
			
		||||
    this.mempoolBlockDeltas = deltas;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private dataToMempoolBlocks(transactions: TransactionExtended[],
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,8 @@ class Mempool {
 | 
			
		||||
                                                    maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => void) | undefined;
 | 
			
		||||
  private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => void) | undefined;
 | 
			
		||||
 | 
			
		||||
  private txPerSecondArray: number[] = [];
 | 
			
		||||
  private txPerSecond: number = 0;
 | 
			
		||||
@ -63,6 +65,11 @@ class Mempool {
 | 
			
		||||
    this.mempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
 | 
			
		||||
    this.asyncMempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getMempool(): { [txid: string]: TransactionExtended } {
 | 
			
		||||
    return this.mempoolCache;
 | 
			
		||||
  }
 | 
			
		||||
@ -72,6 +79,9 @@ class Mempool {
 | 
			
		||||
    if (this.mempoolChangedCallback) {
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, [], []);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.asyncMempoolChangedCallback) {
 | 
			
		||||
      this.asyncMempoolChangedCallback(this.mempoolCache, [], []);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $updateMemPoolInfo() {
 | 
			
		||||
@ -103,12 +113,11 @@ class Mempool {
 | 
			
		||||
    return txTimes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $updateMempool() {
 | 
			
		||||
    logger.debug('Updating mempool');
 | 
			
		||||
  public async $updateMempool(): Promise<void> {
 | 
			
		||||
    logger.debug(`Updating mempool...`);
 | 
			
		||||
    const start = new Date().getTime();
 | 
			
		||||
    let hasChange: boolean = false;
 | 
			
		||||
    const currentMempoolSize = Object.keys(this.mempoolCache).length;
 | 
			
		||||
    let txCount = 0;
 | 
			
		||||
    const transactions = await bitcoinApi.$getRawMempool();
 | 
			
		||||
    const diff = transactions.length - currentMempoolSize;
 | 
			
		||||
    const newTransactions: TransactionExtended[] = [];
 | 
			
		||||
@ -124,7 +133,6 @@ class Mempool {
 | 
			
		||||
        try {
 | 
			
		||||
          const transaction = await transactionUtils.$getTransactionExtended(txid);
 | 
			
		||||
          this.mempoolCache[txid] = transaction;
 | 
			
		||||
          txCount++;
 | 
			
		||||
          if (this.inSync) {
 | 
			
		||||
            this.txPerSecondArray.push(new Date().getTime());
 | 
			
		||||
            this.vBytesPerSecondArray.push({
 | 
			
		||||
@ -133,14 +141,9 @@ class Mempool {
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          hasChange = true;
 | 
			
		||||
          if (diff > 0) {
 | 
			
		||||
            logger.debug('Fetched transaction ' + txCount + ' / ' + diff);
 | 
			
		||||
          } else {
 | 
			
		||||
            logger.debug('Fetched transaction ' + txCount);
 | 
			
		||||
          }
 | 
			
		||||
          newTransactions.push(transaction);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
          logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -194,11 +197,13 @@ class Mempool {
 | 
			
		||||
    if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
 | 
			
		||||
      await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const end = new Date().getTime();
 | 
			
		||||
    const time = end - start;
 | 
			
		||||
    logger.debug(`New mempool size: ${Object.keys(this.mempoolCache).length} Change: ${diff}`);
 | 
			
		||||
    logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
 | 
			
		||||
    logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { Application, Request, Response } from 'express';
 | 
			
		||||
import config from "../../config";
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import audits from '../audit';
 | 
			
		||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
 | 
			
		||||
import BlocksRepository from '../../repositories/BlocksRepository';
 | 
			
		||||
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
 | 
			
		||||
@ -26,7 +27,11 @@ class MiningRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
 | 
			
		||||
    ;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -252,6 +257,52 @@ class MiningRoutes {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getHeightFromTimestamp(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const timestamp = parseInt(req.params.timestamp, 10);
 | 
			
		||||
      // This will prevent people from entering milliseconds etc.
 | 
			
		||||
      // Block timestamps are allowed to be up to 2 hours off, so 24 hours
 | 
			
		||||
      // will never put the maximum value before the most recent block
 | 
			
		||||
      const nowPlus1day = Math.floor(Date.now() / 1000) + 60 * 60 * 24;
 | 
			
		||||
      // Prevent non-integers that are not seconds
 | 
			
		||||
      if (!/^[1-9][0-9]*$/.test(req.params.timestamp) || timestamp > nowPlus1day) {
 | 
			
		||||
        throw new Error(`Invalid timestamp, value must be Unix seconds`);
 | 
			
		||||
      }
 | 
			
		||||
      const result = await BlocksRepository.$getBlockHeightFromTimestamp(
 | 
			
		||||
        timestamp,
 | 
			
		||||
      );
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getBlockAuditScores(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(await audits.$getBlockAuditScores(height, 15));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditScore(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const audit = await BlocksAuditsRepository.$getBlockAuditScore(req.params.hash);
 | 
			
		||||
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
 | 
			
		||||
      res.json(audit || 'null');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new MiningRoutes();
 | 
			
		||||
 | 
			
		||||
@ -14,10 +14,10 @@ interface Pool {
 | 
			
		||||
class PoolsParser {
 | 
			
		||||
  miningPools: any[] = [];
 | 
			
		||||
  unknownPool: any = {
 | 
			
		||||
    'name': "Unknown",
 | 
			
		||||
    'link': "https://learnmeabitcoin.com/technical/coinbase-transaction",
 | 
			
		||||
    'regexes': "[]",
 | 
			
		||||
    'addresses': "[]",
 | 
			
		||||
    'name': 'Unknown',
 | 
			
		||||
    'link': 'https://learnmeabitcoin.com/technical/coinbase-transaction',
 | 
			
		||||
    'regexes': '[]',
 | 
			
		||||
    'addresses': '[]',
 | 
			
		||||
    'slug': 'unknown'
 | 
			
		||||
  };
 | 
			
		||||
  slugWarnFlag = false;
 | 
			
		||||
@ -25,7 +25,7 @@ class PoolsParser {
 | 
			
		||||
  /**
 | 
			
		||||
   * Parse the pools.json file, consolidate the data and dump it into the database
 | 
			
		||||
   */
 | 
			
		||||
  public async migratePoolsJson(poolsJson: object) {
 | 
			
		||||
  public async migratePoolsJson(poolsJson: object): Promise<void> {
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -81,6 +81,7 @@ class PoolsParser {
 | 
			
		||||
    // Finally, we generate the final consolidated pools data
 | 
			
		||||
    const finalPoolDataAdd: Pool[] = [];
 | 
			
		||||
    const finalPoolDataUpdate: Pool[] = [];
 | 
			
		||||
    const finalPoolDataRename: Pool[] = [];
 | 
			
		||||
    for (let i = 0; i < poolNames.length; ++i) {
 | 
			
		||||
      let allAddresses: string[] = [];
 | 
			
		||||
      let allRegexes: string[] = [];
 | 
			
		||||
@ -127,8 +128,26 @@ class PoolsParser {
 | 
			
		||||
          finalPoolDataUpdate.push(poolObj);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.debug(`Add '${finalPoolName}' mining pool`);
 | 
			
		||||
        finalPoolDataAdd.push(poolObj);
 | 
			
		||||
        // Double check that if we're not just renaming a pool (same address same regex)
 | 
			
		||||
        const [poolToRename]: any[] = await DB.query(`
 | 
			
		||||
          SELECT * FROM pools
 | 
			
		||||
          WHERE addresses = ? OR regexes = ?`,
 | 
			
		||||
          [JSON.stringify(poolObj.addresses), JSON.stringify(poolObj.regexes)]
 | 
			
		||||
        );
 | 
			
		||||
        if (poolToRename && poolToRename.length > 0) {
 | 
			
		||||
          // We're actually renaming an existing pool
 | 
			
		||||
          finalPoolDataRename.push({
 | 
			
		||||
            'name': poolObj.name,
 | 
			
		||||
            'link': poolObj.link,
 | 
			
		||||
            'regexes': allRegexes,
 | 
			
		||||
            'addresses': allAddresses,
 | 
			
		||||
            'slug': slug
 | 
			
		||||
          });
 | 
			
		||||
          logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          logger.debug(`Add '${finalPoolName}' mining pool`);
 | 
			
		||||
          finalPoolDataAdd.push(poolObj);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.miningPools.push({
 | 
			
		||||
@ -145,7 +164,9 @@ class PoolsParser {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0) {    
 | 
			
		||||
    if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0 ||
 | 
			
		||||
      finalPoolDataRename.length > 0
 | 
			
		||||
    ) {    
 | 
			
		||||
      logger.debug(`Update pools table now`);
 | 
			
		||||
 | 
			
		||||
      // Add new mining pools into the database
 | 
			
		||||
@ -169,8 +190,22 @@ class PoolsParser {
 | 
			
		||||
        ;`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Rename mining pools
 | 
			
		||||
      const renameQueries: string[] = [];
 | 
			
		||||
      for (let i = 0; i < finalPoolDataRename.length; ++i) {
 | 
			
		||||
        renameQueries.push(`
 | 
			
		||||
          UPDATE pools
 | 
			
		||||
          SET name='${finalPoolDataRename[i].name}', link='${finalPoolDataRename[i].link}',
 | 
			
		||||
            slug='${finalPoolDataRename[i].slug}'
 | 
			
		||||
          WHERE regexes='${JSON.stringify(finalPoolDataRename[i].regexes)}'
 | 
			
		||||
            AND addresses='${JSON.stringify(finalPoolDataRename[i].addresses)}'
 | 
			
		||||
        ;`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        await this.$deleteBlocskToReindex(finalPoolDataUpdate);
 | 
			
		||||
        if (finalPoolDataAdd.length > 0 || updateQueries.length > 0) {
 | 
			
		||||
          await this.$deleteBlocskToReindex(finalPoolDataUpdate);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (finalPoolDataAdd.length > 0) {
 | 
			
		||||
          await DB.query({ sql: queryAdd, timeout: 120000 });
 | 
			
		||||
@ -178,6 +213,9 @@ class PoolsParser {
 | 
			
		||||
        for (const query of updateQueries) {
 | 
			
		||||
          await DB.query({ sql: query, timeout: 120000 });
 | 
			
		||||
        }
 | 
			
		||||
        for (const query of renameQueries) {
 | 
			
		||||
          await DB.query({ sql: query, timeout: 120000 });
 | 
			
		||||
        }
 | 
			
		||||
        await this.insertUnknownPool();
 | 
			
		||||
        logger.info('Mining pools.json import completed');
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										336
									
								
								backend/src/api/tx-selection-worker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								backend/src/api/tx-selection-worker.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,336 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { TransactionExtended, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
 | 
			
		||||
import { PairingHeap } from '../utils/pairing-heap';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import { parentPort } from 'worker_threads';
 | 
			
		||||
 | 
			
		||||
if (parentPort) {
 | 
			
		||||
  parentPort.on('message', (params: { mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null, condenseRest: boolean}) => {
 | 
			
		||||
    const { mempool, blocks } = makeBlockTemplates(params);
 | 
			
		||||
 | 
			
		||||
    // return the result to main thread.
 | 
			
		||||
    if (parentPort) {
 | 
			
		||||
     parentPort.postMessage({ mempool, blocks });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
 | 
			
		||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 | 
			
		||||
*
 | 
			
		||||
* blockLimit: number of blocks to build in total.
 | 
			
		||||
* weightLimit: maximum weight of transactions to consider using the selection algorithm.
 | 
			
		||||
*              if weightLimit is significantly lower than the mempool size, results may start to diverge from getBlockTemplate 
 | 
			
		||||
* condenseRest: whether to ignore excess transactions or append them to the final block.
 | 
			
		||||
*/
 | 
			
		||||
function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: { mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit?: number | null, condenseRest?: boolean | null })
 | 
			
		||||
  : { mempool: { [txid: string]: TransactionExtended }, blocks: MempoolBlockWithTransactions[] } {
 | 
			
		||||
  const start = Date.now();
 | 
			
		||||
  const auditPool: { [txid: string]: AuditTransaction } = {};
 | 
			
		||||
  const mempoolArray: AuditTransaction[] = [];
 | 
			
		||||
  const restOfArray: TransactionExtended[] = [];
 | 
			
		||||
  
 | 
			
		||||
  let weight = 0;
 | 
			
		||||
  const maxWeight = weightLimit ? Math.max(4_000_000 * blockLimit, weightLimit) : Infinity;
 | 
			
		||||
  // grab the top feerate txs up to maxWeight
 | 
			
		||||
  Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
 | 
			
		||||
    weight += tx.weight;
 | 
			
		||||
    if (weight >= maxWeight) {
 | 
			
		||||
      restOfArray.push(tx);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // initializing everything up front helps V8 optimize property access later
 | 
			
		||||
    auditPool[tx.txid] = {
 | 
			
		||||
      txid: tx.txid,
 | 
			
		||||
      fee: tx.fee,
 | 
			
		||||
      size: tx.size,
 | 
			
		||||
      weight: tx.weight,
 | 
			
		||||
      feePerVsize: tx.feePerVsize,
 | 
			
		||||
      vin: tx.vin,
 | 
			
		||||
      relativesSet: false,
 | 
			
		||||
      ancestorMap: new Map<string, AuditTransaction>(),
 | 
			
		||||
      children: new Set<AuditTransaction>(),
 | 
			
		||||
      ancestorFee: 0,
 | 
			
		||||
      ancestorWeight: 0,
 | 
			
		||||
      score: 0,
 | 
			
		||||
      used: false,
 | 
			
		||||
      modified: false,
 | 
			
		||||
      modifiedNode: null,
 | 
			
		||||
    };
 | 
			
		||||
    mempoolArray.push(auditPool[tx.txid]);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Build relatives graph & calculate ancestor scores
 | 
			
		||||
  for (const tx of mempoolArray) {
 | 
			
		||||
    if (!tx.relativesSet) {
 | 
			
		||||
      setRelatives(tx, auditPool);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Sort by descending ancestor score
 | 
			
		||||
  mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
 | 
			
		||||
 | 
			
		||||
  // Build blocks by greedily choosing the highest feerate package
 | 
			
		||||
  // (i.e. the package rooted in the transaction with the best ancestor score)
 | 
			
		||||
  const blocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
  let blockWeight = 4000;
 | 
			
		||||
  let blockSize = 0;
 | 
			
		||||
  let transactions: AuditTransaction[] = [];
 | 
			
		||||
  const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
 | 
			
		||||
  let overflow: AuditTransaction[] = [];
 | 
			
		||||
  let failures = 0;
 | 
			
		||||
  let top = 0;
 | 
			
		||||
  while ((top < mempoolArray.length || !modified.isEmpty()) && (condenseRest || blocks.length < blockLimit)) {
 | 
			
		||||
    // skip invalid transactions
 | 
			
		||||
    while (top < mempoolArray.length && (mempoolArray[top].used || mempoolArray[top].modified)) {
 | 
			
		||||
      top++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Select best next package
 | 
			
		||||
    let nextTx;
 | 
			
		||||
    const nextPoolTx = mempoolArray[top];
 | 
			
		||||
    const nextModifiedTx = modified.peek();
 | 
			
		||||
    if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
 | 
			
		||||
      nextTx = nextPoolTx;
 | 
			
		||||
      top++;
 | 
			
		||||
    } else {
 | 
			
		||||
      modified.pop();
 | 
			
		||||
      if (nextModifiedTx) {
 | 
			
		||||
        nextTx = nextModifiedTx;
 | 
			
		||||
        nextTx.modifiedNode = undefined;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nextTx && !nextTx?.used) {
 | 
			
		||||
      // Check if the package fits into this block
 | 
			
		||||
      if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
 | 
			
		||||
        blockWeight += nextTx.ancestorWeight;
 | 
			
		||||
        const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
 | 
			
		||||
        // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
 | 
			
		||||
        const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
 | 
			
		||||
        const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
 | 
			
		||||
        sortedTxSet.forEach((ancestor, i, arr) => {
 | 
			
		||||
          const mempoolTx = mempool[ancestor.txid];
 | 
			
		||||
          if (ancestor && !ancestor?.used) {
 | 
			
		||||
            ancestor.used = true;
 | 
			
		||||
            // update original copy of this tx with effective fee rate & relatives data
 | 
			
		||||
            mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
 | 
			
		||||
            mempoolTx.ancestors = (Array.from(ancestor.ancestorMap?.values()) as AuditTransaction[]).map((a) => {
 | 
			
		||||
              return {
 | 
			
		||||
                txid: a.txid,
 | 
			
		||||
                fee: a.fee,
 | 
			
		||||
                weight: a.weight,
 | 
			
		||||
              };
 | 
			
		||||
            });
 | 
			
		||||
            mempoolTx.cpfpChecked = true;
 | 
			
		||||
            if (i < arr.length - 1) {
 | 
			
		||||
              mempoolTx.bestDescendant = {
 | 
			
		||||
                txid: arr[arr.length - 1].txid,
 | 
			
		||||
                fee: arr[arr.length - 1].fee,
 | 
			
		||||
                weight: arr[arr.length - 1].weight,
 | 
			
		||||
              };
 | 
			
		||||
            } else {
 | 
			
		||||
              mempoolTx.bestDescendant = null;
 | 
			
		||||
            }
 | 
			
		||||
            transactions.push(ancestor);
 | 
			
		||||
            blockSize += ancestor.size;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // remove these as valid package ancestors for any descendants remaining in the mempool
 | 
			
		||||
        if (sortedTxSet.length) {
 | 
			
		||||
          sortedTxSet.forEach(tx => {
 | 
			
		||||
            updateDescendants(tx, auditPool, modified);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        failures = 0;
 | 
			
		||||
      } else {
 | 
			
		||||
        // hold this package in an overflow list while we check for smaller options
 | 
			
		||||
        overflow.push(nextTx);
 | 
			
		||||
        failures++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // this block is full
 | 
			
		||||
    const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000);
 | 
			
		||||
    const queueEmpty = top >= mempoolArray.length && modified.isEmpty();
 | 
			
		||||
    if ((exceededPackageTries || queueEmpty) && (!condenseRest || blocks.length < blockLimit - 1)) {
 | 
			
		||||
      // construct this block
 | 
			
		||||
      if (transactions.length) {
 | 
			
		||||
        blocks.push(dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
 | 
			
		||||
      }
 | 
			
		||||
      // reset for the next block
 | 
			
		||||
      transactions = [];
 | 
			
		||||
      blockSize = 0;
 | 
			
		||||
      blockWeight = 4000;
 | 
			
		||||
 | 
			
		||||
      // 'overflow' packages didn't fit in this block, but are valid candidates for the next
 | 
			
		||||
      for (const overflowTx of overflow.reverse()) {
 | 
			
		||||
        if (overflowTx.modified) {
 | 
			
		||||
          overflowTx.modifiedNode = modified.add(overflowTx);
 | 
			
		||||
        } else {
 | 
			
		||||
          top--;
 | 
			
		||||
          mempoolArray[top] = overflowTx;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      overflow = [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (condenseRest) {
 | 
			
		||||
    // pack any leftover transactions into the last block
 | 
			
		||||
    for (const tx of overflow) {
 | 
			
		||||
      if (!tx || tx?.used) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      blockWeight += tx.weight;
 | 
			
		||||
      blockSize += tx.size;
 | 
			
		||||
      const mempoolTx = mempool[tx.txid];
 | 
			
		||||
      // update original copy of this tx with effective fee rate & relatives data
 | 
			
		||||
      mempoolTx.effectiveFeePerVsize = tx.score;
 | 
			
		||||
      mempoolTx.ancestors = (Array.from(tx.ancestorMap?.values()) as AuditTransaction[]).map((a) => {
 | 
			
		||||
        return {
 | 
			
		||||
          txid: a.txid,
 | 
			
		||||
          fee: a.fee,
 | 
			
		||||
          weight: a.weight,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
      mempoolTx.bestDescendant = null;
 | 
			
		||||
      mempoolTx.cpfpChecked = true;
 | 
			
		||||
      transactions.push(tx);
 | 
			
		||||
      tx.used = true;
 | 
			
		||||
    }
 | 
			
		||||
    const blockTransactions = transactions.map(t => mempool[t.txid]);
 | 
			
		||||
    restOfArray.forEach(tx => {
 | 
			
		||||
      blockWeight += tx.weight;
 | 
			
		||||
      blockSize += tx.size;
 | 
			
		||||
      tx.effectiveFeePerVsize = tx.feePerVsize;
 | 
			
		||||
      tx.cpfpChecked = false;
 | 
			
		||||
      tx.ancestors = [];
 | 
			
		||||
      tx.bestDescendant = null;
 | 
			
		||||
      blockTransactions.push(tx);
 | 
			
		||||
    });
 | 
			
		||||
    if (blockTransactions.length) {
 | 
			
		||||
      blocks.push(dataToMempoolBlocks(blockTransactions, blockSize, blockWeight, blocks.length));
 | 
			
		||||
    }
 | 
			
		||||
    transactions = [];
 | 
			
		||||
  } else if (transactions.length) {
 | 
			
		||||
    blocks.push(dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const end = Date.now();
 | 
			
		||||
  const time = end - start;
 | 
			
		||||
  logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    mempool,
 | 
			
		||||
    blocks
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// traverse in-mempool ancestors
 | 
			
		||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
 | 
			
		||||
function setRelatives(
 | 
			
		||||
  tx: AuditTransaction,
 | 
			
		||||
  mempool: { [txid: string]: AuditTransaction },
 | 
			
		||||
): void {
 | 
			
		||||
  for (const parent of tx.vin) {
 | 
			
		||||
    const parentTx = mempool[parent.txid];
 | 
			
		||||
    if (parentTx && !tx.ancestorMap?.has(parent.txid)) {
 | 
			
		||||
      tx.ancestorMap.set(parent.txid, parentTx);
 | 
			
		||||
      parentTx.children.add(tx);
 | 
			
		||||
      // visit each node only once
 | 
			
		||||
      if (!parentTx.relativesSet) {
 | 
			
		||||
        setRelatives(parentTx, mempool);
 | 
			
		||||
      }
 | 
			
		||||
      parentTx.ancestorMap.forEach((ancestor) => {
 | 
			
		||||
        tx.ancestorMap.set(ancestor.txid, ancestor);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  tx.ancestorFee = tx.fee || 0;
 | 
			
		||||
  tx.ancestorWeight = tx.weight || 0;
 | 
			
		||||
  tx.ancestorMap.forEach((ancestor) => {
 | 
			
		||||
    tx.ancestorFee += ancestor.fee;
 | 
			
		||||
    tx.ancestorWeight += ancestor.weight;
 | 
			
		||||
  });
 | 
			
		||||
  tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
 | 
			
		||||
  tx.relativesSet = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
 | 
			
		||||
// avoids recursion to limit call stack depth
 | 
			
		||||
function updateDescendants(
 | 
			
		||||
  rootTx: AuditTransaction,
 | 
			
		||||
  mempool: { [txid: string]: AuditTransaction },
 | 
			
		||||
  modified: PairingHeap<AuditTransaction>,
 | 
			
		||||
): void {
 | 
			
		||||
  const descendantSet: Set<AuditTransaction> = new Set();
 | 
			
		||||
  // stack of nodes left to visit
 | 
			
		||||
  const descendants: AuditTransaction[] = [];
 | 
			
		||||
  let descendantTx;
 | 
			
		||||
  let tmpScore;
 | 
			
		||||
  rootTx.children.forEach(childTx => {
 | 
			
		||||
    if (!descendantSet.has(childTx)) {
 | 
			
		||||
      descendants.push(childTx);
 | 
			
		||||
      descendantSet.add(childTx);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  while (descendants.length) {
 | 
			
		||||
    descendantTx = descendants.pop();
 | 
			
		||||
    if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
 | 
			
		||||
      // remove tx as ancestor
 | 
			
		||||
      descendantTx.ancestorMap.delete(rootTx.txid);
 | 
			
		||||
      descendantTx.ancestorFee -= rootTx.fee;
 | 
			
		||||
      descendantTx.ancestorWeight -= rootTx.weight;
 | 
			
		||||
      tmpScore = descendantTx.score;
 | 
			
		||||
      descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
 | 
			
		||||
 | 
			
		||||
      if (!descendantTx.modifiedNode) {
 | 
			
		||||
        descendantTx.modified = true;
 | 
			
		||||
        descendantTx.modifiedNode = modified.add(descendantTx);
 | 
			
		||||
      } else {
 | 
			
		||||
        // rebalance modified heap if score has changed
 | 
			
		||||
        if (descendantTx.score < tmpScore) {
 | 
			
		||||
          modified.decreasePriority(descendantTx.modifiedNode);
 | 
			
		||||
        } else if (descendantTx.score > tmpScore) {
 | 
			
		||||
          modified.increasePriority(descendantTx.modifiedNode);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // add this node's children to the stack
 | 
			
		||||
      descendantTx.children.forEach(childTx => {
 | 
			
		||||
        // visit each node only once
 | 
			
		||||
        if (!descendantSet.has(childTx)) {
 | 
			
		||||
          descendants.push(childTx);
 | 
			
		||||
          descendantSet.add(childTx);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function dataToMempoolBlocks(transactions: TransactionExtended[],
 | 
			
		||||
  blockSize: number, blockWeight: number, blocksIndex: number): MempoolBlockWithTransactions {
 | 
			
		||||
  let rangeLength = 4;
 | 
			
		||||
  if (blocksIndex === 0) {
 | 
			
		||||
    rangeLength = 8;
 | 
			
		||||
  }
 | 
			
		||||
  if (transactions.length > 4000) {
 | 
			
		||||
    rangeLength = 6;
 | 
			
		||||
  } else if (transactions.length > 10000) {
 | 
			
		||||
    rangeLength = 8;
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    blockSize: blockSize,
 | 
			
		||||
    blockVSize: blockWeight / 4,
 | 
			
		||||
    nTx: transactions.length,
 | 
			
		||||
    totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
 | 
			
		||||
    medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
 | 
			
		||||
    feeRange: Common.getFeesInRange(transactions, rangeLength),
 | 
			
		||||
    transactionIds: transactions.map((tx) => tx.txid),
 | 
			
		||||
    transactions: transactions.map((tx) => Common.stripTransaction(tx)),
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -244,13 +244,18 @@ class WebsocketHandler {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) {
 | 
			
		||||
  async handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mempoolBlocks.updateMempoolBlocks(newMempool);
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) {
 | 
			
		||||
      await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(newMempool);
 | 
			
		||||
    }
 | 
			
		||||
    const mBlocks = mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
    const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
 | 
			
		||||
    const mempoolInfo = memPool.getMempoolInfo();
 | 
			
		||||
@ -405,22 +410,25 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]): void {
 | 
			
		||||
 
 | 
			
		||||
  async handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]): Promise<void> {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mBlocks: undefined | MempoolBlock[];
 | 
			
		||||
    let mBlockDeltas: undefined | MempoolBlockDelta[];
 | 
			
		||||
    let matchRate;
 | 
			
		||||
    const _memPool = memPool.getMempool();
 | 
			
		||||
    let matchRate;
 | 
			
		||||
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
      const mempoolCopy = cloneMempool(_memPool);
 | 
			
		||||
      const projectedBlocks = mempoolBlocks.makeBlockTemplates(mempoolCopy, 2);
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) {
 | 
			
		||||
      await mempoolBlocks.makeBlockTemplates(_memPool, 2);
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(_memPool);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      const { censored, added, score } = Audit.auditBlock(transactions, projectedBlocks, mempoolCopy);
 | 
			
		||||
    if (Common.indexingEnabled() && memPool.isInSync()) {
 | 
			
		||||
      const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
 | 
			
		||||
 | 
			
		||||
      const { censored, added, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
 | 
			
		||||
      matchRate = Math.round(score * 100 * 100) / 100;
 | 
			
		||||
 | 
			
		||||
      const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
 | 
			
		||||
@ -459,9 +467,13 @@ class WebsocketHandler {
 | 
			
		||||
      delete _memPool[txId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mempoolBlocks.updateMempoolBlocks(_memPool);
 | 
			
		||||
    mBlocks = mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
    mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) {
 | 
			
		||||
      await mempoolBlocks.makeBlockTemplates(_memPool, 2);
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(_memPool);
 | 
			
		||||
    }
 | 
			
		||||
    const mBlocks = mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
    const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
 | 
			
		||||
 | 
			
		||||
    const da = difficultyAdjustment.getDifficultyAdjustment();
 | 
			
		||||
    const fees = feeApi.getRecommendedFee();
 | 
			
		||||
@ -569,14 +581,4 @@ class WebsocketHandler {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cloneMempool(mempool: { [txid: string]: TransactionExtended }): { [txid: string]: TransactionExtended } {
 | 
			
		||||
  const cloned = {};
 | 
			
		||||
  Object.keys(mempool).forEach(id => {
 | 
			
		||||
    cloned[id] = {
 | 
			
		||||
      ...mempool[id]
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
  return cloned;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new WebsocketHandler();
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ const configFromFile = require(
 | 
			
		||||
 | 
			
		||||
interface IConfig {
 | 
			
		||||
  MEMPOOL: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
 | 
			
		||||
    BACKEND: 'esplora' | 'electrum' | 'none';
 | 
			
		||||
    HTTP_PORT: number;
 | 
			
		||||
@ -28,6 +29,7 @@ interface IConfig {
 | 
			
		||||
    AUTOMATIC_BLOCK_REINDEXING: boolean;
 | 
			
		||||
    POOLS_JSON_URL: string,
 | 
			
		||||
    POOLS_JSON_TREE_URL: string,
 | 
			
		||||
    ADVANCED_TRANSACTION_SELECTION: boolean;
 | 
			
		||||
  };
 | 
			
		||||
  ESPLORA: {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
@ -119,6 +121,7 @@ interface IConfig {
 | 
			
		||||
 | 
			
		||||
const defaults: IConfig = {
 | 
			
		||||
  'MEMPOOL': {
 | 
			
		||||
    'ENABLED': true,
 | 
			
		||||
    'NETWORK': 'mainnet',
 | 
			
		||||
    'BACKEND': 'none',
 | 
			
		||||
    'HTTP_PORT': 8999,
 | 
			
		||||
@ -143,6 +146,7 @@ const defaults: IConfig = {
 | 
			
		||||
    'AUTOMATIC_BLOCK_REINDEXING': false,
 | 
			
		||||
    'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
 | 
			
		||||
    'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
 | 
			
		||||
    'ADVANCED_TRANSACTION_SELECTION': false,
 | 
			
		||||
  },
 | 
			
		||||
  'ESPLORA': {
 | 
			
		||||
    'REST_API_URL': 'http://127.0.0.1:3000',
 | 
			
		||||
@ -224,11 +228,11 @@ const defaults: IConfig = {
 | 
			
		||||
    'BISQ_URL': 'https://bisq.markets/api',
 | 
			
		||||
    'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
 | 
			
		||||
  },
 | 
			
		||||
  "MAXMIND": {
 | 
			
		||||
  'MAXMIND': {
 | 
			
		||||
    'ENABLED': false,
 | 
			
		||||
    "GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
 | 
			
		||||
    "GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
 | 
			
		||||
    "GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
 | 
			
		||||
    'GEOLITE2_CITY': '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
 | 
			
		||||
    'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
 | 
			
		||||
    'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import express from "express";
 | 
			
		||||
import express from 'express';
 | 
			
		||||
import { Application, Request, Response, NextFunction } from 'express';
 | 
			
		||||
import * as http from 'http';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
@ -34,7 +34,7 @@ import miningRoutes from './api/mining/mining-routes';
 | 
			
		||||
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 fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -74,7 +74,7 @@ class Server {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async startServer(worker = false) {
 | 
			
		||||
  async startServer(worker = false): Promise<void> {
 | 
			
		||||
    logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
 | 
			
		||||
 | 
			
		||||
    this.app
 | 
			
		||||
@ -92,7 +92,9 @@ class Server {
 | 
			
		||||
    this.setUpWebsocketHandling();
 | 
			
		||||
 | 
			
		||||
    await syncAssets.syncAssets$();
 | 
			
		||||
    diskCache.loadMempoolCache();
 | 
			
		||||
    if (config.MEMPOOL.ENABLED) {
 | 
			
		||||
      diskCache.loadMempoolCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.DATABASE.ENABLED) {
 | 
			
		||||
      await DB.checkDbConnection();
 | 
			
		||||
@ -127,7 +129,10 @@ class Server {
 | 
			
		||||
    fiatConversion.startService();
 | 
			
		||||
 | 
			
		||||
    this.setUpHttpApiRoutes();
 | 
			
		||||
    this.runMainUpdateLoop();
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.ENABLED) {
 | 
			
		||||
      this.runMainUpdateLoop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.BISQ.ENABLED) {
 | 
			
		||||
      bisq.startBisqService();
 | 
			
		||||
@ -149,7 +154,7 @@ class Server {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async runMainUpdateLoop() {
 | 
			
		||||
  async runMainUpdateLoop(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      try {
 | 
			
		||||
        await memPool.$updateMemPoolInfo();
 | 
			
		||||
@ -183,7 +188,7 @@ class Server {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $runLightningBackend() {
 | 
			
		||||
  async $runLightningBackend(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await fundingTxFetcher.$init();
 | 
			
		||||
      await networkSyncService.$startService();
 | 
			
		||||
@ -195,7 +200,7 @@ class Server {
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  setUpWebsocketHandling() {
 | 
			
		||||
  setUpWebsocketHandling(): void {
 | 
			
		||||
    if (this.wss) {
 | 
			
		||||
      websocketHandler.setWebsocketServer(this.wss);
 | 
			
		||||
    }
 | 
			
		||||
@ -209,19 +214,21 @@ class Server {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    websocketHandler.setupConnectionHandling();
 | 
			
		||||
    statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
 | 
			
		||||
    blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
 | 
			
		||||
    memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
 | 
			
		||||
    if (config.MEMPOOL.ENABLED) {
 | 
			
		||||
      statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
 | 
			
		||||
      memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
 | 
			
		||||
      blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
 | 
			
		||||
    }
 | 
			
		||||
    fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
 | 
			
		||||
    loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setUpHttpApiRoutes() {
 | 
			
		||||
  
 | 
			
		||||
  setUpHttpApiRoutes(): void {
 | 
			
		||||
    bitcoinRoutes.initRoutes(this.app);
 | 
			
		||||
    if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
 | 
			
		||||
    if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
 | 
			
		||||
      statisticsRoutes.initRoutes(this.app);
 | 
			
		||||
    }
 | 
			
		||||
    if (Common.indexingEnabled()) {
 | 
			
		||||
    if (Common.indexingEnabled() && config.MEMPOOL.ENABLED) {
 | 
			
		||||
      miningRoutes.initRoutes(this.app);
 | 
			
		||||
    }
 | 
			
		||||
    if (config.BISQ.ENABLED) {
 | 
			
		||||
@ -238,4 +245,4 @@ class Server {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const server = new Server();
 | 
			
		||||
((): Server => new Server())();
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,11 @@ export interface BlockAudit {
 | 
			
		||||
  matchRate: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditScore {
 | 
			
		||||
  hash: string,
 | 
			
		||||
  matchRate?: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
  blockSize: number;
 | 
			
		||||
  blockVSize: number;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { BlockAudit } from '../mempool.interfaces';
 | 
			
		||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
 | 
			
		||||
class BlocksAuditRepositories {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
			
		||||
@ -72,10 +72,10 @@ class BlocksAuditRepositories {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getShortBlockAudit(hash: string): Promise<any> {
 | 
			
		||||
  public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT hash as id, match_rate as matchRate
 | 
			
		||||
        `SELECT hash, match_rate as matchRate
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
@ -392,6 +392,36 @@ class BlocksRepository {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the first block at or directly after a given timestamp
 | 
			
		||||
   * @param timestamp number unix time in seconds
 | 
			
		||||
   * @returns The height and timestamp of a block (timestamp might vary from given timestamp)
 | 
			
		||||
   */
 | 
			
		||||
  public async $getBlockHeightFromTimestamp(
 | 
			
		||||
    timestamp: number,
 | 
			
		||||
  ): Promise<{ height: number; hash: string; timestamp: number }> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Get first block at or after the given timestamp
 | 
			
		||||
      const query = `SELECT height, hash, blockTimestamp as timestamp FROM blocks
 | 
			
		||||
        WHERE blockTimestamp <= FROM_UNIXTIME(?)
 | 
			
		||||
        ORDER BY blockTimestamp DESC
 | 
			
		||||
        LIMIT 1`;
 | 
			
		||||
      const params = [timestamp];
 | 
			
		||||
      const [rows]: any[][] = await DB.query(query, params);
 | 
			
		||||
      if (rows.length === 0) {
 | 
			
		||||
        throw new Error(`No block was found before timestamp ${timestamp}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return rows[0];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(
 | 
			
		||||
        'Cannot get block height from timestamp from the db. Reason: ' +
 | 
			
		||||
          (e instanceof Error ? e.message : e),
 | 
			
		||||
      );
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return blocks height
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										67
									
								
								backend/src/repositories/NodeRecordsRepository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								backend/src/repositories/NodeRecordsRepository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
import { ResultSetHeader, RowDataPacket } from 'mysql2';
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
 | 
			
		||||
export interface NodeRecord {
 | 
			
		||||
  publicKey: string; // node public key
 | 
			
		||||
  type: number; // TLV extension record type
 | 
			
		||||
  payload: string; // base64 record payload
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class NodesRecordsRepository {
 | 
			
		||||
  public async $saveRecord(record: NodeRecord): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const payloadBytes = Buffer.from(record.payload, 'base64');
 | 
			
		||||
      await DB.query(`
 | 
			
		||||
        INSERT INTO nodes_records(public_key, type, payload)
 | 
			
		||||
        VALUE (?, ?, ?)
 | 
			
		||||
        ON DUPLICATE KEY UPDATE
 | 
			
		||||
          payload = ?
 | 
			
		||||
      `, [record.publicKey, record.type, payloadBytes, payloadBytes]);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
 | 
			
		||||
        logger.err(`Cannot save node record (${[record.publicKey, record.type, record.payload]}) into db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
        // We don't throw, not a critical issue if we miss some nodes records
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
  public async $getRecordTypes(publicKey: string): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `
 | 
			
		||||
        SELECT type FROM nodes_records
 | 
			
		||||
        WHERE public_key = ?
 | 
			
		||||
      `;
 | 
			
		||||
      const [rows] = await DB.query<RowDataPacket[][]>(query, [publicKey]);
 | 
			
		||||
      return rows.map(row => row['type']);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot retrieve custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $deleteUnusedRecords(publicKey: string, recordTypes: number[]): Promise<number> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query;
 | 
			
		||||
      if (recordTypes.length) {
 | 
			
		||||
        query = `
 | 
			
		||||
          DELETE FROM nodes_records
 | 
			
		||||
          WHERE public_key = ?
 | 
			
		||||
          AND type NOT IN (${recordTypes.map(type => `${type}`).join(',')})
 | 
			
		||||
        `;
 | 
			
		||||
      } else {
 | 
			
		||||
        query = `
 | 
			
		||||
          DELETE FROM nodes_records
 | 
			
		||||
          WHERE public_key = ?
 | 
			
		||||
        `;
 | 
			
		||||
      }
 | 
			
		||||
      const [result] = await DB.query<ResultSetHeader>(query, [publicKey]);
 | 
			
		||||
      return result.affectedRows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot delete unused custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      return 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new NodesRecordsRepository();
 | 
			
		||||
@ -13,6 +13,7 @@ import fundingTxFetcher from './sync-tasks/funding-tx-fetcher';
 | 
			
		||||
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
 | 
			
		||||
import { Common } from '../../api/common';
 | 
			
		||||
import blocks from '../../api/blocks';
 | 
			
		||||
import NodeRecordsRepository from '../../repositories/NodeRecordsRepository';
 | 
			
		||||
 | 
			
		||||
class NetworkSyncService {
 | 
			
		||||
  loggerTimer = 0;
 | 
			
		||||
@ -63,6 +64,7 @@ class NetworkSyncService {
 | 
			
		||||
    let progress = 0;
 | 
			
		||||
 | 
			
		||||
    let deletedSockets = 0;
 | 
			
		||||
    let deletedRecords = 0;
 | 
			
		||||
    const graphNodesPubkeys: string[] = [];
 | 
			
		||||
    for (const node of nodes) {
 | 
			
		||||
      const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
 | 
			
		||||
@ -84,8 +86,23 @@ class NetworkSyncService {
 | 
			
		||||
        addresses.push(socket.addr);
 | 
			
		||||
      }
 | 
			
		||||
      deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
 | 
			
		||||
 | 
			
		||||
      const oldRecordTypes = await NodeRecordsRepository.$getRecordTypes(node.pub_key);
 | 
			
		||||
      const customRecordTypes: number[] = [];
 | 
			
		||||
      for (const [type, payload] of Object.entries(node.custom_records || {})) {
 | 
			
		||||
        const numericalType = parseInt(type);
 | 
			
		||||
        await NodeRecordsRepository.$saveRecord({
 | 
			
		||||
          publicKey: node.pub_key,
 | 
			
		||||
          type: numericalType,
 | 
			
		||||
          payload,
 | 
			
		||||
        });
 | 
			
		||||
        customRecordTypes.push(numericalType);
 | 
			
		||||
      }
 | 
			
		||||
      if (oldRecordTypes.reduce((changed, type) => changed || customRecordTypes.indexOf(type) === -1, false)) {
 | 
			
		||||
        deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted`);
 | 
			
		||||
    logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`);
 | 
			
		||||
 | 
			
		||||
    // If a channel if not present in the graph, mark it as inactive
 | 
			
		||||
    await nodesApi.$setNodesInactive(graphNodesPubkeys);
 | 
			
		||||
@ -309,7 +326,7 @@ class NetworkSyncService {
 | 
			
		||||
         └──────────────────┘
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  private async $runClosedChannelsForensics(): Promise<void> {
 | 
			
		||||
  private async $runClosedChannelsForensics(skipUnresolved: boolean = false): Promise<void> {
 | 
			
		||||
    if (!config.ESPLORA.REST_API_URL) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -318,9 +335,18 @@ class NetworkSyncService {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      logger.info(`Started running closed channel forensics...`);
 | 
			
		||||
      const channels = await channelsApi.$getClosedChannelsWithoutReason();
 | 
			
		||||
      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;
 | 
			
		||||
@ -350,6 +376,7 @@ class NetworkSyncService {
 | 
			
		||||
              reason = 3;
 | 
			
		||||
            } else {
 | 
			
		||||
              reason = 2;
 | 
			
		||||
              resolvedForceClose = true;
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            /*
 | 
			
		||||
@ -374,6 +401,9 @@ class NetworkSyncService {
 | 
			
		||||
          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}`);
 | 
			
		||||
 | 
			
		||||
@ -89,6 +89,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "electrum",
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "HTTP_PORT": 8999,
 | 
			
		||||
    "SPAWN_CLUSTER_PROCS": 0,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/",
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "__MEMPOOL_NETWORK__",
 | 
			
		||||
    "BACKEND": "__MEMPOOL_BACKEND__",
 | 
			
		||||
    "ENABLED": __MEMPOOL_ENABLED__,
 | 
			
		||||
    "HTTP_PORT": __MEMPOOL_HTTP_PORT__,
 | 
			
		||||
    "SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
 | 
			
		||||
    "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
# MEMPOOL
 | 
			
		||||
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
 | 
			
		||||
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
 | 
			
		||||
__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
 | 
			
		||||
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
 | 
			
		||||
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
 | 
			
		||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
 | 
			
		||||
@ -111,6 +112,7 @@ mkdir -p "${__MEMPOOL_CACHE_DIR__}"
 | 
			
		||||
 | 
			
		||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_BACKEND__/${__MEMPOOL_BACKEND__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_ENABLED__/${__MEMPOOL_ENABLED__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_HTTP_PORT__/${__MEMPOOL_HTTP_PORT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_SPAWN_CLUSTER_PROCS__/${__MEMPOOL_SPAWN_CLUSTER_PROCS__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,9 @@ WORKDIR /build
 | 
			
		||||
COPY . .
 | 
			
		||||
RUN apt-get update
 | 
			
		||||
RUN apt-get install -y build-essential rsync
 | 
			
		||||
RUN cp mempool-frontend-config.sample.json mempool-frontend-config.json
 | 
			
		||||
RUN npm install --omit=dev --omit=optional
 | 
			
		||||
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
FROM nginx:1.17.8-alpine
 | 
			
		||||
@ -28,7 +30,9 @@ RUN chown -R 1000:1000 /patch && chmod -R 755 /patch && \
 | 
			
		||||
        chown -R 1000:1000 /var/cache/nginx && \
 | 
			
		||||
        chown -R 1000:1000 /var/log/nginx && \
 | 
			
		||||
        chown -R 1000:1000 /etc/nginx/nginx.conf && \
 | 
			
		||||
        chown -R 1000:1000 /etc/nginx/conf.d
 | 
			
		||||
        chown -R 1000:1000 /etc/nginx/conf.d && \
 | 
			
		||||
        chown -R 1000:1000 /var/www/mempool
 | 
			
		||||
 | 
			
		||||
RUN touch /var/run/nginx.pid && \
 | 
			
		||||
        chown -R 1000:1000 /var/run/nginx.pid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,4 +10,51 @@ cp /etc/nginx/nginx.conf /patch/nginx.conf
 | 
			
		||||
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
 | 
			
		||||
cat /patch/nginx.conf > /etc/nginx/nginx.conf
 | 
			
		||||
 | 
			
		||||
# Runtime overrides - read env vars defined in docker compose
 | 
			
		||||
 | 
			
		||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
 | 
			
		||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
 | 
			
		||||
__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
 | 
			
		||||
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
 | 
			
		||||
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
 | 
			
		||||
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
 | 
			
		||||
__ITEMS_PER_PAGE__=${ITEMS_PER_PAGE:=10}
 | 
			
		||||
__KEEP_BLOCKS_AMOUNT__=${KEEP_BLOCKS_AMOUNT:=8}
 | 
			
		||||
__NGINX_PROTOCOL__=${NGINX_PROTOCOL:=http}
 | 
			
		||||
__NGINX_HOSTNAME__=${NGINX_HOSTNAME:=localhost}
 | 
			
		||||
__NGINX_PORT__=${NGINX_PORT:=8999}
 | 
			
		||||
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
 | 
			
		||||
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
 | 
			
		||||
__BASE_MODULE__=${BASE_MODULE:=mempool}
 | 
			
		||||
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
 | 
			
		||||
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
 | 
			
		||||
__BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets}
 | 
			
		||||
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
 | 
			
		||||
__LIGHTNING__=${LIGHTNING:=false}
 | 
			
		||||
 | 
			
		||||
# Export as environment variables to be used by envsubst
 | 
			
		||||
export __TESTNET_ENABLED__
 | 
			
		||||
export __SIGNET_ENABLED__
 | 
			
		||||
export __LIQUID_ENABLED__
 | 
			
		||||
export __LIQUID_TESTNET_ENABLED__
 | 
			
		||||
export __BISQ_ENABLED__
 | 
			
		||||
export __BISQ_SEPARATE_BACKEND__
 | 
			
		||||
export __ITEMS_PER_PAGE__
 | 
			
		||||
export __KEEP_BLOCKS_AMOUNT__
 | 
			
		||||
export __NGINX_PROTOCOL__
 | 
			
		||||
export __NGINX_HOSTNAME__
 | 
			
		||||
export __NGINX_PORT__
 | 
			
		||||
export __BLOCK_WEIGHT_UNITS__
 | 
			
		||||
export __MEMPOOL_BLOCKS_AMOUNT__
 | 
			
		||||
export __BASE_MODULE__
 | 
			
		||||
export __MEMPOOL_WEBSITE_URL__
 | 
			
		||||
export __LIQUID_WEBSITE_URL__
 | 
			
		||||
export __BISQ_WEBSITE_URL__
 | 
			
		||||
export __MINING_DASHBOARD__
 | 
			
		||||
export __LIGHTNING__
 | 
			
		||||
 | 
			
		||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
 | 
			
		||||
echo ${folder}
 | 
			
		||||
envsubst < ${folder}/config.template.js > ${folder}/config.js
 | 
			
		||||
 | 
			
		||||
exec "$@"
 | 
			
		||||
 | 
			
		||||
@ -152,15 +152,14 @@
 | 
			
		||||
            "assets": [
 | 
			
		||||
              "src/favicon.ico",
 | 
			
		||||
              "src/resources",
 | 
			
		||||
              "src/robots.txt"
 | 
			
		||||
              "src/robots.txt",
 | 
			
		||||
              "src/config.js",
 | 
			
		||||
              "src/config.template.js"
 | 
			
		||||
            ],
 | 
			
		||||
            "styles": [
 | 
			
		||||
              "src/styles.scss",
 | 
			
		||||
              "node_modules/@fortawesome/fontawesome-svg-core/styles.css"
 | 
			
		||||
            ],
 | 
			
		||||
            "scripts": [
 | 
			
		||||
              "generated-config.js"
 | 
			
		||||
            ],
 | 
			
		||||
            "vendorChunk": true,
 | 
			
		||||
            "extractLicenses": false,
 | 
			
		||||
            "buildOptimizer": false,
 | 
			
		||||
@ -222,6 +221,10 @@
 | 
			
		||||
              "proxyConfig": "proxy.conf.local.js",
 | 
			
		||||
              "verbose": true
 | 
			
		||||
            },
 | 
			
		||||
            "local-esplora": {
 | 
			
		||||
              "proxyConfig": "proxy.conf.local-esplora.js",
 | 
			
		||||
              "verbose": true
 | 
			
		||||
            },
 | 
			
		||||
            "mixed": {
 | 
			
		||||
              "proxyConfig": "proxy.conf.mixed.js",
 | 
			
		||||
              "verbose": true
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,8 @@ var fs = require('fs');
 | 
			
		||||
const { spawnSync } = require('child_process');
 | 
			
		||||
 | 
			
		||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
 | 
			
		||||
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
 | 
			
		||||
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
 | 
			
		||||
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
 | 
			
		||||
 | 
			
		||||
let settings = [];
 | 
			
		||||
let configContent = {};
 | 
			
		||||
@ -67,10 +68,17 @@ if (process.env.DOCKER_COMMIT_HASH) {
 | 
			
		||||
 | 
			
		||||
const newConfig = `(function (window) {
 | 
			
		||||
  window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
 | 
			
		||||
    window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
 | 
			
		||||
    window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'${obj.value}'` : obj.value};`, '')}
 | 
			
		||||
    window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
 | 
			
		||||
    window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
 | 
			
		||||
  }(global || this));`;
 | 
			
		||||
  }(this));`;
 | 
			
		||||
 | 
			
		||||
const newConfigTemplate = `(function (window) {
 | 
			
		||||
  window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
 | 
			
		||||
    window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'\${__${obj.key}__}'` : `\${__${obj.key}__}`};`, '')}
 | 
			
		||||
    window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
 | 
			
		||||
    window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
 | 
			
		||||
  }(this));`;
 | 
			
		||||
 | 
			
		||||
function readConfig(path) {
 | 
			
		||||
  try {
 | 
			
		||||
@ -89,6 +97,16 @@ function writeConfig(path, config) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function writeConfigTemplate(path, config) {
 | 
			
		||||
  try {
 | 
			
		||||
    fs.writeFileSync(path, config, 'utf8');
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    throw new Error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
 | 
			
		||||
 | 
			
		||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
 | 
			
		||||
 | 
			
		||||
if (currentConfig && currentConfig === newConfig) {
 | 
			
		||||
@ -106,4 +124,4 @@ if (currentConfig && currentConfig === newConfig) {
 | 
			
		||||
  console.log('NEW CONFIG: ', newConfig);
 | 
			
		||||
  writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
 | 
			
		||||
  console.log(`${GENERATED_CONFIG_FILE_NAME} file updated`);
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@
 | 
			
		||||
    "serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
 | 
			
		||||
    "serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging",
 | 
			
		||||
    "start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
 | 
			
		||||
    "start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
 | 
			
		||||
    "start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging",
 | 
			
		||||
    "start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
 | 
			
		||||
    "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										137
									
								
								frontend/proxy.conf.local-esplora.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								frontend/proxy.conf.local-esplora.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,137 @@
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
 | 
			
		||||
const FRONTEND_CONFIG_FILE_NAME = 'mempool-frontend-config.json';
 | 
			
		||||
 | 
			
		||||
let configContent;
 | 
			
		||||
 | 
			
		||||
// Read frontend config 
 | 
			
		||||
try {
 | 
			
		||||
    const rawConfig = fs.readFileSync(FRONTEND_CONFIG_FILE_NAME);
 | 
			
		||||
    configContent = JSON.parse(rawConfig);
 | 
			
		||||
    console.log(`${FRONTEND_CONFIG_FILE_NAME} file found, using provided config`);
 | 
			
		||||
} catch (e) {
 | 
			
		||||
    console.log(e);
 | 
			
		||||
    if (e.code !== 'ENOENT') {
 | 
			
		||||
      throw new Error(e);
 | 
			
		||||
  } else {
 | 
			
		||||
      console.log(`${FRONTEND_CONFIG_FILE_NAME} file not found, using default config`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let PROXY_CONFIG = [];
 | 
			
		||||
 | 
			
		||||
if (configContent && configContent.BASE_MODULE === 'liquid') {
 | 
			
		||||
  PROXY_CONFIG.push(...[
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquid/api/v1/**'],
 | 
			
		||||
      target: `http://127.0.0.1:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      ws: true,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquid": ""
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquid/api/**'],
 | 
			
		||||
      target: `http://127.0.0.1:3000`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquid/api/": ""
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquidtestnet/api/v1/**'],
 | 
			
		||||
      target: `http://127.0.0.1:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      ws: true,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquidtestnet": ""
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/liquidtestnet/api/**'],
 | 
			
		||||
      target: `http://127.0.0.1:3000`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/liquidtestnet/api/": "/"
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if (configContent && configContent.BASE_MODULE === 'bisq') {
 | 
			
		||||
  PROXY_CONFIG.push(...[
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/bisq/api/v1/ws'],
 | 
			
		||||
      target: `http://127.0.0.1:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      ws: true,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/bisq": ""
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/bisq/api/v1/**'],
 | 
			
		||||
      target: `http://127.0.0.1:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      context: ['/bisq/api/**'],
 | 
			
		||||
      target: `http://127.0.0.1:8999`,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      changeOrigin: true,
 | 
			
		||||
      proxyTimeout: 30000,
 | 
			
		||||
      pathRewrite: {
 | 
			
		||||
          "^/bisq/api/": "/api/v1/bisq/"
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PROXY_CONFIG.push(...[
 | 
			
		||||
  {
 | 
			
		||||
    context: ['/testnet/api/v1/lightning/**'],
 | 
			
		||||
    target: `http://127.0.0.1:8999`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
    changeOrigin: true,
 | 
			
		||||
    proxyTimeout: 30000,
 | 
			
		||||
    pathRewrite: {
 | 
			
		||||
        "^/testnet": ""
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    context: ['/api/v1/**'],
 | 
			
		||||
    target: `http://127.0.0.1:8999`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
    ws: true,
 | 
			
		||||
    changeOrigin: true,
 | 
			
		||||
    proxyTimeout: 30000,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    context: ['/api/**'],
 | 
			
		||||
    target: `http://127.0.0.1:3000`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
    changeOrigin: true,
 | 
			
		||||
    proxyTimeout: 30000,
 | 
			
		||||
    pathRewrite: {
 | 
			
		||||
        "^/api": ""
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
console.log(PROXY_CONFIG);
 | 
			
		||||
 | 
			
		||||
module.exports = PROXY_CONFIG;
 | 
			
		||||
@ -79,7 +79,7 @@ export const poolsColor = {
 | 
			
		||||
   'binancepool': '#1E88E5',
 | 
			
		||||
   'viabtc': '#039BE5',
 | 
			
		||||
   'btccom': '#00897B',
 | 
			
		||||
   'slushpool': '#00ACC1',
 | 
			
		||||
   'braiinspool': '#00ACC1',
 | 
			
		||||
   'sbicrypto': '#43A047',
 | 
			
		||||
   'marapool': '#7CB342',
 | 
			
		||||
   'luxor': '#C0CA33',
 | 
			
		||||
 | 
			
		||||
@ -41,10 +41,6 @@
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="td-width" i18n="shared.transaction-count">Transactions</td>
 | 
			
		||||
                <td>{{ blockAudit.tx_count }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="blockAudit.size">Size</td>
 | 
			
		||||
                <td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
 | 
			
		||||
@ -61,6 +57,10 @@
 | 
			
		||||
        <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>
 | 
			
		||||
@ -69,18 +69,10 @@
 | 
			
		||||
                <td i18n="block.missing-txs">Removed txs</td>
 | 
			
		||||
                <td>{{ blockAudit.missingTxs.length }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.missing-txs">Omitted txs</td>
 | 
			
		||||
                <td>{{ numMissing }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.added-txs">Added txs</td>
 | 
			
		||||
                <td>{{ blockAudit.addedTxs.length }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.missing-txs">Included txs</td>
 | 
			
		||||
                <td>{{ numUnexpected }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
@ -97,21 +89,6 @@
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="!error && isLoading">
 | 
			
		||||
    <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]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- OVERVIEW -->
 | 
			
		||||
    <div class="box mb-3">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
@ -123,7 +100,6 @@
 | 
			
		||||
              <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>
 | 
			
		||||
@ -136,7 +112,6 @@
 | 
			
		||||
              <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>
 | 
			
		||||
@ -180,16 +155,16 @@
 | 
			
		||||
      <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"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)"></app-block-overview-graph>
 | 
			
		||||
          [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"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)"></app-block-overview-graph>
 | 
			
		||||
          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div> <!-- row -->
 | 
			
		||||
  </div> <!-- box -->
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
			
		||||
import { Subscription, combineLatest } from 'rxjs';
 | 
			
		||||
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
 | 
			
		||||
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';
 | 
			
		||||
@ -37,6 +38,7 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  webGlEnabled = true;
 | 
			
		||||
  isMobile = window.innerWidth <= 767.98;
 | 
			
		||||
  hoverTx: string;
 | 
			
		||||
 | 
			
		||||
  childChangeSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
@ -51,7 +53,8 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private apiService: ApiService
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.webGlEnabled = detectWebGL();
 | 
			
		||||
  }
 | 
			
		||||
@ -76,69 +79,95 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.auditSubscription = this.route.paramMap.pipe(
 | 
			
		||||
      switchMap((params: ParamMap) => {
 | 
			
		||||
        this.blockHash = params.get('id') || null;
 | 
			
		||||
        if (!this.blockHash) {
 | 
			
		||||
        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)
 | 
			
		||||
          .pipe(
 | 
			
		||||
            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 (isAdded[tx.txid]) {
 | 
			
		||||
                  tx.status = 'added';
 | 
			
		||||
                } else if (index === 0 || 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;
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
      }),
 | 
			
		||||
      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 null;
 | 
			
		||||
        return of(null);
 | 
			
		||||
      }),
 | 
			
		||||
    ).subscribe((blockAudit) => {
 | 
			
		||||
      this.blockAudit = blockAudit;
 | 
			
		||||
@ -189,4 +218,12 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  @Input() orientation = 'left';
 | 
			
		||||
  @Input() flip = true;
 | 
			
		||||
  @Input() disableSpinner = false;
 | 
			
		||||
  @Input() mirrorTxid: string | void;
 | 
			
		||||
  @Output() txClickEvent = new EventEmitter<TransactionStripped>();
 | 
			
		||||
  @Output() txHoverEvent = new EventEmitter<string>();
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockCanvas')
 | 
			
		||||
@ -37,6 +39,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  scene: BlockScene;
 | 
			
		||||
  hoverTx: TxView | void;
 | 
			
		||||
  selectedTx: TxView | void;
 | 
			
		||||
  mirrorTx: TxView | void;
 | 
			
		||||
  tooltipPosition: Position;
 | 
			
		||||
 | 
			
		||||
  readyNextFrame = false;
 | 
			
		||||
@ -63,6 +66,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
        this.scene.setOrientation(this.orientation, this.flip);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (changes.mirrorTxid) {
 | 
			
		||||
      this.setMirror(this.mirrorTxid);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
@ -76,6 +82,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    this.exit(direction);
 | 
			
		||||
    this.hoverTx = null;
 | 
			
		||||
    this.selectedTx = null;
 | 
			
		||||
    this.onTxHover(null);
 | 
			
		||||
    this.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -181,7 +188,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
      this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
 | 
			
		||||
      this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
 | 
			
		||||
      this.start();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
 | 
			
		||||
@ -301,6 +308,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
      }
 | 
			
		||||
      this.hoverTx = null;
 | 
			
		||||
      this.selectedTx = null;
 | 
			
		||||
      this.onTxHover(null);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -352,17 +360,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
            this.selectedTx = selected;
 | 
			
		||||
          } else {
 | 
			
		||||
            this.hoverTx = selected;
 | 
			
		||||
            this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          if (clicked) {
 | 
			
		||||
            this.selectedTx = null;
 | 
			
		||||
          }
 | 
			
		||||
          this.hoverTx = null;
 | 
			
		||||
          this.onTxHover(null);
 | 
			
		||||
        }
 | 
			
		||||
      } else if (clicked) {
 | 
			
		||||
        if (selected === this.selectedTx) {
 | 
			
		||||
          this.hoverTx = this.selectedTx;
 | 
			
		||||
          this.selectedTx = null;
 | 
			
		||||
          this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.selectedTx = selected;
 | 
			
		||||
        }
 | 
			
		||||
@ -370,6 +381,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setMirror(txid: string | void) {
 | 
			
		||||
    if (this.mirrorTx) {
 | 
			
		||||
      this.scene.setHover(this.mirrorTx, false);
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
    if (txid && this.scene.txs[txid]) {
 | 
			
		||||
      this.mirrorTx = this.scene.txs[txid];
 | 
			
		||||
      this.scene.setHover(this.mirrorTx, true);
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxClick(cssX: number, cssY: number) {
 | 
			
		||||
    const x = cssX * window.devicePixelRatio;
 | 
			
		||||
    const y = cssY * window.devicePixelRatio;
 | 
			
		||||
@ -378,6 +401,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
      this.txClickEvent.emit(selected);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTxHover(hoverId: string) {
 | 
			
		||||
    this.txHoverEvent.emit(hoverId);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebGL shader attributes
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ export default class BlockScene {
 | 
			
		||||
    this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
 | 
			
		||||
  resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
    this.gridSize = this.width / this.gridWidth;
 | 
			
		||||
@ -38,7 +38,7 @@ export default class BlockScene {
 | 
			
		||||
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
    if (this.initialised && this.scene) {
 | 
			
		||||
      this.updateAll(performance.now(), 50);
 | 
			
		||||
      this.updateAll(performance.now(), 50, 'left', animate);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -212,7 +212,7 @@ export default class BlockScene {
 | 
			
		||||
    this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
 | 
			
		||||
    this.gridWidth = resolution;
 | 
			
		||||
    this.gridHeight = resolution;
 | 
			
		||||
    this.resize({ width, height });
 | 
			
		||||
    this.resize({ width, height, animate: true });
 | 
			
		||||
    this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
 | 
			
		||||
 | 
			
		||||
    this.txs = {};
 | 
			
		||||
@ -225,14 +225,14 @@ export default class BlockScene {
 | 
			
		||||
    this.animateUntil = Math.max(this.animateUntil, tx.update(update));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left'): void {
 | 
			
		||||
  private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left', animate: boolean = true): void {
 | 
			
		||||
    if (tx.dirty || this.dirty) {
 | 
			
		||||
      this.saveGridToScreenPosition(tx);
 | 
			
		||||
      this.setTxOnScreen(tx, startTime, delay, direction);
 | 
			
		||||
      this.setTxOnScreen(tx, startTime, delay, direction, animate);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left'): void {
 | 
			
		||||
  private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
 | 
			
		||||
    if (!tx.initialised) {
 | 
			
		||||
      const txColor = tx.getColor();
 | 
			
		||||
      this.applyTxUpdate(tx, {
 | 
			
		||||
@ -252,30 +252,42 @@ export default class BlockScene {
 | 
			
		||||
          position: tx.screenPosition,
 | 
			
		||||
          color: txColor
 | 
			
		||||
        },
 | 
			
		||||
        duration: 1000,
 | 
			
		||||
        duration: animate ? 1000 : 1,
 | 
			
		||||
        start: startTime,
 | 
			
		||||
        delay,
 | 
			
		||||
        delay: animate ? delay : 0,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      this.applyTxUpdate(tx, {
 | 
			
		||||
        display: {
 | 
			
		||||
          position: tx.screenPosition
 | 
			
		||||
        },
 | 
			
		||||
        duration: 1000,
 | 
			
		||||
        minDuration: 500,
 | 
			
		||||
        duration: animate ? 1000 : 0,
 | 
			
		||||
        minDuration: animate ? 500 : 0,
 | 
			
		||||
        start: startTime,
 | 
			
		||||
        delay,
 | 
			
		||||
        adjust: true
 | 
			
		||||
        delay: animate ? delay : 0,
 | 
			
		||||
        adjust: animate
 | 
			
		||||
      });
 | 
			
		||||
      if (!animate) {
 | 
			
		||||
        this.applyTxUpdate(tx, {
 | 
			
		||||
          display: {
 | 
			
		||||
            position: tx.screenPosition
 | 
			
		||||
          },
 | 
			
		||||
          duration: 0,
 | 
			
		||||
          minDuration: 0,
 | 
			
		||||
          start: startTime,
 | 
			
		||||
          delay: 0,
 | 
			
		||||
          adjust: false
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateAll(startTime: number, delay: number = 50, direction: string = 'left'): void {
 | 
			
		||||
  private updateAll(startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
 | 
			
		||||
    this.scene.count = 0;
 | 
			
		||||
    const ids = this.getTxList();
 | 
			
		||||
    startTime = startTime || performance.now();
 | 
			
		||||
    for (const id of ids) {
 | 
			
		||||
      this.updateTx(this.txs[id], startTime, delay, direction);
 | 
			
		||||
      this.updateTx(this.txs[id], startTime, delay, direction, animate);
 | 
			
		||||
    }
 | 
			
		||||
    this.dirty = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,8 @@ const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
 | 
			
		||||
const auditColors = {
 | 
			
		||||
  censored: hexToColor('f344df'),
 | 
			
		||||
  missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
 | 
			
		||||
  added: hexToColor('03E1E5'),
 | 
			
		||||
  selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7),
 | 
			
		||||
  added: hexToColor('0099ff'),
 | 
			
		||||
  selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// convert from this class's update format to TxSprite's update format
 | 
			
		||||
 | 
			
		||||
@ -37,9 +37,9 @@
 | 
			
		||||
        <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.missing">missing</td>
 | 
			
		||||
          <td *ngSwitchCase="'missing'" i18n="transaction.audit.omitted">omitted</td>
 | 
			
		||||
          <td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
 | 
			
		||||
          <td *ngSwitchCase="'selected'" i18n="transaction.audit.included">included</td>
 | 
			
		||||
          <td *ngSwitchCase="'selected'" i18n="transaction.audit.extra">extra</td>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
 | 
			
		||||
@ -114,7 +114,7 @@
 | 
			
		||||
                  <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>
 | 
			
		||||
                    <span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ 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 { Transaction, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler, EMPTY } from 'rxjs';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
@ -60,6 +60,8 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  nextBlockTxListSubscription: Subscription = undefined;
 | 
			
		||||
  timeLtrSubscription: Subscription;
 | 
			
		||||
  timeLtr: boolean;
 | 
			
		||||
  fetchAuditScore$ = new Subject<string>();
 | 
			
		||||
  fetchAuditScoreSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
			
		||||
 | 
			
		||||
@ -105,12 +107,30 @@ 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') || '';
 | 
			
		||||
@ -209,6 +229,9 @@ 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;
 | 
			
		||||
@ -311,6 +334,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.networkChangedSubscription.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription.unsubscribe();
 | 
			
		||||
    this.timeLtrSubscription.unsubscribe();
 | 
			
		||||
    this.fetchAuditScoreSubscription?.unsubscribe();
 | 
			
		||||
    this.unsubscribeNextBlockSubscriptions();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -46,22 +46,17 @@
 | 
			
		||||
            <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 *ngIf="block.extras?.matchRate != null" class="clear-link" [routerLink]="['/block-audit/' | relativeUrl, block.id]">
 | 
			
		||||
            <a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null">
 | 
			
		||||
              <div class="progress progress-health">
 | 
			
		||||
                <div class="progress-bar progress-bar-health" role="progressbar"
 | 
			
		||||
                  [ngStyle]="{'width': (100 - (block.extras?.matchRate || 0)) + '%' }"></div>
 | 
			
		||||
                  [ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
 | 
			
		||||
                <div class="progress-text">
 | 
			
		||||
                  <span>{{ block.extras.matchRate }}%</span>
 | 
			
		||||
                  <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>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a>
 | 
			
		||||
            <div *ngIf="block.extras?.matchRate == null" class="progress progress-health">
 | 
			
		||||
              <div class="progress-bar progress-bar-health" role="progressbar"
 | 
			
		||||
                [ngStyle]="{'width': '100%' }"></div>
 | 
			
		||||
              <div class="progress-text">
 | 
			
		||||
                <span>~</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
			
		||||
            <app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
 | 
			
		||||
@ -196,6 +196,10 @@ tr, td, th {
 | 
			
		||||
  @media (max-width: 950px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .progress-text .skeleton-loader {
 | 
			
		||||
    top: -8.5px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.health.widget {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
			
		||||
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
 | 
			
		||||
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
			
		||||
import { BehaviorSubject, combineLatest, concat, Observable, timer, EMPTY, Subscription, of } from 'rxjs';
 | 
			
		||||
import { catchError, delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
@ -12,10 +12,14 @@ import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
  styleUrls: ['./blocks-list.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class BlocksList implements OnInit {
 | 
			
		||||
export class BlocksList implements OnInit, OnDestroy {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
 | 
			
		||||
  blocks$: Observable<BlockExtended[]> = undefined;
 | 
			
		||||
  auditScores: { [hash: string]: number | void } = {};
 | 
			
		||||
 | 
			
		||||
  auditScoreSubscription: Subscription;
 | 
			
		||||
  latestScoreSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  indexingAvailable = false;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
@ -105,6 +109,53 @@ export class BlocksList implements OnInit {
 | 
			
		||||
          return acc;
 | 
			
		||||
        }, [])
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    if (this.indexingAvailable) {
 | 
			
		||||
      this.auditScoreSubscription = this.fromHeightSubject.pipe(
 | 
			
		||||
        switchMap((fromBlockHeight) => {
 | 
			
		||||
          return this.apiService.getBlockAuditScores$(this.page === 1 ? undefined : fromBlockHeight)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              catchError(() => {
 | 
			
		||||
                return EMPTY;
 | 
			
		||||
              })
 | 
			
		||||
            );
 | 
			
		||||
        })
 | 
			
		||||
      ).subscribe((scores) => {
 | 
			
		||||
        Object.values(scores).forEach(score => {
 | 
			
		||||
          this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.latestScoreSubscription = this.stateService.blocks$.pipe(
 | 
			
		||||
        switchMap((block) => {
 | 
			
		||||
          if (block[0]?.extras?.matchRate != null) {
 | 
			
		||||
            return of({
 | 
			
		||||
              hash: block[0].id,
 | 
			
		||||
              matchRate: block[0]?.extras?.matchRate,
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          else if (block[0]?.id && this.auditScores[block[0].id] === undefined) {
 | 
			
		||||
            return this.apiService.getBlockAuditScore$(block[0].id)
 | 
			
		||||
              .pipe(
 | 
			
		||||
                catchError(() => {
 | 
			
		||||
                  return EMPTY;
 | 
			
		||||
                })
 | 
			
		||||
              );
 | 
			
		||||
          } else {
 | 
			
		||||
            return EMPTY;
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
      ).subscribe((score) => {
 | 
			
		||||
        if (score && score.hash) {
 | 
			
		||||
          this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.auditScoreSubscription?.unsubscribe();
 | 
			
		||||
    this.latestScoreSubscription?.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pageChange(page: number) {
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,7 @@
 | 
			
		||||
  <div class="d-flex">
 | 
			
		||||
    <div class="search-box-container mr-2">
 | 
			
		||||
      <input (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
 | 
			
		||||
      
 | 
			
		||||
      <app-search-results #searchResults [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
 | 
			
		||||
    
 | 
			
		||||
      <app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>
 | 
			
		||||
      <button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary">
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { AssetsService } from '../../services/assets.service';
 | 
			
		||||
@ -23,6 +23,16 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
  isTypeaheading$ = new BehaviorSubject<boolean>(false);
 | 
			
		||||
  typeAhead$: Observable<any>;
 | 
			
		||||
  searchForm: FormGroup;
 | 
			
		||||
  dropdownHidden = false;
 | 
			
		||||
 | 
			
		||||
  @HostListener('document:click', ['$event'])
 | 
			
		||||
  onDocumentClick(event) {
 | 
			
		||||
    if (this.elementRef.nativeElement.contains(event.target)) {
 | 
			
		||||
      this.dropdownHidden = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.dropdownHidden = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
 | 
			
		||||
  regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
 | 
			
		||||
@ -45,6 +55,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private elementRef: ElementRef,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
 | 
			
		||||
@ -126,9 +126,13 @@ export class LiquidUnblinding {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkUnblindedTx(tx: Transaction) {
 | 
			
		||||
    const windowLocationHash = window.location.hash.substring('#blinded='.length);
 | 
			
		||||
    if (windowLocationHash.length > 0) {
 | 
			
		||||
      const blinders = this.parseBlinders(windowLocationHash);
 | 
			
		||||
    if (!window.location.hash?.length) {
 | 
			
		||||
      return tx;
 | 
			
		||||
    }
 | 
			
		||||
    const fragmentParams = new URLSearchParams(window.location.hash.slice(1) || '');
 | 
			
		||||
    const blinderStr = fragmentParams.get('blinded');
 | 
			
		||||
    if (blinderStr && blinderStr.length) {
 | 
			
		||||
      const blinders = this.parseBlinders(blinderStr);
 | 
			
		||||
      if (blinders) {
 | 
			
		||||
        this.commitments = await this.makeCommitmentMap(blinders);
 | 
			
		||||
        return this.tryUnblindTx(tx);
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <div class="row graph-wrapper">
 | 
			
		||||
    <tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph>
 | 
			
		||||
    <tx-bowtie-graph [tx]="tx" [width]="1132" [height]="346" [network]="network"></tx-bowtie-graph>
 | 
			
		||||
    <div class="above-bow">
 | 
			
		||||
      <p class="field pair">
 | 
			
		||||
        <span [innerHTML]="'‎' + (tx.size | bytes: 2)"></span>
 | 
			
		||||
 | 
			
		||||
@ -69,7 +69,7 @@
 | 
			
		||||
.graph-wrapper {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  background: #181b2d;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
  padding-bottom: 0;
 | 
			
		||||
 | 
			
		||||
  .above-bow {
 | 
			
		||||
 | 
			
		||||
@ -117,8 +117,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }),
 | 
			
		||||
        switchMap(() => {
 | 
			
		||||
          let transactionObservable$: Observable<Transaction>;
 | 
			
		||||
          if (history.state.data && history.state.data.fee !== -1) {
 | 
			
		||||
            transactionObservable$ = of(history.state.data);
 | 
			
		||||
          const cached = this.stateService.getTxFromCache(this.txId);
 | 
			
		||||
          if (cached && cached.fee !== -1) {
 | 
			
		||||
            transactionObservable$ = of(cached);
 | 
			
		||||
          } else {
 | 
			
		||||
            transactionObservable$ = this.electrsApiService
 | 
			
		||||
              .getTransaction$(this.txId)
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
  <div class="title-block">
 | 
			
		||||
    <div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
 | 
			
		||||
      <span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
 | 
			
		||||
      <a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction.size ? rbfTransaction : null }">
 | 
			
		||||
      <a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]">
 | 
			
		||||
        <span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
 | 
			
		||||
        <span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
@ -209,6 +209,7 @@
 | 
			
		||||
            [maxStrands]="graphExpanded ? maxInOut : 24"
 | 
			
		||||
            [network]="network"
 | 
			
		||||
            [tooltip]="true"
 | 
			
		||||
            [connectors]="true"
 | 
			
		||||
            [inputIndex]="inputIndex" [outputIndex]="outputIndex"
 | 
			
		||||
          >
 | 
			
		||||
          </tx-bowtie-graph>
 | 
			
		||||
 | 
			
		||||
@ -86,7 +86,7 @@
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  background: #181b2d;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
  padding-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -183,8 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
        }),
 | 
			
		||||
        switchMap(() => {
 | 
			
		||||
          let transactionObservable$: Observable<Transaction>;
 | 
			
		||||
          if (history.state.data && history.state.data.fee !== -1) {
 | 
			
		||||
            transactionObservable$ = of(history.state.data);
 | 
			
		||||
          const cached = this.stateService.getTxFromCache(this.txId);
 | 
			
		||||
          if (cached && cached.fee !== -1) {
 | 
			
		||||
            transactionObservable$ = of(cached);
 | 
			
		||||
          } else {
 | 
			
		||||
            transactionObservable$ = this.electrsApiService
 | 
			
		||||
              .getTransaction$(this.txId)
 | 
			
		||||
@ -279,6 +280,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
        this.waitingForTransaction = false;
 | 
			
		||||
      }
 | 
			
		||||
      this.rbfTransaction = rbfTransaction;
 | 
			
		||||
      this.stateService.setTxCache([this.rbfTransaction]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
 | 
			
		||||
@ -402,7 +404,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
  setGraphSize(): void {
 | 
			
		||||
    if (this.graphContainer) {
 | 
			
		||||
      this.graphWidth = this.graphContainer.nativeElement.clientWidth - 24;
 | 
			
		||||
      this.graphWidth = this.graphContainer.nativeElement.clientWidth;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
 | 
			
		||||
  <div *ngIf="!transactionPage" class="header-bg box tx-page-container">
 | 
			
		||||
    <a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]" [state]="{ data: tx }">
 | 
			
		||||
    <a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]">
 | 
			
		||||
      <span style="float: left;" class="d-block d-md-none">{{ tx.txid | shortenString : 16 }}</span>
 | 
			
		||||
      <span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
 | 
			
		||||
    </a>
 | 
			
		||||
 | 
			
		||||
@ -119,7 +119,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.transactionsLength = this.transactions.length;
 | 
			
		||||
 | 
			
		||||
      this.stateService.setTxCache(this.transactions);
 | 
			
		||||
 | 
			
		||||
      this.transactions.forEach((tx) => {
 | 
			
		||||
        tx['@voutLimit'] = true;
 | 
			
		||||
 | 
			
		||||
@ -22,13 +22,13 @@
 | 
			
		||||
 | 
			
		||||
  <ng-template #pegin>
 | 
			
		||||
    <ng-container *ngIf="line.pegin; else pegout">
 | 
			
		||||
      <p>Peg In</p>
 | 
			
		||||
      <p *ngIf="!isConnector">Peg In</p>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <ng-template #pegout>
 | 
			
		||||
    <ng-container *ngIf="line.pegout; else normal">
 | 
			
		||||
      <p>Peg Out</p>
 | 
			
		||||
      <p *ngIf="!isConnector">Peg Out</p>
 | 
			
		||||
      <p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
 | 
			
		||||
      <p class="address">
 | 
			
		||||
        <span class="first">{{ line.pegout.slice(0, -4) }}</span>
 | 
			
		||||
@ -38,7 +38,7 @@
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <ng-template #normal>
 | 
			
		||||
      <p>
 | 
			
		||||
      <p *ngIf="!isConnector">
 | 
			
		||||
        <ng-container [ngSwitch]="line.type">
 | 
			
		||||
          <span *ngSwitchCase="'input'" i18n="transaction.input">Input</span>
 | 
			
		||||
          <span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
 | 
			
		||||
@ -46,6 +46,17 @@
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
 | 
			
		||||
      </p>
 | 
			
		||||
      <ng-container *ngIf="isConnector && line.txid">
 | 
			
		||||
        <p>
 | 
			
		||||
          <span i18n="transaction">Transaction</span> 
 | 
			
		||||
          <span class="first">{{ line.txid.slice(0, 8) }}</span>...
 | 
			
		||||
          <span class="last-four">{{ line.txid.slice(-4) }}</span>
 | 
			
		||||
        </p>
 | 
			
		||||
          <ng-container [ngSwitch]="line.type">
 | 
			
		||||
            <p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>  #{{ line.vout + 1 }}</p>
 | 
			
		||||
            <p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>  #{{ line.vin + 1 }}</p>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
      <p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
 | 
			
		||||
      <p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
 | 
			
		||||
      <p *ngIf="line.type !== 'fee' && line.address" class="address">
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,9 @@ interface Xput {
 | 
			
		||||
  type: 'input' | 'output' | 'fee';
 | 
			
		||||
  value?: number;
 | 
			
		||||
  index?: number;
 | 
			
		||||
  txid?: string;
 | 
			
		||||
  vin?: number;
 | 
			
		||||
  vout?: number;
 | 
			
		||||
  address?: string;
 | 
			
		||||
  rest?: number;
 | 
			
		||||
  coinbase?: boolean;
 | 
			
		||||
@ -21,6 +24,7 @@ interface Xput {
 | 
			
		||||
export class TxBowtieGraphTooltipComponent implements OnChanges {
 | 
			
		||||
  @Input() line: Xput | void;
 | 
			
		||||
  @Input() cursorPosition: { x: number, y: number };
 | 
			
		||||
  @Input() isConnector: boolean = false;
 | 
			
		||||
 | 
			
		||||
  tooltipPosition = { x: 0, y: 0 };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,14 @@
 | 
			
		||||
        <stop offset="0%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="input-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="0%" [attr.stop-color]="gradient[2]" />
 | 
			
		||||
        <stop offset="80%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="output-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="20%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[2]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="input-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
      <stop offset="0%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      <stop offset="2%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
@ -41,6 +49,14 @@
 | 
			
		||||
        <stop offset="98%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="input-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="0%" stop-color="white" />
 | 
			
		||||
        <stop offset="80%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="output-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="20%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="100%" stop-color="white" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="input-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
      <stop offset="0%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      <stop offset="2%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
@ -65,6 +81,22 @@
 | 
			
		||||
    </defs>
 | 
			
		||||
    <path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
 | 
			
		||||
    <ng-container *ngFor="let input of inputs; let i = index">
 | 
			
		||||
      <path *ngIf="connectors && !inputData[i].coinbase && !inputData[i].pegin"
 | 
			
		||||
        [attr.d]="input.connectorPath"
 | 
			
		||||
        class="input connector {{input.class}}"
 | 
			
		||||
        [class.highlight]="inputData[i].index === inputIndex"
 | 
			
		||||
        (pointerover)="onHover($event, 'input-connector', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'input-connector', i);"
 | 
			
		||||
        (click)="onClick($event, 'input-connector', inputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="input.markerPath"
 | 
			
		||||
        class="input marker-target {{input.class}}"
 | 
			
		||||
        [class.highlight]="inputData[i].index === inputIndex"
 | 
			
		||||
        (pointerover)="onHover($event, 'input', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'input', i);"
 | 
			
		||||
        (click)="onClick($event, 'input', inputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="input.path"
 | 
			
		||||
        class="line {{input.class}}"
 | 
			
		||||
@ -77,6 +109,22 @@
 | 
			
		||||
      />
 | 
			
		||||
    </ng-container>
 | 
			
		||||
    <ng-container *ngFor="let output of outputs; let i = index">
 | 
			
		||||
      <path *ngIf="connectors && outspends[outputData[i].index]?.spent"
 | 
			
		||||
        [attr.d]="output.connectorPath"
 | 
			
		||||
        class="output connector {{output.class}}"
 | 
			
		||||
        [class.highlight]="outputData[i].index === outputIndex"
 | 
			
		||||
        (pointerover)="onHover($event, 'output-connector', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'output-connector', i);"
 | 
			
		||||
        (click)="onClick($event, 'output-connector', outputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="output.markerPath"
 | 
			
		||||
        class="output marker-target {{output.class}}"
 | 
			
		||||
        [class.highlight]="outputData[i].index === outputIndex"
 | 
			
		||||
        (pointerover)="onHover($event, 'output', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'output', i);"
 | 
			
		||||
        (click)="onClick($event, 'output', outputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="output.path"
 | 
			
		||||
        class="line {{output.class}}"
 | 
			
		||||
@ -94,5 +142,6 @@
 | 
			
		||||
    *ngIf=[tooltip]
 | 
			
		||||
    [line]="hoverLine"
 | 
			
		||||
    [cursorPosition]="tooltipPosition"
 | 
			
		||||
    [isConnector]="hoverConnector"
 | 
			
		||||
  ></app-tx-bowtie-graph-tooltip>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -22,19 +22,46 @@
 | 
			
		||||
        stroke: url(#output-highlight-gradient);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      z-index: 10;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      &.input {
 | 
			
		||||
        stroke: url(#input-hover-gradient);
 | 
			
		||||
      }
 | 
			
		||||
      &.output {
 | 
			
		||||
        stroke: url(#output-hover-gradient);
 | 
			
		||||
      }
 | 
			
		||||
      &.fee {
 | 
			
		||||
        stroke: url(#fee-hover-gradient);
 | 
			
		||||
      }
 | 
			
		||||
  .line:hover, .marker-target:hover + .line {
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    &.input {
 | 
			
		||||
      stroke: url(#input-hover-gradient);
 | 
			
		||||
    }
 | 
			
		||||
    &.output {
 | 
			
		||||
      stroke: url(#output-hover-gradient);
 | 
			
		||||
    }
 | 
			
		||||
    &.fee {
 | 
			
		||||
      stroke: url(#fee-hover-gradient);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  .connector {
 | 
			
		||||
    stroke: none;
 | 
			
		||||
    opacity: 0.75;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    &.input {
 | 
			
		||||
      fill: url(#input-connector-gradient);
 | 
			
		||||
    }
 | 
			
		||||
    &.output {
 | 
			
		||||
      fill: url(#output-connector-gradient);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .connector:hover {
 | 
			
		||||
    &.input {
 | 
			
		||||
      fill: url(#input-hover-connector-gradient);
 | 
			
		||||
    }
 | 
			
		||||
    &.output {
 | 
			
		||||
      fill: url(#output-hover-connector-gradient);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .marker-target {
 | 
			
		||||
    stroke: none;
 | 
			
		||||
    fill: transparent;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -11,12 +11,17 @@ interface SvgLine {
 | 
			
		||||
  path: string;
 | 
			
		||||
  style: string;
 | 
			
		||||
  class?: string;
 | 
			
		||||
  connectorPath?: string;
 | 
			
		||||
  markerPath?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Xput {
 | 
			
		||||
  type: 'input' | 'output' | 'fee';
 | 
			
		||||
  value?: number;
 | 
			
		||||
  index?: number;
 | 
			
		||||
  txid?: string;
 | 
			
		||||
  vin?: number;
 | 
			
		||||
  vout?: number;
 | 
			
		||||
  address?: string;
 | 
			
		||||
  rest?: number;
 | 
			
		||||
  coinbase?: boolean;
 | 
			
		||||
@ -40,6 +45,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() minWeight = 2; //
 | 
			
		||||
  @Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
 | 
			
		||||
  @Input() tooltip = false;
 | 
			
		||||
  @Input() connectors = false;
 | 
			
		||||
  @Input() inputIndex: number;
 | 
			
		||||
  @Input() outputIndex: number;
 | 
			
		||||
 | 
			
		||||
@ -49,9 +55,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  outputs: SvgLine[];
 | 
			
		||||
  middle: SvgLine;
 | 
			
		||||
  midWidth: number;
 | 
			
		||||
  txWidth: number;
 | 
			
		||||
  connectorWidth: number;
 | 
			
		||||
  combinedWeight: number;
 | 
			
		||||
  isLiquid: boolean = false;
 | 
			
		||||
  hoverLine: Xput | void = null;
 | 
			
		||||
  hoverConnector: boolean = false;
 | 
			
		||||
  tooltipPosition = { x: 0, y: 0 };
 | 
			
		||||
  outspends: Outspend[] = [];
 | 
			
		||||
 | 
			
		||||
@ -59,16 +68,16 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
 | 
			
		||||
 | 
			
		||||
  gradientColors = {
 | 
			
		||||
    '': ['#9339f4', '#105fb0'],
 | 
			
		||||
    bisq: ['#9339f4', '#105fb0'],
 | 
			
		||||
    '': ['#9339f4', '#105fb0', '#9339f400'],
 | 
			
		||||
    bisq: ['#9339f4', '#105fb0', '#9339f400'],
 | 
			
		||||
    // liquid: ['#116761', '#183550'],
 | 
			
		||||
    liquid: ['#09a197', '#0f62af'],
 | 
			
		||||
    liquid: ['#09a197', '#0f62af', '#09a19700'],
 | 
			
		||||
    // 'liquidtestnet': ['#494a4a', '#272e46'],
 | 
			
		||||
    'liquidtestnet': ['#d2d2d2', '#979797'],
 | 
			
		||||
    'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'],
 | 
			
		||||
    // testnet: ['#1d486f', '#183550'],
 | 
			
		||||
    testnet: ['#4edf77', '#10a0af'],
 | 
			
		||||
    testnet: ['#4edf77', '#10a0af', '#4edf7700'],
 | 
			
		||||
    // signet: ['#6f1d5d', '#471850'],
 | 
			
		||||
    signet: ['#d24fc8', '#a84fd2'],
 | 
			
		||||
    signet: ['#d24fc8', '#a84fd2', '#d24fc800'],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  gradient: string[] = ['#105fb0', '#105fb0'];
 | 
			
		||||
@ -118,7 +127,9 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
    this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
 | 
			
		||||
    this.gradient = this.gradientColors[this.network];
 | 
			
		||||
    this.midWidth = Math.min(10, Math.ceil(this.width / 100));
 | 
			
		||||
    this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6));
 | 
			
		||||
    this.txWidth = this.connectors ? Math.max(this.width - 200, this.width * 0.8) : this.width - 20;
 | 
			
		||||
    this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.txWidth - (2 * this.midWidth)) / 6));
 | 
			
		||||
    this.connectorWidth = (this.width - this.txWidth) / 2;
 | 
			
		||||
 | 
			
		||||
    const totalValue = this.calcTotalValue(this.tx);
 | 
			
		||||
    let voutWithFee = this.tx.vout.map((v, i) => {
 | 
			
		||||
@ -141,6 +152,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'input',
 | 
			
		||||
        value: v?.prevout?.value,
 | 
			
		||||
        txid: v.txid,
 | 
			
		||||
        vout: v.vout,
 | 
			
		||||
        address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(),
 | 
			
		||||
        index: i,
 | 
			
		||||
        coinbase: v?.is_coinbase,
 | 
			
		||||
@ -268,7 +281,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
      // required to prevent this line overlapping its neighbor
 | 
			
		||||
 | 
			
		||||
      if (this.tooltip || !xputs[i].rest) {
 | 
			
		||||
        const w = (this.width - Math.max(lastWeight, line.weight)) / 2; // approximate horizontal width of the curved section of the line
 | 
			
		||||
        const w = (this.width - Math.max(lastWeight, line.weight) - (2 * this.connectorWidth)) / 2; // approximate horizontal width of the curved section of the line
 | 
			
		||||
        const y1 = line.outerY;
 | 
			
		||||
        const y2 = line.innerY;
 | 
			
		||||
        const t = (lastWeight + line.weight) / 2; // distance between center of this line and center of previous line
 | 
			
		||||
@ -308,13 +321,15 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
      return {
 | 
			
		||||
        path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset),
 | 
			
		||||
        style: this.makeStyle(line.thickness, xputs[i].type),
 | 
			
		||||
        class: xputs[i].type
 | 
			
		||||
        class: xputs[i].type,
 | 
			
		||||
        connectorPath: this.connectors ? this.makeConnectorPath(side, line.outerY, line.innerY, line.thickness): null,
 | 
			
		||||
        markerPath: this.makeMarkerPath(side, line.outerY, line.innerY, line.thickness),
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  makePath(side: 'in' | 'out', outer: number, inner: number, weight: number, offset: number, pad: number): string {
 | 
			
		||||
    const start = (weight * 0.5);
 | 
			
		||||
    const start = (weight * 0.5) + this.connectorWidth;
 | 
			
		||||
    const curveStart = Math.max(start + 1, pad - offset);
 | 
			
		||||
    const end =  this.width / 2 - (this.midWidth * 0.9) + 1;
 | 
			
		||||
    const curveEnd = end - offset - 10;
 | 
			
		||||
@ -332,6 +347,40 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  makeConnectorPath(side: 'in' | 'out', y: number, inner, weight: number): string {
 | 
			
		||||
    const halfWidth = weight * 0.5;
 | 
			
		||||
    const offset = 10; //Math.max(2, halfWidth * 0.2);
 | 
			
		||||
    const lineEnd = this.connectorWidth;
 | 
			
		||||
 | 
			
		||||
    // align with for svg horizontal gradient bug correction
 | 
			
		||||
    if (Math.round(y) === Math.round(inner)) {
 | 
			
		||||
      y -= 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (side === 'in') {
 | 
			
		||||
      return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L -${10} ${ y + halfWidth} L -${10} ${y - halfWidth}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width + 10} ${ y + halfWidth} L ${this.width + 10} ${y - halfWidth}`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  makeMarkerPath(side: 'in' | 'out', y: number, inner, weight: number): string {
 | 
			
		||||
    const halfWidth = weight * 0.5;
 | 
			
		||||
    const offset = 10; //Math.max(2, halfWidth * 0.2);
 | 
			
		||||
    const lineEnd = this.connectorWidth;
 | 
			
		||||
 | 
			
		||||
    // align with for svg horizontal gradient bug correction
 | 
			
		||||
    if (Math.round(y) === Math.round(inner)) {
 | 
			
		||||
      y -= 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (side === 'in') {
 | 
			
		||||
      return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L ${weight + lineEnd} ${ y + halfWidth} L ${weight + lineEnd} ${y - halfWidth}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width - halfWidth - lineEnd} ${ y + halfWidth} L ${this.width - halfWidth - lineEnd} ${y - halfWidth}`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  makeStyle(minWeight, type): string {
 | 
			
		||||
    if (type === 'fee') {
 | 
			
		||||
      return `stroke-width: ${minWeight}`;
 | 
			
		||||
@ -346,26 +395,31 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onHover(event, side, index): void {
 | 
			
		||||
    if (side === 'input') {
 | 
			
		||||
    if (side.startsWith('input')) {
 | 
			
		||||
      this.hoverLine = {
 | 
			
		||||
        ...this.inputData[index],
 | 
			
		||||
        index
 | 
			
		||||
      };
 | 
			
		||||
      this.hoverConnector = (side === 'input-connector');
 | 
			
		||||
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hoverLine = {
 | 
			
		||||
        ...this.outputData[index]
 | 
			
		||||
        ...this.outputData[index],
 | 
			
		||||
        ...this.outspends[this.outputData[index].index]
 | 
			
		||||
      };
 | 
			
		||||
      this.hoverConnector = (side === 'output-connector');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onBlur(event, side, index): void {
 | 
			
		||||
    this.hoverLine = null;
 | 
			
		||||
    this.hoverConnector = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onClick(event, side, index): void {
 | 
			
		||||
    if (side === 'input') {
 | 
			
		||||
    if (side.startsWith('input')) {
 | 
			
		||||
      const input = this.tx.vin[index];
 | 
			
		||||
      if (input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
 | 
			
		||||
      if (side === 'input-connector' && input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
 | 
			
		||||
        this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], {
 | 
			
		||||
          queryParamsHandling: 'merge',
 | 
			
		||||
          fragment: (new URLSearchParams({
 | 
			
		||||
@ -385,7 +439,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
    } else {
 | 
			
		||||
      const output = this.tx.vout[index];
 | 
			
		||||
      const outspend = this.outspends[index];
 | 
			
		||||
      if (output && outspend && outspend.spent && outspend.txid) {
 | 
			
		||||
      if (side === 'output-connector' && output && outspend && outspend.spent && outspend.txid) {
 | 
			
		||||
        this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
 | 
			
		||||
          queryParamsHandling: 'merge',
 | 
			
		||||
          fragment: (new URLSearchParams({
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,11 @@
 | 
			
		||||
 | 
			
		||||
      <div class="doc-content">
 | 
			
		||||
 | 
			
		||||
        <div id="disclaimer">
 | 
			
		||||
          <table><tr><td><svg viewBox="0 0 304 304" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd" style="fill:#ffc107;fill-opacity:1"><path d="M135.3 34.474c-15.62 27.306-54.206 95.63-85.21 150.534L9.075 257.583C5.382 264.08 6.76 269.217 7.908 271.7c2.326 5.028 7.29 7.537 11.155 8.215l.78.133 264.698.006-.554-.02c4.152.255 9.664-1.24 12.677-6.194 1.926-3.18 3.31-8.589-1.073-16.278L213.637 114.37l-45.351-79.205c-5.681-9.932-12.272-12.022-16.8-12.022-4.42 0-10.818 1.964-16.181 11.331h-.006zm-69.072 159.94c30.997-54.885 69.563-123.184 85.16-150.446l.186-.297c.2.303.393.582.618.981l45.363 79.22s72.377 126.47 78.569 137.283l-247.618-.007 37.719-66.734" style="fill:#ffc107;fill-opacity:1"/><path d="M152.597 247.445c8.02 0 14.518-6.728 14.518-15.025 0-8.29-6.499-15.018-14.518-15.018-8.031 0-14.529 6.728-14.529 15.018 0 8.297 6.498 15.025 14.53 15.025m-.001-147.18c11.586 0 22.23 10.958 20.977 21.7l-9.922 75.564c-.966 6.601-4.95 11.433-11.055 11.433s-10.102-4.832-11.056-11.433l-9.927-75.564c-1.26-10.742 9.39-21.7 20.983-21.7" style="fill:#ffc107;fill-opacity:1"/></g></svg></td><td><p><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="doc-item-container" *ngFor="let item of faq">
 | 
			
		||||
          <h3 *ngIf="item.type === 'category'">{{ item.title }}</h3>
 | 
			
		||||
          <div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}">
 | 
			
		||||
 | 
			
		||||
@ -219,6 +219,22 @@ h3 {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#disclaimer {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
  padding: 24px;
 | 
			
		||||
  margin: 24px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#disclaimer svg {
 | 
			
		||||
  width: 50px;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  margin-right: 32px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#disclaimer p:last-child {
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 992px) {
 | 
			
		||||
 | 
			
		||||
  h3 {
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@
 | 
			
		||||
        <pre><code [innerText]="wrapEsModule(code)"></code></pre>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li ngbNavItem *ngIf="showCodeExample[network] && network !== 'liquid' && network !== 'liquidtestnet'" role="presentation">
 | 
			
		||||
    <li ngbNavItem *ngIf="code.codeTemplate.python && network !== 'liquid' && network !== 'liquidtestnet'" role="presentation">
 | 
			
		||||
      <a ngbNavLink (click)="adjustContainerHeight( $event )" role="tab">Python</a>
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        <div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapEsModule(code)"></app-clipboard></div>
 | 
			
		||||
 | 
			
		||||
@ -152,6 +152,11 @@ export interface RewardStats {
 | 
			
		||||
  totalTx: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditScore {
 | 
			
		||||
  hash: string;
 | 
			
		||||
  matchRate?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITopNodesPerChannels {
 | 
			
		||||
  publicKey: string,
 | 
			
		||||
  alias: string,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								frontend/src/app/lightning/node/liquidity-ad.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/src/app/lightning/node/liquidity-ad.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
export interface ILiquidityAd {
 | 
			
		||||
  funding_weight: number;
 | 
			
		||||
  lease_fee_basis: number; // lease fee rate in parts-per-thousandth
 | 
			
		||||
  lease_fee_base_sat: number; // fixed lease fee in sats
 | 
			
		||||
  channel_fee_max_rate: number; // max routing fee rate in parts-per-thousandth
 | 
			
		||||
  channel_fee_max_base: number; // max routing base fee in milli-sats
 | 
			
		||||
  compact_lease?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function parseLiquidityAdHex(compact_lease: string): ILiquidityAd | false {
 | 
			
		||||
  if (!compact_lease || compact_lease.length < 20 || compact_lease.length > 28) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  try {
 | 
			
		||||
    const liquidityAd: ILiquidityAd = {
 | 
			
		||||
      funding_weight: parseInt(compact_lease.slice(0, 4), 16),
 | 
			
		||||
      lease_fee_basis: parseInt(compact_lease.slice(4, 8), 16),
 | 
			
		||||
      channel_fee_max_rate: parseInt(compact_lease.slice(8, 12), 16),
 | 
			
		||||
      lease_fee_base_sat: parseInt(compact_lease.slice(12, 20), 16),
 | 
			
		||||
      channel_fee_max_base: compact_lease.length > 20 ? parseInt(compact_lease.slice(20), 16) : 0,
 | 
			
		||||
    }
 | 
			
		||||
    if (Object.values(liquidityAd).reduce((valid: boolean, value: number): boolean => (valid && !isNaN(value) && value >= 0), true)) {
 | 
			
		||||
      liquidityAd.compact_lease = compact_lease;
 | 
			
		||||
      return liquidityAd;
 | 
			
		||||
    } else {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -52,6 +52,10 @@
 | 
			
		||||
                <span i18n="unknown">Unknown</span>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="(avgChannelDistance$ | async) as avgDistance;">
 | 
			
		||||
              <td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
 | 
			
		||||
              <td>{{ avgDistance | number : '1.0-0' }} <span class="symbol">km</span> <span class="separator">/</span> {{ kmToMiles(avgDistance) | number : '1.0-0' }} <span class="symbol">mi</span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -125,6 +129,93 @@
 | 
			
		||||
    <app-clipboard [button]="true" [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="hasDetails" [hidden]="!showDetails" id="details" class="details mt-3">
 | 
			
		||||
    <div class="box">
 | 
			
		||||
      <ng-template [ngIf]="liquidityAd">
 | 
			
		||||
        <div class="detail-section">
 | 
			
		||||
          <h5 class="mb-3" i18n="node.liquidity-ad">Liquidity ad</h5>
 | 
			
		||||
          <div class="row">
 | 
			
		||||
            <div class="col-md">
 | 
			
		||||
              <table class="table table-borderless table-striped">
 | 
			
		||||
                <tbody>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <td class="label" i18n="liquidity-ad.lease-fee-rate|Liquidity ad lease fee rate">Lease fee rate</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <span class="d-inline-block">
 | 
			
		||||
                        {{ liquidityAd.lease_fee_basis !== null ? ((liquidityAd.lease_fee_basis * 1000) | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ liquidityAd.lease_fee_basis !== null ? '(' + (liquidityAd.lease_fee_basis / 10 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
 | 
			
		||||
                      </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <td class="label" i18n="liquidity-ad.lease-base-fee">Lease base fee</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <app-sats [valueOverride]="liquidityAd.lease_fee_base_sat === null ? '- ' : undefined" [satoshis]="liquidityAd.lease_fee_base_sat"></app-sats>
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <td class="label" i18n="liquidity-ad.funding-weight">Funding weight</td>
 | 
			
		||||
                    <td [innerHTML]="'‎' + (liquidityAd.funding_weight | wuBytes: 2)"></td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </tbody>
 | 
			
		||||
              </table>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-md">
 | 
			
		||||
              <table class="table table-borderless table-striped">
 | 
			
		||||
                <tbody>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <td class="label" i18n="liquidity-ad.channel-fee-rate|Liquidity ad channel fee rate">Channel fee rate</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <span class="d-inline-block">
 | 
			
		||||
                        {{ liquidityAd.channel_fee_max_rate !== null ? ((liquidityAd.channel_fee_max_rate * 1000) | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ liquidityAd.channel_fee_max_rate !== null ? '(' + (liquidityAd.channel_fee_max_rate / 10 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
 | 
			
		||||
                      </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <td class="label" i18n="liquidity-ad.channel-base-fee">Channel base fee</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <span *ngIf="liquidityAd.channel_fee_max_base !== null">
 | 
			
		||||
                        {{ liquidityAd.channel_fee_max_base | amountShortener : 0 }}
 | 
			
		||||
                        <span class="symbol" i18n="shared.m-sats">mSats</span>
 | 
			
		||||
                      </span>
 | 
			
		||||
                      <span *ngIf="liquidityAd.channel_fee_max_base === null">
 | 
			
		||||
                        -
 | 
			
		||||
                      </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <td class="label" i18n="liquidity-ad.compact-lease">Compact lease</td>
 | 
			
		||||
                    <td class="compact-lease">{{ liquidityAd.compact_lease }}</td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </tbody>
 | 
			
		||||
              </table>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
      <ng-template [ngIf]="tlvRecords?.length">
 | 
			
		||||
        <div class="detail-section">
 | 
			
		||||
          <h5 class="mb-3" i18n="node.tlv.records">TLV extension records</h5>
 | 
			
		||||
          <div class="row">
 | 
			
		||||
            <div class="col">
 | 
			
		||||
              <table class="table table-borderless table-striped">
 | 
			
		||||
                <tbody>
 | 
			
		||||
                  <tr *ngFor="let recordItem of tlvRecords">
 | 
			
		||||
                    <td class="tlv-type">{{ recordItem.type }}</td>
 | 
			
		||||
                    <td class="tlv-payload">{{ recordItem.payload }}</td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </tbody>
 | 
			
		||||
              </table>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="hasDetails" class="text-right mt-3">
 | 
			
		||||
    <button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="node.details|Node Details">Details</button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="!error">
 | 
			
		||||
    <div class="row" *ngIf="node.as_number && node.active_channel_count">
 | 
			
		||||
      <div class="col-sm">
 | 
			
		||||
 | 
			
		||||
@ -72,3 +72,36 @@ app-fiat {
 | 
			
		||||
    height: 28px !important;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.details {
 | 
			
		||||
 | 
			
		||||
  .detail-section {
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tlv-type {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    color: #ffffff66;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tlv-payload {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
    white-space: normal;
 | 
			
		||||
    font-family: "Courier New", Courier, monospace;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .compact-lease {
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
    white-space: normal;
 | 
			
		||||
    font-family: "Courier New", Courier, monospace;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.separator {
 | 
			
		||||
  margin: 0 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,18 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { catchError, map, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
interface CustomRecord {
 | 
			
		||||
  type: string;
 | 
			
		||||
  payload: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-node',
 | 
			
		||||
@ -24,8 +32,16 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
  channelListLoading = false;
 | 
			
		||||
  clearnetSocketCount = 0;
 | 
			
		||||
  torSocketCount = 0;
 | 
			
		||||
  hasDetails = false;
 | 
			
		||||
  showDetails = false;
 | 
			
		||||
  liquidityAd: ILiquidityAd;
 | 
			
		||||
  tlvRecords: CustomRecord[];
 | 
			
		||||
  avgChannelDistance$: Observable<number | null>;
 | 
			
		||||
 | 
			
		||||
  kmToMiles = kmToMiles;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private lightningApiService: LightningApiService,
 | 
			
		||||
    private activatedRoute: ActivatedRoute,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
@ -36,6 +52,8 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          this.publicKey = params.get('public_key');
 | 
			
		||||
          this.tlvRecords = [];
 | 
			
		||||
          this.liquidityAd = null;
 | 
			
		||||
          return this.lightningApiService.getNode$(params.get('public_key'));
 | 
			
		||||
        }),
 | 
			
		||||
        map((node) => {
 | 
			
		||||
@ -79,6 +97,26 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
          return node;
 | 
			
		||||
        }),
 | 
			
		||||
        tap((node) => {
 | 
			
		||||
          this.hasDetails = Object.keys(node.custom_records).length > 0;
 | 
			
		||||
          for (const [type, payload] of Object.entries(node.custom_records)) {
 | 
			
		||||
            if (typeof payload !== 'string') {
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let parsed = false;
 | 
			
		||||
            if (type === '1') {
 | 
			
		||||
              const ad = parseLiquidityAdHex(payload);
 | 
			
		||||
              if (ad) {
 | 
			
		||||
                parsed = true;
 | 
			
		||||
                this.liquidityAd = ad;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            if (!parsed) {
 | 
			
		||||
              this.tlvRecords.push({ type, payload });
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        catchError(err => {
 | 
			
		||||
          this.error = err;
 | 
			
		||||
          return [{
 | 
			
		||||
@ -87,6 +125,30 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
          }];
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.avgChannelDistance$ = this.activatedRoute.paramMap
 | 
			
		||||
    .pipe(
 | 
			
		||||
      switchMap((params: ParamMap) => {
 | 
			
		||||
        return this.apiService.getChannelsGeo$(params.get('public_key'), 'nodepage');
 | 
			
		||||
      }),
 | 
			
		||||
      map((channelsGeo) => {
 | 
			
		||||
        if (channelsGeo?.length) {
 | 
			
		||||
          const totalDistance = channelsGeo.reduce((sum, chan) => {
 | 
			
		||||
            return sum + haversineDistance(chan[3], chan[2], chan[7], chan[6]);
 | 
			
		||||
          }, 0);
 | 
			
		||||
          return totalDistance / channelsGeo.length;
 | 
			
		||||
        } else {
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      catchError(() => {
 | 
			
		||||
        return null;
 | 
			
		||||
      })
 | 
			
		||||
    ) as Observable<number | null>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleShowDetails(): void {
 | 
			
		||||
    this.showDetails = !this.showDetails;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeSocket(index: number) {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { HttpClient, HttpParams } from '@angular/common/http';
 | 
			
		||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
 | 
			
		||||
  PoolStat, BlockExtended, TransactionStripped, RewardStats } from '../interfaces/node-api.interface';
 | 
			
		||||
  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { StateService } from './state.service';
 | 
			
		||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
			
		||||
@ -234,6 +234,19 @@ export class ApiService {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockAuditScores$(from: number): Observable<AuditScore[]> {
 | 
			
		||||
    return this.httpClient.get<AuditScore[]>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
 | 
			
		||||
      (from !== undefined ? `/${from}` : ``)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockAuditScore$(hash: string) : Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(
 | 
			
		||||
      this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/score/` + hash
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
 | 
			
		||||
    return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -112,6 +112,8 @@ export class StateService {
 | 
			
		||||
  timeLtr: BehaviorSubject<boolean>;
 | 
			
		||||
  hideFlow: BehaviorSubject<boolean>;
 | 
			
		||||
 | 
			
		||||
  txCache: { [txid: string]: Transaction } = {};
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(PLATFORM_ID) private platformId: any,
 | 
			
		||||
    @Inject(LOCALE_ID) private locale: string,
 | 
			
		||||
@ -265,4 +267,19 @@ export class StateService {
 | 
			
		||||
  isLiquid() {
 | 
			
		||||
    return this.network === 'liquid' || this.network === 'liquidtestnet';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setTxCache(transactions) {
 | 
			
		||||
    this.txCache = {};
 | 
			
		||||
    transactions.forEach(tx => {
 | 
			
		||||
      this.txCache[tx.txid] = tx;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
  getTxFromCache(txid) {
 | 
			
		||||
    if (this.txCache && this.txCache[txid]) {
 | 
			
		||||
      return this.txCache[txid];
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -118,3 +118,21 @@ export function convertRegion(input, to: 'name' | 'abbreviated'): string {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
 | 
			
		||||
  const rlat1 = lat1 * Math.PI / 180;
 | 
			
		||||
  const rlon1 = lon1 * Math.PI / 180;
 | 
			
		||||
  const rlat2 = lat2 * Math.PI / 180;
 | 
			
		||||
  const rlon2 = lon2 * Math.PI / 180;
 | 
			
		||||
 | 
			
		||||
  const dlat = Math.sin((rlat2 - rlat1) / 2);
 | 
			
		||||
  const dlon = Math.sin((rlon2 - rlon1) / 2);
 | 
			
		||||
  const a = Math.min(1, Math.max(0, (dlat * dlat) + (Math.cos(rlat1) * Math.cos(rlat2) * dlon * dlon)));
 | 
			
		||||
  const d = 2 * 6371 * Math.asin(Math.sqrt(a));
 | 
			
		||||
 | 
			
		||||
  return d;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function kmToMiles(km: number): number {
 | 
			
		||||
  return km * 0.62137119;
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <title>mempool - Bisq Markets</title>
 | 
			
		||||
  <script src="/resources/config.js"></script>
 | 
			
		||||
  <base href="/">
 | 
			
		||||
 | 
			
		||||
  <meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
 | 
			
		||||
@ -31,11 +33,13 @@
 | 
			
		||||
  <link rel="manifest" href="/resources/bisq/favicons/site.webmanifest">
 | 
			
		||||
  <link rel="mask-icon" href="/resources/bisq/favicons/safari-pinned-tab.svg" color="#5bbad5">
 | 
			
		||||
  <link rel="shortcut icon" href="/resources/bisq/favicons/favicon.ico">
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
  <link id="canonical" rel="canonical" href="https://bisq.markets">
 | 
			
		||||
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
  <app-root></app-root>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <title>mempool - Liquid Network</title>
 | 
			
		||||
  <script src="/resources/config.js"></script>
 | 
			
		||||
  <base href="/">
 | 
			
		||||
 | 
			
		||||
  <meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
 | 
			
		||||
@ -17,7 +19,7 @@
 | 
			
		||||
  <meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
 | 
			
		||||
  <meta property="twitter:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
 | 
			
		||||
  <meta property="twitter:domain" content="liquid.network">
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  <link rel="apple-touch-icon" sizes="180x180" href="/resources/liquid/favicons/apple-touch-icon.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="48x48" href="/resources/liquid/favicons/favicon-48x48.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="32x32" href="/resources/liquid/favicons/favicon-32x32.png">
 | 
			
		||||
@ -33,7 +35,9 @@
 | 
			
		||||
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
  <app-root></app-root>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <title>mempool - Bitcoin Explorer</title>
 | 
			
		||||
  <script src="/resources/config.js"></script>
 | 
			
		||||
  <base href="/">
 | 
			
		||||
 | 
			
		||||
  <meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem." />
 | 
			
		||||
@ -17,7 +19,7 @@
 | 
			
		||||
  <meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
 | 
			
		||||
  <meta property="twitter:image:src" content="https://mempool.space/resources/mempool-space-preview.png" />
 | 
			
		||||
  <meta property="twitter:domain" content="mempool.space">
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  <link rel="apple-touch-icon" sizes="180x180" href="/resources/favicons/apple-touch-icon.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="32x32" href="/resources/favicons/favicon-32x32.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="16x16" href="/resources/favicons/favicon-16x16.png">
 | 
			
		||||
@ -32,7 +34,9 @@
 | 
			
		||||
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
  <app-root></app-root>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,13 @@
 | 
			
		||||
		try_files $uri @index-redirect;
 | 
			
		||||
		expires 1h;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	# only cache /resources/config.* for 5 minutes since it changes often
 | 
			
		||||
	location /resources/config. {
 | 
			
		||||
		try_files $uri =404;
 | 
			
		||||
		expires 5m;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	location @index-redirect {
 | 
			
		||||
		rewrite (.*) /$lang/index.html;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8993,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "NETWORK": "signet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8991,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "NETWORK": "testnet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8992,
 | 
			
		||||
 | 
			
		||||
@ -81,6 +81,13 @@ location /resources {
 | 
			
		||||
	try_files $uri /en-US/index.html;
 | 
			
		||||
	expires 1w;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# only cache /resources/config.* for 5 minutes since it changes often
 | 
			
		||||
location /resources/config. {
 | 
			
		||||
	try_files $uri =404;
 | 
			
		||||
	expires 5m;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# cache /main.f40e91d908a068a2.js forever since they never change
 | 
			
		||||
location ~* ^/.+\..+\.(js|css) {
 | 
			
		||||
	try_files /$lang/$uri /en-US/$uri =404;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user