Merge branch 'master' into nymkappa/feature/rename-mining-pool
This commit is contained in:
		
						commit
						5da8f2b6dc
					
				
							
								
								
									
										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
 | 
					        run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Checkout project
 | 
					      - name: Checkout project
 | 
				
			||||||
        uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
 | 
					        uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Init repo for Dockerization
 | 
					      - name: Init repo for Dockerization
 | 
				
			||||||
        run: docker/init.sh "$TAG"
 | 
					        run: docker/init.sh "$TAG"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
        uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1
 | 
					        uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0
 | 
				
			||||||
        id: qemu
 | 
					        id: qemu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Docker buildx action
 | 
					      - name: Setup Docker buildx action
 | 
				
			||||||
        uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 # v1
 | 
					        uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2.2.1
 | 
				
			||||||
        id: buildx
 | 
					        id: buildx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Available platforms
 | 
					      - name: Available platforms
 | 
				
			||||||
        run: echo ${{ steps.buildx.outputs.platforms }}
 | 
					        run: echo ${{ steps.buildx.outputs.platforms }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Cache Docker layers
 | 
					      - name: Cache Docker layers
 | 
				
			||||||
        uses: actions/cache@661fd3eb7f2f20d8c7c84bc2b0509efd7a826628 # v2
 | 
					        uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11
 | 
				
			||||||
        id: cache
 | 
					        id: cache
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          path: /tmp/.buildx-cache
 | 
					          path: /tmp/.buildx-cache
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -3,3 +3,5 @@ data
 | 
				
			|||||||
docker-compose.yml
 | 
					docker-compose.yml
 | 
				
			||||||
backend/mempool-config.json
 | 
					backend/mempool-config.json
 | 
				
			||||||
*.swp
 | 
					*.swp
 | 
				
			||||||
 | 
					frontend/src/resources/config.template.js
 | 
				
			||||||
 | 
					frontend/src/resources/config.js
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
  "MEMPOOL": {
 | 
					  "MEMPOOL": {
 | 
				
			||||||
    "NETWORK": "mainnet",
 | 
					    "NETWORK": "mainnet",
 | 
				
			||||||
    "BACKEND": "electrum",
 | 
					    "BACKEND": "electrum",
 | 
				
			||||||
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "HTTP_PORT": 8999,
 | 
					    "HTTP_PORT": 8999,
 | 
				
			||||||
    "SPAWN_CLUSTER_PROCS": 0,
 | 
					    "SPAWN_CLUSTER_PROCS": 0,
 | 
				
			||||||
    "API_URL_PREFIX": "/api/v1/",
 | 
					    "API_URL_PREFIX": "/api/v1/",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,9 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "MEMPOOL": {
 | 
					  "MEMPOOL": {
 | 
				
			||||||
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "NETWORK": "__MEMPOOL_NETWORK__",
 | 
					    "NETWORK": "__MEMPOOL_NETWORK__",
 | 
				
			||||||
    "BACKEND": "__MEMPOOL_BACKEND__",
 | 
					    "BACKEND": "__MEMPOOL_BACKEND__",
 | 
				
			||||||
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "BLOCKS_SUMMARIES_INDEXING": true,
 | 
					    "BLOCKS_SUMMARIES_INDEXING": true,
 | 
				
			||||||
    "HTTP_PORT": 1,
 | 
					    "HTTP_PORT": 1,
 | 
				
			||||||
    "SPAWN_CLUSTER_PROCS": 2,
 | 
					    "SPAWN_CLUSTER_PROCS": 2,
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ describe('Mempool Backend Config', () => {
 | 
				
			|||||||
      const config = jest.requireActual('../config').default;
 | 
					      const config = jest.requireActual('../config').default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(config.MEMPOOL).toStrictEqual({
 | 
					      expect(config.MEMPOOL).toStrictEqual({
 | 
				
			||||||
 | 
					        ENABLED: true,
 | 
				
			||||||
        NETWORK: 'mainnet',
 | 
					        NETWORK: 'mainnet',
 | 
				
			||||||
        BACKEND: 'none',
 | 
					        BACKEND: 'none',
 | 
				
			||||||
        BLOCKS_SUMMARIES_INDEXING: false,
 | 
					        BLOCKS_SUMMARIES_INDEXING: false,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,10 @@
 | 
				
			|||||||
import logger from '../logger';
 | 
					import config from '../config';
 | 
				
			||||||
import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
 | 
					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
 | 
					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);
 | 
					    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
 | 
					    // 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
 | 
					    // these displaced transactions should occupy the first N weight units of the next projected block
 | 
				
			||||||
    let displacedWeightRemaining = displacedWeight;
 | 
					    let displacedWeightRemaining = displacedWeight;
 | 
				
			||||||
@ -73,6 +76,7 @@ class Audit {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // mark unexpected transactions in the mined block as 'added'
 | 
					    // mark unexpected transactions in the mined block as 'added'
 | 
				
			||||||
    let overflowWeight = 0;
 | 
					    let overflowWeight = 0;
 | 
				
			||||||
 | 
					    let totalWeight = 0;
 | 
				
			||||||
    for (const tx of transactions) {
 | 
					    for (const tx of transactions) {
 | 
				
			||||||
      if (inTemplate[tx.txid]) {
 | 
					      if (inTemplate[tx.txid]) {
 | 
				
			||||||
        matches.push(tx.txid);
 | 
					        matches.push(tx.txid);
 | 
				
			||||||
@ -82,11 +86,13 @@ class Audit {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        overflowWeight += tx.weight;
 | 
					        overflowWeight += tx.weight;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      totalWeight += tx.weight;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // transactions missing from near the end of our template are probably not being censored
 | 
					    // transactions missing from near the end of our template are probably not being censored
 | 
				
			||||||
    let overflowWeightRemaining = overflowWeight;
 | 
					    let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
 | 
				
			||||||
    let lastOverflowRate = 1.00;
 | 
					    let maxOverflowRate = 0;
 | 
				
			||||||
 | 
					    let rateThreshold = 0;
 | 
				
			||||||
    index = projectedBlocks[0].transactionIds.length - 1;
 | 
					    index = projectedBlocks[0].transactionIds.length - 1;
 | 
				
			||||||
    while (index >= 0) {
 | 
					    while (index >= 0) {
 | 
				
			||||||
      const txid = projectedBlocks[0].transactionIds[index];
 | 
					      const txid = projectedBlocks[0].transactionIds[index];
 | 
				
			||||||
@ -94,8 +100,11 @@ class Audit {
 | 
				
			|||||||
        if (isCensored[txid]) {
 | 
					        if (isCensored[txid]) {
 | 
				
			||||||
          delete isCensored[txid];
 | 
					          delete isCensored[txid];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        lastOverflowRate = mempool[txid].effectiveFeePerVsize;
 | 
					        if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) {
 | 
				
			||||||
      } else if (Math.floor(mempool[txid].effectiveFeePerVsize * 100) <= Math.ceil(lastOverflowRate * 100)) { // tolerance of 0.01 sat/vb
 | 
					          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]) {
 | 
					        if (isCensored[txid]) {
 | 
				
			||||||
          delete isCensored[txid];
 | 
					          delete isCensored[txid];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -113,6 +122,45 @@ class Audit {
 | 
				
			|||||||
      score
 | 
					      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();
 | 
					export default new Audit();
 | 
				
			||||||
@ -130,7 +130,7 @@ class Blocks {
 | 
				
			|||||||
    const stripped = block.tx.map((tx) => {
 | 
					    const stripped = block.tx.map((tx) => {
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        txid: tx.txid,
 | 
					        txid: tx.txid,
 | 
				
			||||||
        vsize: tx.vsize,
 | 
					        vsize: tx.weight / 4,
 | 
				
			||||||
        fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
 | 
					        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)
 | 
					        value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000)
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
@ -195,9 +195,9 @@ class Blocks {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const auditSummary = await BlocksAuditsRepository.$getShortBlockAudit(block.id);
 | 
					      const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
 | 
				
			||||||
      if (auditSummary) {
 | 
					      if (auditScore != null) {
 | 
				
			||||||
        blockExtended.extras.matchRate = auditSummary.matchRate;
 | 
					        blockExtended.extras.matchRate = auditScore.matchRate;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import logger from '../logger';
 | 
				
			|||||||
import { Common } from './common';
 | 
					import { Common } from './common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DatabaseMigration {
 | 
					class DatabaseMigration {
 | 
				
			||||||
  private static currentVersion = 41;
 | 
					  private static currentVersion = 43;
 | 
				
			||||||
  private queryTimeout = 120000;
 | 
					  private queryTimeout = 120000;
 | 
				
			||||||
  private statisticsAddedIndexed = false;
 | 
					  private statisticsAddedIndexed = false;
 | 
				
			||||||
  private uniqueLogs: string[] = [];
 | 
					  private uniqueLogs: string[] = [];
 | 
				
			||||||
@ -352,6 +352,14 @@ class DatabaseMigration {
 | 
				
			|||||||
    if (databaseSchemaVersion < 41 && isBitcoin === true) {
 | 
					    if (databaseSchemaVersion < 41 && isBitcoin === true) {
 | 
				
			||||||
      await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
 | 
					      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'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -787,6 +795,19 @@ class DatabaseMigration {
 | 
				
			|||||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
					    ) 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[]) {
 | 
					  public async $truncateIndexedData(tables: string[]) {
 | 
				
			||||||
    const allowedTables = ['blocks', 'hashrates', 'prices'];
 | 
					    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[]> {
 | 
					  public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const query = `SELECT * FROM channels WHERE created IS NULL`;
 | 
					      const query = `SELECT * FROM channels WHERE created IS NULL`;
 | 
				
			||||||
 | 
				
			|||||||
@ -105,6 +105,18 @@ class NodesApi {
 | 
				
			|||||||
        node.closed_channel_count = rows[0].closed_channel_count;
 | 
					        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;
 | 
					      return node;
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : 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
 | 
					 * Convert a clightning "listnode" entry to a lnd node entry
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function convertNode(clNode: any): ILightningApi.Node {
 | 
					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 {
 | 
					  return {
 | 
				
			||||||
    alias: clNode.alias ?? '',
 | 
					    alias: clNode.alias ?? '',
 | 
				
			||||||
    color: `#${clNode.color ?? ''}`,
 | 
					    color: `#${clNode.color ?? ''}`,
 | 
				
			||||||
@ -23,6 +32,7 @@ export function convertNode(clNode: any): ILightningApi.Node {
 | 
				
			|||||||
      };
 | 
					      };
 | 
				
			||||||
    }) ?? [],
 | 
					    }) ?? [],
 | 
				
			||||||
    last_update: clNode?.last_timestamp ?? 0,
 | 
					    last_update: clNode?.last_timestamp ?? 0,
 | 
				
			||||||
 | 
					    custom_records
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -49,6 +49,7 @@ export namespace ILightningApi {
 | 
				
			|||||||
    }[];
 | 
					    }[];
 | 
				
			||||||
    color: string;
 | 
					    color: string;
 | 
				
			||||||
    features: { [key: number]: Feature };
 | 
					    features: { [key: number]: Feature };
 | 
				
			||||||
 | 
					    custom_records?: { [type: number]: string };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export interface Info {
 | 
					  export interface Info {
 | 
				
			||||||
 | 
				
			|||||||
@ -103,12 +103,11 @@ class Mempool {
 | 
				
			|||||||
    return txTimes;
 | 
					    return txTimes;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async $updateMempool() {
 | 
					  public async $updateMempool(): Promise<void> {
 | 
				
			||||||
    logger.debug('Updating mempool');
 | 
					    logger.debug(`Updating mempool...`);
 | 
				
			||||||
    const start = new Date().getTime();
 | 
					    const start = new Date().getTime();
 | 
				
			||||||
    let hasChange: boolean = false;
 | 
					    let hasChange: boolean = false;
 | 
				
			||||||
    const currentMempoolSize = Object.keys(this.mempoolCache).length;
 | 
					    const currentMempoolSize = Object.keys(this.mempoolCache).length;
 | 
				
			||||||
    let txCount = 0;
 | 
					 | 
				
			||||||
    const transactions = await bitcoinApi.$getRawMempool();
 | 
					    const transactions = await bitcoinApi.$getRawMempool();
 | 
				
			||||||
    const diff = transactions.length - currentMempoolSize;
 | 
					    const diff = transactions.length - currentMempoolSize;
 | 
				
			||||||
    const newTransactions: TransactionExtended[] = [];
 | 
					    const newTransactions: TransactionExtended[] = [];
 | 
				
			||||||
@ -124,7 +123,6 @@ class Mempool {
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
          const transaction = await transactionUtils.$getTransactionExtended(txid);
 | 
					          const transaction = await transactionUtils.$getTransactionExtended(txid);
 | 
				
			||||||
          this.mempoolCache[txid] = transaction;
 | 
					          this.mempoolCache[txid] = transaction;
 | 
				
			||||||
          txCount++;
 | 
					 | 
				
			||||||
          if (this.inSync) {
 | 
					          if (this.inSync) {
 | 
				
			||||||
            this.txPerSecondArray.push(new Date().getTime());
 | 
					            this.txPerSecondArray.push(new Date().getTime());
 | 
				
			||||||
            this.vBytesPerSecondArray.push({
 | 
					            this.vBytesPerSecondArray.push({
 | 
				
			||||||
@ -133,14 +131,9 @@ class Mempool {
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          hasChange = true;
 | 
					          hasChange = true;
 | 
				
			||||||
          if (diff > 0) {
 | 
					 | 
				
			||||||
            logger.debug('Fetched transaction ' + txCount + ' / ' + diff);
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            logger.debug('Fetched transaction ' + txCount);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          newTransactions.push(transaction);
 | 
					          newTransactions.push(transaction);
 | 
				
			||||||
        } catch (e) {
 | 
					        } 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));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -197,8 +190,7 @@ class Mempool {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const end = new Date().getTime();
 | 
					    const end = new Date().getTime();
 | 
				
			||||||
    const time = end - start;
 | 
					    const time = end - start;
 | 
				
			||||||
    logger.debug(`New mempool size: ${Object.keys(this.mempoolCache).length} Change: ${diff}`);
 | 
					    logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
 | 
				
			||||||
    logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
 | 
					  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import { Application, Request, Response } from 'express';
 | 
					import { Application, Request, Response } from 'express';
 | 
				
			||||||
import config from "../../config";
 | 
					import config from "../../config";
 | 
				
			||||||
import logger from '../../logger';
 | 
					import logger from '../../logger';
 | 
				
			||||||
 | 
					import audits from '../audit';
 | 
				
			||||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
 | 
					import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
 | 
				
			||||||
import BlocksRepository from '../../repositories/BlocksRepository';
 | 
					import BlocksRepository from '../../repositories/BlocksRepository';
 | 
				
			||||||
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
 | 
					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/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/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/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/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);
 | 
					      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();
 | 
					export default new MiningRoutes();
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ const configFromFile = require(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface IConfig {
 | 
					interface IConfig {
 | 
				
			||||||
  MEMPOOL: {
 | 
					  MEMPOOL: {
 | 
				
			||||||
 | 
					    ENABLED: boolean;
 | 
				
			||||||
    NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
 | 
					    NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
 | 
				
			||||||
    BACKEND: 'esplora' | 'electrum' | 'none';
 | 
					    BACKEND: 'esplora' | 'electrum' | 'none';
 | 
				
			||||||
    HTTP_PORT: number;
 | 
					    HTTP_PORT: number;
 | 
				
			||||||
@ -119,6 +120,7 @@ interface IConfig {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const defaults: IConfig = {
 | 
					const defaults: IConfig = {
 | 
				
			||||||
  'MEMPOOL': {
 | 
					  'MEMPOOL': {
 | 
				
			||||||
 | 
					    'ENABLED': true,
 | 
				
			||||||
    'NETWORK': 'mainnet',
 | 
					    'NETWORK': 'mainnet',
 | 
				
			||||||
    'BACKEND': 'none',
 | 
					    'BACKEND': 'none',
 | 
				
			||||||
    'HTTP_PORT': 8999,
 | 
					    'HTTP_PORT': 8999,
 | 
				
			||||||
@ -224,11 +226,11 @@ const defaults: IConfig = {
 | 
				
			|||||||
    'BISQ_URL': 'https://bisq.markets/api',
 | 
					    'BISQ_URL': 'https://bisq.markets/api',
 | 
				
			||||||
    'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
 | 
					    'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "MAXMIND": {
 | 
					  'MAXMIND': {
 | 
				
			||||||
    'ENABLED': false,
 | 
					    'ENABLED': false,
 | 
				
			||||||
    "GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
 | 
					    'GEOLITE2_CITY': '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
 | 
				
			||||||
    "GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
 | 
					    'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
 | 
				
			||||||
    "GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.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 { Application, Request, Response, NextFunction } from 'express';
 | 
				
			||||||
import * as http from 'http';
 | 
					import * as http from 'http';
 | 
				
			||||||
import * as WebSocket from 'ws';
 | 
					import * as WebSocket from 'ws';
 | 
				
			||||||
@ -34,7 +34,7 @@ import miningRoutes from './api/mining/mining-routes';
 | 
				
			|||||||
import bisqRoutes from './api/bisq/bisq.routes';
 | 
					import bisqRoutes from './api/bisq/bisq.routes';
 | 
				
			||||||
import liquidRoutes from './api/liquid/liquid.routes';
 | 
					import liquidRoutes from './api/liquid/liquid.routes';
 | 
				
			||||||
import bitcoinRoutes from './api/bitcoin/bitcoin.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 {
 | 
					class Server {
 | 
				
			||||||
  private wss: WebSocket.Server | undefined;
 | 
					  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()})`);
 | 
					    logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app
 | 
					    this.app
 | 
				
			||||||
@ -92,7 +92,9 @@ class Server {
 | 
				
			|||||||
    this.setUpWebsocketHandling();
 | 
					    this.setUpWebsocketHandling();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await syncAssets.syncAssets$();
 | 
					    await syncAssets.syncAssets$();
 | 
				
			||||||
    diskCache.loadMempoolCache();
 | 
					    if (config.MEMPOOL.ENABLED) {
 | 
				
			||||||
 | 
					      diskCache.loadMempoolCache();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.DATABASE.ENABLED) {
 | 
					    if (config.DATABASE.ENABLED) {
 | 
				
			||||||
      await DB.checkDbConnection();
 | 
					      await DB.checkDbConnection();
 | 
				
			||||||
@ -127,7 +129,10 @@ class Server {
 | 
				
			|||||||
    fiatConversion.startService();
 | 
					    fiatConversion.startService();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.setUpHttpApiRoutes();
 | 
					    this.setUpHttpApiRoutes();
 | 
				
			||||||
    this.runMainUpdateLoop();
 | 
					
 | 
				
			||||||
 | 
					    if (config.MEMPOOL.ENABLED) {
 | 
				
			||||||
 | 
					      this.runMainUpdateLoop();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.BISQ.ENABLED) {
 | 
					    if (config.BISQ.ENABLED) {
 | 
				
			||||||
      bisq.startBisqService();
 | 
					      bisq.startBisqService();
 | 
				
			||||||
@ -149,7 +154,7 @@ class Server {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async runMainUpdateLoop() {
 | 
					  async runMainUpdateLoop(): Promise<void> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        await memPool.$updateMemPoolInfo();
 | 
					        await memPool.$updateMemPoolInfo();
 | 
				
			||||||
@ -183,7 +188,7 @@ class Server {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $runLightningBackend() {
 | 
					  async $runLightningBackend(): Promise<void> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await fundingTxFetcher.$init();
 | 
					      await fundingTxFetcher.$init();
 | 
				
			||||||
      await networkSyncService.$startService();
 | 
					      await networkSyncService.$startService();
 | 
				
			||||||
@ -195,7 +200,7 @@ class Server {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setUpWebsocketHandling() {
 | 
					  setUpWebsocketHandling(): void {
 | 
				
			||||||
    if (this.wss) {
 | 
					    if (this.wss) {
 | 
				
			||||||
      websocketHandler.setWebsocketServer(this.wss);
 | 
					      websocketHandler.setWebsocketServer(this.wss);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -209,19 +214,21 @@ class Server {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    websocketHandler.setupConnectionHandling();
 | 
					    websocketHandler.setupConnectionHandling();
 | 
				
			||||||
    statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
 | 
					    if (config.MEMPOOL.ENABLED) {
 | 
				
			||||||
    blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
 | 
					      statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
 | 
				
			||||||
    memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
 | 
					      blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
 | 
				
			||||||
 | 
					      memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
 | 
					    fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
 | 
				
			||||||
    loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
 | 
					    loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  setUpHttpApiRoutes() {
 | 
					  setUpHttpApiRoutes(): void {
 | 
				
			||||||
    bitcoinRoutes.initRoutes(this.app);
 | 
					    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);
 | 
					      statisticsRoutes.initRoutes(this.app);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (Common.indexingEnabled()) {
 | 
					    if (Common.indexingEnabled() && config.MEMPOOL.ENABLED) {
 | 
				
			||||||
      miningRoutes.initRoutes(this.app);
 | 
					      miningRoutes.initRoutes(this.app);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (config.BISQ.ENABLED) {
 | 
					    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,
 | 
					  matchRate: number,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AuditScore {
 | 
				
			||||||
 | 
					  hash: string,
 | 
				
			||||||
 | 
					  matchRate?: number,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MempoolBlock {
 | 
					export interface MempoolBlock {
 | 
				
			||||||
  blockSize: number;
 | 
					  blockSize: number;
 | 
				
			||||||
  blockVSize: number;
 | 
					  blockVSize: number;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import DB from '../database';
 | 
					import DB from '../database';
 | 
				
			||||||
import logger from '../logger';
 | 
					import logger from '../logger';
 | 
				
			||||||
import { BlockAudit } from '../mempool.interfaces';
 | 
					import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BlocksAuditRepositories {
 | 
					class BlocksAuditRepositories {
 | 
				
			||||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
					  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 {
 | 
					    try {
 | 
				
			||||||
      const [rows]: any[] = await DB.query(
 | 
					      const [rows]: any[] = await DB.query(
 | 
				
			||||||
        `SELECT hash as id, match_rate as matchRate
 | 
					        `SELECT hash, match_rate as matchRate
 | 
				
			||||||
        FROM blocks_audits
 | 
					        FROM blocks_audits
 | 
				
			||||||
        WHERE blocks_audits.hash = "${hash}"
 | 
					        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
 | 
					   * 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 NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
 | 
				
			||||||
import { Common } from '../../api/common';
 | 
					import { Common } from '../../api/common';
 | 
				
			||||||
import blocks from '../../api/blocks';
 | 
					import blocks from '../../api/blocks';
 | 
				
			||||||
 | 
					import NodeRecordsRepository from '../../repositories/NodeRecordsRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NetworkSyncService {
 | 
					class NetworkSyncService {
 | 
				
			||||||
  loggerTimer = 0;
 | 
					  loggerTimer = 0;
 | 
				
			||||||
@ -63,6 +64,7 @@ class NetworkSyncService {
 | 
				
			|||||||
    let progress = 0;
 | 
					    let progress = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let deletedSockets = 0;
 | 
					    let deletedSockets = 0;
 | 
				
			||||||
 | 
					    let deletedRecords = 0;
 | 
				
			||||||
    const graphNodesPubkeys: string[] = [];
 | 
					    const graphNodesPubkeys: string[] = [];
 | 
				
			||||||
    for (const node of nodes) {
 | 
					    for (const node of nodes) {
 | 
				
			||||||
      const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
 | 
					      const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
 | 
				
			||||||
@ -84,8 +86,23 @@ class NetworkSyncService {
 | 
				
			|||||||
        addresses.push(socket.addr);
 | 
					        addresses.push(socket.addr);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
 | 
					      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
 | 
					    // If a channel if not present in the graph, mark it as inactive
 | 
				
			||||||
    await nodesApi.$setNodesInactive(graphNodesPubkeys);
 | 
					    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) {
 | 
					    if (!config.ESPLORA.REST_API_URL) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -318,9 +335,18 @@ class NetworkSyncService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      logger.info(`Started running closed channel forensics...`);
 | 
					      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) {
 | 
					      for (const channel of channels) {
 | 
				
			||||||
        let reason = 0;
 | 
					        let reason = 0;
 | 
				
			||||||
 | 
					        let resolvedForceClose = false;
 | 
				
			||||||
        // Only Esplora backend can retrieve spent transaction outputs
 | 
					        // Only Esplora backend can retrieve spent transaction outputs
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          let outspends: IEsploraApi.Outspend[] | undefined;
 | 
					          let outspends: IEsploraApi.Outspend[] | undefined;
 | 
				
			||||||
@ -350,6 +376,7 @@ class NetworkSyncService {
 | 
				
			|||||||
              reason = 3;
 | 
					              reason = 3;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
              reason = 2;
 | 
					              reason = 2;
 | 
				
			||||||
 | 
					              resolvedForceClose = true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            /*
 | 
					            /*
 | 
				
			||||||
@ -374,6 +401,9 @@ class NetworkSyncService {
 | 
				
			|||||||
          if (reason) {
 | 
					          if (reason) {
 | 
				
			||||||
            logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
 | 
					            logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
 | 
				
			||||||
            await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, 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) {
 | 
					        } catch (e) {
 | 
				
			||||||
          logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : 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": {
 | 
					  "MEMPOOL": {
 | 
				
			||||||
    "NETWORK": "mainnet",
 | 
					    "NETWORK": "mainnet",
 | 
				
			||||||
    "BACKEND": "electrum",
 | 
					    "BACKEND": "electrum",
 | 
				
			||||||
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "HTTP_PORT": 8999,
 | 
					    "HTTP_PORT": 8999,
 | 
				
			||||||
    "SPAWN_CLUSTER_PROCS": 0,
 | 
					    "SPAWN_CLUSTER_PROCS": 0,
 | 
				
			||||||
    "API_URL_PREFIX": "/api/v1/",
 | 
					    "API_URL_PREFIX": "/api/v1/",
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
  "MEMPOOL": {
 | 
					  "MEMPOOL": {
 | 
				
			||||||
    "NETWORK": "__MEMPOOL_NETWORK__",
 | 
					    "NETWORK": "__MEMPOOL_NETWORK__",
 | 
				
			||||||
    "BACKEND": "__MEMPOOL_BACKEND__",
 | 
					    "BACKEND": "__MEMPOOL_BACKEND__",
 | 
				
			||||||
 | 
					    "ENABLED": __MEMPOOL_ENABLED__,
 | 
				
			||||||
    "HTTP_PORT": __MEMPOOL_HTTP_PORT__,
 | 
					    "HTTP_PORT": __MEMPOOL_HTTP_PORT__,
 | 
				
			||||||
    "SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
 | 
					    "SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
 | 
				
			||||||
    "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
 | 
					    "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@
 | 
				
			|||||||
# MEMPOOL
 | 
					# MEMPOOL
 | 
				
			||||||
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
 | 
					__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
 | 
				
			||||||
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
 | 
					__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
 | 
				
			||||||
 | 
					__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
 | 
				
			||||||
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
 | 
					__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
 | 
				
			||||||
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
 | 
					__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
 | 
				
			||||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
 | 
					__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_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
 | 
				
			||||||
sed -i "s/__MEMPOOL_BACKEND__/${__MEMPOOL_BACKEND__}/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_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_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
 | 
					sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,9 @@ WORKDIR /build
 | 
				
			|||||||
COPY . .
 | 
					COPY . .
 | 
				
			||||||
RUN apt-get update
 | 
					RUN apt-get update
 | 
				
			||||||
RUN apt-get install -y build-essential rsync
 | 
					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 install --omit=dev --omit=optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN npm run build
 | 
					RUN npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM nginx:1.17.8-alpine
 | 
					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/cache/nginx && \
 | 
				
			||||||
        chown -R 1000:1000 /var/log/nginx && \
 | 
					        chown -R 1000:1000 /var/log/nginx && \
 | 
				
			||||||
        chown -R 1000:1000 /etc/nginx/nginx.conf && \
 | 
					        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 && \
 | 
					RUN touch /var/run/nginx.pid && \
 | 
				
			||||||
        chown -R 1000:1000 /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
 | 
					sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
 | 
				
			||||||
cat /patch/nginx.conf > /etc/nginx/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 "$@"
 | 
					exec "$@"
 | 
				
			||||||
 | 
				
			|||||||
@ -152,15 +152,14 @@
 | 
				
			|||||||
            "assets": [
 | 
					            "assets": [
 | 
				
			||||||
              "src/favicon.ico",
 | 
					              "src/favicon.ico",
 | 
				
			||||||
              "src/resources",
 | 
					              "src/resources",
 | 
				
			||||||
              "src/robots.txt"
 | 
					              "src/robots.txt",
 | 
				
			||||||
 | 
					              "src/config.js",
 | 
				
			||||||
 | 
					              "src/config.template.js"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "styles": [
 | 
					            "styles": [
 | 
				
			||||||
              "src/styles.scss",
 | 
					              "src/styles.scss",
 | 
				
			||||||
              "node_modules/@fortawesome/fontawesome-svg-core/styles.css"
 | 
					              "node_modules/@fortawesome/fontawesome-svg-core/styles.css"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "scripts": [
 | 
					 | 
				
			||||||
              "generated-config.js"
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            "vendorChunk": true,
 | 
					            "vendorChunk": true,
 | 
				
			||||||
            "extractLicenses": false,
 | 
					            "extractLicenses": false,
 | 
				
			||||||
            "buildOptimizer": false,
 | 
					            "buildOptimizer": false,
 | 
				
			||||||
@ -222,6 +221,10 @@
 | 
				
			|||||||
              "proxyConfig": "proxy.conf.local.js",
 | 
					              "proxyConfig": "proxy.conf.local.js",
 | 
				
			||||||
              "verbose": true
 | 
					              "verbose": true
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            "local-esplora": {
 | 
				
			||||||
 | 
					              "proxyConfig": "proxy.conf.local-esplora.js",
 | 
				
			||||||
 | 
					              "verbose": true
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            "mixed": {
 | 
					            "mixed": {
 | 
				
			||||||
              "proxyConfig": "proxy.conf.mixed.js",
 | 
					              "proxyConfig": "proxy.conf.mixed.js",
 | 
				
			||||||
              "verbose": true
 | 
					              "verbose": true
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,8 @@ var fs = require('fs');
 | 
				
			|||||||
const { spawnSync } = require('child_process');
 | 
					const { spawnSync } = require('child_process');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
 | 
					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 settings = [];
 | 
				
			||||||
let configContent = {};
 | 
					let configContent = {};
 | 
				
			||||||
@ -67,10 +68,17 @@ if (process.env.DOCKER_COMMIT_HASH) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const newConfig = `(function (window) {
 | 
					const newConfig = `(function (window) {
 | 
				
			||||||
  window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
 | 
					  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.GIT_COMMIT_HASH = '${gitCommitHash}';
 | 
				
			||||||
    window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
 | 
					    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) {
 | 
					function readConfig(path) {
 | 
				
			||||||
  try {
 | 
					  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);
 | 
					const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (currentConfig && currentConfig === newConfig) {
 | 
					if (currentConfig && currentConfig === newConfig) {
 | 
				
			||||||
@ -106,4 +124,4 @@ if (currentConfig && currentConfig === newConfig) {
 | 
				
			|||||||
  console.log('NEW CONFIG: ', newConfig);
 | 
					  console.log('NEW CONFIG: ', newConfig);
 | 
				
			||||||
  writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
 | 
					  writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
 | 
				
			||||||
  console.log(`${GENERATED_CONFIG_FILE_NAME} file updated`);
 | 
					  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-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",
 | 
					    "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": "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: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-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",
 | 
					    "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;
 | 
				
			||||||
@ -41,10 +41,6 @@
 | 
				
			|||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
              </tr>
 | 
					              </tr>
 | 
				
			||||||
              <tr>
 | 
					 | 
				
			||||||
                <td class="td-width" i18n="shared.transaction-count">Transactions</td>
 | 
					 | 
				
			||||||
                <td>{{ blockAudit.tx_count }}</td>
 | 
					 | 
				
			||||||
              </tr>
 | 
					 | 
				
			||||||
              <tr>
 | 
					              <tr>
 | 
				
			||||||
                <td i18n="blockAudit.size">Size</td>
 | 
					                <td i18n="blockAudit.size">Size</td>
 | 
				
			||||||
                <td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
 | 
					                <td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
 | 
				
			||||||
@ -61,6 +57,10 @@
 | 
				
			|||||||
        <div class="col-sm" *ngIf="blockAudit">
 | 
					        <div class="col-sm" *ngIf="blockAudit">
 | 
				
			||||||
          <table class="table table-borderless table-striped">
 | 
					          <table class="table table-borderless table-striped">
 | 
				
			||||||
            <tbody>
 | 
					            <tbody>
 | 
				
			||||||
 | 
					              <tr>
 | 
				
			||||||
 | 
					                <td class="td-width" i18n="shared.transaction-count">Transactions</td>
 | 
				
			||||||
 | 
					                <td>{{ blockAudit.tx_count }}</td>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
              <tr>
 | 
					              <tr>
 | 
				
			||||||
                <td i18n="block.health">Block health</td>
 | 
					                <td i18n="block.health">Block health</td>
 | 
				
			||||||
                <td>{{ blockAudit.matchRate }}%</td>
 | 
					                <td>{{ blockAudit.matchRate }}%</td>
 | 
				
			||||||
@ -69,18 +69,10 @@
 | 
				
			|||||||
                <td i18n="block.missing-txs">Removed txs</td>
 | 
					                <td i18n="block.missing-txs">Removed txs</td>
 | 
				
			||||||
                <td>{{ blockAudit.missingTxs.length }}</td>
 | 
					                <td>{{ blockAudit.missingTxs.length }}</td>
 | 
				
			||||||
              </tr>
 | 
					              </tr>
 | 
				
			||||||
              <tr>
 | 
					 | 
				
			||||||
                <td i18n="block.missing-txs">Omitted txs</td>
 | 
					 | 
				
			||||||
                <td>{{ numMissing }}</td>
 | 
					 | 
				
			||||||
              </tr>
 | 
					 | 
				
			||||||
              <tr>
 | 
					              <tr>
 | 
				
			||||||
                <td i18n="block.added-txs">Added txs</td>
 | 
					                <td i18n="block.added-txs">Added txs</td>
 | 
				
			||||||
                <td>{{ blockAudit.addedTxs.length }}</td>
 | 
					                <td>{{ blockAudit.addedTxs.length }}</td>
 | 
				
			||||||
              </tr>
 | 
					              </tr>
 | 
				
			||||||
              <tr>
 | 
					 | 
				
			||||||
                <td i18n="block.missing-txs">Included txs</td>
 | 
					 | 
				
			||||||
                <td>{{ numUnexpected }}</td>
 | 
					 | 
				
			||||||
              </tr>
 | 
					 | 
				
			||||||
            </tbody>
 | 
					            </tbody>
 | 
				
			||||||
          </table>
 | 
					          </table>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@ -97,21 +89,6 @@
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ng-template [ngIf]="!error && isLoading">
 | 
					  <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 -->
 | 
					    <!-- OVERVIEW -->
 | 
				
			||||||
    <div class="box mb-3">
 | 
					    <div class="box mb-3">
 | 
				
			||||||
      <div class="row">
 | 
					      <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>
 | 
				
			||||||
              <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>
 | 
					            </tbody>
 | 
				
			||||||
          </table>
 | 
					          </table>
 | 
				
			||||||
        </div>
 | 
					        </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>
 | 
				
			||||||
              <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>
 | 
					            </tbody>
 | 
				
			||||||
          </table>
 | 
					          </table>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@ -180,16 +155,16 @@
 | 
				
			|||||||
      <div class="col-sm" *ngIf="webGlEnabled">
 | 
					      <div class="col-sm" *ngIf="webGlEnabled">
 | 
				
			||||||
        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
 | 
					        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
 | 
				
			||||||
        <app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
 | 
					        <app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
 | 
				
			||||||
          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
 | 
					          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
 | 
				
			||||||
          (txClickEvent)="onTxClick($event)"></app-block-overview-graph>
 | 
					          (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- ADDED TX RENDERING -->
 | 
					      <!-- ADDED TX RENDERING -->
 | 
				
			||||||
      <div class="col-sm" *ngIf="webGlEnabled && !isMobile">
 | 
					      <div class="col-sm" *ngIf="webGlEnabled && !isMobile">
 | 
				
			||||||
        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
 | 
					        <h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
 | 
				
			||||||
        <app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
 | 
					        <app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
 | 
				
			||||||
          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
 | 
					          [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
 | 
				
			||||||
          (txClickEvent)="onTxClick($event)"></app-block-overview-graph>
 | 
					          (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div> <!-- row -->
 | 
					    </div> <!-- row -->
 | 
				
			||||||
  </div> <!-- box -->
 | 
					  </div> <!-- box -->
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
 | 
					import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
 | 
				
			||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
					import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
				
			||||||
import { Subscription, combineLatest } from 'rxjs';
 | 
					import { Subscription, combineLatest, of } from 'rxjs';
 | 
				
			||||||
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
 | 
					import { map, switchMap, startWith, catchError, filter } from 'rxjs/operators';
 | 
				
			||||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
					import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
 | 
					import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
				
			||||||
import { StateService } from '../../services/state.service';
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
import { detectWebGL } from '../../shared/graphs.utils';
 | 
					import { detectWebGL } from '../../shared/graphs.utils';
 | 
				
			||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
					import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
				
			||||||
@ -37,6 +38,7 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
  isLoading = true;
 | 
					  isLoading = true;
 | 
				
			||||||
  webGlEnabled = true;
 | 
					  webGlEnabled = true;
 | 
				
			||||||
  isMobile = window.innerWidth <= 767.98;
 | 
					  isMobile = window.innerWidth <= 767.98;
 | 
				
			||||||
 | 
					  hoverTx: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  childChangeSubscription: Subscription;
 | 
					  childChangeSubscription: Subscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -51,7 +53,8 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
    private route: ActivatedRoute,
 | 
					    private route: ActivatedRoute,
 | 
				
			||||||
    public stateService: StateService,
 | 
					    public stateService: StateService,
 | 
				
			||||||
    private router: Router,
 | 
					    private router: Router,
 | 
				
			||||||
    private apiService: ApiService
 | 
					    private apiService: ApiService,
 | 
				
			||||||
 | 
					    private electrsApiService: ElectrsApiService,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.webGlEnabled = detectWebGL();
 | 
					    this.webGlEnabled = detectWebGL();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -76,69 +79,95 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.auditSubscription = this.route.paramMap.pipe(
 | 
					    this.auditSubscription = this.route.paramMap.pipe(
 | 
				
			||||||
      switchMap((params: ParamMap) => {
 | 
					      switchMap((params: ParamMap) => {
 | 
				
			||||||
        this.blockHash = params.get('id') || null;
 | 
					        const blockHash = params.get('id') || null;
 | 
				
			||||||
        if (!this.blockHash) {
 | 
					        if (!blockHash) {
 | 
				
			||||||
          return null;
 | 
					          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)
 | 
					        return this.apiService.getBlockAudit$(this.blockHash)
 | 
				
			||||||
          .pipe(
 | 
					      }),
 | 
				
			||||||
            map((response) => {
 | 
					      filter((response) => response != null),
 | 
				
			||||||
              const blockAudit = response.body;
 | 
					      map((response) => {
 | 
				
			||||||
              const inTemplate = {};
 | 
					        const blockAudit = response.body;
 | 
				
			||||||
              const inBlock = {};
 | 
					        const inTemplate = {};
 | 
				
			||||||
              const isAdded = {};
 | 
					        const inBlock = {};
 | 
				
			||||||
              const isCensored = {};
 | 
					        const isAdded = {};
 | 
				
			||||||
              const isMissing = {};
 | 
					        const isCensored = {};
 | 
				
			||||||
              const isSelected = {};
 | 
					        const isMissing = {};
 | 
				
			||||||
              this.numMissing = 0;
 | 
					        const isSelected = {};
 | 
				
			||||||
              this.numUnexpected = 0;
 | 
					        this.numMissing = 0;
 | 
				
			||||||
              for (const tx of blockAudit.template) {
 | 
					        this.numUnexpected = 0;
 | 
				
			||||||
                inTemplate[tx.txid] = true;
 | 
					        for (const tx of blockAudit.template) {
 | 
				
			||||||
              }
 | 
					          inTemplate[tx.txid] = true;
 | 
				
			||||||
              for (const tx of blockAudit.transactions) {
 | 
					        }
 | 
				
			||||||
                inBlock[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.addedTxs) {
 | 
				
			||||||
              }
 | 
					          isAdded[txid] = true;
 | 
				
			||||||
              for (const txid of blockAudit.missingTxs) {
 | 
					        }
 | 
				
			||||||
                isCensored[txid] = true;
 | 
					        for (const txid of blockAudit.missingTxs) {
 | 
				
			||||||
              }
 | 
					          isCensored[txid] = true;
 | 
				
			||||||
              // set transaction statuses
 | 
					        }
 | 
				
			||||||
              for (const tx of blockAudit.template) {
 | 
					        // set transaction statuses
 | 
				
			||||||
                if (isCensored[tx.txid]) {
 | 
					        for (const tx of blockAudit.template) {
 | 
				
			||||||
                  tx.status = 'censored';
 | 
					          if (isCensored[tx.txid]) {
 | 
				
			||||||
                } else if (inBlock[tx.txid]) {
 | 
					            tx.status = 'censored';
 | 
				
			||||||
                  tx.status = 'found';
 | 
					          } else if (inBlock[tx.txid]) {
 | 
				
			||||||
                } else {
 | 
					            tx.status = 'found';
 | 
				
			||||||
                  tx.status = 'missing';
 | 
					          } else {
 | 
				
			||||||
                  isMissing[tx.txid] = true;
 | 
					            tx.status = 'missing';
 | 
				
			||||||
                  this.numMissing++;
 | 
					            isMissing[tx.txid] = true;
 | 
				
			||||||
                }
 | 
					            this.numMissing++;
 | 
				
			||||||
              }
 | 
					          }
 | 
				
			||||||
              for (const [index, tx] of blockAudit.transactions.entries()) {
 | 
					        }
 | 
				
			||||||
                if (isAdded[tx.txid]) {
 | 
					        for (const [index, tx] of blockAudit.transactions.entries()) {
 | 
				
			||||||
                  tx.status = 'added';
 | 
					          if (index === 0) {
 | 
				
			||||||
                } else if (index === 0 || inTemplate[tx.txid]) {
 | 
					            tx.status = null;
 | 
				
			||||||
                  tx.status = 'found';
 | 
					          } else if (isAdded[tx.txid]) {
 | 
				
			||||||
                } else {
 | 
					            tx.status = 'added';
 | 
				
			||||||
                  tx.status = 'selected';
 | 
					          } else if (inTemplate[tx.txid]) {
 | 
				
			||||||
                  isSelected[tx.txid] = true;
 | 
					            tx.status = 'found';
 | 
				
			||||||
                  this.numUnexpected++;
 | 
					          } else {
 | 
				
			||||||
                }
 | 
					            tx.status = 'selected';
 | 
				
			||||||
              }
 | 
					            isSelected[tx.txid] = true;
 | 
				
			||||||
              for (const tx of blockAudit.transactions) {
 | 
					            this.numUnexpected++;
 | 
				
			||||||
                inBlock[tx.txid] = true;
 | 
					          }
 | 
				
			||||||
              }
 | 
					        }
 | 
				
			||||||
              return blockAudit;
 | 
					        for (const tx of blockAudit.transactions) {
 | 
				
			||||||
            })
 | 
					          inBlock[tx.txid] = true;
 | 
				
			||||||
          );
 | 
					        }
 | 
				
			||||||
 | 
					        return blockAudit;
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
      catchError((err) => {
 | 
					      catchError((err) => {
 | 
				
			||||||
        console.log(err);
 | 
					        console.log(err);
 | 
				
			||||||
        this.error = err;
 | 
					        this.error = err;
 | 
				
			||||||
        this.isLoading = false;
 | 
					        this.isLoading = false;
 | 
				
			||||||
        return null;
 | 
					        return of(null);
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
    ).subscribe((blockAudit) => {
 | 
					    ).subscribe((blockAudit) => {
 | 
				
			||||||
      this.blockAudit = 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}`);
 | 
					    const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
 | 
				
			||||||
    this.router.navigate([url]);
 | 
					    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() orientation = 'left';
 | 
				
			||||||
  @Input() flip = true;
 | 
					  @Input() flip = true;
 | 
				
			||||||
  @Input() disableSpinner = false;
 | 
					  @Input() disableSpinner = false;
 | 
				
			||||||
 | 
					  @Input() mirrorTxid: string | void;
 | 
				
			||||||
  @Output() txClickEvent = new EventEmitter<TransactionStripped>();
 | 
					  @Output() txClickEvent = new EventEmitter<TransactionStripped>();
 | 
				
			||||||
 | 
					  @Output() txHoverEvent = new EventEmitter<string>();
 | 
				
			||||||
  @Output() readyEvent = new EventEmitter();
 | 
					  @Output() readyEvent = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ViewChild('blockCanvas')
 | 
					  @ViewChild('blockCanvas')
 | 
				
			||||||
@ -37,6 +39,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
  scene: BlockScene;
 | 
					  scene: BlockScene;
 | 
				
			||||||
  hoverTx: TxView | void;
 | 
					  hoverTx: TxView | void;
 | 
				
			||||||
  selectedTx: TxView | void;
 | 
					  selectedTx: TxView | void;
 | 
				
			||||||
 | 
					  mirrorTx: TxView | void;
 | 
				
			||||||
  tooltipPosition: Position;
 | 
					  tooltipPosition: Position;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  readyNextFrame = false;
 | 
					  readyNextFrame = false;
 | 
				
			||||||
@ -63,6 +66,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
        this.scene.setOrientation(this.orientation, this.flip);
 | 
					        this.scene.setOrientation(this.orientation, this.flip);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (changes.mirrorTxid) {
 | 
				
			||||||
 | 
					      this.setMirror(this.mirrorTxid);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnDestroy(): void {
 | 
					  ngOnDestroy(): void {
 | 
				
			||||||
@ -76,6 +82,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
    this.exit(direction);
 | 
					    this.exit(direction);
 | 
				
			||||||
    this.hoverTx = null;
 | 
					    this.hoverTx = null;
 | 
				
			||||||
    this.selectedTx = null;
 | 
					    this.selectedTx = null;
 | 
				
			||||||
 | 
					    this.onTxHover(null);
 | 
				
			||||||
    this.start();
 | 
					    this.start();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -301,6 +308,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      this.hoverTx = null;
 | 
					      this.hoverTx = null;
 | 
				
			||||||
      this.selectedTx = null;
 | 
					      this.selectedTx = null;
 | 
				
			||||||
 | 
					      this.onTxHover(null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -352,17 +360,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
            this.selectedTx = selected;
 | 
					            this.selectedTx = selected;
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            this.hoverTx = selected;
 | 
					            this.hoverTx = selected;
 | 
				
			||||||
 | 
					            this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          if (clicked) {
 | 
					          if (clicked) {
 | 
				
			||||||
            this.selectedTx = null;
 | 
					            this.selectedTx = null;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          this.hoverTx = null;
 | 
					          this.hoverTx = null;
 | 
				
			||||||
 | 
					          this.onTxHover(null);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else if (clicked) {
 | 
					      } else if (clicked) {
 | 
				
			||||||
        if (selected === this.selectedTx) {
 | 
					        if (selected === this.selectedTx) {
 | 
				
			||||||
          this.hoverTx = this.selectedTx;
 | 
					          this.hoverTx = this.selectedTx;
 | 
				
			||||||
          this.selectedTx = null;
 | 
					          this.selectedTx = null;
 | 
				
			||||||
 | 
					          this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          this.selectedTx = selected;
 | 
					          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) {
 | 
					  onTxClick(cssX: number, cssY: number) {
 | 
				
			||||||
    const x = cssX * window.devicePixelRatio;
 | 
					    const x = cssX * window.devicePixelRatio;
 | 
				
			||||||
    const y = cssY * window.devicePixelRatio;
 | 
					    const y = cssY * window.devicePixelRatio;
 | 
				
			||||||
@ -378,6 +401,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
      this.txClickEvent.emit(selected);
 | 
					      this.txClickEvent.emit(selected);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onTxHover(hoverId: string) {
 | 
				
			||||||
 | 
					    this.txHoverEvent.emit(hoverId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WebGL shader attributes
 | 
					// WebGL shader attributes
 | 
				
			||||||
 | 
				
			|||||||
@ -12,8 +12,8 @@ const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
 | 
				
			|||||||
const auditColors = {
 | 
					const auditColors = {
 | 
				
			||||||
  censored: hexToColor('f344df'),
 | 
					  censored: hexToColor('f344df'),
 | 
				
			||||||
  missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
 | 
					  missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
 | 
				
			||||||
  added: hexToColor('03E1E5'),
 | 
					  added: hexToColor('0099ff'),
 | 
				
			||||||
  selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7),
 | 
					  selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// convert from this class's update format to TxSprite's update format
 | 
					// convert from this class's update format to TxSprite's update format
 | 
				
			||||||
 | 
				
			|||||||
@ -37,9 +37,9 @@
 | 
				
			|||||||
        <ng-container [ngSwitch]="tx?.status">
 | 
					        <ng-container [ngSwitch]="tx?.status">
 | 
				
			||||||
          <td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
 | 
					          <td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
 | 
				
			||||||
          <td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</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="'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>
 | 
					        </ng-container>
 | 
				
			||||||
      </tr>
 | 
					      </tr>
 | 
				
			||||||
    </tbody>
 | 
					    </tbody>
 | 
				
			||||||
 | 
				
			|||||||
@ -114,7 +114,7 @@
 | 
				
			|||||||
                  <td i18n="block.health">Block health</td>
 | 
					                  <td i18n="block.health">Block health</td>
 | 
				
			||||||
                  <td>
 | 
					                  <td>
 | 
				
			||||||
                    <a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
 | 
					                    <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>
 | 
					                  </td>
 | 
				
			||||||
                </tr>
 | 
					                </tr>
 | 
				
			||||||
              </ng-template>
 | 
					              </ng-template>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
				
			|||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
					import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
				
			||||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
 | 
					import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
 | 
				
			||||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
 | 
					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 { StateService } from '../../services/state.service';
 | 
				
			||||||
import { SeoService } from '../../services/seo.service';
 | 
					import { SeoService } from '../../services/seo.service';
 | 
				
			||||||
import { WebsocketService } from '../../services/websocket.service';
 | 
					import { WebsocketService } from '../../services/websocket.service';
 | 
				
			||||||
@ -60,6 +60,8 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
  nextBlockTxListSubscription: Subscription = undefined;
 | 
					  nextBlockTxListSubscription: Subscription = undefined;
 | 
				
			||||||
  timeLtrSubscription: Subscription;
 | 
					  timeLtrSubscription: Subscription;
 | 
				
			||||||
  timeLtr: boolean;
 | 
					  timeLtr: boolean;
 | 
				
			||||||
 | 
					  fetchAuditScore$ = new Subject<string>();
 | 
				
			||||||
 | 
					  fetchAuditScoreSubscription: Subscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
					  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -105,12 +107,30 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (block.id === this.blockHash) {
 | 
					        if (block.id === this.blockHash) {
 | 
				
			||||||
          this.block = block;
 | 
					          this.block = block;
 | 
				
			||||||
 | 
					          if (this.block.id && this.block?.extras?.matchRate == null) {
 | 
				
			||||||
 | 
					            this.fetchAuditScore$.next(this.block.id);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          if (block?.extras?.reward != undefined) {
 | 
					          if (block?.extras?.reward != undefined) {
 | 
				
			||||||
            this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
					            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(
 | 
					    const block$ = this.route.paramMap.pipe(
 | 
				
			||||||
      switchMap((params: ParamMap) => {
 | 
					      switchMap((params: ParamMap) => {
 | 
				
			||||||
        const blockHash: string = params.get('id') || '';
 | 
					        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.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
 | 
					        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.isLoadingTransactions = true;
 | 
				
			||||||
        this.transactions = null;
 | 
					        this.transactions = null;
 | 
				
			||||||
        this.transactionsError = null;
 | 
					        this.transactionsError = null;
 | 
				
			||||||
@ -311,6 +334,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    this.networkChangedSubscription.unsubscribe();
 | 
					    this.networkChangedSubscription.unsubscribe();
 | 
				
			||||||
    this.queryParamsSubscription.unsubscribe();
 | 
					    this.queryParamsSubscription.unsubscribe();
 | 
				
			||||||
    this.timeLtrSubscription.unsubscribe();
 | 
					    this.timeLtrSubscription.unsubscribe();
 | 
				
			||||||
 | 
					    this.fetchAuditScoreSubscription?.unsubscribe();
 | 
				
			||||||
    this.unsubscribeNextBlockSubscriptions();
 | 
					    this.unsubscribeNextBlockSubscriptions();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -46,22 +46,17 @@
 | 
				
			|||||||
            <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
 | 
					            <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
          <td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
					          <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 progress-health">
 | 
				
			||||||
                <div class="progress-bar progress-bar-health" role="progressbar"
 | 
					                <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">
 | 
					                <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>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </a>
 | 
					            </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>
 | 
				
			||||||
          <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
 | 
					          <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>
 | 
					            <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) {
 | 
					  @media (max-width: 950px) {
 | 
				
			||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .progress-text .skeleton-loader {
 | 
				
			||||||
 | 
					    top: -8.5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.health.widget {
 | 
					.health.widget {
 | 
				
			||||||
  width: 25%;
 | 
					  width: 25%;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
					import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
				
			||||||
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
 | 
					import { BehaviorSubject, combineLatest, concat, Observable, timer, EMPTY, Subscription, of } from 'rxjs';
 | 
				
			||||||
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
 | 
					import { catchError, delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
 | 
				
			||||||
import { BlockExtended } from '../../interfaces/node-api.interface';
 | 
					import { BlockExtended } from '../../interfaces/node-api.interface';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
import { StateService } from '../../services/state.service';
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
@ -12,10 +12,14 @@ import { WebsocketService } from '../../services/websocket.service';
 | 
				
			|||||||
  styleUrls: ['./blocks-list.component.scss'],
 | 
					  styleUrls: ['./blocks-list.component.scss'],
 | 
				
			||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class BlocksList implements OnInit {
 | 
					export class BlocksList implements OnInit, OnDestroy {
 | 
				
			||||||
  @Input() widget: boolean = false;
 | 
					  @Input() widget: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  blocks$: Observable<BlockExtended[]> = undefined;
 | 
					  blocks$: Observable<BlockExtended[]> = undefined;
 | 
				
			||||||
 | 
					  auditScores: { [hash: string]: number | void } = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  auditScoreSubscription: Subscription;
 | 
				
			||||||
 | 
					  latestScoreSubscription: Subscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  indexingAvailable = false;
 | 
					  indexingAvailable = false;
 | 
				
			||||||
  isLoading = true;
 | 
					  isLoading = true;
 | 
				
			||||||
@ -105,6 +109,53 @@ export class BlocksList implements OnInit {
 | 
				
			|||||||
          return acc;
 | 
					          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) {
 | 
					  pageChange(page: number) {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,9 +2,7 @@
 | 
				
			|||||||
  <div class="d-flex">
 | 
					  <div class="d-flex">
 | 
				
			||||||
    <div class="search-box-container mr-2">
 | 
					    <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">
 | 
					      <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 [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
 | 
				
			||||||
      <app-search-results #searchResults [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary">
 | 
					      <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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
				
			||||||
import { Router } from '@angular/router';
 | 
					import { Router } from '@angular/router';
 | 
				
			||||||
import { AssetsService } from '../../services/assets.service';
 | 
					import { AssetsService } from '../../services/assets.service';
 | 
				
			||||||
@ -23,6 +23,16 @@ export class SearchFormComponent implements OnInit {
 | 
				
			|||||||
  isTypeaheading$ = new BehaviorSubject<boolean>(false);
 | 
					  isTypeaheading$ = new BehaviorSubject<boolean>(false);
 | 
				
			||||||
  typeAhead$: Observable<any>;
 | 
					  typeAhead$: Observable<any>;
 | 
				
			||||||
  searchForm: FormGroup;
 | 
					  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})$/;
 | 
					  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}$/;
 | 
					  regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
 | 
				
			||||||
@ -45,6 +55,7 @@ export class SearchFormComponent implements OnInit {
 | 
				
			|||||||
    private electrsApiService: ElectrsApiService,
 | 
					    private electrsApiService: ElectrsApiService,
 | 
				
			||||||
    private apiService: ApiService,
 | 
					    private apiService: ApiService,
 | 
				
			||||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
					    private relativeUrlPipe: RelativeUrlPipe,
 | 
				
			||||||
 | 
					    private elementRef: ElementRef,
 | 
				
			||||||
  ) { }
 | 
					  ) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit(): void {
 | 
					  ngOnInit(): void {
 | 
				
			||||||
 | 
				
			|||||||
@ -117,8 +117,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
        }),
 | 
					        }),
 | 
				
			||||||
        switchMap(() => {
 | 
					        switchMap(() => {
 | 
				
			||||||
          let transactionObservable$: Observable<Transaction>;
 | 
					          let transactionObservable$: Observable<Transaction>;
 | 
				
			||||||
          if (history.state.data && history.state.data.fee !== -1) {
 | 
					          const cached = this.stateService.getTxFromCache(this.txId);
 | 
				
			||||||
            transactionObservable$ = of(history.state.data);
 | 
					          if (cached && cached.fee !== -1) {
 | 
				
			||||||
 | 
					            transactionObservable$ = of(cached);
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            transactionObservable$ = this.electrsApiService
 | 
					            transactionObservable$ = this.electrsApiService
 | 
				
			||||||
              .getTransaction$(this.txId)
 | 
					              .getTransaction$(this.txId)
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
  <div class="title-block">
 | 
					  <div class="title-block">
 | 
				
			||||||
    <div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
 | 
					    <div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
 | 
				
			||||||
      <span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
 | 
					      <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-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
 | 
				
			||||||
        <span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
 | 
					        <span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
 | 
				
			|||||||
@ -183,8 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
        }),
 | 
					        }),
 | 
				
			||||||
        switchMap(() => {
 | 
					        switchMap(() => {
 | 
				
			||||||
          let transactionObservable$: Observable<Transaction>;
 | 
					          let transactionObservable$: Observable<Transaction>;
 | 
				
			||||||
          if (history.state.data && history.state.data.fee !== -1) {
 | 
					          const cached = this.stateService.getTxFromCache(this.txId);
 | 
				
			||||||
            transactionObservable$ = of(history.state.data);
 | 
					          if (cached && cached.fee !== -1) {
 | 
				
			||||||
 | 
					            transactionObservable$ = of(cached);
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            transactionObservable$ = this.electrsApiService
 | 
					            transactionObservable$ = this.electrsApiService
 | 
				
			||||||
              .getTransaction$(this.txId)
 | 
					              .getTransaction$(this.txId)
 | 
				
			||||||
@ -279,6 +280,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
        this.waitingForTransaction = false;
 | 
					        this.waitingForTransaction = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      this.rbfTransaction = rbfTransaction;
 | 
					      this.rbfTransaction = rbfTransaction;
 | 
				
			||||||
 | 
					      this.stateService.setTxCache([this.rbfTransaction]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
 | 
					    this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
 | 
					<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
 | 
				
			||||||
  <div *ngIf="!transactionPage" class="header-bg box tx-page-container">
 | 
					  <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-block d-md-none">{{ tx.txid | shortenString : 16 }}</span>
 | 
				
			||||||
      <span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
 | 
					      <span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
 | 
				
			|||||||
@ -119,7 +119,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.transactionsLength = this.transactions.length;
 | 
					      this.transactionsLength = this.transactions.length;
 | 
				
			||||||
 | 
					      this.stateService.setTxCache(this.transactions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.transactions.forEach((tx) => {
 | 
					      this.transactions.forEach((tx) => {
 | 
				
			||||||
        tx['@voutLimit'] = true;
 | 
					        tx['@voutLimit'] = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -152,6 +152,11 @@ export interface RewardStats {
 | 
				
			|||||||
  totalTx: number;
 | 
					  totalTx: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AuditScore {
 | 
				
			||||||
 | 
					  hash: string;
 | 
				
			||||||
 | 
					  matchRate?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ITopNodesPerChannels {
 | 
					export interface ITopNodesPerChannels {
 | 
				
			||||||
  publicKey: string,
 | 
					  publicKey: string,
 | 
				
			||||||
  alias: 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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -125,6 +125,93 @@
 | 
				
			|||||||
    <app-clipboard [button]="true" [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
 | 
					    <app-clipboard [button]="true" [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
 | 
				
			||||||
  </div>
 | 
					  </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 *ngIf="!error">
 | 
				
			||||||
    <div class="row" *ngIf="node.as_number && node.active_channel_count">
 | 
					    <div class="row" *ngIf="node.as_number && node.active_channel_count">
 | 
				
			||||||
      <div class="col-sm">
 | 
					      <div class="col-sm">
 | 
				
			||||||
 | 
				
			|||||||
@ -72,3 +72,32 @@ app-fiat {
 | 
				
			|||||||
    height: 28px !important;
 | 
					    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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,16 @@
 | 
				
			|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
				
			||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
					import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
				
			||||||
import { Observable } from 'rxjs';
 | 
					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 { SeoService } from '../../services/seo.service';
 | 
				
			||||||
import { LightningApiService } from '../lightning-api.service';
 | 
					import { LightningApiService } from '../lightning-api.service';
 | 
				
			||||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
 | 
					import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
 | 
				
			||||||
 | 
					import { ILiquidityAd, parseLiquidityAdHex } from './liquidity-ad';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface CustomRecord {
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  payload: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-node',
 | 
					  selector: 'app-node',
 | 
				
			||||||
@ -24,6 +30,10 @@ export class NodeComponent implements OnInit {
 | 
				
			|||||||
  channelListLoading = false;
 | 
					  channelListLoading = false;
 | 
				
			||||||
  clearnetSocketCount = 0;
 | 
					  clearnetSocketCount = 0;
 | 
				
			||||||
  torSocketCount = 0;
 | 
					  torSocketCount = 0;
 | 
				
			||||||
 | 
					  hasDetails = false;
 | 
				
			||||||
 | 
					  showDetails = false;
 | 
				
			||||||
 | 
					  liquidityAd: ILiquidityAd;
 | 
				
			||||||
 | 
					  tlvRecords: CustomRecord[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private lightningApiService: LightningApiService,
 | 
					    private lightningApiService: LightningApiService,
 | 
				
			||||||
@ -36,6 +46,8 @@ export class NodeComponent implements OnInit {
 | 
				
			|||||||
      .pipe(
 | 
					      .pipe(
 | 
				
			||||||
        switchMap((params: ParamMap) => {
 | 
					        switchMap((params: ParamMap) => {
 | 
				
			||||||
          this.publicKey = params.get('public_key');
 | 
					          this.publicKey = params.get('public_key');
 | 
				
			||||||
 | 
					          this.tlvRecords = [];
 | 
				
			||||||
 | 
					          this.liquidityAd = null;
 | 
				
			||||||
          return this.lightningApiService.getNode$(params.get('public_key'));
 | 
					          return this.lightningApiService.getNode$(params.get('public_key'));
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        map((node) => {
 | 
					        map((node) => {
 | 
				
			||||||
@ -79,6 +91,26 @@ export class NodeComponent implements OnInit {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          return node;
 | 
					          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 => {
 | 
					        catchError(err => {
 | 
				
			||||||
          this.error = err;
 | 
					          this.error = err;
 | 
				
			||||||
          return [{
 | 
					          return [{
 | 
				
			||||||
@ -89,6 +121,10 @@ export class NodeComponent implements OnInit {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toggleShowDetails(): void {
 | 
				
			||||||
 | 
					    this.showDetails = !this.showDetails;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  changeSocket(index: number) {
 | 
					  changeSocket(index: number) {
 | 
				
			||||||
    this.selectedSocketIndex = index;
 | 
					    this.selectedSocketIndex = index;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Injectable } from '@angular/core';
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
import { HttpClient, HttpParams } from '@angular/common/http';
 | 
					import { HttpClient, HttpParams } from '@angular/common/http';
 | 
				
			||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
 | 
					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 { Observable } from 'rxjs';
 | 
				
			||||||
import { StateService } from './state.service';
 | 
					import { StateService } from './state.service';
 | 
				
			||||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
					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> {
 | 
					  getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
 | 
				
			||||||
    return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
 | 
					    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>;
 | 
					  timeLtr: BehaviorSubject<boolean>;
 | 
				
			||||||
  hideFlow: BehaviorSubject<boolean>;
 | 
					  hideFlow: BehaviorSubject<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  txCache: { [txid: string]: Transaction } = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(PLATFORM_ID) private platformId: any,
 | 
					    @Inject(PLATFORM_ID) private platformId: any,
 | 
				
			||||||
    @Inject(LOCALE_ID) private locale: string,
 | 
					    @Inject(LOCALE_ID) private locale: string,
 | 
				
			||||||
@ -265,4 +267,19 @@ export class StateService {
 | 
				
			|||||||
  isLiquid() {
 | 
					  isLiquid() {
 | 
				
			||||||
    return this.network === 'liquid' || this.network === 'liquidtestnet';
 | 
					    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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,10 @@
 | 
				
			|||||||
<!doctype html>
 | 
					<!doctype html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
  <meta charset="utf-8">
 | 
					  <meta charset="utf-8">
 | 
				
			||||||
  <title>mempool - Bisq Markets</title>
 | 
					  <title>mempool - Bisq Markets</title>
 | 
				
			||||||
 | 
					  <script src="/resources/config.js"></script>
 | 
				
			||||||
  <base href="/">
 | 
					  <base href="/">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
 | 
					  <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="manifest" href="/resources/bisq/favicons/site.webmanifest">
 | 
				
			||||||
  <link rel="mask-icon" href="/resources/bisq/favicons/safari-pinned-tab.svg" color="#5bbad5">
 | 
					  <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 rel="shortcut icon" href="/resources/bisq/favicons/favicon.ico">
 | 
				
			||||||
 
 | 
					
 | 
				
			||||||
  <link id="canonical" rel="canonical" href="https://bisq.markets">
 | 
					  <link id="canonical" rel="canonical" href="https://bisq.markets">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
  <app-root></app-root>
 | 
					  <app-root></app-root>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,10 @@
 | 
				
			|||||||
<!doctype html>
 | 
					<!doctype html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
  <meta charset="utf-8">
 | 
					  <meta charset="utf-8">
 | 
				
			||||||
  <title>mempool - Liquid Network</title>
 | 
					  <title>mempool - Liquid Network</title>
 | 
				
			||||||
 | 
					  <script src="/resources/config.js"></script>
 | 
				
			||||||
  <base href="/">
 | 
					  <base href="/">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
 | 
					  <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: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:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
 | 
				
			||||||
  <meta property="twitter:domain" content="liquid.network">
 | 
					  <meta property="twitter:domain" content="liquid.network">
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  <link rel="apple-touch-icon" sizes="180x180" href="/resources/liquid/favicons/apple-touch-icon.png">
 | 
					  <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="48x48" href="/resources/liquid/favicons/favicon-48x48.png">
 | 
				
			||||||
  <link rel="icon" type="image/png" sizes="32x32" href="/resources/liquid/favicons/favicon-32x32.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">
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
  <app-root></app-root>
 | 
					  <app-root></app-root>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,10 @@
 | 
				
			|||||||
<!doctype html>
 | 
					<!doctype html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
  <meta charset="utf-8">
 | 
					  <meta charset="utf-8">
 | 
				
			||||||
  <title>mempool - Bitcoin Explorer</title>
 | 
					  <title>mempool - Bitcoin Explorer</title>
 | 
				
			||||||
 | 
					  <script src="/resources/config.js"></script>
 | 
				
			||||||
  <base href="/">
 | 
					  <base href="/">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem." />
 | 
					  <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: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:image:src" content="https://mempool.space/resources/mempool-space-preview.png" />
 | 
				
			||||||
  <meta property="twitter:domain" content="mempool.space">
 | 
					  <meta property="twitter:domain" content="mempool.space">
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  <link rel="apple-touch-icon" sizes="180x180" href="/resources/favicons/apple-touch-icon.png">
 | 
					  <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="32x32" href="/resources/favicons/favicon-32x32.png">
 | 
				
			||||||
  <link rel="icon" type="image/png" sizes="16x16" href="/resources/favicons/favicon-16x16.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">
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
  <app-root></app-root>
 | 
					  <app-root></app-root>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,13 @@
 | 
				
			|||||||
		try_files $uri @index-redirect;
 | 
							try_files $uri @index-redirect;
 | 
				
			||||||
		expires 1h;
 | 
							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 {
 | 
						location @index-redirect {
 | 
				
			||||||
		rewrite (.*) /$lang/index.html;
 | 
							rewrite (.*) /$lang/index.html;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "MEMPOOL": {
 | 
					  "MEMPOOL": {
 | 
				
			||||||
 | 
					    "ENABLED": false,
 | 
				
			||||||
    "NETWORK": "mainnet",
 | 
					    "NETWORK": "mainnet",
 | 
				
			||||||
    "BACKEND": "esplora",
 | 
					    "BACKEND": "esplora",
 | 
				
			||||||
    "HTTP_PORT": 8993,
 | 
					    "HTTP_PORT": 8993,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "MEMPOOL": {
 | 
					  "MEMPOOL": {
 | 
				
			||||||
 | 
					    "ENABLED": false,
 | 
				
			||||||
    "NETWORK": "signet",
 | 
					    "NETWORK": "signet",
 | 
				
			||||||
    "BACKEND": "esplora",
 | 
					    "BACKEND": "esplora",
 | 
				
			||||||
    "HTTP_PORT": 8991,
 | 
					    "HTTP_PORT": 8991,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "MEMPOOL": {
 | 
					  "MEMPOOL": {
 | 
				
			||||||
 | 
					    "ENABLED": false,
 | 
				
			||||||
    "NETWORK": "testnet",
 | 
					    "NETWORK": "testnet",
 | 
				
			||||||
    "BACKEND": "esplora",
 | 
					    "BACKEND": "esplora",
 | 
				
			||||||
    "HTTP_PORT": 8992,
 | 
					    "HTTP_PORT": 8992,
 | 
				
			||||||
 | 
				
			|||||||
@ -81,6 +81,13 @@ location /resources {
 | 
				
			|||||||
	try_files $uri /en-US/index.html;
 | 
						try_files $uri /en-US/index.html;
 | 
				
			||||||
	expires 1w;
 | 
						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
 | 
					# cache /main.f40e91d908a068a2.js forever since they never change
 | 
				
			||||||
location ~* ^/.+\..+\.(js|css) {
 | 
					location ~* ^/.+\..+\.(js|css) {
 | 
				
			||||||
	try_files /$lang/$uri /en-US/$uri =404;
 | 
						try_files /$lang/$uri /en-US/$uri =404;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user