Merge branch 'master' into dependabot/npm_and_yarn/backend/mysql2-3.9.1
This commit is contained in:
		
						commit
						582eca1fdd
					
				
							
								
								
									
										36
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -115,6 +115,10 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Sync-assets
 | 
			
		||||
        run: npm run sync-assets-dev
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          MEMPOOL_CDN: 1
 | 
			
		||||
          VERBOSE: 1
 | 
			
		||||
        working-directory: assets/frontend
 | 
			
		||||
 | 
			
		||||
      - name: Zip mining-pool assets
 | 
			
		||||
@ -237,6 +241,8 @@ jobs:
 | 
			
		||||
        working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
 | 
			
		||||
        env: 
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          MEMPOOL_CDN: 1
 | 
			
		||||
          VERBOSE: 1
 | 
			
		||||
  
 | 
			
		||||
  e2e:
 | 
			
		||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
			
		||||
@ -329,4 +335,32 @@ jobs:
 | 
			
		||||
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
  validate_docker_json:
 | 
			
		||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
			
		||||
    runs-on: "ubuntu-latest"
 | 
			
		||||
    name: Validate generated backend Docker JSON
 | 
			
		||||
 | 
			
		||||
    steps: 
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: docker
 | 
			
		||||
      
 | 
			
		||||
      - name: Install jq
 | 
			
		||||
        run: sudo apt-get install jq -y
 | 
			
		||||
 | 
			
		||||
      - name: Create new start script to run on CI
 | 
			
		||||
        run: |
 | 
			
		||||
          sed '$d' start.sh > start_ci.sh
 | 
			
		||||
        working-directory: docker/docker/backend
 | 
			
		||||
 | 
			
		||||
      - name: Run the script to generate the sample JSON
 | 
			
		||||
        run: |
 | 
			
		||||
          sh start_ci.sh
 | 
			
		||||
        working-directory: docker/docker/backend
 | 
			
		||||
 | 
			
		||||
      - name: Validate JSON syntax
 | 
			
		||||
        run: |
 | 
			
		||||
          cat mempool-config.json | jq
 | 
			
		||||
        working-directory: docker/docker/backend
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -6,3 +6,4 @@ backend/mempool-config.json
 | 
			
		||||
frontend/src/resources/config.template.js
 | 
			
		||||
frontend/src/resources/config.js
 | 
			
		||||
target
 | 
			
		||||
docker/backend/start_ci.sh
 | 
			
		||||
							
								
								
									
										6
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -7,6 +7,12 @@ mempool-config.json
 | 
			
		||||
pools.json
 | 
			
		||||
icons.json
 | 
			
		||||
 | 
			
		||||
# docker
 | 
			
		||||
Dockerfile
 | 
			
		||||
GeoIP
 | 
			
		||||
start.sh
 | 
			
		||||
wait-for-it.sh
 | 
			
		||||
 | 
			
		||||
# compiled output
 | 
			
		||||
/dist
 | 
			
		||||
/tmp
 | 
			
		||||
 | 
			
		||||
@ -646,7 +646,7 @@ class BisqMarketsApi {
 | 
			
		||||
        case 'year':
 | 
			
		||||
            return strtotime('midnight first day of january', ts);
 | 
			
		||||
        default:
 | 
			
		||||
            throw new Error('Unsupported interval: ' + interval);
 | 
			
		||||
            throw new Error('Unsupported interval');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -106,6 +106,7 @@ export namespace IBitcoinApi {
 | 
			
		||||
      address?: string;              //  (string) bitcoin address
 | 
			
		||||
      addresses?: string[];           //  (string) bitcoin addresses
 | 
			
		||||
      pegout_chain?: string;         //  (string) Elements peg-out chain
 | 
			
		||||
      pegout_address?: string;       //  (string) Elements peg-out address
 | 
			
		||||
      pegout_addresses?: string[];   //  (string) Elements peg-out addresses
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import config from '../config';
 | 
			
		||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces';
 | 
			
		||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import diskCache from './disk-cache';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
@ -451,7 +451,9 @@ class Blocks {
 | 
			
		||||
        if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
          const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
 | 
			
		||||
          const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
 | 
			
		||||
          await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
 | 
			
		||||
          if (cpfpSummary) {
 | 
			
		||||
            await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
 | 
			
		||||
        }
 | 
			
		||||
@ -995,11 +997,11 @@ class Blocks {
 | 
			
		||||
    return state;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateTimerProgress(state, msg) {
 | 
			
		||||
  private updateTimerProgress(state, msg): void {
 | 
			
		||||
    state.progress = msg;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private clearTimer(state) {
 | 
			
		||||
  private clearTimer(state): void {
 | 
			
		||||
    if (state.timer) {
 | 
			
		||||
      clearTimeout(state.timer);
 | 
			
		||||
    }
 | 
			
		||||
@ -1088,13 +1090,19 @@ class Blocks {
 | 
			
		||||
      summary = {
 | 
			
		||||
        id: hash,
 | 
			
		||||
        transactions: cpfpSummary.transactions.map(tx => {
 | 
			
		||||
          let flags: number = 0;
 | 
			
		||||
          try {
 | 
			
		||||
            flags = tx.flags || Common.getTransactionFlags(tx);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            txid: tx.txid,
 | 
			
		||||
            fee: tx.fee || 0,
 | 
			
		||||
            vsize: tx.vsize,
 | 
			
		||||
            value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
 | 
			
		||||
            rate: tx.effectiveFeePerVsize,
 | 
			
		||||
            flags: tx.flags || Common.getTransactionFlags(tx),
 | 
			
		||||
            flags: flags,
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
      };
 | 
			
		||||
@ -1284,7 +1292,7 @@ class Blocks {
 | 
			
		||||
    return blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditSummary(hash: string): Promise<any> {
 | 
			
		||||
  public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
      return BlocksAuditsRepository.$getBlockAudit(hash);
 | 
			
		||||
    } else {
 | 
			
		||||
@ -1304,7 +1312,7 @@ class Blocks {
 | 
			
		||||
    return this.currentBlockHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
 | 
			
		||||
  public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
 | 
			
		||||
    let transactions = txs;
 | 
			
		||||
    if (!transactions) {
 | 
			
		||||
      if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
@ -1319,14 +1327,19 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
 | 
			
		||||
    if (transactions?.length != null) {
 | 
			
		||||
      const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
 | 
			
		||||
 | 
			
		||||
    await this.$saveCpfp(hash, height, summary);
 | 
			
		||||
      await this.$saveCpfp(hash, height, summary);
 | 
			
		||||
 | 
			
		||||
    const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
 | 
			
		||||
    await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
 | 
			
		||||
      const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
 | 
			
		||||
      await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
 | 
			
		||||
 | 
			
		||||
    return summary;
 | 
			
		||||
      return summary;
 | 
			
		||||
    } else {
 | 
			
		||||
      logger.err(`Cannot index CPFP for block ${height} - missing transaction data`);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository';
 | 
			
		||||
import { isIP } from 'net';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
import { isPoint } from '../utils/secp256k1';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
export class Common {
 | 
			
		||||
  static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
 | 
			
		||||
    '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
 | 
			
		||||
@ -245,7 +246,8 @@ export class Common {
 | 
			
		||||
    } else if (tx.version === 2) {
 | 
			
		||||
      flags |= TransactionFlags.v2;
 | 
			
		||||
    }
 | 
			
		||||
    const reusedAddresses: { [address: string ]: number } = {};
 | 
			
		||||
    const reusedInputAddresses: { [address: string ]: number } = {};
 | 
			
		||||
    const reusedOutputAddresses: { [address: string ]: number } = {};
 | 
			
		||||
    const inValues = {};
 | 
			
		||||
    const outValues = {};
 | 
			
		||||
    let rbf = false;
 | 
			
		||||
@ -261,6 +263,9 @@ export class Common {
 | 
			
		||||
        case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
 | 
			
		||||
        case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
 | 
			
		||||
        case 'v1_p2tr': {
 | 
			
		||||
          if (!vin.witness?.length) {
 | 
			
		||||
            throw new Error('Taproot input missing witness data');
 | 
			
		||||
          }
 | 
			
		||||
          flags |= TransactionFlags.p2tr;
 | 
			
		||||
          // in taproot, if the last witness item begins with 0x50, it's an annex
 | 
			
		||||
          const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
 | 
			
		||||
@ -286,7 +291,7 @@ export class Common {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (vin.prevout?.scriptpubkey_address) {
 | 
			
		||||
        reusedAddresses[vin.prevout?.scriptpubkey_address] = (reusedAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1;
 | 
			
		||||
        reusedInputAddresses[vin.prevout?.scriptpubkey_address] = (reusedInputAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1;
 | 
			
		||||
      }
 | 
			
		||||
      inValues[vin.prevout?.value || Math.random()] = (inValues[vin.prevout?.value || Math.random()] || 0) + 1;
 | 
			
		||||
    }
 | 
			
		||||
@ -301,7 +306,7 @@ export class Common {
 | 
			
		||||
        case 'p2pk': {
 | 
			
		||||
          flags |= TransactionFlags.p2pk;
 | 
			
		||||
          // detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
 | 
			
		||||
          hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2));
 | 
			
		||||
          hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey?.slice(2, -2));
 | 
			
		||||
        } break;
 | 
			
		||||
        case 'multisig': {
 | 
			
		||||
          flags |= TransactionFlags.p2ms;
 | 
			
		||||
@ -321,7 +326,7 @@ export class Common {
 | 
			
		||||
        case 'op_return': flags |= TransactionFlags.op_return; break;
 | 
			
		||||
      }
 | 
			
		||||
      if (vout.scriptpubkey_address) {
 | 
			
		||||
        reusedAddresses[vout.scriptpubkey_address] = (reusedAddresses[vout.scriptpubkey_address] || 0) + 1;
 | 
			
		||||
        reusedOutputAddresses[vout.scriptpubkey_address] = (reusedOutputAddresses[vout.scriptpubkey_address] || 0) + 1;
 | 
			
		||||
      }
 | 
			
		||||
      outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1;
 | 
			
		||||
    }
 | 
			
		||||
@ -331,7 +336,7 @@ export class Common {
 | 
			
		||||
    
 | 
			
		||||
    // fast but bad heuristic to detect possible coinjoins
 | 
			
		||||
    // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
 | 
			
		||||
    const addressReuse = Object.values(reusedAddresses).reduce((acc, count) => Math.max(acc, count), 0) > 1;
 | 
			
		||||
    const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
 | 
			
		||||
    if (!addressReuse && tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) {
 | 
			
		||||
      flags |= TransactionFlags.coinjoin;
 | 
			
		||||
    }
 | 
			
		||||
@ -348,7 +353,12 @@ export class Common {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static classifyTransaction(tx: TransactionExtended): TransactionClassified {
 | 
			
		||||
    const flags = Common.getTransactionFlags(tx);
 | 
			
		||||
    let flags = 0;
 | 
			
		||||
    try {
 | 
			
		||||
      flags = Common.getTransactionFlags(tx);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
    tx.flags = flags;
 | 
			
		||||
    return {
 | 
			
		||||
      ...Common.stripTransaction(tx),
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 67;
 | 
			
		||||
  private static currentVersion = 68;
 | 
			
		||||
  private queryTimeout = 3600_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -566,6 +566,20 @@ class DatabaseMigration {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
 | 
			
		||||
      await this.updateToSchemaVersion(67);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (databaseSchemaVersion < 68 && config.MEMPOOL.NETWORK === "liquid") {
 | 
			
		||||
      await this.$executeQuery('TRUNCATE TABLE elements_pegs');
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
 | 
			
		||||
      await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_elements_block';`);
 | 
			
		||||
      // Create the federation_addresses table and add the two Liquid Federation change addresses in
 | 
			
		||||
      await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
 | 
			
		||||
      await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4')`); // Federation change address
 | 
			
		||||
      await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('3EiAcrzq1cELXScc98KeCswGWZaPGceT1d')`); // Federation change address
 | 
			
		||||
      // Create the federation_txos table that uses the federation_addresses table as a foreign key
 | 
			
		||||
      await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
 | 
			
		||||
      await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`);
 | 
			
		||||
      await this.updateToSchemaVersion(68);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -813,6 +827,32 @@ class DatabaseMigration {
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateFederationAddressesTableQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS federation_addresses (
 | 
			
		||||
      bitcoinaddress varchar(100) NOT NULL,
 | 
			
		||||
      PRIMARY KEY (bitcoinaddress)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateFederationTxosTableQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS federation_txos (
 | 
			
		||||
      txid varchar(65) NOT NULL,
 | 
			
		||||
      txindex int(11) NOT NULL,
 | 
			
		||||
      bitcoinaddress varchar(100) NOT NULL,
 | 
			
		||||
      amount bigint(20) unsigned NOT NULL,
 | 
			
		||||
      blocknumber int(11) unsigned NOT NULL,
 | 
			
		||||
      blocktime int(11) unsigned NOT NULL,
 | 
			
		||||
      unspent tinyint(1) NOT NULL,
 | 
			
		||||
      lastblockupdate int(11) unsigned NOT NULL,
 | 
			
		||||
      lasttimeupdate int(11) unsigned NOT NULL,
 | 
			
		||||
      pegtxid varchar(65) NOT NULL,
 | 
			
		||||
      pegindex int(11) NOT NULL,
 | 
			
		||||
      pegblocktime int(11) unsigned NOT NULL,
 | 
			
		||||
      PRIMARY KEY (txid, txindex), 
 | 
			
		||||
      FOREIGN KEY (bitcoinaddress) REFERENCES federation_addresses (bitcoinaddress)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreatePoolsTableQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS pools (
 | 
			
		||||
      id int(11) NOT NULL AUTO_INCREMENT,
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,12 @@ import { Common } from '../common';
 | 
			
		||||
import DB from '../../database';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
 | 
			
		||||
const federationChangeAddresses = ['bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4', '3EiAcrzq1cELXScc98KeCswGWZaPGceT1d'];
 | 
			
		||||
const auditBlockOffsetWithTip = 1; // Wait for 1 block confirmation before processing the block in the audit process to reduce the risk of reorgs
 | 
			
		||||
 | 
			
		||||
class ElementsParser {
 | 
			
		||||
  private isRunning = false;
 | 
			
		||||
  private isUtxosUpdatingRunning = false;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
@ -32,12 +36,6 @@ class ElementsParser {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPegDataByMonth(): Promise<any> {
 | 
			
		||||
    const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $parseBlock(block: IBitcoinApi.Block) {
 | 
			
		||||
    for (const tx of block.tx) {
 | 
			
		||||
      await this.$parseInputs(tx, block);
 | 
			
		||||
@ -55,29 +53,30 @@ class ElementsParser {
 | 
			
		||||
 | 
			
		||||
  protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) {
 | 
			
		||||
    const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true);
 | 
			
		||||
    const bitcoinBlock: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(bitcoinTx.blockhash);
 | 
			
		||||
    const prevout = bitcoinTx.vout[input.vout || 0];
 | 
			
		||||
    const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[0]) || '';
 | 
			
		||||
    await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex,
 | 
			
		||||
      outputAddress, bitcoinTx.txid, prevout.n, 1);
 | 
			
		||||
      outputAddress, bitcoinTx.txid, prevout.n, bitcoinBlock.height, bitcoinBlock.time, 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
 | 
			
		||||
    for (const output of tx.vout) {
 | 
			
		||||
      if (output.scriptPubKey.pegout_chain) {
 | 
			
		||||
        await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
 | 
			
		||||
          (output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0);
 | 
			
		||||
          (output.scriptPubKey.pegout_address || ''), '', 0, 0, 0, 0);
 | 
			
		||||
      }
 | 
			
		||||
      if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata'
 | 
			
		||||
        && output.value && output.value > 0 && output.asset && output.asset === Common.nativeAssetId) {
 | 
			
		||||
        await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
 | 
			
		||||
          (output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 1);
 | 
			
		||||
          (output.scriptPubKey.pegout_address || ''), '', 0, 0, 0, 1);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string,
 | 
			
		||||
    txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise<void> {
 | 
			
		||||
    const query = `INSERT INTO elements_pegs(
 | 
			
		||||
    txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, bitcoinblock: number, bitcoinBlockTime: number, final_tx: number): Promise<void> {
 | 
			
		||||
    const query = `INSERT IGNORE INTO elements_pegs(
 | 
			
		||||
        block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
 | 
			
		||||
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
 | 
			
		||||
 | 
			
		||||
@ -85,7 +84,22 @@ class ElementsParser {
 | 
			
		||||
      height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
 | 
			
		||||
    ];
 | 
			
		||||
    await DB.query(query, params);
 | 
			
		||||
    logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`);
 | 
			
		||||
    logger.debug(`Saved L-BTC peg from Liquid block height #${height} with TXID ${txid}.`);
 | 
			
		||||
 | 
			
		||||
    if (amount > 0) { // Peg-in
 | 
			
		||||
  
 | 
			
		||||
      // Add the address to the federation addresses table
 | 
			
		||||
      await DB.query(`INSERT IGNORE INTO federation_addresses (bitcoinaddress) VALUES (?)`, [bitcoinaddress]);
 | 
			
		||||
 | 
			
		||||
      // Add the UTXO to the federation txos table
 | 
			
		||||
      const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
 | 
			
		||||
      const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, txid, txindex, blockTime];
 | 
			
		||||
      await DB.query(query_utxos, params_utxos);
 | 
			
		||||
      const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
 | 
			
		||||
      await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
 | 
			
		||||
      logger.debug(`Saved new Federation UTXO ${bitcointxid}:${bitcoinindex} belonging to ${bitcoinaddress} to federation txos`);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $getLatestBlockHeightFromDatabase(): Promise<number> {
 | 
			
		||||
@ -98,6 +112,327 @@ class ElementsParser {
 | 
			
		||||
    const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`;
 | 
			
		||||
    await DB.query(query, [blockHeight]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ///////////// FEDERATION AUDIT //////////////
 | 
			
		||||
 | 
			
		||||
  public async $updateFederationUtxos() {
 | 
			
		||||
    if (this.isUtxosUpdatingRunning) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.isUtxosUpdatingRunning = true;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let auditProgress = await this.$getAuditProgress();
 | 
			
		||||
      // If no peg in transaction was found in the database, return
 | 
			
		||||
      if (!auditProgress.lastBlockAudit) {
 | 
			
		||||
        logger.debug(`No Federation UTXOs found in the database. Waiting for some to be confirmed before starting the Federation UTXOs audit`);
 | 
			
		||||
        this.isUtxosUpdatingRunning = false;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
 | 
			
		||||
      // If the bitcoin blockchain is not synced yet, return
 | 
			
		||||
      if (bitcoinBlocksToSync.bitcoinHeaders > bitcoinBlocksToSync.bitcoinBlocks + 1) {
 | 
			
		||||
        logger.debug(`Bitcoin client is not synced yet. ${bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks} blocks remaining to sync before the Federation audit process can start`);
 | 
			
		||||
        this.isUtxosUpdatingRunning = false;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      auditProgress.lastBlockAudit++;
 | 
			
		||||
 | 
			
		||||
      // Logging
 | 
			
		||||
      let indexedThisRun = 0;
 | 
			
		||||
      let timer = Date.now() / 1000;
 | 
			
		||||
      const startedAt = Date.now() / 1000;
 | 
			
		||||
      const indexingSpeeds: number[] = [];
 | 
			
		||||
 | 
			
		||||
      while (auditProgress.lastBlockAudit <= auditProgress.confirmedTip) {
 | 
			
		||||
 | 
			
		||||
        // First, get the current UTXOs that need to be scanned in the block
 | 
			
		||||
        const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit);
 | 
			
		||||
 | 
			
		||||
        // Get the peg-out addresses that need to be scanned
 | 
			
		||||
        const redeemAddresses = await this.$getRedeemAddressesToScan();
 | 
			
		||||
 | 
			
		||||
        // The fast way: check if these UTXOs are still unspent as of the current block with gettxout
 | 
			
		||||
        let spentAsTip: any[];
 | 
			
		||||
        let unspentAsTip: any[];
 | 
			
		||||
        if (auditProgress.confirmedTip - auditProgress.lastBlockAudit <= 150) { // If the audit status is not too far in the past, we can use gettxout (fast way)
 | 
			
		||||
          const utxosToParse = await this.$getFederationUtxosToParse(utxos);
 | 
			
		||||
          spentAsTip = utxosToParse.spentAsTip;
 | 
			
		||||
          unspentAsTip = utxosToParse.unspentAsTip;
 | 
			
		||||
          logger.debug(`Found ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`);
 | 
			
		||||
          logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`);
 | 
			
		||||
        } else { // If the audit status is too far in the past, it is useless and wasteful to look for still unspent txos since they will all be spent as of the tip
 | 
			
		||||
          spentAsTip = utxos;
 | 
			
		||||
          unspentAsTip = [];
 | 
			
		||||
 | 
			
		||||
          // Logging
 | 
			
		||||
          const elapsedSeconds = (Date.now() / 1000) - timer;
 | 
			
		||||
          if (elapsedSeconds > 5) {
 | 
			
		||||
            const runningFor = (Date.now() / 1000) - startedAt;
 | 
			
		||||
            const blockPerSeconds = indexedThisRun / elapsedSeconds;
 | 
			
		||||
            indexingSpeeds.push(blockPerSeconds);
 | 
			
		||||
            if (indexingSpeeds.length > 100) indexingSpeeds.shift(); // Keep the length of the up to 100 last indexing speeds
 | 
			
		||||
            const meanIndexingSpeed = indexingSpeeds.reduce((a, b) => a + b, 0) / indexingSpeeds.length;
 | 
			
		||||
            const eta = (auditProgress.confirmedTip - auditProgress.lastBlockAudit) / meanIndexingSpeed;
 | 
			
		||||
            logger.debug(`Scanning ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses at Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip} | ~${meanIndexingSpeed.toFixed(2)} blocks/sec | elapsed: ${(runningFor / 60).toFixed(0)} minutes | ETA: ${(eta / 60).toFixed(0)} minutes`);
 | 
			
		||||
            timer = Date.now() / 1000;
 | 
			
		||||
            indexedThisRun = 0;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The slow way: parse the block to look for the spending tx
 | 
			
		||||
        const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit);
 | 
			
		||||
        const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2);
 | 
			
		||||
        await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses);
 | 
			
		||||
 | 
			
		||||
        // Finally, update the lastblockupdate of the remaining UTXOs and save to the database
 | 
			
		||||
        const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
 | 
			
		||||
        await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
 | 
			
		||||
 | 
			
		||||
        auditProgress = await this.$getAuditProgress();
 | 
			
		||||
        auditProgress.lastBlockAudit++;
 | 
			
		||||
        indexedThisRun++;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.isUtxosUpdatingRunning = false;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.isUtxosUpdatingRunning = false;
 | 
			
		||||
      throw new Error(e instanceof Error ? e.message : 'Error');
 | 
			
		||||
    } 
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the UTXOs that need to be scanned in block height (UTXOs that were last updated in the block height - 1)
 | 
			
		||||
  protected async $getFederationUtxosToScan(height: number) { 
 | 
			
		||||
    const query = `SELECT txid, txindex, bitcoinaddress, amount FROM federation_txos WHERE lastblockupdate = ? AND unspent = 1`;
 | 
			
		||||
    const [rows] = await DB.query(query, [height - 1]);
 | 
			
		||||
    return rows as any[];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Returns the UTXOs that are spent as of tip and need to be scanned
 | 
			
		||||
  protected async $getFederationUtxosToParse(utxos: any[]): Promise<any> {
 | 
			
		||||
    const spentAsTip: any[] = [];
 | 
			
		||||
    const unspentAsTip: any[] = [];
 | 
			
		||||
 | 
			
		||||
    for (const utxo of utxos) {
 | 
			
		||||
      const result = await bitcoinSecondClient.getTxOut(utxo.txid, utxo.txindex, false);
 | 
			
		||||
      result ? unspentAsTip.push(utxo) : spentAsTip.push(utxo);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return {spentAsTip, unspentAsTip};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddressesData: any[] = []) {
 | 
			
		||||
    const redeemAddresses: string[] = redeemAddressesData.map(redeemAddress => redeemAddress.bitcoinaddress);
 | 
			
		||||
    for (const tx of block.tx) {
 | 
			
		||||
      let mightRedeemInThisTx = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs...
 | 
			
		||||
      // Check if the Federation UTXOs that was spent as of tip are spent in this block
 | 
			
		||||
      for (const input of tx.vin) {
 | 
			
		||||
        const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout);
 | 
			
		||||
        if (txo) {
 | 
			
		||||
          mightRedeemInThisTx = true;
 | 
			
		||||
          await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
 | 
			
		||||
          // Remove the TXO from the utxo array
 | 
			
		||||
          spentAsTip.splice(spentAsTip.indexOf(txo), 1);
 | 
			
		||||
          logger.debug(`Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height}`);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // Check if an output is sent to a change address of the federation
 | 
			
		||||
      for (const output of tx.vout) {
 | 
			
		||||
        if (output.scriptPubKey.address && federationChangeAddresses.includes(output.scriptPubKey.address)) {
 | 
			
		||||
          // Check that the UTXO was not already added in the DB by previous scans
 | 
			
		||||
          const [rows_check] = await DB.query(`SELECT txid FROM federation_txos WHERE txid = ? AND txindex = ?`, [tx.txid, output.n]) as any[];
 | 
			
		||||
          if (rows_check.length === 0) {
 | 
			
		||||
            const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
 | 
			
		||||
            const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, '', 0, 0];
 | 
			
		||||
            await DB.query(query_utxos, params_utxos);
 | 
			
		||||
            // Add the UTXO to the utxo array
 | 
			
		||||
            spentAsTip.push({
 | 
			
		||||
              txid: tx.txid,
 | 
			
		||||
              txindex: output.n,
 | 
			
		||||
              bitcoinaddress: output.scriptPubKey.address,
 | 
			
		||||
              amount: output.value * 100000000
 | 
			
		||||
            });
 | 
			
		||||
            logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${output.value * 100000000} sats), change address: ${output.scriptPubKey.address}`);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (mightRedeemInThisTx && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) {
 | 
			
		||||
          // Find the number of times output.scriptPubKey.address appears in redeemAddresses. There can be address reuse for peg-outs...
 | 
			
		||||
          const matchingAddress: any[] = redeemAddressesData.filter(redeemAddress => redeemAddress.bitcoinaddress === output.scriptPubKey.address && -redeemAddress.amount === Math.round(output.value * 100000000));
 | 
			
		||||
          if (matchingAddress.length > 0) {
 | 
			
		||||
            if (matchingAddress.length > 1) {
 | 
			
		||||
              // If there are more than one peg out address with the same amount, we can't know which one redeemed the UTXO: we take the oldest one
 | 
			
		||||
              matchingAddress.sort((a, b) => a.datetime - b.datetime);
 | 
			
		||||
              logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}, datetime ${matchingAddress[0].datetime}`);
 | 
			
		||||
            } else {
 | 
			
		||||
              logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}`);
 | 
			
		||||
            }
 | 
			
		||||
            const query_add_redeem = `UPDATE elements_pegs SET bitcointxid = ?, bitcoinindex = ? WHERE bitcoinaddress = ? AND amount = ? AND datetime = ?`;
 | 
			
		||||
            const params_add_redeem: (string | number)[] = [tx.txid, output.n, matchingAddress[0].bitcoinaddress, matchingAddress[0].amount, matchingAddress[0].datetime];
 | 
			
		||||
            await DB.query(query_add_redeem, params_add_redeem);
 | 
			
		||||
            const index = redeemAddressesData.indexOf(matchingAddress[0]);
 | 
			
		||||
            redeemAddressesData.splice(index, 1);
 | 
			
		||||
            redeemAddresses.splice(index, 1);
 | 
			
		||||
          } else { // The output amount does not match the peg-out amount... log it
 | 
			
		||||
            logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${output.scriptPubKey.address} but output amount ${Math.round(output.value * 100000000)} does not match the peg-out amount!`);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    for (const utxo of spentAsTip) {
 | 
			
		||||
      await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]);    
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const utxo of unspentAsTip) {
 | 
			
		||||
      await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, utxo.txid, utxo.txindex]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $saveLastBlockAuditToDatabase(blockHeight: number) {
 | 
			
		||||
    const query = `UPDATE state SET number = ? WHERE name = 'last_bitcoin_block_audit'`;
 | 
			
		||||
    await DB.query(query, [blockHeight]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the bitcoin block where the audit process was last updated
 | 
			
		||||
  protected async $getAuditProgress(): Promise<any> {
 | 
			
		||||
    const lastblockaudit = await this.$getLastBlockAudit();
 | 
			
		||||
    const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
 | 
			
		||||
    return {
 | 
			
		||||
      lastBlockAudit: lastblockaudit,
 | 
			
		||||
      confirmedTip: bitcoinBlocksToSync.bitcoinBlocks - auditBlockOffsetWithTip,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the bitcoin blocks remaining to be synced
 | 
			
		||||
  protected async $getBitcoinBlockchainState(): Promise<any> {
 | 
			
		||||
    const result = await bitcoinSecondClient.getBlockchainInfo();
 | 
			
		||||
    return {
 | 
			
		||||
      bitcoinBlocks: result.blocks,
 | 
			
		||||
      bitcoinHeaders: result.headers,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $getLastBlockAudit(): Promise<number> {
 | 
			
		||||
    const query = `SELECT number FROM state WHERE name = 'last_bitcoin_block_audit'`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows[0]['number'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $getRedeemAddressesToScan(): Promise<any[]> {
 | 
			
		||||
    const query = `SELECT datetime, amount, bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`;
 | 
			
		||||
    const [rows]: any[] = await DB.query(query);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ///////////// DATA QUERY //////////////
 | 
			
		||||
 | 
			
		||||
  public async $getAuditStatus(): Promise<any> {
 | 
			
		||||
    const lastBlockAudit = await this.$getLastBlockAudit();
 | 
			
		||||
    const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
 | 
			
		||||
    return {
 | 
			
		||||
      bitcoinBlocks: bitcoinBlocksToSync.bitcoinBlocks,
 | 
			
		||||
      bitcoinHeaders: bitcoinBlocksToSync.bitcoinHeaders,
 | 
			
		||||
      lastBlockAudit: lastBlockAudit,
 | 
			
		||||
      isAuditSynced: bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks <= 2 && bitcoinBlocksToSync.bitcoinBlocks - lastBlockAudit <= 3,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPegDataByMonth(): Promise<any> {
 | 
			
		||||
    const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getFederationReservesByMonth(): Promise<any> {
 | 
			
		||||
    const query = `
 | 
			
		||||
    SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(blocktime), '%Y-%m-01') AS date FROM federation_txos 
 | 
			
		||||
    WHERE
 | 
			
		||||
        (blocktime > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime) - INTERVAL 1 MONTH) + INTERVAL 1 DAY))
 | 
			
		||||
      AND 
 | 
			
		||||
        ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime)) + INTERVAL 1 DAY)))
 | 
			
		||||
    GROUP BY 
 | 
			
		||||
        date;`;          
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the current L-BTC pegs and the last Liquid block it was updated
 | 
			
		||||
  public async $getCurrentLbtcSupply(): Promise<any> {
 | 
			
		||||
    const [rows] = await DB.query(`SELECT SUM(amount) AS LBTC_supply FROM elements_pegs;`);
 | 
			
		||||
    const lastblockupdate = await this.$getLatestBlockHeightFromDatabase();
 | 
			
		||||
    const hash = await bitcoinClient.getBlockHash(lastblockupdate);
 | 
			
		||||
    return {
 | 
			
		||||
      amount: rows[0]['LBTC_supply'],
 | 
			
		||||
      lastBlockUpdate: lastblockupdate,
 | 
			
		||||
      hash: hash
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the current reserves of the federation and the last Bitcoin block it was updated
 | 
			
		||||
  public async $getCurrentFederationReserves(): Promise<any> {
 | 
			
		||||
    const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1;`);
 | 
			
		||||
    const lastblockaudit = await this.$getLastBlockAudit();
 | 
			
		||||
    const hash = await bitcoinSecondClient.getBlockHash(lastblockaudit);
 | 
			
		||||
    return {
 | 
			
		||||
      amount: rows[0]['total_balance'],
 | 
			
		||||
      lastBlockUpdate: lastblockaudit,
 | 
			
		||||
      hash: hash
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get all of the federation addresses, most balances first
 | 
			
		||||
  public async $getFederationAddresses(): Promise<any> {
 | 
			
		||||
    const query = `SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos WHERE unspent = 1 GROUP BY bitcoinaddress ORDER BY balance DESC;`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get all of the UTXOs held by the federation, most recent first
 | 
			
		||||
  public async $getFederationUtxos(): Promise<any> {
 | 
			
		||||
    const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime FROM federation_txos WHERE unspent = 1 ORDER BY blocktime DESC;`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get all of the federation addresses one month ago, most balances first
 | 
			
		||||
  public async $getFederationAddressesOneMonthAgo(): Promise<any> {
 | 
			
		||||
    const query = `
 | 
			
		||||
    SELECT COUNT(*) AS addresses_count_one_month FROM (
 | 
			
		||||
      SELECT bitcoinaddress, SUM(amount) AS balance
 | 
			
		||||
      FROM federation_txos 
 | 
			
		||||
      WHERE
 | 
			
		||||
          (blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))
 | 
			
		||||
        AND
 | 
			
		||||
          ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))))
 | 
			
		||||
      GROUP BY bitcoinaddress
 | 
			
		||||
    ) AS result;`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get all of the UTXOs held by the federation one month ago, most recent first
 | 
			
		||||
  public async $getFederationUtxosOneMonthAgo(): Promise<any> {
 | 
			
		||||
    const query = `
 | 
			
		||||
    SELECT COUNT(*) AS utxos_count_one_month FROM federation_txos 
 | 
			
		||||
    WHERE
 | 
			
		||||
        (blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))
 | 
			
		||||
      AND
 | 
			
		||||
        ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))))
 | 
			
		||||
    ORDER BY blocktime DESC;`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get recent pegouts from the federation (3 months old)
 | 
			
		||||
  public async $getRecentPegouts(): Promise<any> {
 | 
			
		||||
    const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs WHERE amount < 0 AND datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -90, CURRENT_TIMESTAMP())) ORDER BY blocktime;`;
 | 
			
		||||
    const [rows] = await DB.query(query);
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new ElementsParser();
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,16 @@ class LiquidRoutes {
 | 
			
		||||
    
 | 
			
		||||
    if (config.DATABASE.ENABLED) {
 | 
			
		||||
      app
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs', this.$getElementsPegs)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves', this.$getFederationReserves)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/month', this.$getFederationReservesByMonth)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegouts', this.$getPegOuts)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses', this.$getFederationAddresses)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/previous-month', this.$getFederationAddressesOneMonthAgo)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos', this.$getFederationUtxos)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/previous-month', this.$getFederationUtxosOneMonthAgo)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus)
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -63,11 +72,123 @@ class LiquidRoutes {
 | 
			
		||||
  private async $getElementsPegsByMonth(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const pegs = await elementsParser.$getPegDataByMonth();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
 | 
			
		||||
      res.json(pegs);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFederationReservesByMonth(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const reserves = await elementsParser.$getFederationReservesByMonth();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
 | 
			
		||||
      res.json(reserves);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getElementsPegs(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const currentSupply = await elementsParser.$getCurrentLbtcSupply();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(currentSupply);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFederationReserves(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const currentReserves = await elementsParser.$getCurrentFederationReserves();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(currentReserves);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFederationAuditStatus(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const auditStatus = await elementsParser.$getAuditStatus();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(auditStatus);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFederationAddresses(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const federationAddresses = await elementsParser.$getFederationAddresses();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(federationAddresses);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFederationAddressesOneMonthAgo(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const federationAddresses = await elementsParser.$getFederationAddressesOneMonthAgo();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString());
 | 
			
		||||
      res.json(federationAddresses);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFederationUtxos(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const federationUtxos = await elementsParser.$getFederationUtxos();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(federationUtxos);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFederationUtxosOneMonthAgo(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const federationUtxos = await elementsParser.$getFederationUtxosOneMonthAgo();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString());
 | 
			
		||||
      res.json(federationUtxos);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getPegOuts(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const recentPegOuts = await elementsParser.$getRecentPegouts();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
      res.json(recentPegOuts);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new LiquidRoutes();
 | 
			
		||||
 | 
			
		||||
@ -142,7 +142,7 @@ class Mining {
 | 
			
		||||
  public async $getPoolStat(slug: string): Promise<object> {
 | 
			
		||||
    const pool = await PoolsRepository.$getPool(slug);
 | 
			
		||||
    if (!pool) {
 | 
			
		||||
      throw new Error('This mining pool does not exist ' + escape(slug));
 | 
			
		||||
      throw new Error('This mining pool does not exist');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const blockCount: number = await BlocksRepository.$blockCount(pool.id);
 | 
			
		||||
 | 
			
		||||
@ -266,6 +266,7 @@ class Server {
 | 
			
		||||
      blocks.setNewBlockCallback(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
          await elementsParser.$parse();
 | 
			
		||||
          await elementsParser.$updateFederationUtxos();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -59,7 +59,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAudit(hash: string): Promise<any> {
 | 
			
		||||
  public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
 | 
			
		||||
@ -75,8 +75,8 @@ class BlocksAuditRepositories {
 | 
			
		||||
        expected_weight as expectedWeight
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
        WHERE blocks_audits.hash = ?
 | 
			
		||||
      `, [hash]);
 | 
			
		||||
      
 | 
			
		||||
      if (rows.length) {
 | 
			
		||||
        rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
 | 
			
		||||
@ -101,8 +101,8 @@ class BlocksAuditRepositories {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
        WHERE blocks_audits.hash = ?
 | 
			
		||||
      `, [hash]);
 | 
			
		||||
      return rows[0];
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import logger from '../logger';
 | 
			
		||||
import { Common } from '../api/common';
 | 
			
		||||
import PoolsRepository from './PoolsRepository';
 | 
			
		||||
import HashratesRepository from './HashratesRepository';
 | 
			
		||||
import { escape } from 'mysql2';
 | 
			
		||||
import { RowDataPacket, escape } from 'mysql2';
 | 
			
		||||
import BlocksSummariesRepository from './BlocksSummariesRepository';
 | 
			
		||||
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
 | 
			
		||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
 | 
			
		||||
@ -478,7 +478,7 @@ class BlocksRepository {
 | 
			
		||||
  public async $getBlocksByPool(slug: string, startHeight?: number): Promise<BlockExtended[]> {
 | 
			
		||||
    const pool = await PoolsRepository.$getPool(slug);
 | 
			
		||||
    if (!pool) {
 | 
			
		||||
      throw new Error('This mining pool does not exist ' + escape(slug));
 | 
			
		||||
      throw new Error('This mining pool does not exist');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
@ -802,10 +802,10 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get a list of blocks that have been indexed
 | 
			
		||||
   */
 | 
			
		||||
  public async $getIndexedBlocks(): Promise<any[]> {
 | 
			
		||||
  public async $getIndexedBlocks(): Promise<{ height: number, hash: string }[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`);
 | 
			
		||||
      return rows;
 | 
			
		||||
      const [rows] = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`) as RowDataPacket[][];
 | 
			
		||||
      return rows as { height: number, hash: string }[];
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
@ -815,7 +815,7 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get a list of blocks that have not had CPFP data indexed
 | 
			
		||||
   */
 | 
			
		||||
   public async $getCPFPUnindexedBlocks(): Promise<any[]> {
 | 
			
		||||
   public async $getCPFPUnindexedBlocks(): Promise<number[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockchainInfo = await bitcoinClient.getBlockchainInfo();
 | 
			
		||||
      const currentBlockHeight = blockchainInfo.blocks;
 | 
			
		||||
@ -825,13 +825,13 @@ class BlocksRepository {
 | 
			
		||||
      }
 | 
			
		||||
      const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
 | 
			
		||||
 | 
			
		||||
      const [rows]: any[] = await DB.query(`
 | 
			
		||||
      const [rows] = await DB.query(`
 | 
			
		||||
        SELECT height
 | 
			
		||||
        FROM compact_cpfp_clusters
 | 
			
		||||
        WHERE height <= ? AND height >= ?
 | 
			
		||||
        GROUP BY height
 | 
			
		||||
        ORDER BY height DESC;
 | 
			
		||||
      `, [currentBlockHeight, minHeight]);
 | 
			
		||||
      `, [currentBlockHeight, minHeight]) as RowDataPacket[][];
 | 
			
		||||
 | 
			
		||||
      const indexedHeights = {};
 | 
			
		||||
      rows.forEach((row) => { indexedHeights[row.height] = true; });
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { BlockSummary, TransactionClassified } from '../mempool.interfaces';
 | 
			
		||||
@ -69,7 +70,7 @@ class BlocksSummariesRepository {
 | 
			
		||||
 | 
			
		||||
  public async $getIndexedSummariesId(): Promise<string[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
 | 
			
		||||
      const [rows] = await DB.query(`SELECT id from blocks_summaries`) as RowDataPacket[][];
 | 
			
		||||
      return rows.map(row => row.id);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
 | 
			
		||||
@ -139,7 +139,7 @@ class HashratesRepository {
 | 
			
		||||
  public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> {
 | 
			
		||||
    const pool = await PoolsRepository.$getPool(slug);
 | 
			
		||||
    if (!pool) {
 | 
			
		||||
      throw new Error('This mining pool does not exist ' + escape(slug));
 | 
			
		||||
      throw new Error('This mining pool does not exist');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find hashrate boundaries
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,9 @@ const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
 | 
			
		||||
 * @returns {boolean} true if the point is on the SECP256K1 curve
 | 
			
		||||
 */
 | 
			
		||||
export function isPoint(pointHex: string): boolean {
 | 
			
		||||
  if (!pointHex?.length) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  if (
 | 
			
		||||
    !(
 | 
			
		||||
      // is uncompressed
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/jamesblacklock.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/jamesblacklock.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 20, 2023.
 | 
			
		||||
 | 
			
		||||
Signed: jamesblacklock
 | 
			
		||||
@ -1,3 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
 | 
			
		||||
 | 
			
		||||
Signed: natsee
 | 
			
		||||
Signed: natsoni
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
    "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
 | 
			
		||||
    "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
 | 
			
		||||
    "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__
 | 
			
		||||
    "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
 | 
			
		||||
    "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
 | 
			
		||||
 | 
			
		||||
# ESPLORA
 | 
			
		||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
 | 
			
		||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
 | 
			
		||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=""}
 | 
			
		||||
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
 | 
			
		||||
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
 | 
			
		||||
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -6,6 +6,13 @@
 | 
			
		||||
/out-tsc
 | 
			
		||||
server.run.js
 | 
			
		||||
 | 
			
		||||
# docker
 | 
			
		||||
Dockerfile
 | 
			
		||||
entrypoint.sh
 | 
			
		||||
nginx-mempool.conf
 | 
			
		||||
nginx.conf
 | 
			
		||||
wait-for
 | 
			
		||||
 | 
			
		||||
# Only exists if Bazel was run
 | 
			
		||||
/bazel-out
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -45,28 +45,30 @@ export class AcceleratorDashboardComponent implements OnInit {
 | 
			
		||||
    this.pendingAccelerations$ = interval(30000).pipe(
 | 
			
		||||
      startWith(true),
 | 
			
		||||
      switchMap(() => {
 | 
			
		||||
        return this.apiService.getAccelerations$();
 | 
			
		||||
      }),
 | 
			
		||||
      catchError((e) => {
 | 
			
		||||
        return of([]);
 | 
			
		||||
        return this.apiService.getAccelerations$().pipe(
 | 
			
		||||
          catchError(() => {
 | 
			
		||||
            return of([]);
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
      }),
 | 
			
		||||
      share(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.accelerations$ = this.stateService.chainTip$.pipe(
 | 
			
		||||
      distinctUntilChanged(),
 | 
			
		||||
      switchMap((chainTip) => {
 | 
			
		||||
        return this.apiService.getAccelerationHistory$({ timeframe: '1m' });
 | 
			
		||||
      }),
 | 
			
		||||
      catchError((e) => {
 | 
			
		||||
        return of([]);
 | 
			
		||||
      switchMap(() => {
 | 
			
		||||
        return this.apiService.getAccelerationHistory$({ timeframe: '1m' }).pipe(
 | 
			
		||||
          catchError(() => {
 | 
			
		||||
            return of([]);
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
      }),
 | 
			
		||||
      share(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.minedAccelerations$ = this.accelerations$.pipe(
 | 
			
		||||
      map(accelerations => {
 | 
			
		||||
        return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status))
 | 
			
		||||
        return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <ng-template #default>
 | 
			
		||||
    ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
 | 
			
		||||
    <span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template>
 | 
			
		||||
    <span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
 | 
			
		||||
    <ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
 | 
			
		||||
    <ng-template [ngIf]="network === 'testnet'">t</ng-template>
 | 
			
		||||
    <ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ export class AmountComponent implements OnInit, OnDestroy {
 | 
			
		||||
  @Input() noFiat = false;
 | 
			
		||||
  @Input() addPlus = false;
 | 
			
		||||
  @Input() blockConversion: Price;
 | 
			
		||||
  @Input() forceBtc: boolean = false;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,13 @@ import { MiningService } from '../../services/mining.service';
 | 
			
		||||
import { download } from '../../shared/graphs.utils';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
interface Hashrate {
 | 
			
		||||
  timestamp: number;
 | 
			
		||||
  avgHashRate: number;
 | 
			
		||||
  share: number;
 | 
			
		||||
  poolName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-hashrate-chart-pools',
 | 
			
		||||
  templateUrl: './hashrate-chart-pools.component.html',
 | 
			
		||||
@ -32,6 +39,7 @@ export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
  miningWindowPreference: string;
 | 
			
		||||
  radioGroupForm: UntypedFormGroup;
 | 
			
		||||
 | 
			
		||||
  hashrates: Hashrate[];
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
@ -87,56 +95,9 @@ export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
          return this.apiService.getHistoricalPoolsHashrate$(timespan)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap((response) => {
 | 
			
		||||
                const hashrates = response.body;
 | 
			
		||||
                this.hashrates = response.body;
 | 
			
		||||
                // Prepare series (group all hashrates data point by pool)
 | 
			
		||||
                const grouped = {};
 | 
			
		||||
                for (const hashrate of hashrates) {
 | 
			
		||||
                  if (!grouped.hasOwnProperty(hashrate.poolName)) {
 | 
			
		||||
                    grouped[hashrate.poolName] = [];
 | 
			
		||||
                  }
 | 
			
		||||
                  grouped[hashrate.poolName].push(hashrate);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const series = [];
 | 
			
		||||
                const legends = [];
 | 
			
		||||
                for (const name in grouped) {
 | 
			
		||||
                  series.push({
 | 
			
		||||
                    zlevel: 0,
 | 
			
		||||
                    stack: 'Total',
 | 
			
		||||
                    name: name,
 | 
			
		||||
                    showSymbol: false,
 | 
			
		||||
                    symbol: 'none',
 | 
			
		||||
                    data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]),
 | 
			
		||||
                    type: 'line',
 | 
			
		||||
                    lineStyle: { width: 0 },
 | 
			
		||||
                    areaStyle: { opacity: 1 },
 | 
			
		||||
                    smooth: true,
 | 
			
		||||
                    color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
 | 
			
		||||
                    emphasis: {
 | 
			
		||||
                      disabled: true,
 | 
			
		||||
                      scale: false,
 | 
			
		||||
                    },
 | 
			
		||||
                  });
 | 
			
		||||
 | 
			
		||||
                  legends.push({
 | 
			
		||||
                    name: name,
 | 
			
		||||
                    inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
                    textStyle: {
 | 
			
		||||
                      color: 'white',
 | 
			
		||||
                    },
 | 
			
		||||
                    icon: 'roundRect',
 | 
			
		||||
                    itemStyle: {
 | 
			
		||||
                      color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
 | 
			
		||||
                    },
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  legends: legends,
 | 
			
		||||
                  series: series,
 | 
			
		||||
                });
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
 | 
			
		||||
                const series = this.applyHashrates();
 | 
			
		||||
                if (series.length === 0) {
 | 
			
		||||
                  this.cd.markForCheck();
 | 
			
		||||
                  throw new Error();
 | 
			
		||||
@ -156,6 +117,77 @@ export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  applyHashrates(): any[] {
 | 
			
		||||
    const times: { [time: number]: { hashrates: { [pool: string]: Hashrate } } } = {};
 | 
			
		||||
    const pools = {};
 | 
			
		||||
    for (const hashrate of this.hashrates) {
 | 
			
		||||
      if (!times[hashrate.timestamp]) {
 | 
			
		||||
        times[hashrate.timestamp] = { hashrates: {} };
 | 
			
		||||
      }
 | 
			
		||||
      times[hashrate.timestamp].hashrates[hashrate.poolName] = hashrate;
 | 
			
		||||
      if (!pools[hashrate.poolName]) {
 | 
			
		||||
        pools[hashrate.poolName] = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const sortedTimes = Object.keys(times).sort((a,b) => parseInt(a) - parseInt(b)).map(time => ({ time: parseInt(time), hashrates: times[time].hashrates }));
 | 
			
		||||
    const lastHashrates = sortedTimes[sortedTimes.length - 1].hashrates;
 | 
			
		||||
    const sortedPools = Object.keys(pools).sort((a,b) => {
 | 
			
		||||
      if (lastHashrates[b]?.share ?? lastHashrates[a]?.share ?? false) {
 | 
			
		||||
        // sort by descending share of hashrate in latest period
 | 
			
		||||
        return (lastHashrates[b]?.share || 0) - (lastHashrates[a]?.share || 0);
 | 
			
		||||
      } else {
 | 
			
		||||
        // tiebreak by pool name
 | 
			
		||||
        b < a;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const series = [];
 | 
			
		||||
    const legends = [];
 | 
			
		||||
    for (const name of sortedPools) {
 | 
			
		||||
      const data = sortedTimes.map(({ time, hashrates }) => {
 | 
			
		||||
        return [time * 1000, (hashrates[name]?.share || 0) * 100];
 | 
			
		||||
      });
 | 
			
		||||
      series.push({
 | 
			
		||||
        zlevel: 0,
 | 
			
		||||
        stack: 'Total',
 | 
			
		||||
        name: name,
 | 
			
		||||
        showSymbol: false,
 | 
			
		||||
        symbol: 'none',
 | 
			
		||||
        data,
 | 
			
		||||
        type: 'line',
 | 
			
		||||
        lineStyle: { width: 0 },
 | 
			
		||||
        areaStyle: { opacity: 1 },
 | 
			
		||||
        smooth: true,
 | 
			
		||||
        color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
 | 
			
		||||
        emphasis: {
 | 
			
		||||
          disabled: true,
 | 
			
		||||
          scale: false,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      legends.push({
 | 
			
		||||
        name: name,
 | 
			
		||||
        inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'white',
 | 
			
		||||
        },
 | 
			
		||||
        icon: 'roundRect',
 | 
			
		||||
        itemStyle: {
 | 
			
		||||
          color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.prepareChartOptions({
 | 
			
		||||
      legends: legends,
 | 
			
		||||
      series: series,
 | 
			
		||||
    });
 | 
			
		||||
    this.isLoading = false;
 | 
			
		||||
 | 
			
		||||
    return series;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data) {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (data.series.length === 0) {
 | 
			
		||||
@ -256,6 +288,7 @@ export class HashrateChartPoolsComponent implements OnInit {
 | 
			
		||||
        },
 | 
			
		||||
      }],
 | 
			
		||||
    };
 | 
			
		||||
    this.cd.markForCheck();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartInit(ec) {
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,6 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  template: ('widget' | 'advanced') = 'widget';
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
 | 
			
		||||
  pegsChartOption: EChartsOption = {};
 | 
			
		||||
  pegsChartInitOption = {
 | 
			
		||||
    renderer: 'svg'
 | 
			
		||||
  };
 | 
			
		||||
@ -41,20 +40,24 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
    if (!this.data) {
 | 
			
		||||
    if (!this.data?.liquidPegs) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels);
 | 
			
		||||
    if (!this.data.liquidReserves) {
 | 
			
		||||
      this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels, this.data.liquidReserves.series);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  rendered() {
 | 
			
		||||
    if (!this.data) {
 | 
			
		||||
    if (!this.data.liquidPegs) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.isLoading = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createChartOptions(series: number[], labels: string[]): EChartsOption {
 | 
			
		||||
  createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[],): EChartsOption {
 | 
			
		||||
    return {
 | 
			
		||||
      grid: {
 | 
			
		||||
        height: this.height,
 | 
			
		||||
@ -99,17 +102,18 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
          type: 'line',
 | 
			
		||||
        },
 | 
			
		||||
        formatter: (params: any) => {
 | 
			
		||||
          const colorSpan = (color: string) => `<span class="indicator" style="background-color: #116761;"></span>`;
 | 
			
		||||
          const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
 | 
			
		||||
          let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
 | 
			
		||||
          params.map((item: any, index: number) => {
 | 
			
		||||
          for (let index = params.length - 1; index >= 0; index--) {
 | 
			
		||||
            const item = params[index];
 | 
			
		||||
            if (index < 26) {
 | 
			
		||||
              itemFormatted += `<div class="item">
 | 
			
		||||
                <div class="indicator-container">${colorSpan(item.color)}</div>
 | 
			
		||||
                <div class="grow"></div>
 | 
			
		||||
                <div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">L-BTC</span></div>
 | 
			
		||||
                <div style="margin-right: 5px"></div>
 | 
			
		||||
                <div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">${item.seriesName}</span></div>
 | 
			
		||||
              </div>`;
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          }
 | 
			
		||||
          return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
@ -138,20 +142,34 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          data: series,
 | 
			
		||||
          data: pegSeries,
 | 
			
		||||
          name: 'L-BTC',
 | 
			
		||||
          color: '#116761',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          stack: 'total',
 | 
			
		||||
          smooth: false,
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            opacity: 0.2,
 | 
			
		||||
            color: '#116761',
 | 
			
		||||
          },
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 3,
 | 
			
		||||
            width: 2,
 | 
			
		||||
            color: '#116761',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          data: reservesSeries,
 | 
			
		||||
          name: 'BTC',
 | 
			
		||||
          color: '#EA983B',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 2,
 | 
			
		||||
            color: '#EA983B',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -78,6 +78,9 @@
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" id="btn-assets">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-audit">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/audit']" (click)="collapse()"><fa-icon [icon]="['fas', 'scale-balanced']" [fixedWidth]="true" i18n-title="master-page.btc-reserves-audit" title="BTC Reserves Audit"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li [hidden]="isMobile" class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,72 @@
 | 
			
		||||
<div [ngClass]="{'widget': widget}">
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
  <div style="min-height: 295px">
 | 
			
		||||
    <table class="table table-borderless">
 | 
			
		||||
      <thead style="vertical-align: middle;">
 | 
			
		||||
        <th class="address text-left" [ngClass]="{'widget': widget}" i18n="shared.address">Address</th>
 | 
			
		||||
        <th class="amount text-right" [ngClass]="{'widget': widget}" i18n="address.balance">Balance</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody *ngIf="federationAddresses$ | async as addresses; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
 | 
			
		||||
        <ng-container *ngIf="widget; else regularRows">
 | 
			
		||||
          <tr *ngFor="let address of addresses | slice:0:5">
 | 
			
		||||
            <td class="address text-left widget">
 | 
			
		||||
              <a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
 | 
			
		||||
                <app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right widget">
 | 
			
		||||
              <app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <ng-template #regularRows>
 | 
			
		||||
          <tr *ngFor="let address of addresses | slice:(page - 1) * pageSize:page * pageSize">
 | 
			
		||||
            <td class="address text-left">
 | 
			
		||||
              <a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
 | 
			
		||||
                <app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right">
 | 
			
		||||
              <app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </tbody>
 | 
			
		||||
      <ng-template #skeleton>
 | 
			
		||||
        <tbody *ngIf="widget; else regularRowsSkeleton">
 | 
			
		||||
          <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
            <td class="address text-left widget">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 400px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right widget">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 350px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
        <ng-template #regularRowsSkeleton>
 | 
			
		||||
          <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
            <td class="address text-left">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 600px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 400px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
    <ngb-pagination *ngIf="!widget && federationAddresses$ | async as addresses" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
 | 
			
		||||
      [collectionSize]="addresses.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
 | 
			
		||||
      (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
 | 
			
		||||
    </ngb-pagination>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="!widget">
 | 
			
		||||
      <div class="clearfix"></div>
 | 
			
		||||
      <br>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
.spinner-border {
 | 
			
		||||
  height: 25px;
 | 
			
		||||
  width: 25px;
 | 
			
		||||
  margin-top: 13px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr, td, th {
 | 
			
		||||
  border: 0px;
 | 
			
		||||
  padding-top: 0.65rem !important;
 | 
			
		||||
  padding-bottom: 0.6rem !important;
 | 
			
		||||
  padding-right: 2rem !important;
 | 
			
		||||
  .widget {
 | 
			
		||||
    padding-right: 1rem !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clear-link {
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabled {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  opacity: 0.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress {
 | 
			
		||||
  background-color: #2d3348;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.address {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 160px;
 | 
			
		||||
}
 | 
			
		||||
.address.widget {
 | 
			
		||||
  width: 60%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.amount {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
}
 | 
			
		||||
.amount.widget {
 | 
			
		||||
  width: 40%;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,109 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
			
		||||
import { Observable, Subject, combineLatest, of, timer } from 'rxjs';
 | 
			
		||||
import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../../services/api.service';
 | 
			
		||||
import { Env, StateService } from '../../../services/state.service';
 | 
			
		||||
import { AuditStatus, CurrentPegs, FederationAddress } from '../../../interfaces/node-api.interface';
 | 
			
		||||
import { WebsocketService } from '../../../services/websocket.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-federation-addresses-list',
 | 
			
		||||
  templateUrl: './federation-addresses-list.component.html',
 | 
			
		||||
  styleUrls: ['./federation-addresses-list.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class FederationAddressesListComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() federationAddresses$: Observable<FederationAddress[]>;
 | 
			
		||||
 | 
			
		||||
  env: Env;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  page = 1;
 | 
			
		||||
  pageSize = 15;
 | 
			
		||||
  maxSize = window.innerWidth <= 767.98 ? 3 : 5;
 | 
			
		||||
  skeletonLines: number[] = [];
 | 
			
		||||
  auditStatus$: Observable<AuditStatus>;
 | 
			
		||||
  auditUpdated$: Observable<boolean>;
 | 
			
		||||
  lastReservesBlockUpdate: number = 0;
 | 
			
		||||
  currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
  lastPegBlockUpdate: number = 0;
 | 
			
		||||
  lastPegAmount: string = '';
 | 
			
		||||
  isLoad: boolean = true;
 | 
			
		||||
 | 
			
		||||
  private destroy$ = new Subject();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLoading = !this.widget;
 | 
			
		||||
    this.env = this.stateService.env;
 | 
			
		||||
    this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.websocketService.want(['blocks']);
 | 
			
		||||
      this.auditStatus$ = this.stateService.blocks$.pipe(
 | 
			
		||||
        takeUntil(this.destroy$),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
 | 
			
		||||
        tap(() => this.isLoad = false),
 | 
			
		||||
        switchMap(() => this.apiService.federationAuditSynced$()),
 | 
			
		||||
        shareReplay(1)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.currentPeg$ = this.auditStatus$.pipe(
 | 
			
		||||
        filter(auditStatus => auditStatus.isAuditSynced === true),
 | 
			
		||||
        switchMap(_ =>
 | 
			
		||||
          this.apiService.liquidPegs$().pipe(
 | 
			
		||||
            filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
 | 
			
		||||
            tap((currentPegs) => {
 | 
			
		||||
              this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.auditUpdated$ = combineLatest([
 | 
			
		||||
        this.auditStatus$,
 | 
			
		||||
        this.currentPeg$
 | 
			
		||||
      ]).pipe(
 | 
			
		||||
        filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
 | 
			
		||||
        map(([auditStatus, currentPeg]) => ({
 | 
			
		||||
          lastBlockAudit: auditStatus.lastBlockAudit,
 | 
			
		||||
          currentPegAmount: currentPeg.amount
 | 
			
		||||
        })),
 | 
			
		||||
        switchMap(({ lastBlockAudit, currentPegAmount }) => {
 | 
			
		||||
          const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
 | 
			
		||||
          const amountCheck = currentPegAmount !== this.lastPegAmount;
 | 
			
		||||
          this.lastReservesBlockUpdate = lastBlockAudit;
 | 
			
		||||
          this.lastPegAmount = currentPegAmount;
 | 
			
		||||
          return of(blockAuditCheck || amountCheck);
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.federationAddresses$ = this.auditUpdated$.pipe(
 | 
			
		||||
        filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        switchMap(_ => this.apiService.federationAddresses$()),
 | 
			
		||||
        tap(_ => this.isLoading = false),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.destroy$.next(1);
 | 
			
		||||
    this.destroy$.complete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pageChange(page: number): void {
 | 
			
		||||
    this.page = page;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
<div *ngIf="(federationAddresses$ | async) as federationAddresses; else loadingData">
 | 
			
		||||
  
 | 
			
		||||
    <div class="fee-estimation-container">
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
 | 
			
		||||
          <h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
 | 
			
		||||
        </a>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <div class="fee-text">{{ federationAddresses.length }} <span i18n="shared.addresses">addresses</span></div>
 | 
			
		||||
          <span class="fiat" *ngIf="(federationAddressesOneMonthAgo$ | async) as federationAddressesOneMonthAgo; else loadingSkeleton" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom">
 | 
			
		||||
            <app-change [current]="federationAddresses.length" [previous]="federationAddressesOneMonthAgo.addresses_count_one_month"></app-change>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingData>
 | 
			
		||||
  <div class="fee-estimation-container loading-container">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
 | 
			
		||||
        <h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
 | 
			
		||||
      </a>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingSkeleton>
 | 
			
		||||
  <div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 2px; margin-bottom: 5px;"></div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,75 @@
 | 
			
		||||
.fee-estimation-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  @media (min-width: 376px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }  
 | 
			
		||||
  .item {
 | 
			
		||||
    max-width: 300px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: -webkit-fill-available;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      margin: 0 auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-title {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      color: #4a68b9;
 | 
			
		||||
      font-size: 10px;
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-text {
 | 
			
		||||
      font-size: 22px;
 | 
			
		||||
      span {
 | 
			
		||||
        font-size: 11px;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: -2px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-text span {
 | 
			
		||||
      color: #ffffff66;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      top: 0px;
 | 
			
		||||
    }
 | 
			
		||||
    .fee-text{
 | 
			
		||||
      border-bottom: 1px solid #ffffff1c;
 | 
			
		||||
      width: fit-content;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      line-height: 1.45;
 | 
			
		||||
      padding: 0px 2px;
 | 
			
		||||
    }
 | 
			
		||||
    .fiat {
 | 
			
		||||
      display: block;
 | 
			
		||||
      font-size: 14px !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-container{
 | 
			
		||||
  min-height: 76px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  .skeleton-loader {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      max-width: 90px;
 | 
			
		||||
      margin: 15px auto 3px;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin: 10px auto 3px;
 | 
			
		||||
      max-width: 55px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin-bottom: 4px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { FederationAddress } from '../../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-federation-addresses-stats',
 | 
			
		||||
  templateUrl: './federation-addresses-stats.component.html',
 | 
			
		||||
  styleUrls: ['./federation-addresses-stats.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class FederationAddressesStatsComponent implements OnInit {
 | 
			
		||||
  @Input() federationAddresses$: Observable<FederationAddress[]>;
 | 
			
		||||
  @Input() federationAddressesOneMonthAgo$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,109 @@
 | 
			
		||||
<div [ngClass]="{'widget': widget}">
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
  
 | 
			
		||||
  <div style="min-height: 295px">
 | 
			
		||||
    <table class="table table-borderless">
 | 
			
		||||
      <thead style="vertical-align: middle;">
 | 
			
		||||
        <th class="txid text-left" [ngClass]="{'widget': widget}" i18n="transaction.output">Output</th>
 | 
			
		||||
        <th class="address text-left" *ngIf="!widget" i18n="shared.address">Address</th>
 | 
			
		||||
        <th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
 | 
			
		||||
        <th class="pegin text-left" *ngIf="!widget" i18n="liquid.related-peg-in">Related Peg-In</th>
 | 
			
		||||
        <th class="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody *ngIf="federationUtxos$ | async as utxos; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
 | 
			
		||||
        <ng-container *ngIf="widget; else regularRows">
 | 
			
		||||
          <tr *ngFor="let utxo of utxos | slice:0:6">
 | 
			
		||||
            <td class="txid text-left widget">
 | 
			
		||||
              <a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + utxo.txid + ':' + utxo.txindex }}" target="_blank" style="color:#b86d12">
 | 
			
		||||
                <app-truncate [text]="utxo.txid + ':' + utxo.txindex" [lastChars]="6"></app-truncate>
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right widget">
 | 
			
		||||
              <app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="timestamp text-left widget">
 | 
			
		||||
              <app-time kind="since" [time]="utxo.blocktime"></app-time>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <ng-template #regularRows>
 | 
			
		||||
          <tr *ngFor="let utxo of utxos | slice:(page - 1) * pageSize:page * pageSize">
 | 
			
		||||
            <td class="txid text-left">
 | 
			
		||||
              <a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + utxo.txid + ':' + utxo.txindex }}" target="_blank" style="color:#b86d12">
 | 
			
		||||
                <app-truncate [text]="utxo.txid + ':' + utxo.txindex" [lastChars]="6"></app-truncate>
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="address text-left">
 | 
			
		||||
              <a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + utxo.bitcoinaddress }}" target="_blank" style="color:#b86d12">
 | 
			
		||||
                <app-truncate [text]="utxo.bitcoinaddress" [lastChars]="6"></app-truncate>
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right">
 | 
			
		||||
              <app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="pegin text-left">
 | 
			
		||||
              <ng-container *ngIf="utxo.pegtxid; else noPeginMessage">
 | 
			
		||||
                <a [routerLink]="['/tx' | relativeUrl, utxo.pegtxid]" [fragment]="'vin=' + utxo.pegindex">
 | 
			
		||||
                  <app-truncate [text]="utxo.pegtxid + ':' + utxo.pegindex" [lastChars]="6"></app-truncate>
 | 
			
		||||
                </a>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
              <ng-template #noPeginMessage>
 | 
			
		||||
                <i><span class="text-muted" i18n="liquid.change-output">Change output</span></i>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="timestamp text-left">
 | 
			
		||||
              ‎{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
              <div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </tbody>
 | 
			
		||||
      <ng-template #skeleton>
 | 
			
		||||
        <tbody *ngIf="widget; else regularRowsSkeleton">
 | 
			
		||||
          <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
            <td class="txid text-left widget">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 400px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right widget">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="timestamp text-left widget">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
        <ng-template #regularRowsSkeleton>
 | 
			
		||||
          <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
            <td class="txid text-left">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="address text-left">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="amount text-right">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 140px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="pegin text-left">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="timestamp text-left">
 | 
			
		||||
              <span class="skeleton-loader" style="max-width: 140px"></span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
    <ngb-pagination *ngIf="!widget && federationUtxos$ | async as utxos" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
 | 
			
		||||
      [collectionSize]="utxos.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
 | 
			
		||||
      (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
 | 
			
		||||
    </ngb-pagination>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="!widget">
 | 
			
		||||
      <div class="clearfix"></div>
 | 
			
		||||
      <br>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,94 @@
 | 
			
		||||
.spinner-border {
 | 
			
		||||
  height: 25px;
 | 
			
		||||
  width: 25px;
 | 
			
		||||
  margin-top: 13px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr, td, th {
 | 
			
		||||
  border: 0px;
 | 
			
		||||
  padding-top: 0.65rem !important;
 | 
			
		||||
  padding-bottom: 0.6rem !important;
 | 
			
		||||
  padding-right: 2rem !important;
 | 
			
		||||
  .widget {
 | 
			
		||||
    padding-right: 1rem !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clear-link {
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabled {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  opacity: 0.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress {
 | 
			
		||||
  background-color: #2d3348;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.txid {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 160px;
 | 
			
		||||
}
 | 
			
		||||
.txid.widget {
 | 
			
		||||
  width: 40%;
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.address {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 160px;
 | 
			
		||||
  @media (max-width: 527px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.amount {
 | 
			
		||||
  width: 12%;
 | 
			
		||||
}
 | 
			
		||||
.amount.widget {
 | 
			
		||||
  width: 30%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pegin {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 160px;
 | 
			
		||||
  @media (max-width: 872px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.timestamp {
 | 
			
		||||
  width: 18%;
 | 
			
		||||
  @media (max-width: 800px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 1000px) {
 | 
			
		||||
    .relative-time {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.timestamp.widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  @media (min-width: 768px) AND (max-width: 1050px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 767px) {
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 500px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,109 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
			
		||||
import { Observable, Subject, combineLatest, of, timer } from 'rxjs';
 | 
			
		||||
import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../../services/api.service';
 | 
			
		||||
import { Env, StateService } from '../../../services/state.service';
 | 
			
		||||
import { AuditStatus, CurrentPegs, FederationUtxo } from '../../../interfaces/node-api.interface';
 | 
			
		||||
import { WebsocketService } from '../../../services/websocket.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-federation-utxos-list',
 | 
			
		||||
  templateUrl: './federation-utxos-list.component.html',
 | 
			
		||||
  styleUrls: ['./federation-utxos-list.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class FederationUtxosListComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() federationUtxos$: Observable<FederationUtxo[]>;
 | 
			
		||||
 | 
			
		||||
  env: Env;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  page = 1;
 | 
			
		||||
  pageSize = 15;
 | 
			
		||||
  maxSize = window.innerWidth <= 767.98 ? 3 : 5;
 | 
			
		||||
  skeletonLines: number[] = [];
 | 
			
		||||
  auditStatus$: Observable<AuditStatus>;
 | 
			
		||||
  auditUpdated$: Observable<boolean>;
 | 
			
		||||
  lastReservesBlockUpdate: number = 0;
 | 
			
		||||
  currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
  lastPegBlockUpdate: number = 0;
 | 
			
		||||
  lastPegAmount: string = '';
 | 
			
		||||
  isLoad: boolean = true;
 | 
			
		||||
 | 
			
		||||
  private destroy$ = new Subject();
 | 
			
		||||
  
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLoading = !this.widget;
 | 
			
		||||
    this.env = this.stateService.env;
 | 
			
		||||
    this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
 | 
			
		||||
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.websocketService.want(['blocks']);
 | 
			
		||||
      this.auditStatus$ = this.stateService.blocks$.pipe(
 | 
			
		||||
        takeUntil(this.destroy$),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
 | 
			
		||||
        tap(() => this.isLoad = false),
 | 
			
		||||
        switchMap(() => this.apiService.federationAuditSynced$()),
 | 
			
		||||
        shareReplay(1)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.currentPeg$ = this.auditStatus$.pipe(
 | 
			
		||||
        filter(auditStatus => auditStatus.isAuditSynced === true),
 | 
			
		||||
        switchMap(_ =>
 | 
			
		||||
          this.apiService.liquidPegs$().pipe(
 | 
			
		||||
            filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
 | 
			
		||||
            tap((currentPegs) => {
 | 
			
		||||
              this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.auditUpdated$ = combineLatest([
 | 
			
		||||
        this.auditStatus$,
 | 
			
		||||
        this.currentPeg$
 | 
			
		||||
      ]).pipe(
 | 
			
		||||
        filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
 | 
			
		||||
        map(([auditStatus, currentPeg]) => ({
 | 
			
		||||
          lastBlockAudit: auditStatus.lastBlockAudit,
 | 
			
		||||
          currentPegAmount: currentPeg.amount
 | 
			
		||||
        })),
 | 
			
		||||
        switchMap(({ lastBlockAudit, currentPegAmount }) => {
 | 
			
		||||
          const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
 | 
			
		||||
          const amountCheck = currentPegAmount !== this.lastPegAmount;
 | 
			
		||||
          this.lastReservesBlockUpdate = lastBlockAudit;
 | 
			
		||||
          this.lastPegAmount = currentPegAmount;
 | 
			
		||||
          return of(blockAuditCheck || amountCheck);
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.federationUtxos$ = this.auditUpdated$.pipe(
 | 
			
		||||
        filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        switchMap(_ => this.apiService.federationUtxos$()),
 | 
			
		||||
        tap(_ => this.isLoading = false),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.destroy$.next(1);
 | 
			
		||||
    this.destroy$.complete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pageChange(page: number): void {
 | 
			
		||||
    this.page = page;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
<div class="container-xl">
 | 
			
		||||
  <div>
 | 
			
		||||
    <h1 i18n="liquid.federation-wallet">Liquid Federation Wallet</h1>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="nav-container">
 | 
			
		||||
    <ul class="nav nav-pills">
 | 
			
		||||
      <li class="nav-item">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/audit/wallet/utxos' | relativeUrl]" routerLinkActive="active">UTXOs</a>
 | 
			
		||||
        
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]" routerLinkActive="active"><ng-container i18n="mining.addresses">Addresses</ng-container></a>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
  <router-outlet></router-outlet>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<br>
 | 
			
		||||
@ -0,0 +1,13 @@
 | 
			
		||||
ul {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767.98px) {
 | 
			
		||||
  .nav-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { SeoService } from '../../../services/seo.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-federation-wallet',
 | 
			
		||||
  templateUrl: './federation-wallet.component.html',
 | 
			
		||||
  styleUrls: ['./federation-wallet.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class FederationWalletComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private seoService: SeoService
 | 
			
		||||
  ) {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@993e5bc509c26db81d93018e24a6afe6e50cae52:Liquid Federation Wallet`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,139 @@
 | 
			
		||||
<div class="container-xl">
 | 
			
		||||
  <div [ngClass]="{'widget': widget}">
 | 
			
		||||
 | 
			
		||||
    <div *ngIf="!widget">
 | 
			
		||||
      <h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="clearfix"></div>
 | 
			
		||||
    
 | 
			
		||||
    <div style="min-height: 295px">
 | 
			
		||||
      <table class="table table-borderless">
 | 
			
		||||
        <thead style="vertical-align: middle;">
 | 
			
		||||
          <th class="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th>
 | 
			
		||||
          <th class="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
 | 
			
		||||
          <th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
 | 
			
		||||
          <th class="output text-left" *ngIf="!widget" i18n="liquid.fund-redemption-tx">Fund / Redemption Tx</th>
 | 
			
		||||
          <th class="address text-left" *ngIf="!widget" i18n="liquid.bitcoin-address">BTC Address</th>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody *ngIf="recentPegs$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
 | 
			
		||||
          <ng-container *ngIf="widget; else regularRows">
 | 
			
		||||
            <tr *ngFor="let peg of pegs | slice:0:6">
 | 
			
		||||
              <td class="transaction text-left widget">
 | 
			
		||||
                <ng-container *ngIf="peg.amount > 0">
 | 
			
		||||
                  <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
 | 
			
		||||
                    <app-truncate [text]="peg.txid"></app-truncate>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
                <ng-container *ngIf="peg.amount < 0">
 | 
			
		||||
                  <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
 | 
			
		||||
                    <app-truncate [text]="peg.txid"></app-truncate>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="timestamp text-left widget">
 | 
			
		||||
                <app-time kind="since" [time]="peg.blocktime"></app-time>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
 | 
			
		||||
                <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-template #regularRows>
 | 
			
		||||
            <tr *ngFor="let peg of pegs | slice:(page - 1) * pageSize:page * pageSize">
 | 
			
		||||
              <td class="transaction text-left">
 | 
			
		||||
                <ng-container *ngIf="peg.amount > 0">
 | 
			
		||||
                  <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
 | 
			
		||||
                    <app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
                <ng-container *ngIf="peg.amount < 0">
 | 
			
		||||
                  <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
 | 
			
		||||
                    <app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="timestamp text-left">
 | 
			
		||||
                ‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
                <div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
 | 
			
		||||
                <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="output text-left">
 | 
			
		||||
                <ng-container *ngIf="peg.bitcointxid; else redeemInProgress">
 | 
			
		||||
                  <a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + peg.bitcointxid + ':' + peg.bitcoinindex }}" target="_blank" style="color:#b86d12">
 | 
			
		||||
                    <app-truncate [text]="peg.bitcointxid + ':' + peg.bitcoinindex" [lastChars]="6"></app-truncate>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
                <ng-template #redeemInProgress>
 | 
			
		||||
                  <ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
 | 
			
		||||
                    <i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i>
 | 
			
		||||
                  </ng-container>
 | 
			
		||||
                </ng-template>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="address text-left">
 | 
			
		||||
                <ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
 | 
			
		||||
                  <a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + peg.bitcoinaddress }}" target="_blank" style="color:#b86d12">
 | 
			
		||||
                    <app-truncate [text]="peg.bitcoinaddress" [lastChars]="6"></app-truncate>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        </tbody>
 | 
			
		||||
        <ng-template #skeleton>
 | 
			
		||||
          <tbody *ngIf="widget; else regularRowsSkeleton">
 | 
			
		||||
            <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
              <td class="transaction text-left widget">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 400px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="timestamp text-left widget">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="amount text-right widget">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
          <ng-template #regularRowsSkeleton>
 | 
			
		||||
            <tr *ngFor="let item of skeletonLines">
 | 
			
		||||
              <td class="transaction text-left">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="timestamp text-left">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 140px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="amount text-right">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 140px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="output text-left">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 300px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="address text-left">
 | 
			
		||||
                <span class="skeleton-loader" style="max-width: 140px"></span>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </table>
 | 
			
		||||
 | 
			
		||||
      <ngb-pagination *ngIf="!widget && recentPegs$ | async as pegs" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
 | 
			
		||||
        [collectionSize]="pegs.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
 | 
			
		||||
        (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
 | 
			
		||||
      </ngb-pagination>
 | 
			
		||||
 | 
			
		||||
      <ng-template [ngIf]="!widget">
 | 
			
		||||
        <div class="clearfix"></div>
 | 
			
		||||
        <br>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<br>
 | 
			
		||||
 | 
			
		||||
<ng-template #noRedeem>
 | 
			
		||||
  <span class="text-muted">-</span>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,107 @@
 | 
			
		||||
.spinner-border {
 | 
			
		||||
  height: 25px;
 | 
			
		||||
  width: 25px;
 | 
			
		||||
  margin-top: 13px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr, td, th {
 | 
			
		||||
  border: 0px;
 | 
			
		||||
  padding-top: 0.65rem !important;
 | 
			
		||||
  padding-bottom: 0.6rem !important;
 | 
			
		||||
  padding-right: 2rem !important;
 | 
			
		||||
  .widget {
 | 
			
		||||
    padding-right: 1rem !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clear-link {
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabled {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  opacity: 0.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress {
 | 
			
		||||
  background-color: #2d3348;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.transaction {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 120px;
 | 
			
		||||
}
 | 
			
		||||
.transaction.widget {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.address {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 160px;
 | 
			
		||||
  @media (max-width: 527px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.amount {
 | 
			
		||||
  width: 0%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.output {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 160px;
 | 
			
		||||
  @media (max-width: 800px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.address {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-width: 160px;
 | 
			
		||||
  @media (max-width: 960px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.timestamp {
 | 
			
		||||
  width: 0%;
 | 
			
		||||
  @media (max-width: 650px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 1000px) {
 | 
			
		||||
    .relative-time {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.timestamp.widget {
 | 
			
		||||
  @media (min-width: 768px) AND (max-width: 1050px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 767px) {
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 500px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.credit {
 | 
			
		||||
  color: #7CB342;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.debit {
 | 
			
		||||
  color: #D81B60;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,154 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
			
		||||
import { Observable, Subject, combineLatest, of, timer } from 'rxjs';
 | 
			
		||||
import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../../services/api.service';
 | 
			
		||||
import { Env, StateService } from '../../../services/state.service';
 | 
			
		||||
import { AuditStatus, CurrentPegs, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface';
 | 
			
		||||
import { WebsocketService } from '../../../services/websocket.service';
 | 
			
		||||
import { SeoService } from '../../../services/seo.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-recent-pegs-list',
 | 
			
		||||
  templateUrl: './recent-pegs-list.component.html',
 | 
			
		||||
  styleUrls: ['./recent-pegs-list.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class RecentPegsListComponent implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() recentPegIns$: Observable<RecentPeg[]> = of([]);
 | 
			
		||||
  @Input() recentPegOuts$: Observable<RecentPeg[]> = of([]);
 | 
			
		||||
 | 
			
		||||
  env: Env;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  page = 1;
 | 
			
		||||
  pageSize = 15;
 | 
			
		||||
  maxSize = window.innerWidth <= 767.98 ? 3 : 5;
 | 
			
		||||
  skeletonLines: number[] = [];
 | 
			
		||||
  auditStatus$: Observable<AuditStatus>;
 | 
			
		||||
  auditUpdated$: Observable<boolean>;
 | 
			
		||||
  federationUtxos$: Observable<FederationUtxo[]>;
 | 
			
		||||
  recentPegs$: Observable<RecentPeg[]>;
 | 
			
		||||
  lastReservesBlockUpdate: number = 0;
 | 
			
		||||
  currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
  lastPegBlockUpdate: number = 0;
 | 
			
		||||
  lastPegAmount: string = '';
 | 
			
		||||
  isLoad: boolean = true;
 | 
			
		||||
 | 
			
		||||
  private destroy$ = new Subject();
 | 
			
		||||
  
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private seoService: SeoService
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLoading = !this.widget;
 | 
			
		||||
    this.env = this.stateService.env;
 | 
			
		||||
    this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
 | 
			
		||||
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.seoService.setTitle($localize`:@@a8b0889ea1b41888f1e247f2731cc9322198ca04:Recent Peg-In / Out's`);
 | 
			
		||||
      this.websocketService.want(['blocks']);
 | 
			
		||||
      this.auditStatus$ = this.stateService.blocks$.pipe(
 | 
			
		||||
        takeUntil(this.destroy$),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
 | 
			
		||||
        tap(() => this.isLoad = false),
 | 
			
		||||
        switchMap(() => this.apiService.federationAuditSynced$()),
 | 
			
		||||
        shareReplay(1)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.currentPeg$ = this.auditStatus$.pipe(
 | 
			
		||||
        filter(auditStatus => auditStatus.isAuditSynced === true),
 | 
			
		||||
        switchMap(_ =>
 | 
			
		||||
          this.apiService.liquidPegs$().pipe(
 | 
			
		||||
            filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
 | 
			
		||||
            tap((currentPegs) => {
 | 
			
		||||
              this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.auditUpdated$ = combineLatest([
 | 
			
		||||
        this.auditStatus$,
 | 
			
		||||
        this.currentPeg$
 | 
			
		||||
      ]).pipe(
 | 
			
		||||
        filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
 | 
			
		||||
        map(([auditStatus, currentPeg]) => ({
 | 
			
		||||
          lastBlockAudit: auditStatus.lastBlockAudit,
 | 
			
		||||
          currentPegAmount: currentPeg.amount
 | 
			
		||||
        })),
 | 
			
		||||
        switchMap(({ lastBlockAudit, currentPegAmount }) => {
 | 
			
		||||
          const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
 | 
			
		||||
          const amountCheck = currentPegAmount !== this.lastPegAmount;
 | 
			
		||||
          this.lastReservesBlockUpdate = lastBlockAudit;
 | 
			
		||||
          this.lastPegAmount = currentPegAmount;
 | 
			
		||||
          return of(blockAuditCheck || amountCheck);
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.federationUtxos$ = this.auditUpdated$.pipe(
 | 
			
		||||
        filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        switchMap(_ => this.apiService.federationUtxos$()),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.recentPegIns$ = this.federationUtxos$.pipe(
 | 
			
		||||
        map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => {
 | 
			
		||||
          return {
 | 
			
		||||
            txid: utxo.pegtxid,
 | 
			
		||||
            txindex: utxo.pegindex,
 | 
			
		||||
            amount: utxo.amount,
 | 
			
		||||
            bitcoinaddress: utxo.bitcoinaddress,
 | 
			
		||||
            bitcointxid: utxo.txid,
 | 
			
		||||
            bitcoinindex: utxo.txindex,
 | 
			
		||||
            blocktime: utxo.pegblocktime,
 | 
			
		||||
          }
 | 
			
		||||
        })),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.recentPegOuts$ = this.auditUpdated$.pipe(
 | 
			
		||||
        filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        switchMap(_ => this.apiService.recentPegOuts$()),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
  
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.recentPegs$ = combineLatest([
 | 
			
		||||
      this.recentPegIns$,
 | 
			
		||||
      this.recentPegOuts$
 | 
			
		||||
    ]).pipe(
 | 
			
		||||
      map(([recentPegIns, recentPegOuts]) => {
 | 
			
		||||
        return [
 | 
			
		||||
          ...recentPegIns,
 | 
			
		||||
          ...recentPegOuts
 | 
			
		||||
        ].sort((a, b) => {
 | 
			
		||||
          return b.blocktime - a.blocktime;
 | 
			
		||||
        });
 | 
			
		||||
      }),
 | 
			
		||||
      filter(recentPegs => recentPegs.length > 0),
 | 
			
		||||
      tap(_ => this.isLoading = false),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.destroy$.next(1);
 | 
			
		||||
    this.destroy$.complete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pageChange(page: number): void {
 | 
			
		||||
    this.page = page;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
<div class="fee-estimation-container">
 | 
			
		||||
  <div class="item">
 | 
			
		||||
    <a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
 | 
			
		||||
      <h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,71 @@
 | 
			
		||||
.fee-estimation-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  padding-bottom: 1rem;
 | 
			
		||||
  @media (min-width: 376px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }  
 | 
			
		||||
  .item {
 | 
			
		||||
    max-width: 300px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: -webkit-fill-available;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      margin: 0 auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-title {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      color: #4a68b9;
 | 
			
		||||
      font-size: 10px;
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-text {
 | 
			
		||||
      font-size: 22px;
 | 
			
		||||
      span {
 | 
			
		||||
        font-size: 11px;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: -2px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-text span {
 | 
			
		||||
      color: #ffffff66;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      top: 0px;
 | 
			
		||||
    }
 | 
			
		||||
    .fee-text{
 | 
			
		||||
      width: fit-content;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      line-height: 1.45;
 | 
			
		||||
      padding: 0px 2px;
 | 
			
		||||
    }
 | 
			
		||||
    .fiat {
 | 
			
		||||
      display: block;
 | 
			
		||||
      font-size: 14px !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  .skeleton-loader {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      max-width: 90px;
 | 
			
		||||
      margin: 15px auto 3px;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin: 10px auto 3px;
 | 
			
		||||
      max-width: 55px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin-bottom: 4px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-recent-pegs-stats',
 | 
			
		||||
  templateUrl: './recent-pegs-stats.component.html',
 | 
			
		||||
  styleUrls: ['./recent-pegs-stats.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class RecentPegsStatsComponent implements OnInit {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,98 @@
 | 
			
		||||
<div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress">
 | 
			
		||||
 | 
			
		||||
  <div class="row row-cols-1 row-cols-md-2">
 | 
			
		||||
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
 | 
			
		||||
          <app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col" style="margin-bottom: 1.47rem">
 | 
			
		||||
      <div class="card"> 
 | 
			
		||||
        <div class="card-title">
 | 
			
		||||
          <app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body pl-0" style="padding-top: 10px;">
 | 
			
		||||
          <app-reserves-ratio-graph [data]="fullHistory$ | async"></app-reserves-ratio-graph>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-recent-pegs-stats></app-recent-pegs-stats>
 | 
			
		||||
          <app-recent-pegs-list [recentPegIns$]="recentPegIns$" [recentPegOuts$]="recentPegOuts$"[widget]="true"></app-recent-pegs-list>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col" style="margin-bottom: 1.47rem">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <app-federation-addresses-stats [federationAddresses$]="federationAddresses$" [federationAddressesOneMonthAgo$]="federationAddressesOneMonthAgo$"></app-federation-addresses-stats>
 | 
			
		||||
          <app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingSkeleton>
 | 
			
		||||
  <div class="container-xl dashboard-container">
 | 
			
		||||
 | 
			
		||||
    <div class="row row-cols-1 row-cols-md-2">
 | 
			
		||||
  
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <app-reserves-supply-stats></app-reserves-supply-stats>
 | 
			
		||||
            <app-reserves-ratio></app-reserves-ratio>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="col" style="margin-bottom: 1.47rem">
 | 
			
		||||
        <div class="card"> 
 | 
			
		||||
          <div class="card-title">
 | 
			
		||||
            <app-reserves-ratio-stats></app-reserves-ratio-stats>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="card-body pl-0" style="padding-top: 10px;">
 | 
			
		||||
            <app-reserves-ratio-graph></app-reserves-ratio-graph>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
  
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <app-recent-pegs-stats></app-recent-pegs-stats>
 | 
			
		||||
            <app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
  
 | 
			
		||||
      <div class="col" style="margin-bottom: 1.47rem">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <app-federation-addresses-stats></app-federation-addresses-stats>
 | 
			
		||||
            <app-federation-addresses-list [widget]="true"></app-federation-addresses-list>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #auditInProgress>
 | 
			
		||||
  <ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeleton">
 | 
			
		||||
    <div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeleton">
 | 
			
		||||
      <span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,138 @@
 | 
			
		||||
.dashboard-container {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
  .col {
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title {
 | 
			
		||||
  padding-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-body.pool-ranking {
 | 
			
		||||
  padding: 1.25rem 0.25rem 0.75rem 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
.card-text {
 | 
			
		||||
  font-size: 22px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#blockchain-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow-x: scroll;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
  scrollbar-width: none;
 | 
			
		||||
  -ms-overflow-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#blockchain-container::-webkit-scrollbar {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-border {
 | 
			
		||||
  -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.in-progress-message {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more-padding {
 | 
			
		||||
  padding: 24px 20px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-wrapper {
 | 
			
		||||
  .card {
 | 
			
		||||
    height: auto !important;
 | 
			
		||||
  }
 | 
			
		||||
  .card-body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex: inherit;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: space-around;
 | 
			
		||||
    padding: 22px 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.skeleton-loader {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: block;
 | 
			
		||||
  &:first-child {
 | 
			
		||||
    max-width: 90px;
 | 
			
		||||
    margin: 15px auto 3px;
 | 
			
		||||
  }
 | 
			
		||||
  &:last-child {
 | 
			
		||||
    margin: 10px auto 3px;
 | 
			
		||||
    max-width: 55px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  font-size: 22px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.lastest-blocks-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  tr, td, th {
 | 
			
		||||
    border: 0px;
 | 
			
		||||
    padding-top: 0.65rem !important;
 | 
			
		||||
    padding-bottom: 0.8rem !important;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-height {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-fee {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-pool {
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    width: 30%;
 | 
			
		||||
 | 
			
		||||
    @media (max-width: 875px) {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .pool-name {
 | 
			
		||||
      margin-left: 1em;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .table-cell-acceleration-count {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    width: 20%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  height: 385px;
 | 
			
		||||
}
 | 
			
		||||
.list-card {
 | 
			
		||||
  height: 410px;
 | 
			
		||||
  @media (max-width: 767px) {
 | 
			
		||||
    height: auto;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mempool-block-wrapper {
 | 
			
		||||
  max-height: 380px;
 | 
			
		||||
  max-width: 380px;
 | 
			
		||||
  margin: auto;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,204 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { SeoService } from '../../../services/seo.service';
 | 
			
		||||
import { WebsocketService } from '../../../services/websocket.service';
 | 
			
		||||
import { StateService } from '../../../services/state.service';
 | 
			
		||||
import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs';
 | 
			
		||||
import { ApiService } from '../../../services/api.service';
 | 
			
		||||
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-reserves-audit-dashboard',
 | 
			
		||||
  templateUrl: './reserves-audit-dashboard.component.html',
 | 
			
		||||
  styleUrls: ['./reserves-audit-dashboard.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ReservesAuditDashboardComponent implements OnInit {
 | 
			
		||||
  auditStatus$: Observable<AuditStatus>;
 | 
			
		||||
  auditUpdated$: Observable<boolean>;
 | 
			
		||||
  currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
  currentReserves$: Observable<CurrentPegs>;
 | 
			
		||||
  federationUtxos$: Observable<FederationUtxo[]>;
 | 
			
		||||
  recentPegIns$: Observable<RecentPeg[]>;
 | 
			
		||||
  recentPegOuts$: Observable<RecentPeg[]>;
 | 
			
		||||
  federationAddresses$: Observable<FederationAddress[]>;
 | 
			
		||||
  federationAddressesOneMonthAgo$: Observable<any>;
 | 
			
		||||
  liquidPegsMonth$: Observable<any>;
 | 
			
		||||
  liquidReservesMonth$: Observable<any>;
 | 
			
		||||
  fullHistory$: Observable<any>;
 | 
			
		||||
  isLoad: boolean = true;
 | 
			
		||||
  private lastPegBlockUpdate: number = 0;
 | 
			
		||||
  private lastPegAmount: string = '';
 | 
			
		||||
  private lastReservesBlockUpdate: number = 0;
 | 
			
		||||
 | 
			
		||||
  private destroy$ = new Subject();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@liquid.reserves-audit:Reserves Audit Dashboard`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.websocketService.want(['blocks', 'mempool-blocks']);
 | 
			
		||||
 | 
			
		||||
    this.auditStatus$ = this.stateService.blocks$.pipe(
 | 
			
		||||
      takeUntil(this.destroy$),
 | 
			
		||||
      throttleTime(40000),
 | 
			
		||||
      delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
 | 
			
		||||
      tap(() => this.isLoad = false),
 | 
			
		||||
      switchMap(() => this.apiService.federationAuditSynced$()),
 | 
			
		||||
      shareReplay(1),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.currentPeg$ = this.auditStatus$.pipe(
 | 
			
		||||
      filter(auditStatus => auditStatus.isAuditSynced === true),
 | 
			
		||||
      switchMap(_ =>
 | 
			
		||||
        this.apiService.liquidPegs$().pipe(
 | 
			
		||||
          filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
 | 
			
		||||
          tap((currentPegs) => {
 | 
			
		||||
            this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.auditUpdated$ = combineLatest([
 | 
			
		||||
      this.auditStatus$,
 | 
			
		||||
      this.currentPeg$
 | 
			
		||||
    ]).pipe(
 | 
			
		||||
      filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
 | 
			
		||||
      map(([auditStatus, currentPeg]) => ({
 | 
			
		||||
        lastBlockAudit: auditStatus.lastBlockAudit,
 | 
			
		||||
        currentPegAmount: currentPeg.amount
 | 
			
		||||
      })),
 | 
			
		||||
      switchMap(({ lastBlockAudit, currentPegAmount }) => {
 | 
			
		||||
        const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
 | 
			
		||||
        const amountCheck = currentPegAmount !== this.lastPegAmount;
 | 
			
		||||
        this.lastPegAmount = currentPegAmount;
 | 
			
		||||
        return of(blockAuditCheck || amountCheck);
 | 
			
		||||
      }),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.currentReserves$ = this.auditUpdated$.pipe(
 | 
			
		||||
      filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
      throttleTime(40000),
 | 
			
		||||
      switchMap(_ =>
 | 
			
		||||
        this.apiService.liquidReserves$().pipe(
 | 
			
		||||
          filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate),
 | 
			
		||||
          tap((currentReserves) => {
 | 
			
		||||
            this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate;
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.federationUtxos$ = this.auditUpdated$.pipe(
 | 
			
		||||
      filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
      throttleTime(40000),
 | 
			
		||||
      switchMap(_ => this.apiService.federationUtxos$()),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.recentPegIns$ = this.federationUtxos$.pipe(
 | 
			
		||||
      map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => {
 | 
			
		||||
        return {
 | 
			
		||||
          txid: utxo.pegtxid,
 | 
			
		||||
          txindex: utxo.pegindex,
 | 
			
		||||
          amount: utxo.amount,
 | 
			
		||||
          bitcoinaddress: utxo.bitcoinaddress,
 | 
			
		||||
          bitcointxid: utxo.txid,
 | 
			
		||||
          bitcoinindex: utxo.txindex,
 | 
			
		||||
          blocktime: utxo.pegblocktime,
 | 
			
		||||
        }
 | 
			
		||||
      })),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.recentPegOuts$ = this.auditUpdated$.pipe(
 | 
			
		||||
      filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
      throttleTime(40000),
 | 
			
		||||
      switchMap(_ => this.apiService.recentPegOuts$()),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.federationAddresses$ = this.auditUpdated$.pipe(
 | 
			
		||||
      filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
      throttleTime(40000),
 | 
			
		||||
      switchMap(_ => this.apiService.federationAddresses$()),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.federationAddressesOneMonthAgo$ = interval(60 * 60 * 1000)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        startWith(0),
 | 
			
		||||
        switchMap(() => this.apiService.federationAddressesOneMonthAgo$())
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.liquidPegsMonth$ = interval(60 * 60 * 1000)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        startWith(0),
 | 
			
		||||
        switchMap(() => this.apiService.listLiquidPegsMonth$()),
 | 
			
		||||
        map((pegs) => {
 | 
			
		||||
          const labels = pegs.map(stats => stats.date);
 | 
			
		||||
          const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
 | 
			
		||||
          series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
 | 
			
		||||
          return {
 | 
			
		||||
            series,
 | 
			
		||||
            labels
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
        share(),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
 | 
			
		||||
      startWith(0),
 | 
			
		||||
      switchMap(() => this.apiService.listLiquidReservesMonth$()),
 | 
			
		||||
      map(reserves => {
 | 
			
		||||
        const labels = reserves.map(stats => stats.date);
 | 
			
		||||
        const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
 | 
			
		||||
        return {
 | 
			
		||||
          series,
 | 
			
		||||
          labels
 | 
			
		||||
        };
 | 
			
		||||
      }),
 | 
			
		||||
      share()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$])
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
 | 
			
		||||
          liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
 | 
			
		||||
 | 
			
		||||
          if (liquidPegs.series.length === liquidReserves?.series.length) {
 | 
			
		||||
            liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
 | 
			
		||||
          } else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
 | 
			
		||||
            liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000);
 | 
			
		||||
            liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]);
 | 
			
		||||
          } else {
 | 
			
		||||
            liquidReserves = {
 | 
			
		||||
              series: [],
 | 
			
		||||
              labels: []
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            liquidPegs,
 | 
			
		||||
            liquidReserves
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.destroy$.next(1);
 | 
			
		||||
    this.destroy$.complete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<div *ngIf="(unbackedMonths$ | async) as unbackedMonths; else loadingData">
 | 
			
		||||
  <ng-container *ngIf="unbackedMonths.historyComplete; else loadingData">
 | 
			
		||||
    <div class="fee-estimation-container">
 | 
			
		||||
      <div class="item"> 
 | 
			
		||||
        <h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5> 
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}">
 | 
			
		||||
            {{ unbackedMonths.total }} <span i18n="liquid.unpeg-event">Unpeg Event</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item"> 
 | 
			
		||||
        <h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5> 
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <div class="fee-text" [ngClass]="{'danger' : unbackedMonths.avg < 1, 'correct': unbackedMonths.avg >= 1}">
 | 
			
		||||
            {{ unbackedMonths.avg.toFixed(5) }}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingData>
 | 
			
		||||
  <div class="fee-estimation-container loading-container">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="item"> 
 | 
			
		||||
      <h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5> 
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,63 @@
 | 
			
		||||
.fee-estimation-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  @media (min-width: 376px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }  
 | 
			
		||||
  .item {
 | 
			
		||||
    max-width: 300px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: -webkit-fill-available;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      margin: 0 auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-title {
 | 
			
		||||
      margin-bottom: 4px;
 | 
			
		||||
      color: #4a68b9;
 | 
			
		||||
      font-size: 10px;
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-text {
 | 
			
		||||
      font-size: 22px;
 | 
			
		||||
      span {
 | 
			
		||||
        font-size: 11px;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: -2px;
 | 
			
		||||
      }
 | 
			
		||||
      .danger {
 | 
			
		||||
        color: #D81B60;
 | 
			
		||||
      }
 | 
			
		||||
      .correct {
 | 
			
		||||
        color: #7CB342;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-text span {
 | 
			
		||||
      color: #ffffff66;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      top: 0px;
 | 
			
		||||
    }
 | 
			
		||||
    .fee-text{
 | 
			
		||||
      width: fit-content;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      line-height: 1.45;
 | 
			
		||||
      padding: 0px 2px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-container{
 | 
			
		||||
  min-height: 76px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  .skeleton-loader {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    max-width: 90px;
 | 
			
		||||
    margin: 15px auto 3px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,51 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { Observable, map } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-reserves-ratio-stats',
 | 
			
		||||
  templateUrl: './reserves-ratio-stats.component.html',
 | 
			
		||||
  styleUrls: ['./reserves-ratio-stats.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ReservesRatioStatsComponent implements OnInit {
 | 
			
		||||
  @Input() fullHistory$: Observable<any>;
 | 
			
		||||
  unbackedMonths$: Observable<any>
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (!this.fullHistory$) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.unbackedMonths$ = this.fullHistory$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((fullHistory) => {
 | 
			
		||||
          if (fullHistory.liquidPegs.series.length !== fullHistory.liquidReserves.series.length) {
 | 
			
		||||
            return {
 | 
			
		||||
              historyComplete: false, 
 | 
			
		||||
              total: null
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
          // Only check the last 3 years
 | 
			
		||||
          let ratioSeries = fullHistory.liquidReserves.series.map((value: number, index: number) => value / fullHistory.liquidPegs.series[index]);
 | 
			
		||||
          ratioSeries = ratioSeries.slice(Math.max(ratioSeries.length - 36, 0));          
 | 
			
		||||
          let total = 0;
 | 
			
		||||
          let avg = 0;
 | 
			
		||||
          for (let i = 0; i < ratioSeries.length; i++) {
 | 
			
		||||
            avg += ratioSeries[i];
 | 
			
		||||
            if (ratioSeries[i] < 1) {
 | 
			
		||||
              total++;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          avg = avg / ratioSeries.length;
 | 
			
		||||
          return {
 | 
			
		||||
            historyComplete: true, 
 | 
			
		||||
            total: total,
 | 
			
		||||
            avg: avg,
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,4 @@
 | 
			
		||||
<div class="echarts" echarts [initOpts]="ratioHistoryChartInitOptions" [options]="ratioHistoryChartOptions" (chartRendered)="rendered()"></div>
 | 
			
		||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
  <div class="spinner-border text-light"></div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
.loadingGraphs {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: calc(50% - 16px);
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,195 @@
 | 
			
		||||
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
 | 
			
		||||
import { formatDate, formatNumber } from '@angular/common';
 | 
			
		||||
import { EChartsOption } from '../../../graphs/echarts';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-reserves-ratio-graph',
 | 
			
		||||
  templateUrl: './reserves-ratio-graph.component.html',
 | 
			
		||||
  styleUrls: ['./reserves-ratio-graph.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ReservesRatioGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() data: any;
 | 
			
		||||
  ratioHistoryChartOptions: EChartsOption;
 | 
			
		||||
  ratioSeries: number[] = [];
 | 
			
		||||
 | 
			
		||||
  height: number | string = '200';
 | 
			
		||||
  right: number | string = '10';
 | 
			
		||||
  top: number | string = '20';
 | 
			
		||||
  left: number | string = '50';
 | 
			
		||||
  template: ('widget' | 'advanced') = 'widget';
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
 | 
			
		||||
  ratioHistoryChartInitOptions = {
 | 
			
		||||
    renderer: 'svg'
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) private locale: string,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.isLoading = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
    if (!this.data) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Compute the ratio series: the ratio of the reserves to the pegs
 | 
			
		||||
    this.ratioSeries = this.data.liquidReserves.series.map((value: number, index: number) => value / this.data.liquidPegs.series[index]);
 | 
			
		||||
    // Truncate the ratio series and labels series to last 3 years
 | 
			
		||||
    this.ratioSeries = this.ratioSeries.slice(Math.max(this.ratioSeries.length - 36, 0));
 | 
			
		||||
    this.data.liquidPegs.labels = this.data.liquidPegs.labels.slice(Math.max(this.data.liquidPegs.labels.length - 36, 0));
 | 
			
		||||
    // Cut the values that are too high or too low
 | 
			
		||||
    this.ratioSeries = this.ratioSeries.map((value: number) => Math.min(Math.max(value, 0.995), 1.005));
 | 
			
		||||
    this.ratioHistoryChartOptions = this.createChartOptions(this.ratioSeries, this.data.liquidPegs.labels);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  rendered() {
 | 
			
		||||
    if (!this.data) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.isLoading = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createChartOptions(ratioSeries: number[], labels: string[]): EChartsOption {
 | 
			
		||||
    return {
 | 
			
		||||
      grid: {
 | 
			
		||||
        height: this.height,
 | 
			
		||||
        right: this.right,
 | 
			
		||||
        top: this.top,
 | 
			
		||||
        left: this.left,
 | 
			
		||||
      },
 | 
			
		||||
      animation: false,
 | 
			
		||||
      dataZoom: [{
 | 
			
		||||
        type: 'inside',
 | 
			
		||||
        realtime: true,
 | 
			
		||||
        zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
 | 
			
		||||
        maxSpan: 100,
 | 
			
		||||
        minSpan: 10,
 | 
			
		||||
      }, {
 | 
			
		||||
        show: (this.template === 'advanced') ? true : false,
 | 
			
		||||
        type: 'slider',
 | 
			
		||||
        brushSelect: false,
 | 
			
		||||
        realtime: true,
 | 
			
		||||
        selectedDataBackground: {
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            color: '#fff',
 | 
			
		||||
            opacity: 0.45,
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            opacity: 0,
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }],
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        position: (pos, params, el, elRect, size) => {
 | 
			
		||||
          const obj = { top: -20 };
 | 
			
		||||
          obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
 | 
			
		||||
          return obj;
 | 
			
		||||
        },
 | 
			
		||||
        extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'};
 | 
			
		||||
                      background: transparent;
 | 
			
		||||
                      border: none;
 | 
			
		||||
                      box-shadow: none;`,
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line',
 | 
			
		||||
        },
 | 
			
		||||
        formatter: (params: any) => {
 | 
			
		||||
          const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
 | 
			
		||||
          let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
 | 
			
		||||
          const item = params[0];
 | 
			
		||||
          const formattedValue = formatNumber(item.value, this.locale, '1.5-5');
 | 
			
		||||
          const symbol = (item.value === 1.005) ? '≥ ' : (item.value === 0.995) ? '≤ ' : '';
 | 
			
		||||
          itemFormatted += `<div class="item">
 | 
			
		||||
            <div class="indicator-container">${colorSpan(item.color)}</div>
 | 
			
		||||
            <div style="margin-right: 5px"></div>
 | 
			
		||||
            <div class="value">${symbol}${formattedValue}</div>
 | 
			
		||||
          </div>`;
 | 
			
		||||
          return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: {
 | 
			
		||||
        type: 'category',
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          align: 'center',
 | 
			
		||||
          fontSize: 11,
 | 
			
		||||
          lineHeight: 12
 | 
			
		||||
        },
 | 
			
		||||
        boundaryGap: false,
 | 
			
		||||
        data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`),
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: {
 | 
			
		||||
        type: 'value',
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          fontSize: 11,
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            type: 'dotted',
 | 
			
		||||
            color: '#ffffff66',
 | 
			
		||||
            opacity: 0.25,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        min: 0.995,
 | 
			
		||||
        max: 1.005,
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          data: ratioSeries,
 | 
			
		||||
          name: '',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 3,
 | 
			
		||||
            
 | 
			
		||||
          },
 | 
			
		||||
          markLine: {
 | 
			
		||||
            silent: true,
 | 
			
		||||
            symbol: 'none',
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#fff',
 | 
			
		||||
              opacity: 1,
 | 
			
		||||
              width: 1,
 | 
			
		||||
            },
 | 
			
		||||
            data: [{
 | 
			
		||||
              yAxis: 1,
 | 
			
		||||
              label: {
 | 
			
		||||
                show: false,
 | 
			
		||||
                color: '#ffffff',
 | 
			
		||||
              }
 | 
			
		||||
            }],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      visualMap: {
 | 
			
		||||
        show: false,
 | 
			
		||||
        top: 50,
 | 
			
		||||
        right: 10,
 | 
			
		||||
        pieces: [{
 | 
			
		||||
          gt: 0,
 | 
			
		||||
          lte: 0.999,
 | 
			
		||||
          color: '#D81B60'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          gt: 0.999,
 | 
			
		||||
          lte: 1.001,
 | 
			
		||||
          color: '#FDD835'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          gt: 1.001,
 | 
			
		||||
          lte: 2,
 | 
			
		||||
          color: '#7CB342'
 | 
			
		||||
        }
 | 
			
		||||
        ],
 | 
			
		||||
        outOfRange: {
 | 
			
		||||
          color: '#999'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,4 @@
 | 
			
		||||
<div class="echarts" echarts [initOpts]="ratioChartInitOptions" [options]="ratioChartOptions" (chartRendered)="rendered()"></div>
 | 
			
		||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
  <div class="spinner-border text-light"></div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
.loadingGraphs {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: calc(50% - 16px);
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,126 @@
 | 
			
		||||
import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
 | 
			
		||||
import { EChartsOption } from '../../../graphs/echarts';
 | 
			
		||||
import { CurrentPegs } from '../../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-reserves-ratio',
 | 
			
		||||
  templateUrl: './reserves-ratio.component.html',
 | 
			
		||||
  styleUrls: ['./reserves-ratio.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ReservesRatioComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() currentPeg: CurrentPegs;
 | 
			
		||||
  @Input() currentReserves: CurrentPegs;
 | 
			
		||||
  ratioChartOptions: EChartsOption;
 | 
			
		||||
 | 
			
		||||
  height: number | string = '200';
 | 
			
		||||
  right: number | string = '10';
 | 
			
		||||
  top: number | string = '20';
 | 
			
		||||
  left: number | string = '50';
 | 
			
		||||
  template: ('widget' | 'advanced') = 'widget';
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
 | 
			
		||||
  ratioChartInitOptions = {
 | 
			
		||||
    renderer: 'svg'
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.isLoading = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
    if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.ratioChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  rendered() {
 | 
			
		||||
    if (!this.currentPeg || !this.currentReserves) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.isLoading = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createChartOptions(currentPeg: CurrentPegs, currentReserves: CurrentPegs): EChartsOption {
 | 
			
		||||
    return {
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'gauge',
 | 
			
		||||
          startAngle: 180,
 | 
			
		||||
          endAngle: 0,
 | 
			
		||||
          center: ['50%', '70%'],
 | 
			
		||||
          radius: '100%',
 | 
			
		||||
          min: 0.999,
 | 
			
		||||
          max: 1.001,
 | 
			
		||||
          splitNumber: 2,
 | 
			
		||||
          axisLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              width: 6,
 | 
			
		||||
              color: [
 | 
			
		||||
                [0.49, '#D81B60'],
 | 
			
		||||
                [1, '#7CB342']
 | 
			
		||||
              ]
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            color: 'inherit',        
 | 
			
		||||
            fontFamily: 'inherit',    
 | 
			
		||||
          },
 | 
			
		||||
          pointer: {
 | 
			
		||||
            icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
 | 
			
		||||
            length: '50%',
 | 
			
		||||
            width: 16,
 | 
			
		||||
            offsetCenter: [0, '-27%'],
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              color: 'auto'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          axisTick: {
 | 
			
		||||
            length: 12,
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: 'auto',
 | 
			
		||||
              width: 2
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            length: 20,
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: 'auto',
 | 
			
		||||
              width: 5
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          title: {
 | 
			
		||||
            show: true,
 | 
			
		||||
            offsetCenter: [0, '-117.5%'],
 | 
			
		||||
            fontSize: 18,
 | 
			
		||||
            color: '#4a68b9',
 | 
			
		||||
            fontFamily: 'inherit',
 | 
			
		||||
            fontWeight: 500,
 | 
			
		||||
          },
 | 
			
		||||
          detail: {
 | 
			
		||||
            fontSize: 25,
 | 
			
		||||
            offsetCenter: [0, '-0%'],
 | 
			
		||||
            valueAnimation: true,
 | 
			
		||||
            fontFamily: 'inherit',
 | 
			
		||||
            fontWeight: 500,
 | 
			
		||||
            formatter: function (value) {
 | 
			
		||||
              return (value).toFixed(5);
 | 
			
		||||
            },
 | 
			
		||||
            color: 'inherit'
 | 
			
		||||
          },
 | 
			
		||||
          data: [
 | 
			
		||||
            {
 | 
			
		||||
              value: parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount),
 | 
			
		||||
              name: 'Peg-O-Meter'
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
<div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData">
 | 
			
		||||
  <div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData">
 | 
			
		||||
    <div class="fee-estimation-container">
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
 | 
			
		||||
          <span class="fiat">
 | 
			
		||||
            <span>As of block <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves</h5>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div>
 | 
			
		||||
          <span class="fiat">
 | 
			
		||||
            <span>As of block <a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingData>
 | 
			
		||||
  <div class="fee-estimation-container loading-container">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,73 @@
 | 
			
		||||
.fee-estimation-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  @media (min-width: 376px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }  
 | 
			
		||||
  .item {
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: -webkit-fill-available;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      margin: 0 auto 0px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-title {
 | 
			
		||||
      color: #4a68b9;
 | 
			
		||||
      font-size: 10px;
 | 
			
		||||
      margin-bottom: 4px;  
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .card-text {
 | 
			
		||||
      font-size: 22px;
 | 
			
		||||
      span {
 | 
			
		||||
        font-size: 11px;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: -2px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
    .card-text span {
 | 
			
		||||
      color: #ffffff66;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      top: 0px;
 | 
			
		||||
    }
 | 
			
		||||
    .fee-text{
 | 
			
		||||
      border-bottom: 1px solid #ffffff1c;
 | 
			
		||||
      width: fit-content;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      line-height: 1.45;
 | 
			
		||||
      padding: 0px 2px;
 | 
			
		||||
    }
 | 
			
		||||
    .fiat {
 | 
			
		||||
      display: block;
 | 
			
		||||
      font-size: 14px !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-container{
 | 
			
		||||
  min-height: 76px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  .skeleton-loader {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      max-width: 90px;
 | 
			
		||||
      margin: 15px auto 3px;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin: 10px auto 3px;
 | 
			
		||||
      max-width: 55px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Env, StateService } from '../../../services/state.service';
 | 
			
		||||
import { CurrentPegs } from '../../../interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-reserves-supply-stats',
 | 
			
		||||
  templateUrl: './reserves-supply-stats.component.html',
 | 
			
		||||
  styleUrls: ['./reserves-supply-stats.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ReservesSupplyStatsComponent implements OnInit {
 | 
			
		||||
  @Input() currentReserves$: Observable<CurrentPegs>;
 | 
			
		||||
  @Input() currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
 | 
			
		||||
  env: Env;
 | 
			
		||||
 | 
			
		||||
  constructor(private stateService: StateService) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.env = this.stateService.env;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -540,7 +540,7 @@
 | 
			
		||||
              </ng-container>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </div>
 | 
			
		||||
          <button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
 | 
			
		||||
          <button *ngIf="cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
 | 
			
		||||
@ -507,7 +507,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!found && txFeePerVSize < mempoolBlocks[mempoolBlocks.length - 1].feeRange[0]) {
 | 
			
		||||
      if (!found && mempoolBlocks.length && txFeePerVSize < mempoolBlocks[mempoolBlocks.length - 1].feeRange[0]) {
 | 
			
		||||
        this.txInBlockIndex = 7;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <ng-template #liquidPegs>
 | 
			
		||||
            <app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
 | 
			
		||||
            <app-lbtc-pegs-graph [data]="fullHistory$ | async"></app-lbtc-pegs-graph>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -270,8 +270,16 @@
 | 
			
		||||
  <div class="mempool-info-data">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
 | 
			
		||||
      <ng-container *ngIf="(liquidPegsMonth$ | async) as liquidPegsMonth; else loadingTransactions">
 | 
			
		||||
        <p class="card-text">{{ liquidPegsMonth.series.slice(-1)[0] | number: '1.2-2' }} <span>L-BTC</span></p>
 | 
			
		||||
      <ng-container *ngIf="(currentPeg$ | async) as currentPeg; else loadingTransactions">
 | 
			
		||||
        <p i18n-ngbTooltip="liquid.last-elements-audit-block" [ngbTooltip]="'L-BTC supply last updated at Liquid block ' + (currentPeg.lastBlockUpdate)" placement="top" class="card-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></p>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <a class="title-link" [routerLink]="['/audit' | relativeUrl]">
 | 
			
		||||
        <h5 class="card-title"><ng-container i18n="dashboard.btc-reserves">BTC Reserves</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
 | 
			
		||||
      </a>
 | 
			
		||||
      <ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions">
 | 
			
		||||
        <p i18n-ngbTooltip="liquid.last-bitcoin-audit-block" [ngbTooltip]="'BTC reserves last updated at Bitcoin block ' + (currentReserves.lastBlockUpdate)" placement="top" class="card-text">{{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} <span class="bitcoin-color">BTC</span></p>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -97,6 +97,9 @@
 | 
			
		||||
        color: #ffffff66;
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
      }
 | 
			
		||||
      .bitcoin-color {
 | 
			
		||||
        color: #b86d12;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .progress {
 | 
			
		||||
      width: 90%;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
 | 
			
		||||
import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { combineLatest, EMPTY, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
 | 
			
		||||
import { catchError, delayWhen, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
 | 
			
		||||
import { AuditStatus, BlockExtended, CurrentPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
 | 
			
		||||
import { ApiService } from '../services/api.service';
 | 
			
		||||
import { StateService } from '../services/state.service';
 | 
			
		||||
@ -47,8 +47,20 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
  transactionsWeightPerSecondOptions: any;
 | 
			
		||||
  isLoadingWebSocket$: Observable<boolean>;
 | 
			
		||||
  liquidPegsMonth$: Observable<any>;
 | 
			
		||||
  currentPeg$: Observable<CurrentPegs>;
 | 
			
		||||
  auditStatus$: Observable<AuditStatus>;
 | 
			
		||||
  auditUpdated$: Observable<boolean>;
 | 
			
		||||
  liquidReservesMonth$: Observable<any>;
 | 
			
		||||
  currentReserves$: Observable<CurrentPegs>;
 | 
			
		||||
  fullHistory$: Observable<any>;
 | 
			
		||||
  isLoad: boolean = true;
 | 
			
		||||
  currencySubscription: Subscription;
 | 
			
		||||
  currency: string;
 | 
			
		||||
  private lastPegBlockUpdate: number = 0;
 | 
			
		||||
  private lastPegAmount: string = '';
 | 
			
		||||
  private lastReservesBlockUpdate: number = 0;
 | 
			
		||||
 | 
			
		||||
  private destroy$ = new Subject();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
@ -64,6 +76,8 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.currencySubscription.unsubscribe();
 | 
			
		||||
    this.websocketService.stopTrackRbfSummary();
 | 
			
		||||
    this.destroy$.next(1);
 | 
			
		||||
    this.destroy$.complete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
@ -82,35 +96,35 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
      this.stateService.mempoolInfo$,
 | 
			
		||||
      this.stateService.vbytesPerSecond$
 | 
			
		||||
    ])
 | 
			
		||||
    .pipe(
 | 
			
		||||
      map(([mempoolInfo, vbytesPerSecond]) => {
 | 
			
		||||
        const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map(([mempoolInfo, vbytesPerSecond]) => {
 | 
			
		||||
          const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
 | 
			
		||||
 | 
			
		||||
        let progressColor = 'bg-success';
 | 
			
		||||
        if (vbytesPerSecond > 1667) {
 | 
			
		||||
          progressColor = 'bg-warning';
 | 
			
		||||
        }
 | 
			
		||||
        if (vbytesPerSecond > 3000) {
 | 
			
		||||
          progressColor = 'bg-danger';
 | 
			
		||||
        }
 | 
			
		||||
          let progressColor = 'bg-success';
 | 
			
		||||
          if (vbytesPerSecond > 1667) {
 | 
			
		||||
            progressColor = 'bg-warning';
 | 
			
		||||
          }
 | 
			
		||||
          if (vbytesPerSecond > 3000) {
 | 
			
		||||
            progressColor = 'bg-danger';
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
 | 
			
		||||
        let mempoolSizeProgress = 'bg-danger';
 | 
			
		||||
        if (mempoolSizePercentage <= 50) {
 | 
			
		||||
          mempoolSizeProgress = 'bg-success';
 | 
			
		||||
        } else if (mempoolSizePercentage <= 75) {
 | 
			
		||||
          mempoolSizeProgress = 'bg-warning';
 | 
			
		||||
        }
 | 
			
		||||
          const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
 | 
			
		||||
          let mempoolSizeProgress = 'bg-danger';
 | 
			
		||||
          if (mempoolSizePercentage <= 50) {
 | 
			
		||||
            mempoolSizeProgress = 'bg-success';
 | 
			
		||||
          } else if (mempoolSizePercentage <= 75) {
 | 
			
		||||
            mempoolSizeProgress = 'bg-warning';
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          memPoolInfo: mempoolInfo,
 | 
			
		||||
          vBytesPerSecond: vbytesPerSecond,
 | 
			
		||||
          progressWidth: percent + '%',
 | 
			
		||||
          progressColor: progressColor,
 | 
			
		||||
          mempoolSizeProgress: mempoolSizeProgress,
 | 
			
		||||
        };
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
          return {
 | 
			
		||||
            memPoolInfo: mempoolInfo,
 | 
			
		||||
            vBytesPerSecond: vbytesPerSecond,
 | 
			
		||||
            progressWidth: percent + '%',
 | 
			
		||||
            progressColor: progressColor,
 | 
			
		||||
            mempoolSizeProgress: mempoolSizeProgress,
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
@ -204,18 +218,114 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
 | 
			
		||||
      this.liquidPegsMonth$ = this.apiService.listLiquidPegsMonth$()
 | 
			
		||||
      this.auditStatus$ = this.stateService.blocks$.pipe(
 | 
			
		||||
        takeUntil(this.destroy$),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
 | 
			
		||||
        tap(() => this.isLoad = false),
 | 
			
		||||
        switchMap(() => this.apiService.federationAuditSynced$()),
 | 
			
		||||
        shareReplay(1)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      ////////// Pegs historical data //////////
 | 
			
		||||
      this.liquidPegsMonth$ = this.auditStatus$.pipe(
 | 
			
		||||
        throttleTime(60 * 60 * 1000),
 | 
			
		||||
        switchMap(() => this.apiService.listLiquidPegsMonth$()),
 | 
			
		||||
        map((pegs) => {
 | 
			
		||||
          const labels = pegs.map(stats => stats.date);
 | 
			
		||||
          const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
 | 
			
		||||
          series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
 | 
			
		||||
          return {
 | 
			
		||||
            series,
 | 
			
		||||
            labels
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
        share(),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.currentPeg$ = this.auditStatus$.pipe(
 | 
			
		||||
        switchMap(_ =>
 | 
			
		||||
          this.apiService.liquidPegs$().pipe(
 | 
			
		||||
            filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
 | 
			
		||||
            tap((currentPegs) => {
 | 
			
		||||
              this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      ////////// BTC Reserves historical data //////////
 | 
			
		||||
      this.auditUpdated$ = combineLatest([
 | 
			
		||||
        this.auditStatus$,
 | 
			
		||||
        this.currentPeg$
 | 
			
		||||
      ]).pipe(
 | 
			
		||||
        filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
 | 
			
		||||
        map(([auditStatus, currentPeg]) => ({
 | 
			
		||||
          lastBlockAudit: auditStatus.lastBlockAudit,
 | 
			
		||||
          currentPegAmount: currentPeg.amount
 | 
			
		||||
        })),
 | 
			
		||||
        switchMap(({ lastBlockAudit, currentPegAmount }) => {
 | 
			
		||||
          const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
 | 
			
		||||
          const amountCheck = currentPegAmount !== this.lastPegAmount;
 | 
			
		||||
          this.lastPegAmount = currentPegAmount;
 | 
			
		||||
          return of(blockAuditCheck || amountCheck);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.liquidReservesMonth$ = this.auditStatus$.pipe(
 | 
			
		||||
        throttleTime(60 * 60 * 1000),
 | 
			
		||||
        switchMap((auditStatus) => {
 | 
			
		||||
          return auditStatus.isAuditSynced ? this.apiService.listLiquidReservesMonth$() : EMPTY;
 | 
			
		||||
        }),
 | 
			
		||||
        map(reserves => {
 | 
			
		||||
          const labels = reserves.map(stats => stats.date);
 | 
			
		||||
          const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
 | 
			
		||||
          return {
 | 
			
		||||
            series,
 | 
			
		||||
            labels
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.currentReserves$ = this.auditUpdated$.pipe(
 | 
			
		||||
        filter(auditUpdated => auditUpdated === true),
 | 
			
		||||
        throttleTime(40000),
 | 
			
		||||
        switchMap(_ =>
 | 
			
		||||
          this.apiService.liquidReserves$().pipe(
 | 
			
		||||
            filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate),
 | 
			
		||||
            tap((currentReserves) => {
 | 
			
		||||
              this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate;
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
        share()
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$.pipe(startWith(null)), this.currentReserves$.pipe(startWith(null))])
 | 
			
		||||
        .pipe(
 | 
			
		||||
          map((pegs) => {
 | 
			
		||||
            const labels = pegs.map(stats => stats.date);
 | 
			
		||||
            const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
 | 
			
		||||
            series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
 | 
			
		||||
          map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
 | 
			
		||||
            liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
 | 
			
		||||
 | 
			
		||||
            if (liquidPegs.series.length === liquidReserves?.series.length) {
 | 
			
		||||
              liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
 | 
			
		||||
            } else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
 | 
			
		||||
              liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000);
 | 
			
		||||
              liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]);
 | 
			
		||||
            } else {
 | 
			
		||||
              liquidReserves = {
 | 
			
		||||
                series: [],
 | 
			
		||||
                labels: []
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
              series,
 | 
			
		||||
              labels
 | 
			
		||||
              liquidPegs,
 | 
			
		||||
              liquidReserves
 | 
			
		||||
            };
 | 
			
		||||
          }),
 | 
			
		||||
          share(),
 | 
			
		||||
          share()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// Import tree-shakeable echarts
 | 
			
		||||
import * as echarts from 'echarts/core';
 | 
			
		||||
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart } from 'echarts/charts';
 | 
			
		||||
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts';
 | 
			
		||||
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
 | 
			
		||||
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
 | 
			
		||||
// Typescript interfaces
 | 
			
		||||
@ -12,6 +12,6 @@ echarts.use([
 | 
			
		||||
  TitleComponent, TooltipComponent, GridComponent,
 | 
			
		||||
  LegendComponent, GeoComponent, DataZoomComponent,
 | 
			
		||||
  VisualMapComponent, MarkLineComponent,
 | 
			
		||||
  LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart
 | 
			
		||||
  LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart
 | 
			
		||||
]);
 | 
			
		||||
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
 | 
			
		||||
@ -76,6 +76,46 @@ export interface LiquidPegs {
 | 
			
		||||
  date: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CurrentPegs {
 | 
			
		||||
  amount: string;
 | 
			
		||||
  lastBlockUpdate: number;
 | 
			
		||||
  hash: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FederationAddress { 
 | 
			
		||||
  bitcoinaddress: string;
 | 
			
		||||
  balance: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FederationUtxo {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  txindex: number;
 | 
			
		||||
  bitcoinaddress: string;
 | 
			
		||||
  amount: number;
 | 
			
		||||
  blocknumber: number;
 | 
			
		||||
  blocktime: number;
 | 
			
		||||
  pegtxid: string;
 | 
			
		||||
  pegindex: number;
 | 
			
		||||
  pegblocktime: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RecentPeg {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  txindex: number;
 | 
			
		||||
  amount: number;
 | 
			
		||||
  bitcoinaddress: string;
 | 
			
		||||
  bitcointxid: string;
 | 
			
		||||
  bitcoinindex: number;
 | 
			
		||||
  blocktime: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditStatus {
 | 
			
		||||
  bitcoinBlocks: number;
 | 
			
		||||
  bitcoinHeaders: number;
 | 
			
		||||
  lastBlockAudit: number;
 | 
			
		||||
  isAuditSynced: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITranslators { [language: string]: string; }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,10 @@ import { NgModule } from '@angular/core';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { Routes, RouterModule } from '@angular/router';
 | 
			
		||||
import { SharedModule } from '../shared/shared.module';
 | 
			
		||||
import { NgxEchartsModule } from 'ngx-echarts';
 | 
			
		||||
import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import { StartComponent } from '../components/start/start.component';
 | 
			
		||||
import { AddressComponent } from '../components/address/address.component';
 | 
			
		||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
 | 
			
		||||
@ -13,6 +15,17 @@ import { AssetsComponent } from '../components/assets/assets.component';
 | 
			
		||||
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'
 | 
			
		||||
import { AssetComponent } from '../components/asset/asset.component';
 | 
			
		||||
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
 | 
			
		||||
import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component';
 | 
			
		||||
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component';
 | 
			
		||||
import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component';
 | 
			
		||||
import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component';
 | 
			
		||||
import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component';
 | 
			
		||||
import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component';
 | 
			
		||||
import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component';
 | 
			
		||||
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
 | 
			
		||||
import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component';
 | 
			
		||||
import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component';
 | 
			
		||||
import { ReservesRatioGraphComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -64,6 +77,44 @@ const routes: Routes = [
 | 
			
		||||
        data: { preload: true, networkSpecific: true },
 | 
			
		||||
        loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'audit',
 | 
			
		||||
        data: { networks: ['liquid'] },
 | 
			
		||||
        component: StartComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: '',
 | 
			
		||||
            data: { networks: ['liquid'] },
 | 
			
		||||
            component: ReservesAuditDashboardComponent,
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'audit/wallet',
 | 
			
		||||
        data: { networks: ['liquid'] },
 | 
			
		||||
        component: FederationWalletComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'utxos',
 | 
			
		||||
            data: { networks: ['liquid'] },
 | 
			
		||||
            component: FederationUtxosListComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'addresses',
 | 
			
		||||
            data: { networks: ['liquid'] },
 | 
			
		||||
            component: FederationAddressesListComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: '**',
 | 
			
		||||
            redirectTo: 'utxos'
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'audit/pegs',
 | 
			
		||||
        data: { networks: ['liquid'] },
 | 
			
		||||
        component: RecentPegsListComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'assets',
 | 
			
		||||
        data: { networks: ['liquid'] },
 | 
			
		||||
@ -123,9 +174,23 @@ export class LiquidRoutingModule { }
 | 
			
		||||
    CommonModule,
 | 
			
		||||
    LiquidRoutingModule,
 | 
			
		||||
    SharedModule,
 | 
			
		||||
    NgxEchartsModule.forRoot({
 | 
			
		||||
      echarts: () => import('../graphs/echarts').then(m => m.echarts),
 | 
			
		||||
    })
 | 
			
		||||
  ],
 | 
			
		||||
  declarations: [
 | 
			
		||||
    LiquidMasterPageComponent,
 | 
			
		||||
    ReservesAuditDashboardComponent,
 | 
			
		||||
    ReservesSupplyStatsComponent,
 | 
			
		||||
    RecentPegsStatsComponent,
 | 
			
		||||
    RecentPegsListComponent,
 | 
			
		||||
    FederationWalletComponent,
 | 
			
		||||
    FederationUtxosListComponent,
 | 
			
		||||
    FederationAddressesStatsComponent,
 | 
			
		||||
    FederationAddressesListComponent,
 | 
			
		||||
    ReservesRatioComponent,
 | 
			
		||||
    ReservesRatioStatsComponent,
 | 
			
		||||
    ReservesRatioGraphComponent,
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class LiquidMasterPageModule { }
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
 | 
			
		||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
 | 
			
		||||
  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
 | 
			
		||||
  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg } from '../interfaces/node-api.interface';
 | 
			
		||||
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
 | 
			
		||||
import { StateService } from './state.service';
 | 
			
		||||
import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface';
 | 
			
		||||
@ -178,10 +178,46 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.get<RbfTree[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/' + (fullRbf ? 'fullrbf/' : '') + 'replacements/' + (after || ''));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  liquidPegs$(): Observable<CurrentPegs> {
 | 
			
		||||
    return this.httpClient.get<CurrentPegs>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
 | 
			
		||||
    return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  liquidReserves$(): Observable<CurrentPegs> {
 | 
			
		||||
    return this.httpClient.get<CurrentPegs>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listLiquidReservesMonth$(): Observable<LiquidPegs[]> {
 | 
			
		||||
    return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/month');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  federationAuditSynced$(): Observable<AuditStatus> {
 | 
			
		||||
    return this.httpClient.get<AuditStatus>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/status');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  federationAddresses$(): Observable<FederationAddress[]> {
 | 
			
		||||
    return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  federationUtxos$(): Observable<FederationUtxo[]> {
 | 
			
		||||
    return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  recentPegOuts$(): Observable<RecentPeg[]> {
 | 
			
		||||
    return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegouts');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  federationAddressesOneMonthAgo$(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  federationUtxosOneMonthAgo$(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listFeaturedAssets$(): Observable<any[]> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/featured');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
 | 
			
		||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
 | 
			
		||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
 | 
			
		||||
  faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
 | 
			
		||||
  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
 | 
			
		||||
import { MenuComponent } from '../components/menu/menu.component';
 | 
			
		||||
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
 | 
			
		||||
@ -385,5 +385,6 @@ export class SharedModule {
 | 
			
		||||
    library.addIcons(faUserCircle);
 | 
			
		||||
    library.addIcons(faCheck);
 | 
			
		||||
    library.addIcons(faRocket);
 | 
			
		||||
    library.addIcons(faScaleBalanced);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@ var crypto = require('crypto');
 | 
			
		||||
var path = require('node:path');
 | 
			
		||||
const LOG_TAG = '[sync-assets]';
 | 
			
		||||
let verbose = false;
 | 
			
		||||
let MEMPOOL_CDN = false;
 | 
			
		||||
let DRY_RUN = false;
 | 
			
		||||
 | 
			
		||||
if (parseInt(process.env.SKIP_SYNC) === 1) {
 | 
			
		||||
  console.log(`${LOG_TAG} SKIP_SYNC is set, not checking any assets`);
 | 
			
		||||
@ -15,6 +17,18 @@ if (parseInt(process.env.VERBOSE) === 1) {
 | 
			
		||||
  verbose = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (parseInt(process.env.MEMPOOL_CDN) === 1) {
 | 
			
		||||
  console.log(`${LOG_TAG} MEMPOOL_CDN is set, assets will be downloaded from mempool.space`);
 | 
			
		||||
  MEMPOOL_CDN = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (parseInt(process.env.DRY_RUN) === 1) {
 | 
			
		||||
  console.log(`${LOG_TAG} DRY_RUN is set, not downloading any assets`);
 | 
			
		||||
  DRY_RUN = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const githubSecret = process.env.GITHUB_TOKEN;
 | 
			
		||||
 | 
			
		||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
 | 
			
		||||
let configContent = {};
 | 
			
		||||
 | 
			
		||||
@ -46,8 +60,6 @@ try {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const githubSecret = process.env.GITHUB_TOKEN;
 | 
			
		||||
 | 
			
		||||
function download(filename, url) {
 | 
			
		||||
  https.get(url, (response) => {
 | 
			
		||||
    if (response.statusCode < 200 || response.statusCode > 299) {
 | 
			
		||||
@ -60,7 +72,7 @@ function download(filename, url) {
 | 
			
		||||
  })
 | 
			
		||||
  .on('finish', () => {
 | 
			
		||||
    if (verbose) {
 | 
			
		||||
      console.log(`${LOG_TAG} Finished downloading ${url} to ${filename}`);
 | 
			
		||||
      console.log(`${LOG_TAG} \tFinished downloading ${url} to ${filename}`);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@ -72,7 +84,7 @@ function getLocalHash(filePath) {
 | 
			
		||||
  const hash = crypto.createHash('sha1').update(bufferWithHeader).digest('hex');
 | 
			
		||||
 | 
			
		||||
  if (verbose) {
 | 
			
		||||
    console.log(`${LOG_TAG} \tgetLocalHash ${filePath} ${hash}`);
 | 
			
		||||
    console.log(`${LOG_TAG} \t\tgetLocalHash ${filePath} ${hash}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return hash;
 | 
			
		||||
@ -80,7 +92,7 @@ function getLocalHash(filePath) {
 | 
			
		||||
 | 
			
		||||
function downloadMiningPoolLogos$() {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    console.log(`${LOG_TAG} Checking if mining pool logos needs downloading or updating...`);
 | 
			
		||||
    console.log(`${LOG_TAG} \tChecking if mining pool logos needs downloading or updating...`);
 | 
			
		||||
    const options = {
 | 
			
		||||
      host: 'api.github.com',
 | 
			
		||||
      path: '/repos/mempool/mining-pool-logos/contents/',
 | 
			
		||||
@ -110,29 +122,54 @@ function downloadMiningPoolLogos$() {
 | 
			
		||||
          }
 | 
			
		||||
          let downloadedCount = 0;
 | 
			
		||||
          for (const poolLogo of poolLogos) {
 | 
			
		||||
            if (verbose) {
 | 
			
		||||
              console.log(`${LOG_TAG} Processing ${poolLogo.name}`);
 | 
			
		||||
            }
 | 
			
		||||
            const filePath = `${PATH}/mining-pools/${poolLogo.name}`;
 | 
			
		||||
            if (fs.existsSync(filePath)) {
 | 
			
		||||
              const localHash = getLocalHash(filePath);
 | 
			
		||||
              if (verbose) {
 | 
			
		||||
                console.log(`${LOG_TAG} Remote ${poolLogo.name} logo hash ${poolLogo.sha}`);
 | 
			
		||||
                console.log(`${LOG_TAG} \tchecking if ${filePath} exists: ${fs.existsSync(filePath)}`);
 | 
			
		||||
                console.log(`${LOG_TAG} \t\tremote ${poolLogo.name} logo hash ${poolLogo.sha}`);
 | 
			
		||||
                console.log(`${LOG_TAG} \t\t\tchecking if ${filePath} exists: ${fs.existsSync(filePath)}`);
 | 
			
		||||
              }
 | 
			
		||||
              if (localHash !== poolLogo.sha) {
 | 
			
		||||
                console.log(`${LOG_TAG} \t\t${poolLogo.name} is different on the remote, downloading...`);
 | 
			
		||||
                download(filePath, poolLogo.download_url);
 | 
			
		||||
                downloadedCount++;
 | 
			
		||||
                console.log(`${LOG_TAG} \t\t\t\t${poolLogo.name} is different on the remote, downloading...`);
 | 
			
		||||
                let download_url = poolLogo.download_url;
 | 
			
		||||
                if (MEMPOOL_CDN) {
 | 
			
		||||
                  download_url = download_url.replace("raw.githubusercontent.com/mempool/mining-pool-logos/master", "mempool.space/resources/mining-pools");
 | 
			
		||||
                }
 | 
			
		||||
                if (DRY_RUN) {
 | 
			
		||||
                  console.log(`${LOG_TAG} \t\tDRY_RUN is set, not downloading ${poolLogo.name} but we should`);
 | 
			
		||||
                } else {
 | 
			
		||||
                  if (verbose) {
 | 
			
		||||
                    console.log(`${LOG_TAG} \t\tDownloading ${download_url} to ${filePath}`);
 | 
			
		||||
                  }
 | 
			
		||||
                  download(filePath, download_url);
 | 
			
		||||
                  downloadedCount++;
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                console.log(`${LOG_TAG} \t\t${poolLogo.name} is already up to date. Skipping.`);
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log(`${LOG_TAG} ${poolLogo.name} is missing, downloading...`);
 | 
			
		||||
              console.log(`${LOG_TAG} \t\t${poolLogo.name} is missing, downloading...`);
 | 
			
		||||
              const miningPoolsDir = `${PATH}/mining-pools/`;
 | 
			
		||||
              if (!fs.existsSync(miningPoolsDir)){
 | 
			
		||||
                fs.mkdirSync(miningPoolsDir, { recursive: true });
 | 
			
		||||
              }
 | 
			
		||||
              download(filePath, poolLogo.download_url);
 | 
			
		||||
              downloadedCount++;
 | 
			
		||||
              let download_url = poolLogo.download_url;
 | 
			
		||||
              if (MEMPOOL_CDN) {
 | 
			
		||||
                download_url = download_url.replace("raw.githubusercontent.com/mempool/mining-pool-logos/master", "mempool.space/resources/mining-pools");
 | 
			
		||||
              }
 | 
			
		||||
              if (DRY_RUN) {
 | 
			
		||||
                console.log(`${LOG_TAG} DRY_RUN is set, not downloading ${poolLogo.name} but it should`);
 | 
			
		||||
              } else {
 | 
			
		||||
                console.log(`${LOG_TAG} \tDownloading ${download_url} to ${filePath}`);
 | 
			
		||||
                download(filePath, download_url);
 | 
			
		||||
                downloadedCount++;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          console.log(`${LOG_TAG} Downloaded ${downloadedCount} and skipped ${poolLogos.length - downloadedCount} existing mining pool logos`);
 | 
			
		||||
          console.log(`${LOG_TAG} \t\tDownloaded ${downloadedCount} and skipped ${poolLogos.length - downloadedCount} existing mining pool logos`);
 | 
			
		||||
          resolve();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          reject(`Unable to download mining pool logos. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
@ -148,7 +185,7 @@ function downloadMiningPoolLogos$() {
 | 
			
		||||
 | 
			
		||||
function downloadPromoVideoSubtiles$() {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    console.log(`${LOG_TAG} Checking if promo video subtitles needs downloading or updating...`);
 | 
			
		||||
    console.log(`${LOG_TAG} \tChecking if promo video subtitles needs downloading or updating...`);
 | 
			
		||||
    const options = {
 | 
			
		||||
      host: 'api.github.com',
 | 
			
		||||
      path: '/repos/mempool/mempool-promo/contents/subtitles',
 | 
			
		||||
@ -157,7 +194,7 @@ function downloadPromoVideoSubtiles$() {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (githubSecret) {
 | 
			
		||||
      console.log(`${LOG_TAG} Downloading the promo video subtitles with authentication`);
 | 
			
		||||
      console.log(`${LOG_TAG} \tDownloading the promo video subtitles with authentication`);
 | 
			
		||||
      options.headers['authorization'] = `Bearer ${githubSecret}`;
 | 
			
		||||
      options.headers['X-GitHub-Api-Version'] = '2022-11-28';
 | 
			
		||||
    }
 | 
			
		||||
@ -179,27 +216,53 @@ function downloadPromoVideoSubtiles$() {
 | 
			
		||||
          }
 | 
			
		||||
          let downloadedCount = 0;
 | 
			
		||||
          for (const language of videoLanguages) {
 | 
			
		||||
            if (verbose) {
 | 
			
		||||
              console.log(`${LOG_TAG} Processing ${language.name}`);
 | 
			
		||||
            }
 | 
			
		||||
            const filePath = `${PATH}/promo-video/${language.name}`;
 | 
			
		||||
            if (fs.existsSync(filePath)) {
 | 
			
		||||
              if (verbose) {
 | 
			
		||||
                console.log(`${LOG_TAG} ${language.name} remote promo video hash ${language.sha}`);
 | 
			
		||||
                console.log(`${LOG_TAG} \t${language.name} remote promo video hash ${language.sha}`);
 | 
			
		||||
              }
 | 
			
		||||
              const localHash = getLocalHash(filePath);
 | 
			
		||||
 | 
			
		||||
              if (localHash !== language.sha) {
 | 
			
		||||
                console.log(`${LOG_TAG} ${language.name} is different on the remote, updating`);
 | 
			
		||||
                download(filePath, language.download_url);
 | 
			
		||||
                downloadedCount++;
 | 
			
		||||
                console.log(`${LOG_TAG} \t\t${language.name} is different on the remote, updating`);
 | 
			
		||||
                let download_url = language.download_url;
 | 
			
		||||
                if (MEMPOOL_CDN) {
 | 
			
		||||
                  download_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/subtitles", "mempool.space/resources/promo-video");
 | 
			
		||||
                }
 | 
			
		||||
                if (DRY_RUN) {
 | 
			
		||||
                  console.log(`${LOG_TAG} \t\tDRY_RUN is set, not downloading ${language.name} but we should`);
 | 
			
		||||
                } else {
 | 
			
		||||
                  if (verbose) {
 | 
			
		||||
                    console.log(`${LOG_TAG} \t\tdownloading ${download_url} to ${filePath}`);
 | 
			
		||||
                  }
 | 
			
		||||
                  download(filePath, download_url);
 | 
			
		||||
                  downloadedCount++;
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                console.log(`${LOG_TAG} \t\t${language.name} is already up to date. Skipping.`);
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log(`${LOG_TAG} ${language.name} is missing, downloading`);
 | 
			
		||||
              console.log(`${LOG_TAG} \t\t${language.name} is missing, downloading`);
 | 
			
		||||
              const promoVideosDir = `${PATH}/promo-video/`;
 | 
			
		||||
              if (!fs.existsSync(promoVideosDir)){
 | 
			
		||||
                fs.mkdirSync(promoVideosDir, { recursive: true });
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              download(filePath, language.download_url);
 | 
			
		||||
              downloadedCount++;
 | 
			
		||||
              let download_url = language.download_url;
 | 
			
		||||
              if (MEMPOOL_CDN) {
 | 
			
		||||
                download_url = downloadownload_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/subtitles", "mempool.space/resources/promo-video");
 | 
			
		||||
              }
 | 
			
		||||
              if (DRY_RUN) {
 | 
			
		||||
                console.log(`${LOG_TAG} \tDRY_RUN is set, not downloading ${language.name} but we should`);
 | 
			
		||||
              } else {
 | 
			
		||||
                if (verbose) {
 | 
			
		||||
                  console.log(`${LOG_TAG} downloading ${download_url} to ${filePath}`);
 | 
			
		||||
                }
 | 
			
		||||
                download(filePath, download_url);
 | 
			
		||||
                downloadedCount++;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          console.log(`${LOG_TAG} Downloaded ${downloadedCount} and skipped ${videoLanguages.length - downloadedCount} existing video subtitles`);
 | 
			
		||||
@ -218,7 +281,7 @@ function downloadPromoVideoSubtiles$() {
 | 
			
		||||
 | 
			
		||||
function downloadPromoVideo$() {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    console.log(`${LOG_TAG} Checking if promo video needs downloading or updating...`);
 | 
			
		||||
    console.log(`${LOG_TAG} \tChecking if promo video needs downloading or updating...`);
 | 
			
		||||
    const options = {
 | 
			
		||||
      host: 'api.github.com',
 | 
			
		||||
      path: '/repos/mempool/mempool-promo/contents',
 | 
			
		||||
@ -227,7 +290,7 @@ function downloadPromoVideo$() {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (githubSecret) {
 | 
			
		||||
      console.log(`${LOG_TAG} Downloading the promo video with authentication`);
 | 
			
		||||
      console.log(`${LOG_TAG} \tDownloading the promo video with authentication`);
 | 
			
		||||
      options.headers['authorization'] = `Bearer ${githubSecret}`;
 | 
			
		||||
      options.headers['X-GitHub-Api-Version'] = '2022-11-28';
 | 
			
		||||
    }
 | 
			
		||||
@ -256,14 +319,36 @@ function downloadPromoVideo$() {
 | 
			
		||||
 | 
			
		||||
              if (localHash !== item.sha) {
 | 
			
		||||
                console.log(`${LOG_TAG} \tmempool-promo.mp4 is different on the remote, updating`);
 | 
			
		||||
                download(filePath, item.download_url);
 | 
			
		||||
                console.log(`${LOG_TAG} \tmempool-promo.mp4 downloaded.`);
 | 
			
		||||
                let download_url = item.download_url;
 | 
			
		||||
                if (MEMPOOL_CDN) {
 | 
			
		||||
                  download_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/promo.mp4", "mempool.space/resources/promo-video/mempool-promo.mp4");
 | 
			
		||||
                }
 | 
			
		||||
                if (DRY_RUN) {
 | 
			
		||||
                  console.log(`${LOG_TAG} DRY_RUN is set, not downloading mempool-promo.mp4 but we should`);
 | 
			
		||||
                } else {
 | 
			
		||||
                  if (verbose) {
 | 
			
		||||
                    console.log(`${LOG_TAG} downloading ${download_url} to ${filePath}`);
 | 
			
		||||
                  }
 | 
			
		||||
                  download(filePath, download_url);
 | 
			
		||||
                  console.log(`${LOG_TAG} \tmempool-promo.mp4 downloaded.`);
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                console.log(`${LOG_TAG} \tmempool-promo.mp4 is already up to date. Skipping.`);
 | 
			
		||||
                console.log(`${LOG_TAG} \t\tmempool-promo.mp4 is already up to date. Skipping.`);
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log(`${LOG_TAG} \tmempool-promo.mp4 is missing, downloading`);
 | 
			
		||||
              download(filePath, item.download_url);
 | 
			
		||||
              let download_url = item.download_url;
 | 
			
		||||
              if (MEMPOOL_CDN) {
 | 
			
		||||
                download_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/promo.mp4", "mempool.space/resources/promo-video/mempool-promo.mp4");
 | 
			
		||||
              }
 | 
			
		||||
              if (DRY_RUN) {
 | 
			
		||||
                console.log(`${LOG_TAG} DRY_RUN is set, not downloading mempool-promo.mp4 but we should`);
 | 
			
		||||
              } else {
 | 
			
		||||
                if (verbose) {
 | 
			
		||||
                  console.log(`${LOG_TAG} downloading ${download_url} to ${filePath}`);
 | 
			
		||||
                }
 | 
			
		||||
                download(filePath, download_url);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          resolve();
 | 
			
		||||
@ -300,7 +385,7 @@ if (configContent.BASE_MODULE && configContent.BASE_MODULE === 'liquid') {
 | 
			
		||||
  download(`${PATH}/assets-testnet.minimal.json`, testnetAssetsMinimalJsonUrl);
 | 
			
		||||
} else {
 | 
			
		||||
  if (verbose) {
 | 
			
		||||
    console.log(`${LOG_TAG} BASE_MODULE is not set to Liquid (${configContent.BASE_MODULE}), skipping downloading assets`);
 | 
			
		||||
    console.log(`${LOG_TAG} BASE_MODULE is not set to Liquid (currently ${configContent.BASE_MODULE}), skipping downloading assets`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,30 +25,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3001",
 | 
			
		||||
      "http://node202.va1.mempool.space:3001",
 | 
			
		||||
      "http://node203.va1.mempool.space:3001",
 | 
			
		||||
      "http://node204.va1.mempool.space:3001",
 | 
			
		||||
      "http://node205.va1.mempool.space:3001",
 | 
			
		||||
      "http://node206.va1.mempool.space:3001",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3001",
 | 
			
		||||
      "http://node201.va1.mempool.space:3001",
 | 
			
		||||
      "http://node202.va1.mempool.space:3001",
 | 
			
		||||
      "http://node203.va1.mempool.space:3001",
 | 
			
		||||
      "http://node204.va1.mempool.space:3001",
 | 
			
		||||
      "http://node205.va1.mempool.space:3001",
 | 
			
		||||
      "http://node206.va1.mempool.space:3001",
 | 
			
		||||
      "http://node207.va1.mempool.space:3001",
 | 
			
		||||
      "http://node208.va1.mempool.space:3001",
 | 
			
		||||
      "http://node209.va1.mempool.space:3001",
 | 
			
		||||
      "http://node210.va1.mempool.space:3001",
 | 
			
		||||
      "http://node211.va1.mempool.space:3001",
 | 
			
		||||
      "http://node212.va1.mempool.space:3001",
 | 
			
		||||
      "http://node213.va1.mempool.space:3001",
 | 
			
		||||
      "http://node214.va1.mempool.space:3001",
 | 
			
		||||
      "http://node201.fra.mempool.space:3001",
 | 
			
		||||
      "http://node202.fra.mempool.space:3001",
 | 
			
		||||
      "http://node203.fra.mempool.space:3001",
 | 
			
		||||
      "http://node204.fra.mempool.space:3001",
 | 
			
		||||
      "http://node205.fra.mempool.space:3001",
 | 
			
		||||
      "http://node206.fra.mempool.space:3001",
 | 
			
		||||
      "http://node207.fra.mempool.space:3001",
 | 
			
		||||
      "http://node208.fra.mempool.space:3001",
 | 
			
		||||
      "http://node209.fra.mempool.space:3001",
 | 
			
		||||
      "http://node210.fra.mempool.space:3001",
 | 
			
		||||
      "http://node211.fra.mempool.space:3001",
 | 
			
		||||
      "http://node212.fra.mempool.space:3001",
 | 
			
		||||
      "http://node213.fra.mempool.space:3001",
 | 
			
		||||
      "http://node214.fra.mempool.space:3001",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3001"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3001",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3001"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
 | 
			
		||||
@ -25,30 +25,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3004",
 | 
			
		||||
      "http://node202.va1.mempool.space:3004",
 | 
			
		||||
      "http://node203.va1.mempool.space:3004",
 | 
			
		||||
      "http://node204.va1.mempool.space:3004",
 | 
			
		||||
      "http://node205.va1.mempool.space:3004",
 | 
			
		||||
      "http://node206.va1.mempool.space:3004",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3004",
 | 
			
		||||
      "http://node201.va1.mempool.space:3004",
 | 
			
		||||
      "http://node202.va1.mempool.space:3004",
 | 
			
		||||
      "http://node203.va1.mempool.space:3004",
 | 
			
		||||
      "http://node204.va1.mempool.space:3004",
 | 
			
		||||
      "http://node205.va1.mempool.space:3004",
 | 
			
		||||
      "http://node206.va1.mempool.space:3004",
 | 
			
		||||
      "http://node207.va1.mempool.space:3004",
 | 
			
		||||
      "http://node208.va1.mempool.space:3004",
 | 
			
		||||
      "http://node209.va1.mempool.space:3004",
 | 
			
		||||
      "http://node210.va1.mempool.space:3004",
 | 
			
		||||
      "http://node211.va1.mempool.space:3004",
 | 
			
		||||
      "http://node212.va1.mempool.space:3004",
 | 
			
		||||
      "http://node213.va1.mempool.space:3004",
 | 
			
		||||
      "http://node214.va1.mempool.space:3004",
 | 
			
		||||
      "http://node201.fra.mempool.space:3004",
 | 
			
		||||
      "http://node202.fra.mempool.space:3004",
 | 
			
		||||
      "http://node203.fra.mempool.space:3004",
 | 
			
		||||
      "http://node204.fra.mempool.space:3004",
 | 
			
		||||
      "http://node205.fra.mempool.space:3004",
 | 
			
		||||
      "http://node206.fra.mempool.space:3004",
 | 
			
		||||
      "http://node207.fra.mempool.space:3004",
 | 
			
		||||
      "http://node208.fra.mempool.space:3004",
 | 
			
		||||
      "http://node209.fra.mempool.space:3004",
 | 
			
		||||
      "http://node210.fra.mempool.space:3004",
 | 
			
		||||
      "http://node211.fra.mempool.space:3004",
 | 
			
		||||
      "http://node212.fra.mempool.space:3004",
 | 
			
		||||
      "http://node213.fra.mempool.space:3004",
 | 
			
		||||
      "http://node214.fra.mempool.space:3004",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3004"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3004",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3004"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
 | 
			
		||||
@ -18,30 +18,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3000",
 | 
			
		||||
      "http://node202.va1.mempool.space:3000",
 | 
			
		||||
      "http://node203.va1.mempool.space:3000",
 | 
			
		||||
      "http://node204.va1.mempool.space:3000",
 | 
			
		||||
      "http://node205.va1.mempool.space:3000",
 | 
			
		||||
      "http://node206.va1.mempool.space:3000",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node201.va1.mempool.space:3000",
 | 
			
		||||
      "http://node202.va1.mempool.space:3000",
 | 
			
		||||
      "http://node203.va1.mempool.space:3000",
 | 
			
		||||
      "http://node204.va1.mempool.space:3000",
 | 
			
		||||
      "http://node205.va1.mempool.space:3000",
 | 
			
		||||
      "http://node206.va1.mempool.space:3000",
 | 
			
		||||
      "http://node207.va1.mempool.space:3000",
 | 
			
		||||
      "http://node208.va1.mempool.space:3000",
 | 
			
		||||
      "http://node209.va1.mempool.space:3000",
 | 
			
		||||
      "http://node210.va1.mempool.space:3000",
 | 
			
		||||
      "http://node211.va1.mempool.space:3000",
 | 
			
		||||
      "http://node212.va1.mempool.space:3000",
 | 
			
		||||
      "http://node213.va1.mempool.space:3000",
 | 
			
		||||
      "http://node214.va1.mempool.space:3000",
 | 
			
		||||
      "http://node201.fra.mempool.space:3000",
 | 
			
		||||
      "http://node202.fra.mempool.space:3000",
 | 
			
		||||
      "http://node203.fra.mempool.space:3000",
 | 
			
		||||
      "http://node204.fra.mempool.space:3000",
 | 
			
		||||
      "http://node205.fra.mempool.space:3000",
 | 
			
		||||
      "http://node206.fra.mempool.space:3000",
 | 
			
		||||
      "http://node207.fra.mempool.space:3000",
 | 
			
		||||
      "http://node208.fra.mempool.space:3000",
 | 
			
		||||
      "http://node209.fra.mempool.space:3000",
 | 
			
		||||
      "http://node210.fra.mempool.space:3000",
 | 
			
		||||
      "http://node211.fra.mempool.space:3000",
 | 
			
		||||
      "http://node212.fra.mempool.space:3000",
 | 
			
		||||
      "http://node213.fra.mempool.space:3000",
 | 
			
		||||
      "http://node214.fra.mempool.space:3000",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3000"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3000"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
 | 
			
		||||
@ -39,30 +39,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3000",
 | 
			
		||||
      "http://node202.va1.mempool.space:3000",
 | 
			
		||||
      "http://node203.va1.mempool.space:3000",
 | 
			
		||||
      "http://node204.va1.mempool.space:3000",
 | 
			
		||||
      "http://node205.va1.mempool.space:3000",
 | 
			
		||||
      "http://node206.va1.mempool.space:3000",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3000",
 | 
			
		||||
      "http://node201.va1.mempool.space:3000",
 | 
			
		||||
      "http://node202.va1.mempool.space:3000",
 | 
			
		||||
      "http://node203.va1.mempool.space:3000",
 | 
			
		||||
      "http://node204.va1.mempool.space:3000",
 | 
			
		||||
      "http://node205.va1.mempool.space:3000",
 | 
			
		||||
      "http://node206.va1.mempool.space:3000",
 | 
			
		||||
      "http://node207.va1.mempool.space:3000",
 | 
			
		||||
      "http://node208.va1.mempool.space:3000",
 | 
			
		||||
      "http://node209.va1.mempool.space:3000",
 | 
			
		||||
      "http://node210.va1.mempool.space:3000",
 | 
			
		||||
      "http://node211.va1.mempool.space:3000",
 | 
			
		||||
      "http://node212.va1.mempool.space:3000",
 | 
			
		||||
      "http://node213.va1.mempool.space:3000",
 | 
			
		||||
      "http://node214.va1.mempool.space:3000",
 | 
			
		||||
      "http://node201.fra.mempool.space:3000",
 | 
			
		||||
      "http://node202.fra.mempool.space:3000",
 | 
			
		||||
      "http://node203.fra.mempool.space:3000",
 | 
			
		||||
      "http://node204.fra.mempool.space:3000",
 | 
			
		||||
      "http://node205.fra.mempool.space:3000",
 | 
			
		||||
      "http://node206.fra.mempool.space:3000",
 | 
			
		||||
      "http://node207.fra.mempool.space:3000",
 | 
			
		||||
      "http://node208.fra.mempool.space:3000",
 | 
			
		||||
      "http://node209.fra.mempool.space:3000",
 | 
			
		||||
      "http://node210.fra.mempool.space:3000",
 | 
			
		||||
      "http://node211.fra.mempool.space:3000",
 | 
			
		||||
      "http://node212.fra.mempool.space:3000",
 | 
			
		||||
      "http://node213.fra.mempool.space:3000",
 | 
			
		||||
      "http://node214.fra.mempool.space:3000",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3000"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3000",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3000"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
@ -82,30 +106,54 @@
 | 
			
		||||
    "AUDIT": true,
 | 
			
		||||
    "AUDIT_START_HEIGHT": 774000,
 | 
			
		||||
    "SERVERS": [
 | 
			
		||||
      "node201.va1.mempool.space",
 | 
			
		||||
      "node202.va1.mempool.space",
 | 
			
		||||
      "node203.va1.mempool.space",
 | 
			
		||||
      "node204.va1.mempool.space",
 | 
			
		||||
      "node205.va1.mempool.space",
 | 
			
		||||
      "node206.va1.mempool.space",
 | 
			
		||||
      "node201.fmt.mempool.space",
 | 
			
		||||
      "node202.fmt.mempool.space",
 | 
			
		||||
      "node203.fmt.mempool.space",
 | 
			
		||||
      "node204.fmt.mempool.space",
 | 
			
		||||
      "node205.fmt.mempool.space",
 | 
			
		||||
      "node206.fmt.mempool.space",
 | 
			
		||||
      "node201.va1.mempool.space",
 | 
			
		||||
      "node202.va1.mempool.space",
 | 
			
		||||
      "node203.va1.mempool.space",
 | 
			
		||||
      "node204.va1.mempool.space",
 | 
			
		||||
      "node205.va1.mempool.space",
 | 
			
		||||
      "node206.va1.mempool.space",
 | 
			
		||||
      "node207.va1.mempool.space",
 | 
			
		||||
      "node208.va1.mempool.space",
 | 
			
		||||
      "node209.va1.mempool.space",
 | 
			
		||||
      "node210.va1.mempool.space",
 | 
			
		||||
      "node211.va1.mempool.space",
 | 
			
		||||
      "node212.va1.mempool.space",
 | 
			
		||||
      "node213.va1.mempool.space",
 | 
			
		||||
      "node214.va1.mempool.space",
 | 
			
		||||
      "node201.fra.mempool.space",
 | 
			
		||||
      "node202.fra.mempool.space",
 | 
			
		||||
      "node203.fra.mempool.space",
 | 
			
		||||
      "node204.fra.mempool.space",
 | 
			
		||||
      "node205.fra.mempool.space",
 | 
			
		||||
      "node206.fra.mempool.space",
 | 
			
		||||
      "node207.fra.mempool.space",
 | 
			
		||||
      "node208.fra.mempool.space",
 | 
			
		||||
      "node209.fra.mempool.space",
 | 
			
		||||
      "node210.fra.mempool.space",
 | 
			
		||||
      "node211.fra.mempool.space",
 | 
			
		||||
      "node212.fra.mempool.space",
 | 
			
		||||
      "node213.fra.mempool.space",
 | 
			
		||||
      "node214.fra.mempool.space",
 | 
			
		||||
      "node201.tk7.mempool.space",
 | 
			
		||||
      "node202.tk7.mempool.space",
 | 
			
		||||
      "node203.tk7.mempool.space",
 | 
			
		||||
      "node204.tk7.mempool.space",
 | 
			
		||||
      "node205.tk7.mempool.space",
 | 
			
		||||
      "node206.tk7.mempool.space"
 | 
			
		||||
      "node206.tk7.mempool.space",
 | 
			
		||||
      "node207.tk7.mempool.space",
 | 
			
		||||
      "node208.tk7.mempool.space",
 | 
			
		||||
      "node209.tk7.mempool.space",
 | 
			
		||||
      "node210.tk7.mempool.space",
 | 
			
		||||
      "node211.tk7.mempool.space",
 | 
			
		||||
      "node212.tk7.mempool.space",
 | 
			
		||||
      "node213.tk7.mempool.space",
 | 
			
		||||
      "node214.tk7.mempool.space"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "REDIS": {
 | 
			
		||||
 | 
			
		||||
@ -18,30 +18,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3003",
 | 
			
		||||
      "http://node202.va1.mempool.space:3003",
 | 
			
		||||
      "http://node203.va1.mempool.space:3003",
 | 
			
		||||
      "http://node204.va1.mempool.space:3003",
 | 
			
		||||
      "http://node205.va1.mempool.space:3003",
 | 
			
		||||
      "http://node206.va1.mempool.space:3003",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node201.va1.mempool.space:3003",
 | 
			
		||||
      "http://node202.va1.mempool.space:3003",
 | 
			
		||||
      "http://node203.va1.mempool.space:3003",
 | 
			
		||||
      "http://node204.va1.mempool.space:3003",
 | 
			
		||||
      "http://node205.va1.mempool.space:3003",
 | 
			
		||||
      "http://node206.va1.mempool.space:3003",
 | 
			
		||||
      "http://node207.va1.mempool.space:3003",
 | 
			
		||||
      "http://node208.va1.mempool.space:3003",
 | 
			
		||||
      "http://node209.va1.mempool.space:3003",
 | 
			
		||||
      "http://node210.va1.mempool.space:3003",
 | 
			
		||||
      "http://node211.va1.mempool.space:3003",
 | 
			
		||||
      "http://node212.va1.mempool.space:3003",
 | 
			
		||||
      "http://node213.va1.mempool.space:3003",
 | 
			
		||||
      "http://node214.va1.mempool.space:3003",
 | 
			
		||||
      "http://node201.fra.mempool.space:3003",
 | 
			
		||||
      "http://node202.fra.mempool.space:3003",
 | 
			
		||||
      "http://node203.fra.mempool.space:3003",
 | 
			
		||||
      "http://node204.fra.mempool.space:3003",
 | 
			
		||||
      "http://node205.fra.mempool.space:3003",
 | 
			
		||||
      "http://node206.fra.mempool.space:3003",
 | 
			
		||||
      "http://node207.fra.mempool.space:3003",
 | 
			
		||||
      "http://node208.fra.mempool.space:3003",
 | 
			
		||||
      "http://node209.fra.mempool.space:3003",
 | 
			
		||||
      "http://node210.fra.mempool.space:3003",
 | 
			
		||||
      "http://node211.fra.mempool.space:3003",
 | 
			
		||||
      "http://node212.fra.mempool.space:3003",
 | 
			
		||||
      "http://node213.fra.mempool.space:3003",
 | 
			
		||||
      "http://node214.fra.mempool.space:3003",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3003"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3003"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
 | 
			
		||||
@ -27,30 +27,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3003",
 | 
			
		||||
      "http://node202.va1.mempool.space:3003",
 | 
			
		||||
      "http://node203.va1.mempool.space:3003",
 | 
			
		||||
      "http://node204.va1.mempool.space:3003",
 | 
			
		||||
      "http://node205.va1.mempool.space:3003",
 | 
			
		||||
      "http://node206.va1.mempool.space:3003",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3003",
 | 
			
		||||
      "http://node201.va1.mempool.space:3003",
 | 
			
		||||
      "http://node202.va1.mempool.space:3003",
 | 
			
		||||
      "http://node203.va1.mempool.space:3003",
 | 
			
		||||
      "http://node204.va1.mempool.space:3003",
 | 
			
		||||
      "http://node205.va1.mempool.space:3003",
 | 
			
		||||
      "http://node206.va1.mempool.space:3003",
 | 
			
		||||
      "http://node207.va1.mempool.space:3003",
 | 
			
		||||
      "http://node208.va1.mempool.space:3003",
 | 
			
		||||
      "http://node209.va1.mempool.space:3003",
 | 
			
		||||
      "http://node210.va1.mempool.space:3003",
 | 
			
		||||
      "http://node211.va1.mempool.space:3003",
 | 
			
		||||
      "http://node212.va1.mempool.space:3003",
 | 
			
		||||
      "http://node213.va1.mempool.space:3003",
 | 
			
		||||
      "http://node214.va1.mempool.space:3003",
 | 
			
		||||
      "http://node201.fra.mempool.space:3003",
 | 
			
		||||
      "http://node202.fra.mempool.space:3003",
 | 
			
		||||
      "http://node203.fra.mempool.space:3003",
 | 
			
		||||
      "http://node204.fra.mempool.space:3003",
 | 
			
		||||
      "http://node205.fra.mempool.space:3003",
 | 
			
		||||
      "http://node206.fra.mempool.space:3003",
 | 
			
		||||
      "http://node207.fra.mempool.space:3003",
 | 
			
		||||
      "http://node208.fra.mempool.space:3003",
 | 
			
		||||
      "http://node209.fra.mempool.space:3003",
 | 
			
		||||
      "http://node210.fra.mempool.space:3003",
 | 
			
		||||
      "http://node211.fra.mempool.space:3003",
 | 
			
		||||
      "http://node212.fra.mempool.space:3003",
 | 
			
		||||
      "http://node213.fra.mempool.space:3003",
 | 
			
		||||
      "http://node214.fra.mempool.space:3003",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3003"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3003",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3003"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
 | 
			
		||||
@ -18,30 +18,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3002",
 | 
			
		||||
      "http://node202.va1.mempool.space:3002",
 | 
			
		||||
      "http://node203.va1.mempool.space:3002",
 | 
			
		||||
      "http://node204.va1.mempool.space:3002",
 | 
			
		||||
      "http://node205.va1.mempool.space:3002",
 | 
			
		||||
      "http://node206.va1.mempool.space:3002",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node201.va1.mempool.space:3002",
 | 
			
		||||
      "http://node202.va1.mempool.space:3002",
 | 
			
		||||
      "http://node203.va1.mempool.space:3002",
 | 
			
		||||
      "http://node204.va1.mempool.space:3002",
 | 
			
		||||
      "http://node205.va1.mempool.space:3002",
 | 
			
		||||
      "http://node206.va1.mempool.space:3002",
 | 
			
		||||
      "http://node207.va1.mempool.space:3002",
 | 
			
		||||
      "http://node208.va1.mempool.space:3002",
 | 
			
		||||
      "http://node209.va1.mempool.space:3002",
 | 
			
		||||
      "http://node210.va1.mempool.space:3002",
 | 
			
		||||
      "http://node211.va1.mempool.space:3002",
 | 
			
		||||
      "http://node212.va1.mempool.space:3002",
 | 
			
		||||
      "http://node213.va1.mempool.space:3002",
 | 
			
		||||
      "http://node214.va1.mempool.space:3002",
 | 
			
		||||
      "http://node201.fra.mempool.space:3002",
 | 
			
		||||
      "http://node202.fra.mempool.space:3002",
 | 
			
		||||
      "http://node203.fra.mempool.space:3002",
 | 
			
		||||
      "http://node204.fra.mempool.space:3002",
 | 
			
		||||
      "http://node205.fra.mempool.space:3002",
 | 
			
		||||
      "http://node206.fra.mempool.space:3002",
 | 
			
		||||
      "http://node207.fra.mempool.space:3002",
 | 
			
		||||
      "http://node208.fra.mempool.space:3002",
 | 
			
		||||
      "http://node209.fra.mempool.space:3002",
 | 
			
		||||
      "http://node210.fra.mempool.space:3002",
 | 
			
		||||
      "http://node211.fra.mempool.space:3002",
 | 
			
		||||
      "http://node212.fra.mempool.space:3002",
 | 
			
		||||
      "http://node213.fra.mempool.space:3002",
 | 
			
		||||
      "http://node214.fra.mempool.space:3002",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3002"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3002"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
 | 
			
		||||
@ -27,30 +27,54 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet",
 | 
			
		||||
    "FALLBACK": [
 | 
			
		||||
      "http://node201.va1.mempool.space:3002",
 | 
			
		||||
      "http://node202.va1.mempool.space:3002",
 | 
			
		||||
      "http://node203.va1.mempool.space:3002",
 | 
			
		||||
      "http://node204.va1.mempool.space:3002",
 | 
			
		||||
      "http://node205.va1.mempool.space:3002",
 | 
			
		||||
      "http://node206.va1.mempool.space:3002",
 | 
			
		||||
      "http://node201.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node202.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node203.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node204.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node205.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node206.fmt.mempool.space:3002",
 | 
			
		||||
      "http://node201.va1.mempool.space:3002",
 | 
			
		||||
      "http://node202.va1.mempool.space:3002",
 | 
			
		||||
      "http://node203.va1.mempool.space:3002",
 | 
			
		||||
      "http://node204.va1.mempool.space:3002",
 | 
			
		||||
      "http://node205.va1.mempool.space:3002",
 | 
			
		||||
      "http://node206.va1.mempool.space:3002",
 | 
			
		||||
      "http://node207.va1.mempool.space:3002",
 | 
			
		||||
      "http://node208.va1.mempool.space:3002",
 | 
			
		||||
      "http://node209.va1.mempool.space:3002",
 | 
			
		||||
      "http://node210.va1.mempool.space:3002",
 | 
			
		||||
      "http://node211.va1.mempool.space:3002",
 | 
			
		||||
      "http://node212.va1.mempool.space:3002",
 | 
			
		||||
      "http://node213.va1.mempool.space:3002",
 | 
			
		||||
      "http://node214.va1.mempool.space:3002",
 | 
			
		||||
      "http://node201.fra.mempool.space:3002",
 | 
			
		||||
      "http://node202.fra.mempool.space:3002",
 | 
			
		||||
      "http://node203.fra.mempool.space:3002",
 | 
			
		||||
      "http://node204.fra.mempool.space:3002",
 | 
			
		||||
      "http://node205.fra.mempool.space:3002",
 | 
			
		||||
      "http://node206.fra.mempool.space:3002",
 | 
			
		||||
      "http://node207.fra.mempool.space:3002",
 | 
			
		||||
      "http://node208.fra.mempool.space:3002",
 | 
			
		||||
      "http://node209.fra.mempool.space:3002",
 | 
			
		||||
      "http://node210.fra.mempool.space:3002",
 | 
			
		||||
      "http://node211.fra.mempool.space:3002",
 | 
			
		||||
      "http://node212.fra.mempool.space:3002",
 | 
			
		||||
      "http://node213.fra.mempool.space:3002",
 | 
			
		||||
      "http://node214.fra.mempool.space:3002",
 | 
			
		||||
      "http://node201.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node202.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node203.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node204.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node205.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node206.tk7.mempool.space:3002"
 | 
			
		||||
      "http://node206.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node207.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node208.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node209.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node210.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node211.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node212.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node213.tk7.mempool.space:3002",
 | 
			
		||||
      "http://node214.tk7.mempool.space:3002"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
#!/usr/bin/env zsh
 | 
			
		||||
hostname=mempool.space
 | 
			
		||||
hostname=$(hostname)
 | 
			
		||||
 | 
			
		||||
heat()
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,12 @@
 | 
			
		||||
# proxy cache
 | 
			
		||||
proxy_cache_path /var/cache/nginx/api keys_zone=api:20m levels=1:2 inactive=365d max_size=2000m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:20m levels=1:2 inactive=365d max_size=2000m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:20m levels=1:2 inactive=365d max_size=5000m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/services keys_zone=services:20m levels=1:2 inactive=365d max_size=100m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/services keys_zone=services:20m levels=1:2 inactive=30d max_size=200m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/apihot keys_zone=apihot:20m levels=1:2 inactive=60m max_size=20m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/apiwarm keys_zone=apiwarm:20m levels=1:2 inactive=24h max_size=200m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/apinormal keys_zone=apinormal:200m levels=1:2 inactive=30d max_size=2000m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inactive=365d max_size=2000m;
 | 
			
		||||
 | 
			
		||||
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:500m levels=1:2 inactive=365d max_size=5000m;
 | 
			
		||||
proxy_cache_path /var/cache/nginx/markets keys_zone=markets:20m levels=1:2 inactive=365d max_size=100m;
 | 
			
		||||
types_hash_max_size 4096;
 | 
			
		||||
proxy_buffer_size 8k;
 | 
			
		||||
proxy_buffer_size 8k;
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ location @mempool-api-v1-lightning {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -77,7 +77,7 @@ location @mempool-api-v1-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -94,9 +94,11 @@ location @mempool-api-v1-cache-hot {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apihot;
 | 
			
		||||
	proxy_cache_valid 200 1s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
	expires 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
location @mempool-api-v1-cache-warm {
 | 
			
		||||
@ -109,7 +111,7 @@ location @mempool-api-v1-cache-warm {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
}
 | 
			
		||||
@ -122,7 +124,7 @@ location @mempool-api-v1-cache-normal {
 | 
			
		||||
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
	proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apinormal;
 | 
			
		||||
	proxy_cache_valid 200 2s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -167,7 +169,7 @@ location @esplora-api-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ location @mempool-liquid-api-v1-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ location @mempool-liquid-api-v1-cache-warm {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
}
 | 
			
		||||
@ -105,7 +105,7 @@ location @mempool-liquid-api-v1-cache-normal {
 | 
			
		||||
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
	proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apinormal;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -150,7 +150,7 @@ location @esplora-liquid-api-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ location @mempool-liquidtestnet-api-v1-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@ location @mempool-liquidtestnet-api-v1-cache-warm {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
}
 | 
			
		||||
@ -109,7 +109,7 @@ location @mempool-liquidtestnet-api-v1-cache-normal {
 | 
			
		||||
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
	proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apinormal;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -154,7 +154,7 @@ location @esplora-liquidtestnet-api-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ location @mempool-signet-api-v1-lightning {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ location @mempool-signet-api-v1-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@ location @mempool-signet-api-v1-cache-warm {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
}
 | 
			
		||||
@ -109,7 +109,7 @@ location @mempool-signet-api-v1-cache-normal {
 | 
			
		||||
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
	proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apinormal;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -154,7 +154,7 @@ location @esplora-signet-api-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ location @mempool-testnet-api-v1-lightning {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ location @mempool-testnet-api-v1-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@ location @mempool-testnet-api-v1-cache-warm {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apiwarm;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
}
 | 
			
		||||
@ -109,7 +109,7 @@ location @mempool-testnet-api-v1-cache-normal {
 | 
			
		||||
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
	proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apinormal;
 | 
			
		||||
	proxy_cache_valid 200 10s;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
@ -154,7 +154,7 @@ location @esplora-testnet-api-cache-forever {
 | 
			
		||||
 | 
			
		||||
	proxy_cache_background_update on;
 | 
			
		||||
	proxy_cache_use_stale updating;
 | 
			
		||||
	proxy_cache api;
 | 
			
		||||
	proxy_cache apicold;
 | 
			
		||||
	proxy_cache_valid 200 30d;
 | 
			
		||||
	proxy_redirect off;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user