Merge branch 'master' into mononaut/more-fee-bands
This commit is contained in:
		
						commit
						42d5650bc0
					
				@ -1,5 +1,5 @@
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
 | 
			
		||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { Worker } from 'worker_threads';
 | 
			
		||||
@ -104,8 +104,12 @@ class MempoolBlocks {
 | 
			
		||||
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
 | 
			
		||||
    const mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
    let blockSize = 0;
 | 
			
		||||
    let blockWeight = 0;
 | 
			
		||||
    let blockVsize = 0;
 | 
			
		||||
    let blockFees = 0;
 | 
			
		||||
    const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
 | 
			
		||||
    let transactionIds: string[] = [];
 | 
			
		||||
    let transactions: TransactionExtended[] = [];
 | 
			
		||||
    transactionsSorted.forEach((tx) => {
 | 
			
		||||
      if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
 | 
			
		||||
@ -116,9 +120,14 @@ class MempoolBlocks {
 | 
			
		||||
        };
 | 
			
		||||
        blockWeight += tx.weight;
 | 
			
		||||
        blockVsize += tx.vsize;
 | 
			
		||||
        blockSize += tx.size;
 | 
			
		||||
        blockFees += tx.fee;
 | 
			
		||||
        if (blockVsize <= sizeLimit) {
 | 
			
		||||
          transactions.push(tx);
 | 
			
		||||
        }
 | 
			
		||||
        transactionIds.push(tx.txid);
 | 
			
		||||
      } else {
 | 
			
		||||
        mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
 | 
			
		||||
        mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
 | 
			
		||||
        blockVsize = 0;
 | 
			
		||||
        tx.position = {
 | 
			
		||||
          block: mempoolBlocks.length,
 | 
			
		||||
@ -126,11 +135,14 @@ class MempoolBlocks {
 | 
			
		||||
        };
 | 
			
		||||
        blockVsize += tx.vsize;
 | 
			
		||||
        blockWeight = tx.weight;
 | 
			
		||||
        blockSize = tx.size;
 | 
			
		||||
        blockFees = tx.fee;
 | 
			
		||||
        transactionIds = [tx.txid];
 | 
			
		||||
        transactions = [tx];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (transactions.length) {
 | 
			
		||||
      mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
 | 
			
		||||
      mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return mempoolBlocks;
 | 
			
		||||
@ -178,6 +190,8 @@ class MempoolBlocks {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
 | 
			
		||||
    const start = Date.now();
 | 
			
		||||
 | 
			
		||||
    // reset mempool short ids
 | 
			
		||||
    this.resetUids();
 | 
			
		||||
    for (const tx of Object.values(newMempool)) {
 | 
			
		||||
@ -194,7 +208,7 @@ class MempoolBlocks {
 | 
			
		||||
          fee: entry.fee,
 | 
			
		||||
          weight: entry.weight,
 | 
			
		||||
          feePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
          effectiveFeePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
          effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
 | 
			
		||||
          inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
@ -216,7 +230,7 @@ class MempoolBlocks {
 | 
			
		||||
    // run the block construction algorithm in a separate thread, and wait for a result
 | 
			
		||||
    let threadErrorListener;
 | 
			
		||||
    try {
 | 
			
		||||
      const workerResultPromise = new Promise<{ blocks: CompactThreadTransaction[][], clusters: Map<number, number[]> }>((resolve, reject) => {
 | 
			
		||||
      const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
 | 
			
		||||
        threadErrorListener = reject;
 | 
			
		||||
        this.txSelectionWorker?.once('message', (result): void => {
 | 
			
		||||
          resolve(result);
 | 
			
		||||
@ -224,19 +238,14 @@ class MempoolBlocks {
 | 
			
		||||
        this.txSelectionWorker?.once('error', reject);
 | 
			
		||||
      });
 | 
			
		||||
      this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
 | 
			
		||||
      let { blocks, clusters } = this.convertResultTxids(await workerResultPromise);
 | 
			
		||||
      // filter out stale transactions
 | 
			
		||||
      const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
 | 
			
		||||
      blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
 | 
			
		||||
      const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
 | 
			
		||||
      if (filteredCount < unfilteredCount) {
 | 
			
		||||
        logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from makeBlockTemplates`);
 | 
			
		||||
      }
 | 
			
		||||
      const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
 | 
			
		||||
 | 
			
		||||
      // clean up thread error listener
 | 
			
		||||
      this.txSelectionWorker?.removeListener('error', threadErrorListener);
 | 
			
		||||
 | 
			
		||||
      return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
 | 
			
		||||
      const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
 | 
			
		||||
      logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
 | 
			
		||||
      return processed;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -250,6 +259,8 @@ class MempoolBlocks {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const start = Date.now();
 | 
			
		||||
 | 
			
		||||
    for (const tx of Object.values(added)) {
 | 
			
		||||
      this.setUid(tx);
 | 
			
		||||
    }
 | 
			
		||||
@ -262,7 +273,7 @@ class MempoolBlocks {
 | 
			
		||||
        fee: entry.fee,
 | 
			
		||||
        weight: entry.weight,
 | 
			
		||||
        feePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
        effectiveFeePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
        effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
 | 
			
		||||
        inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
@ -270,7 +281,7 @@ class MempoolBlocks {
 | 
			
		||||
    // run the block construction algorithm in a separate thread, and wait for a result
 | 
			
		||||
    let threadErrorListener;
 | 
			
		||||
    try {
 | 
			
		||||
      const workerResultPromise = new Promise<{ blocks: CompactThreadTransaction[][], clusters: Map<number, number[]> }>((resolve, reject) => {
 | 
			
		||||
      const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
 | 
			
		||||
        threadErrorListener = reject;
 | 
			
		||||
        this.txSelectionWorker?.once('message', (result): void => {
 | 
			
		||||
          resolve(result);
 | 
			
		||||
@ -278,53 +289,78 @@ class MempoolBlocks {
 | 
			
		||||
        this.txSelectionWorker?.once('error', reject);
 | 
			
		||||
      });
 | 
			
		||||
      this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
 | 
			
		||||
      let { blocks, clusters } = this.convertResultTxids(await workerResultPromise);
 | 
			
		||||
      // filter out stale transactions
 | 
			
		||||
      const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
 | 
			
		||||
      blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
 | 
			
		||||
      const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
 | 
			
		||||
      if (filteredCount < unfilteredCount) {
 | 
			
		||||
        logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from updateBlockTemplates`);
 | 
			
		||||
      }
 | 
			
		||||
      const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
 | 
			
		||||
 | 
			
		||||
      this.removeUids(removedUids);
 | 
			
		||||
 | 
			
		||||
      // clean up thread error listener
 | 
			
		||||
      this.txSelectionWorker?.removeListener('error', threadErrorListener);
 | 
			
		||||
 | 
			
		||||
      this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
 | 
			
		||||
      this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
 | 
			
		||||
      logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processBlockTemplates(mempool, blocks: ThreadTransaction[][], clusters, saveResults): MempoolBlockWithTransactions[] {
 | 
			
		||||
    // update this thread's mempool with the results
 | 
			
		||||
    blocks.forEach((block, blockIndex) => {
 | 
			
		||||
      let runningVsize = 0;
 | 
			
		||||
      block.forEach(tx => {
 | 
			
		||||
        if (tx.txid && tx.txid in mempool) {
 | 
			
		||||
          // save position in projected blocks
 | 
			
		||||
          mempool[tx.txid].position = {
 | 
			
		||||
            block: blockIndex,
 | 
			
		||||
            vsize: runningVsize + (mempool[tx.txid].vsize / 2),
 | 
			
		||||
          };
 | 
			
		||||
          runningVsize += mempool[tx.txid].vsize;
 | 
			
		||||
 | 
			
		||||
          if (tx.effectiveFeePerVsize != null) {
 | 
			
		||||
            mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
 | 
			
		||||
  private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] {
 | 
			
		||||
    for (const txid of Object.keys(rates)) {
 | 
			
		||||
      if (txid in mempool) {
 | 
			
		||||
        mempool[txid].effectiveFeePerVsize = rates[txid];
 | 
			
		||||
      }
 | 
			
		||||
          if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
 | 
			
		||||
    const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
 | 
			
		||||
    // update this thread's mempool with the results
 | 
			
		||||
    for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
 | 
			
		||||
      const block: string[] = blocks[blockIndex];
 | 
			
		||||
      let txid: string;
 | 
			
		||||
      let mempoolTx: TransactionExtended;
 | 
			
		||||
      let totalSize = 0;
 | 
			
		||||
      let totalVsize = 0;
 | 
			
		||||
      let totalWeight = 0;
 | 
			
		||||
      let totalFees = 0;
 | 
			
		||||
      const transactions: TransactionExtended[] = [];
 | 
			
		||||
      for (let txIndex = 0; txIndex < block.length; txIndex++) {
 | 
			
		||||
        txid = block[txIndex];
 | 
			
		||||
        if (txid) {
 | 
			
		||||
          mempoolTx = mempool[txid];
 | 
			
		||||
          // save position in projected blocks
 | 
			
		||||
          mempoolTx.position = {
 | 
			
		||||
            block: blockIndex,
 | 
			
		||||
            vsize: totalVsize + (mempoolTx.vsize / 2),
 | 
			
		||||
          };
 | 
			
		||||
          mempoolTx.cpfpChecked = true;
 | 
			
		||||
 | 
			
		||||
          totalSize += mempoolTx.size;
 | 
			
		||||
          totalVsize += mempoolTx.vsize;
 | 
			
		||||
          totalWeight += mempoolTx.weight;
 | 
			
		||||
          totalFees += mempoolTx.fee;
 | 
			
		||||
 | 
			
		||||
          if (totalVsize <= sizeLimit) {
 | 
			
		||||
            transactions.push(mempoolTx);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      readyBlocks.push({
 | 
			
		||||
        transactionIds: block,
 | 
			
		||||
        transactions,
 | 
			
		||||
        totalSize,
 | 
			
		||||
        totalWeight,
 | 
			
		||||
        totalFees
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const cluster of Object.values(clusters)) {
 | 
			
		||||
      for (const memberTxid of cluster) {
 | 
			
		||||
        if (memberTxid in mempool) {
 | 
			
		||||
          const mempoolTx = mempool[memberTxid];
 | 
			
		||||
          const ancestors: Ancestor[] = [];
 | 
			
		||||
          const descendants: Ancestor[] = [];
 | 
			
		||||
            const cluster = clusters[tx.cpfpRoot];
 | 
			
		||||
          let matched = false;
 | 
			
		||||
          cluster.forEach(txid => {
 | 
			
		||||
              if (!txid || !mempool[txid]) {
 | 
			
		||||
                logger.warn('projected transaction ancestor missing from mempool cache');
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              if (txid === tx.txid) {
 | 
			
		||||
            if (txid === memberTxid) {
 | 
			
		||||
              matched = true;
 | 
			
		||||
            } else {
 | 
			
		||||
              const relative = {
 | 
			
		||||
@ -339,23 +375,14 @@ class MempoolBlocks {
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
            mempool[tx.txid].ancestors = ancestors;
 | 
			
		||||
            mempool[tx.txid].descendants = descendants;
 | 
			
		||||
            mempool[tx.txid].bestDescendant = null;
 | 
			
		||||
          mempoolTx.ancestors = ancestors;
 | 
			
		||||
          mempoolTx.descendants = descendants;
 | 
			
		||||
          mempoolTx.bestDescendant = null;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
          mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
 | 
			
		||||
        } else {
 | 
			
		||||
          logger.warn('projected transaction missing from mempool cache');
 | 
			
		||||
    }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // unpack the condensed blocks into proper mempool blocks
 | 
			
		||||
    const mempoolBlocks = blocks.map((transactions) => {
 | 
			
		||||
      return this.dataToMempoolBlocks(transactions.map(tx => {
 | 
			
		||||
        return mempool[tx.txid] || null;
 | 
			
		||||
      }).filter(tx => !!tx));
 | 
			
		||||
    });
 | 
			
		||||
    const mempoolBlocks = readyBlocks.map(b => this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees));
 | 
			
		||||
 | 
			
		||||
    if (saveResults) {
 | 
			
		||||
      const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
 | 
			
		||||
@ -366,27 +393,17 @@ class MempoolBlocks {
 | 
			
		||||
    return mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private dataToMempoolBlocks(transactions: TransactionExtended[]): MempoolBlockWithTransactions {
 | 
			
		||||
    let totalSize = 0;
 | 
			
		||||
    let totalWeight = 0;
 | 
			
		||||
    const fitTransactions: TransactionExtended[] = [];
 | 
			
		||||
    transactions.forEach(tx => {
 | 
			
		||||
      totalSize += tx.size;
 | 
			
		||||
      totalWeight += tx.weight;
 | 
			
		||||
      if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) {
 | 
			
		||||
        fitTransactions.push(tx);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number): MempoolBlockWithTransactions {
 | 
			
		||||
    const feeStats = Common.calcEffectiveFeeStatistics(transactions);
 | 
			
		||||
    return {
 | 
			
		||||
      blockSize: totalSize,
 | 
			
		||||
      blockVSize: totalWeight / 4,
 | 
			
		||||
      nTx: transactions.length,
 | 
			
		||||
      totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
 | 
			
		||||
      blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
 | 
			
		||||
      nTx: transactionIds.length,
 | 
			
		||||
      totalFees: totalFees,
 | 
			
		||||
      medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
 | 
			
		||||
      feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
 | 
			
		||||
      transactionIds: transactions.map((tx) => tx.txid),
 | 
			
		||||
      transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
 | 
			
		||||
      transactionIds: transactionIds,
 | 
			
		||||
      transactions: transactions.map((tx) => Common.stripTransaction(tx)),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -415,14 +432,16 @@ class MempoolBlocks {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private convertResultTxids({ blocks, clusters }: { blocks: any[][], clusters: Map<number, number[]>})
 | 
			
		||||
    : { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] }} {
 | 
			
		||||
    for (const block of blocks) {
 | 
			
		||||
      for (const tx of block) {
 | 
			
		||||
        tx.txid = this.uidMap.get(tx.uid);
 | 
			
		||||
        if (tx.cpfpRoot) {
 | 
			
		||||
          tx.cpfpRoot = this.uidMap.get(tx.cpfpRoot);
 | 
			
		||||
        }
 | 
			
		||||
  private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
 | 
			
		||||
    : { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
 | 
			
		||||
    const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
 | 
			
		||||
      return this.uidMap.get(uid) || '';
 | 
			
		||||
    }));
 | 
			
		||||
    const convertedRates = {};
 | 
			
		||||
    for (const rateUid of rates.keys()) {
 | 
			
		||||
      const rateTxid = this.uidMap.get(rateUid);
 | 
			
		||||
      if (rateTxid) {
 | 
			
		||||
        convertedRates[rateTxid] = rates.get(rateUid);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const convertedClusters = {};
 | 
			
		||||
@ -435,7 +454,7 @@ class MempoolBlocks {
 | 
			
		||||
        convertedClusters[rootTxid] = members;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return { blocks, clusters: convertedClusters } as { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] }};
 | 
			
		||||
    return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { CompactThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
 | 
			
		||||
import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces';
 | 
			
		||||
import { PairingHeap } from '../utils/pairing-heap';
 | 
			
		||||
import { parentPort } from 'worker_threads';
 | 
			
		||||
 | 
			
		||||
@ -19,11 +19,11 @@ if (parentPort) {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const { blocks, clusters } = makeBlockTemplates(mempool);
 | 
			
		||||
    const { blocks, rates, clusters } = makeBlockTemplates(mempool);
 | 
			
		||||
 | 
			
		||||
    // return the result to main thread.
 | 
			
		||||
    if (parentPort) {
 | 
			
		||||
      parentPort.postMessage({ blocks, clusters });
 | 
			
		||||
      parentPort.postMessage({ blocks, rates, clusters });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@ -33,14 +33,14 @@ if (parentPort) {
 | 
			
		||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 | 
			
		||||
*/
 | 
			
		||||
function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
  : { blocks: CompactThreadTransaction[][], clusters: Map<number, number[]> } {
 | 
			
		||||
  : { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> } {
 | 
			
		||||
  const start = Date.now();
 | 
			
		||||
  const auditPool: Map<number, AuditTransaction> = new Map();
 | 
			
		||||
  const mempoolArray: AuditTransaction[] = [];
 | 
			
		||||
  const restOfArray: CompactThreadTransaction[] = [];
 | 
			
		||||
  const cpfpClusters: Map<number, number[]> = new Map();
 | 
			
		||||
  
 | 
			
		||||
  mempool.forEach(tx => {
 | 
			
		||||
    tx.dirty = false;
 | 
			
		||||
    // initializing everything up front helps V8 optimize property access later
 | 
			
		||||
    auditPool.set(tx.uid, {
 | 
			
		||||
      uid: tx.uid,
 | 
			
		||||
@ -81,9 +81,8 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
 | 
			
		||||
  // Build blocks by greedily choosing the highest feerate package
 | 
			
		||||
  // (i.e. the package rooted in the transaction with the best ancestor score)
 | 
			
		||||
  const blocks: CompactThreadTransaction[][] = [];
 | 
			
		||||
  const blocks: number[][] = [];
 | 
			
		||||
  let blockWeight = 4000;
 | 
			
		||||
  let blockSize = 0;
 | 
			
		||||
  let transactions: AuditTransaction[] = [];
 | 
			
		||||
  const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
 | 
			
		||||
    if (a.score === b.score) {
 | 
			
		||||
@ -139,13 +138,16 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
          ancestor.used = true;
 | 
			
		||||
          ancestor.usedBy = nextTx.uid;
 | 
			
		||||
          // update original copy of this tx with effective fee rate & relatives data
 | 
			
		||||
          if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) {
 | 
			
		||||
            mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
 | 
			
		||||
          if (isCluster) {
 | 
			
		||||
            mempoolTx.cpfpRoot = nextTx.uid;
 | 
			
		||||
            mempoolTx.dirty = true;
 | 
			
		||||
          }
 | 
			
		||||
          if (mempoolTx.cpfpRoot !== nextTx.uid) {
 | 
			
		||||
            mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
 | 
			
		||||
            mempoolTx.dirty;
 | 
			
		||||
          }
 | 
			
		||||
          mempoolTx.cpfpChecked = true;
 | 
			
		||||
          transactions.push(ancestor);
 | 
			
		||||
          blockSize += ancestor.size;
 | 
			
		||||
          blockWeight += ancestor.weight;
 | 
			
		||||
          used.push(ancestor);
 | 
			
		||||
        }
 | 
			
		||||
@ -171,11 +173,10 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
    if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
 | 
			
		||||
      // construct this block
 | 
			
		||||
      if (transactions.length) {
 | 
			
		||||
        blocks.push(transactions.map(t => mempool.get(t.uid) as CompactThreadTransaction));
 | 
			
		||||
        blocks.push(transactions.map(t => t.uid));
 | 
			
		||||
      }
 | 
			
		||||
      // reset for the next block
 | 
			
		||||
      transactions = [];
 | 
			
		||||
      blockSize = 0;
 | 
			
		||||
      blockWeight = 4000;
 | 
			
		||||
 | 
			
		||||
      // 'overflow' packages didn't fit in this block, but are valid candidates for the next
 | 
			
		||||
@ -196,14 +197,22 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
  }
 | 
			
		||||
  // add the final unbounded block if it contains any transactions
 | 
			
		||||
  if (transactions.length > 0) {
 | 
			
		||||
    blocks.push(transactions.map(t => mempool.get(t.uid) as CompactThreadTransaction));
 | 
			
		||||
    blocks.push(transactions.map(t => t.uid));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // get map of dirty transactions
 | 
			
		||||
  const rates = new Map<number, number>();
 | 
			
		||||
  for (const tx of mempool.values()) {
 | 
			
		||||
    if (tx?.dirty) {
 | 
			
		||||
      rates.set(tx.uid, tx.effectiveFeePerVsize || tx.feePerVsize);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const end = Date.now();
 | 
			
		||||
  const time = end - start;
 | 
			
		||||
  logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
 | 
			
		||||
 | 
			
		||||
  return { blocks, clusters: cpfpClusters };
 | 
			
		||||
  return { blocks, rates, clusters: cpfpClusters };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// traverse in-mempool ancestors
 | 
			
		||||
 | 
			
		||||
@ -114,6 +114,7 @@ export interface CompactThreadTransaction {
 | 
			
		||||
  inputs: number[];
 | 
			
		||||
  cpfpRoot?: string;
 | 
			
		||||
  cpfpChecked?: boolean;
 | 
			
		||||
  dirty?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ThreadTransaction {
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
 | 
			
		||||
import { StartComponent } from './components/start/start.component';
 | 
			
		||||
import { TransactionComponent } from './components/transaction/transaction.component';
 | 
			
		||||
import { BlockComponent } from './components/block/block.component';
 | 
			
		||||
import { ClockMinedComponent as ClockMinedComponent } from './components/clock/clock-mined.component';
 | 
			
		||||
import { ClockMempoolComponent as ClockMempoolComponent } from './components/clock/clock-mempool.component';
 | 
			
		||||
import { AddressComponent } from './components/address/address.component';
 | 
			
		||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
			
		||||
import { AboutComponent } from './components/about/about.component';
 | 
			
		||||
@ -355,6 +357,14 @@ let routes: Routes = [
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'clock-mined',
 | 
			
		||||
    component: ClockMinedComponent,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'clock-mempool',
 | 
			
		||||
    component: ClockMempoolComponent,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'status',
 | 
			
		||||
    data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  @Input() unavailable: boolean = false;
 | 
			
		||||
  @Input() auditHighlighting: boolean = false;
 | 
			
		||||
  @Input() blockConversion: Price;
 | 
			
		||||
  @Input() pixelAlign: boolean = false;
 | 
			
		||||
  @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
 | 
			
		||||
  @Output() txHoverEvent = new EventEmitter<string>();
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
@ -201,7 +202,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
      this.start();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
 | 
			
		||||
        blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, highlighting: this.auditHighlighting });
 | 
			
		||||
        blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
 | 
			
		||||
        highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign });
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ export default class BlockScene {
 | 
			
		||||
  gridWidth: number;
 | 
			
		||||
  gridHeight: number;
 | 
			
		||||
  gridSize: number;
 | 
			
		||||
  pixelAlign: boolean;
 | 
			
		||||
  vbytesPerUnit: number;
 | 
			
		||||
  unitPadding: number;
 | 
			
		||||
  unitWidth: number;
 | 
			
		||||
@ -23,19 +24,24 @@ export default class BlockScene {
 | 
			
		||||
  animateUntil = 0;
 | 
			
		||||
  dirty: boolean;
 | 
			
		||||
 | 
			
		||||
  constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
 | 
			
		||||
  constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
 | 
			
		||||
      { width: number, height: number, resolution: number, blockLimit: number,
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
 | 
			
		||||
  ) {
 | 
			
		||||
    this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
 | 
			
		||||
    this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
    this.gridSize = this.width / this.gridWidth;
 | 
			
		||||
    if (this.pixelAlign) {
 | 
			
		||||
      this.unitPadding =  Math.max(1, Math.floor(this.gridSize / 2.5));
 | 
			
		||||
      this.unitWidth = this.gridSize - (this.unitPadding);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.unitPadding =  width / 500;
 | 
			
		||||
      this.unitWidth = this.gridSize - (this.unitPadding * 2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.dirty = true;
 | 
			
		||||
    if (this.initialised && this.scene) {
 | 
			
		||||
@ -209,14 +215,15 @@ export default class BlockScene {
 | 
			
		||||
    this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
 | 
			
		||||
  private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
 | 
			
		||||
      { width: number, height: number, resolution: number, blockLimit: number,
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
 | 
			
		||||
        orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
 | 
			
		||||
  ): void {
 | 
			
		||||
    this.orientation = orientation;
 | 
			
		||||
    this.flip = flip;
 | 
			
		||||
    this.vertexArray = vertexArray;
 | 
			
		||||
    this.highlightingEnabled = highlighting;
 | 
			
		||||
    this.pixelAlign = pixelAlign;
 | 
			
		||||
 | 
			
		||||
    this.scene = {
 | 
			
		||||
      count: 0,
 | 
			
		||||
@ -342,7 +349,12 @@ export default class BlockScene {
 | 
			
		||||
  private gridToScreen(position: Square | void): Square {
 | 
			
		||||
    if (position) {
 | 
			
		||||
      const slotSize = (position.s * this.gridSize);
 | 
			
		||||
      const squareSize = slotSize - (this.unitPadding * 2);
 | 
			
		||||
      let squareSize;
 | 
			
		||||
      if (this.pixelAlign) {
 | 
			
		||||
        squareSize = slotSize - (this.unitPadding);
 | 
			
		||||
      } else {
 | 
			
		||||
        squareSize = slotSize - (this.unitPadding * 2);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // The grid is laid out notionally left-to-right, bottom-to-top,
 | 
			
		||||
      // so we rotate and/or flip the y axis to match the target configuration.
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,26 @@
 | 
			
		||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr"
 | 
			
		||||
  [style.left]="static ? (offset || 0) + 'px' : null"
 | 
			
		||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [class.minimal]="minimal"
 | 
			
		||||
  [style.left]="static ? (offset || 0) + 'px' : null" [style.--block-size]="blockWidth+'px'"
 | 
			
		||||
  *ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate">
 | 
			
		||||
  <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
 | 
			
		||||
    <ng-container *ngIf="connected && block && !block.loading && !block.placeholder; else placeholderBlock">
 | 
			
		||||
      <div
 | 
			
		||||
        *ngIf="minimal && spotlight < 0 && chainTip + spotlight + 1 === block.height"
 | 
			
		||||
        class="spotlight-bottom"
 | 
			
		||||
        [style.left]="blockStyles[i].left"
 | 
			
		||||
      ></div>
 | 
			
		||||
      <div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
 | 
			
		||||
        class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
 | 
			
		||||
        [class.offscreen]="!static && count && i >= count"
 | 
			
		||||
        id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
 | 
			
		||||
        [class.blink-bg]="isSpecial(block.height)">
 | 
			
		||||
        <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
 | 
			
		||||
          class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
 | 
			
		||||
        <div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
 | 
			
		||||
        <div *ngIf="!minimal" [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
 | 
			
		||||
          <a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
 | 
			
		||||
            }}</a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="block-body">
 | 
			
		||||
          <ng-container *ngIf="!minimal">
 | 
			
		||||
            <div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
 | 
			
		||||
              ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
 | 
			
		||||
                i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
			
		||||
@ -48,6 +55,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
 | 
			
		||||
              <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
 | 
			
		||||
          <a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
 | 
			
		||||
@ -79,11 +87,11 @@
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingBlocksTemplate>
 | 
			
		||||
  <div class="blocks-container" [class.time-ltr]="timeLtr">
 | 
			
		||||
  <div class="blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'">
 | 
			
		||||
    <div class="flashing">
 | 
			
		||||
      <div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn">
 | 
			
		||||
        <div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}"
 | 
			
		||||
          [ngStyle]="emptyBlockStyles[i]"></div>
 | 
			
		||||
          [ngStyle]="emptyBlockStyles[i]" [class.offscreen]="!static && count && i >= count"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
.bitcoin-block {
 | 
			
		||||
  width: 125px;
 | 
			
		||||
  height: 125px;
 | 
			
		||||
  width: var(--block-size);
 | 
			
		||||
  height: var(--block-size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockLink {
 | 
			
		||||
@ -22,7 +22,11 @@
 | 
			
		||||
.mined-block {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
  transition: background 2s, left 2s, transform 1s;
 | 
			
		||||
  transition: background 2s, left 2s, transform 1s, opacity 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mined-block.offscreen {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mined-block.placeholder-block {
 | 
			
		||||
@ -35,9 +39,11 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blocks-container {
 | 
			
		||||
  --block-size: 125px;
 | 
			
		||||
  --block-offset: calc(0.32 * var(--block-size));
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
  left: 40px;
 | 
			
		||||
  left: var(--block-offset);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-body {
 | 
			
		||||
@ -77,11 +83,11 @@
 | 
			
		||||
 | 
			
		||||
.bitcoin-block::after {
 | 
			
		||||
  content: '';
 | 
			
		||||
  width: 125px;
 | 
			
		||||
  height: 24px;
 | 
			
		||||
  width: var(--block-size);
 | 
			
		||||
  height: calc(0.192 * var(--block-size));
 | 
			
		||||
  position:absolute;
 | 
			
		||||
  top: -24px;
 | 
			
		||||
  left: -20px;
 | 
			
		||||
  top: calc(-0.192 * var(--block-size));
 | 
			
		||||
  left: calc(-0.16 * var(--block-size));
 | 
			
		||||
  background-color: #232838;
 | 
			
		||||
  transform:skew(40deg);
 | 
			
		||||
  transform-origin:top;
 | 
			
		||||
@ -89,11 +95,11 @@
 | 
			
		||||
 | 
			
		||||
.bitcoin-block::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  width: 20px;
 | 
			
		||||
  height: 125px;
 | 
			
		||||
  width: calc(0.16 * var(--block-size));
 | 
			
		||||
  height: var(--block-size);
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -12px;
 | 
			
		||||
  left: -20px;
 | 
			
		||||
  top: calc(-0.096 * var(--block-size));
 | 
			
		||||
  left: calc(-0.16 * var(--block-size));
 | 
			
		||||
  background-color: #191c27;
 | 
			
		||||
 | 
			
		||||
  transform: skewY(50deg);
 | 
			
		||||
@ -169,3 +175,15 @@
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spotlight-bottom {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: calc(0.6 * var(--block-size));
 | 
			
		||||
  height: calc(0.25 * var(--block-size));
 | 
			
		||||
  border-left: solid calc(0.3 * var(--block-size)) transparent;
 | 
			
		||||
  border-bottom: solid calc(0.3 * var(--block-size)) white;
 | 
			
		||||
  border-right: solid calc(0.3 * var(--block-size)) transparent;
 | 
			
		||||
  transform: translate(calc(0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
}
 | 
			
		||||
@ -24,6 +24,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  @Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
 | 
			
		||||
  @Input() loadingTip: boolean = false;
 | 
			
		||||
  @Input() connected: boolean = true;
 | 
			
		||||
  @Input() minimal: boolean = false;
 | 
			
		||||
  @Input() blockWidth: number = 125;
 | 
			
		||||
  @Input() spotlight: number = 0;
 | 
			
		||||
  
 | 
			
		||||
  specialBlocks = specialBlocks;
 | 
			
		||||
  network = '';
 | 
			
		||||
@ -51,6 +54,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  timeLtrSubscription: Subscription;
 | 
			
		||||
  timeLtr: boolean;
 | 
			
		||||
 | 
			
		||||
  blockOffset: number = 155;
 | 
			
		||||
  dividerBlockOffset: number = 205;
 | 
			
		||||
  blockPadding: number = 30;
 | 
			
		||||
 | 
			
		||||
  gradientColors = {
 | 
			
		||||
    '': ['#9339f4', '#105fb0'],
 | 
			
		||||
    bisq: ['#9339f4', '#105fb0'],
 | 
			
		||||
@ -118,7 +125,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
 | 
			
		||||
          this.blockStyles = [];
 | 
			
		||||
          if (this.blocksFilled && block.height > this.chainTip) {
 | 
			
		||||
            this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
 | 
			
		||||
            this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              this.blockStyles = [];
 | 
			
		||||
              this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
 | 
			
		||||
@ -159,6 +166,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
    if (changes.blockWidth && this.blockWidth) {
 | 
			
		||||
      this.blockPadding = 0.24 * this.blockWidth;
 | 
			
		||||
      this.blockOffset = this.blockWidth + this.blockPadding;
 | 
			
		||||
      this.dividerBlockOffset = this.blockOffset + (0.4 * this.blockWidth);
 | 
			
		||||
      this.blockStyles = [];
 | 
			
		||||
      this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
 | 
			
		||||
    }
 | 
			
		||||
    if (this.static) {
 | 
			
		||||
      const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
 | 
			
		||||
      this.updateStaticBlocks(animateSlide);
 | 
			
		||||
@ -191,14 +205,14 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
      }
 | 
			
		||||
      this.arrowVisible = true;
 | 
			
		||||
      if (newBlockFromLeft) {
 | 
			
		||||
        this.arrowLeftPx = blockindex * 155 + 30 - 205;
 | 
			
		||||
        this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding - this.dividerBlockOffset;
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          this.arrowTransition = '2s';
 | 
			
		||||
          this.arrowLeftPx = blockindex * 155 + 30;
 | 
			
		||||
          this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
 | 
			
		||||
          this.cd.markForCheck();
 | 
			
		||||
        }, 50);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.arrowLeftPx = blockindex * 155 + 30;
 | 
			
		||||
        this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
 | 
			
		||||
        if (!animate) {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            this.arrowTransition = '2s';
 | 
			
		||||
@ -245,7 +259,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
    this.blocks = this.blocks.slice(0, this.count);
 | 
			
		||||
    this.blockStyles = [];
 | 
			
		||||
    this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0)));
 | 
			
		||||
    this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -this.blockOffset : 0)));
 | 
			
		||||
    this.cd.markForCheck();
 | 
			
		||||
    if (animateSlide) {
 | 
			
		||||
      // animate blocks slide right
 | 
			
		||||
@ -287,7 +301,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      left: addLeft + 155 * index + 'px',
 | 
			
		||||
      left: addLeft + this.blockOffset * index + 'px',
 | 
			
		||||
      background: `repeating-linear-gradient(
 | 
			
		||||
        #2d3348,
 | 
			
		||||
        #2d3348 ${greenBackgroundHeight}%,
 | 
			
		||||
@ -309,7 +323,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    const addLeft = animateEnterFrom || 0;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      left: addLeft + (155 * index) + 'px',
 | 
			
		||||
      left: addLeft + (this.blockOffset * index) + 'px',
 | 
			
		||||
      background: "#2d3348",
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
@ -317,7 +331,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) {
 | 
			
		||||
    const addLeft = animateEnterFrom || 0;
 | 
			
		||||
    return {
 | 
			
		||||
      left: addLeft + (155 * index) + 'px',
 | 
			
		||||
      left: addLeft + (this.blockOffset * index) + 'px',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -325,7 +339,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    const addLeft = animateEnterFrom || 0;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
 | 
			
		||||
      left: addLeft + this.blockOffset * this.emptyBlocks.indexOf(block) + 'px',
 | 
			
		||||
      background: "#2d3348",
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<div class="clock-face" [style]="faceStyle">
 | 
			
		||||
  <ng-content></ng-content>
 | 
			
		||||
  <svg
 | 
			
		||||
    class="cut-out"
 | 
			
		||||
    width="384"
 | 
			
		||||
    height="384"
 | 
			
		||||
    viewBox="0 0 384 384"
 | 
			
		||||
  >
 | 
			
		||||
    <g>
 | 
			
		||||
      <path
 | 
			
		||||
        class="face"
 | 
			
		||||
        d="M 0,0 V 384 H 384 V 0 Z M 192,15 A 177,177 0 0 1 369,192 177,177 0 0 1 192,369 177,177 0 0 1 15,192 177,177 0 0 1 192,15 Z"
 | 
			
		||||
      />
 | 
			
		||||
    </g>
 | 
			
		||||
  </svg>
 | 
			
		||||
 | 
			
		||||
  <svg
 | 
			
		||||
    class="demo-dial"
 | 
			
		||||
    width="384"
 | 
			
		||||
    height="384"
 | 
			
		||||
    viewBox="0 0 384 384"
 | 
			
		||||
  >
 | 
			
		||||
    <defs>
 | 
			
		||||
      <pattern id="dial-gradient" patternUnits="userSpaceOnUse" width="384" height="384">
 | 
			
		||||
        <image class="dial-gradient-img" href="/resources/clock/gradient.png" x="0" y="0" width="384" height="384" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
 | 
			
		||||
      </pattern>
 | 
			
		||||
    </defs>
 | 
			
		||||
 | 
			
		||||
    <path *ngFor="let angle of minorTicks" class="tick minor" d="M 192,27 v 10" [style.transform]="'rotate(' + angle + 'deg)'"/>
 | 
			
		||||
    <path *ngFor="let angle of majorTicks" class="tick major" d="M 192,27 v 18" [style.transform]="'rotate(' + angle + 'deg)'"/>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngFor="let segment of segments; trackBy: trackBySegment">
 | 
			
		||||
      <path class="block-segment" [attr.d]="segment.path" />
 | 
			
		||||
      <!-- <circle class="segment-mark start" [attr.cx]="segment.start.x" [attr.cy]="segment.start.y" r="2" style="fill:green;stroke:white;stroke-width:1px;" />
 | 
			
		||||
      <circle class="segment-mark end" [attr.cx]="segment.end.x" [attr.cy]="segment.end.y" r="2" style="fill:red;stroke:white;stroke-width:1px;" /> -->
 | 
			
		||||
    </ng-container>
 | 
			
		||||
    <!-- <polyline points="468.750,82.031 468.750,35 " id="polyline322" style="fill:none;stroke:#ffffff;stroke-width:4.84839;stroke-dasharray:none;stroke-opacity:1" transform="matrix(0.41250847,0,0,0.93092534,-1.3627708,-32.692008)" /> -->
 | 
			
		||||
    <path class="tick very major" d="M 192,0 v 45" />
 | 
			
		||||
    <path id="hour" class="gnomon hour" d="M 178,3 206,3 192,40 Z" [style.transform]="'rotate(' + (hours * 30) + 'deg)'" />
 | 
			
		||||
    <path id="minute" class="gnomon minute" d="M 180,4 204,4 192,38 Z" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
 | 
			
		||||
  </svg>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,69 @@
 | 
			
		||||
.clock-face {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  height: 84.375%;
 | 
			
		||||
  margin: auto;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
 | 
			
		||||
  .cut-out, .demo-dial {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
 | 
			
		||||
    .face {
 | 
			
		||||
      fill: #11131f;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .gnomon {
 | 
			
		||||
    transform-origin: center;
 | 
			
		||||
    stroke-linejoin: round;
 | 
			
		||||
 | 
			
		||||
    &.minute {
 | 
			
		||||
      fill:#80C2E1;
 | 
			
		||||
      stroke:#80C2E1;
 | 
			
		||||
      stroke-width: 2px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.hour {
 | 
			
		||||
      fill: #105fb0;
 | 
			
		||||
      stroke: #105fb0;
 | 
			
		||||
      stroke-width: 6px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tick {
 | 
			
		||||
    transform-origin: center;
 | 
			
		||||
    fill: none;
 | 
			
		||||
    stroke: white;
 | 
			
		||||
    stroke-width: 2px;
 | 
			
		||||
    stroke-linecap: butt;
 | 
			
		||||
 | 
			
		||||
    &.minor {
 | 
			
		||||
      stroke-opacity: 0.5;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.very.major {
 | 
			
		||||
      stroke-width: 4px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .block-segment {
 | 
			
		||||
    fill: none;
 | 
			
		||||
    stroke: url(#dial-gradient);
 | 
			
		||||
    stroke-width: 18px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dial-segment {
 | 
			
		||||
    fill: none;
 | 
			
		||||
    stroke: white;
 | 
			
		||||
    stroke-width: 2px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dial-gradient-img {
 | 
			
		||||
    transform-origin: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										148
									
								
								frontend/src/app/components/clock-face/clock-face.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								frontend/src/app/components/clock-face/clock-face.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,148 @@
 | 
			
		||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { Subscription, tap, timer } from 'rxjs';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-clock-face',
 | 
			
		||||
  templateUrl: './clock-face.component.html',
 | 
			
		||||
  styleUrls: ['./clock-face.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  @Input() size: number = 300;
 | 
			
		||||
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  timeSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  faceStyle;
 | 
			
		||||
  dialPath;
 | 
			
		||||
  blockTimes = [];
 | 
			
		||||
  segments = [];
 | 
			
		||||
  hours: number = 0;
 | 
			
		||||
  minutes: number = 0;
 | 
			
		||||
  minorTicks: number[] = [];
 | 
			
		||||
  majorTicks: number[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private cd: ChangeDetectorRef
 | 
			
		||||
  ) {
 | 
			
		||||
    this.updateTime();
 | 
			
		||||
    this.makeTicks();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.timeSubscription = timer(0, 250).pipe(
 | 
			
		||||
      tap(() => {
 | 
			
		||||
        this.updateTime();
 | 
			
		||||
      })
 | 
			
		||||
    ).subscribe();
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
        if (block) {
 | 
			
		||||
          this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
 | 
			
		||||
          // using block-reported times, so ensure they are sorted chronologically
 | 
			
		||||
          this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
 | 
			
		||||
          this.updateSegments();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    this.faceStyle = {
 | 
			
		||||
      width: `${this.size}px`,
 | 
			
		||||
      height: `${this.size}px`,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.timeSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateTime(): void {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    const seconds = now.getSeconds() + (now.getMilliseconds() / 1000);
 | 
			
		||||
    this.minutes = (now.getMinutes() + (seconds / 60)) % 60;
 | 
			
		||||
    this.hours = now.getHours() + (this.minutes / 60);
 | 
			
		||||
    this.updateSegments();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateSegments(): void {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    this.blockTimes = this.blockTimes.filter(time => (now.getTime() - time[1].getTime()) <= 3600000);
 | 
			
		||||
    const tail = new Date(now.getTime() - 3600000);
 | 
			
		||||
    const hourStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours());
 | 
			
		||||
 | 
			
		||||
    const times = [
 | 
			
		||||
      ['start', tail],
 | 
			
		||||
      ...this.blockTimes,
 | 
			
		||||
      ['end', now],
 | 
			
		||||
    ];
 | 
			
		||||
    const minuteTimes = times.map(time => {
 | 
			
		||||
      return [time[0], (time[1].getTime() - hourStart.getTime()) / 60000];
 | 
			
		||||
    });
 | 
			
		||||
    this.segments = [];
 | 
			
		||||
    const r = 174;
 | 
			
		||||
    const cx = 192;
 | 
			
		||||
    const cy = cx;
 | 
			
		||||
    for (let i = 1; i < minuteTimes.length; i++) {
 | 
			
		||||
      const arc = this.getArc(minuteTimes[i-1][1], minuteTimes[i][1], r, cx, cy);
 | 
			
		||||
      if (arc) {
 | 
			
		||||
        arc.id = minuteTimes[i][0];
 | 
			
		||||
        this.segments.push(arc);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const arc = this.getArc(minuteTimes[0][1], minuteTimes[1][1], r, cx, cy);
 | 
			
		||||
    if (arc) {
 | 
			
		||||
      this.dialPath = arc.path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.cd.markForCheck();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getArc(startTime, endTime, r, cx, cy): any {
 | 
			
		||||
    const startDegrees = (startTime + 0.2) * 6;
 | 
			
		||||
      const endDegrees = (endTime - 0.2) * 6;
 | 
			
		||||
      const start = this.getPointOnCircle(startDegrees, r, cx, cy);
 | 
			
		||||
      const end = this.getPointOnCircle(endDegrees, r, cx, cy);
 | 
			
		||||
      const arcLength = endDegrees - startDegrees;
 | 
			
		||||
      // merge gaps and omit lines shorter than 1 degree
 | 
			
		||||
      if (arcLength >= 1) {
 | 
			
		||||
        const path = `M ${start.x} ${start.y} A ${r} ${r} 0 ${arcLength > 180 ? 1 : 0} 1 ${end.x} ${end.y}`;
 | 
			
		||||
        return {
 | 
			
		||||
          path,
 | 
			
		||||
          start,
 | 
			
		||||
          end
 | 
			
		||||
        };
 | 
			
		||||
      } else {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getPointOnCircle(deg, r, cx, cy) {
 | 
			
		||||
    const modDeg = ((deg % 360) + 360) % 360;
 | 
			
		||||
    const rad = (modDeg * Math.PI) / 180;
 | 
			
		||||
    return {
 | 
			
		||||
      x: cx + (r * Math.sin(rad)),
 | 
			
		||||
      y: cy - (r * Math.cos(rad)),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  makeTicks() {
 | 
			
		||||
    this.minorTicks = [];
 | 
			
		||||
    this.majorTicks = [];
 | 
			
		||||
    for (let i = 1; i < 60; i++) {
 | 
			
		||||
      if (i % 5 === 0) {
 | 
			
		||||
        this.majorTicks.push(i * 6);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.minorTicks.push(i * 6);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackBySegment(index: number, segment) {
 | 
			
		||||
    return segment.id;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
<app-clock mode="mempool"></app-clock>
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-clock-mempool',
 | 
			
		||||
  templateUrl: './clock-mempool.component.html',
 | 
			
		||||
})
 | 
			
		||||
export class ClockMempoolComponent {}
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
<app-clock mode="block"></app-clock>
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-clock-mined',
 | 
			
		||||
  templateUrl: './clock-mined.component.html',
 | 
			
		||||
})
 | 
			
		||||
export class ClockMinedComponent {}
 | 
			
		||||
							
								
								
									
										67
									
								
								frontend/src/app/components/clock/clock.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								frontend/src/app/components/clock/clock.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
<div class="clock-wrapper" [style]="wrapperStyle">
 | 
			
		||||
  <div class="clockchain-bar" [style.height]="chainHeight + 'px'">
 | 
			
		||||
    <div class="clockchain">
 | 
			
		||||
      <app-clockchain [width]="chainWidth" [height]="chainHeight" [mode]="mode"></app-clockchain>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="clock-face">
 | 
			
		||||
    <app-clock-face [size]="clockSize">
 | 
			
		||||
      <div class="block-wrapper">
 | 
			
		||||
        <ng-container *ngIf="block && block.height >= 0">
 | 
			
		||||
          <ng-container *ngIf="mode === 'block'; else mempoolMode;">
 | 
			
		||||
            <div class="block-cube">
 | 
			
		||||
              <div class="side top"></div>
 | 
			
		||||
              <div class="side bottom"></div>
 | 
			
		||||
              <div class="side right" [style]="blockStyle"></div>
 | 
			
		||||
              <div class="side left" [style]="blockStyle"></div>
 | 
			
		||||
              <div class="side front" [style]="blockStyle"></div>
 | 
			
		||||
              <div class="side back" [style]="blockStyle"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-template #mempoolMode>
 | 
			
		||||
            <div class="block-sizer" [style]="blockSizerStyle">
 | 
			
		||||
              <app-mempool-block-overview [index]="0" [pixelAlign]="true"></app-mempool-block-overview>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <div class="fader"></div>
 | 
			
		||||
          <div class="title-wrapper">
 | 
			
		||||
            <h1 class="block-height">{{ block.height }}</h1>
 | 
			
		||||
          </div>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </div>
 | 
			
		||||
    </app-clock-face>
 | 
			
		||||
  </div>
 | 
			
		||||
  <ng-container *ngIf="!hideStats">
 | 
			
		||||
    <div class="stats top left">
 | 
			
		||||
      <p class="label" i18n="clock.fiat-price">fiat price</p>
 | 
			
		||||
      <p>
 | 
			
		||||
        <app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="stats top right">
 | 
			
		||||
      <p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
 | 
			
		||||
      <p *ngIf="recommendedFees$ | async as recommendedFees;" i18n="shared.sat-vbyte|sat/vB">{{ recommendedFees.fastestFee + 300 }} sat/vB</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div *ngIf="mode !== 'mempool' && block" class="stats bottom left">
 | 
			
		||||
      <p [innerHTML]="block.size | bytes: 2"></p>
 | 
			
		||||
      <p class="label" i18n="clock.block-size">block size</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div *ngIf="mode !== 'mempool' && block" class="stats bottom right">
 | 
			
		||||
      <p class="force-wrap">
 | 
			
		||||
        <ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
 | 
			
		||||
        <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
 | 
			
		||||
        <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
 | 
			
		||||
      <div *ngIf="mode === 'mempool'" class="stats bottom left">
 | 
			
		||||
        <p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
 | 
			
		||||
        <p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div *ngIf="mode === 'mempool'" class="stats bottom right">
 | 
			
		||||
        <p>{{ mempoolInfo.size | number }}</p>
 | 
			
		||||
        <p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										190
									
								
								frontend/src/app/components/clock/clock.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								frontend/src/app/components/clock/clock.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,190 @@
 | 
			
		||||
.clock-wrapper {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: flex-start;
 | 
			
		||||
 | 
			
		||||
  --chain-height: 60px;
 | 
			
		||||
  --clock-width: 300px;
 | 
			
		||||
 | 
			
		||||
  .clockchain-bar, .clock-face {
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    flex-grow: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .clockchain-bar {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 15.625%;
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
    // overflow: hidden;
 | 
			
		||||
    // background: #1d1f31;
 | 
			
		||||
    // box-shadow: 0 0 15px #000;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .clock-face {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    height: 84.375%;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .stats {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 3;
 | 
			
		||||
 | 
			
		||||
    p {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      font-size: calc(0.055 * var(--clock-width));
 | 
			
		||||
      line-height: calc(0.05 * var(--clock-width));
 | 
			
		||||
      opacity: 0.8;
 | 
			
		||||
 | 
			
		||||
      &.force-wrap {
 | 
			
		||||
        word-spacing: 10000px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ::ng-deep .symbol {
 | 
			
		||||
        font-size: inherit;
 | 
			
		||||
        color: white;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .label {
 | 
			
		||||
      font-size: calc(0.04 * var(--clock-width));
 | 
			
		||||
      line-height: calc(0.05 * var(--clock-width));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.top {
 | 
			
		||||
      top: calc(var(--chain-height) + 2%);
 | 
			
		||||
    }
 | 
			
		||||
    &.bottom {
 | 
			
		||||
      bottom: 2%;
 | 
			
		||||
    }
 | 
			
		||||
    &.left {
 | 
			
		||||
      left: 5%;
 | 
			
		||||
    }
 | 
			
		||||
    &.right {
 | 
			
		||||
      right: 5%;
 | 
			
		||||
      text-align: end;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-wrapper {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  .block-height {
 | 
			
		||||
    font-size: calc(0.2 * var(--clock-width));
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    background: radial-gradient(rgba(0,0,0,0.5), transparent 67%);
 | 
			
		||||
    padding: calc(0.05 * var(--clock-width)) calc(0.15 * var(--clock-width));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-wrapper {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
 | 
			
		||||
  .block-sizer {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .fader {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    background: radial-gradient(transparent 0%, transparent 44%, #11131f 58%, #11131f 100%);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .block-cube {
 | 
			
		||||
    --side-width: calc(0.4 * var(--clock-width));
 | 
			
		||||
    --half-side: calc(0.2 * var(--clock-width));
 | 
			
		||||
    --neg-half-side: calc(-0.2 * var(--clock-width));
 | 
			
		||||
    transform-style: preserve-3d;
 | 
			
		||||
    animation: block-spin 60s infinite linear;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: -1;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    left: 50%;
 | 
			
		||||
    transform: translate(-50%, -50%);
 | 
			
		||||
    width: var(--side-width);
 | 
			
		||||
    height: var(--side-width);
 | 
			
		||||
 | 
			
		||||
    .side {
 | 
			
		||||
      width: var(--side-width);
 | 
			
		||||
      height: var(--side-width);
 | 
			
		||||
      line-height: 100px;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      background: #232838;
 | 
			
		||||
      display: block;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .side.top {
 | 
			
		||||
      transform: rotateX(90deg); 
 | 
			
		||||
      margin-top: var(--neg-half-side);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .side.bottom {
 | 
			
		||||
      background: #105fb0;
 | 
			
		||||
      transform: rotateX(-90deg); 
 | 
			
		||||
      margin-top: var(--half-side);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .side.right {
 | 
			
		||||
      transform: rotateY(90deg); 
 | 
			
		||||
      margin-left: var(--half-side);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .side.left {
 | 
			
		||||
      transform: rotateY(-90deg); 
 | 
			
		||||
      margin-left: var(--neg-half-side);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .side.front {
 | 
			
		||||
      transform: translateZ(var(--half-side));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .side.back {
 | 
			
		||||
      transform: translateZ(var(--neg-half-side));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes block-spin {
 | 
			
		||||
  0% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(0deg);}
 | 
			
		||||
  100% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(-360deg);}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										105
									
								
								frontend/src/app/components/clock/clock.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								frontend/src/app/components/clock/clock.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,105 @@
 | 
			
		||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { BlockExtended } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-clock',
 | 
			
		||||
  templateUrl: './clock.component.html',
 | 
			
		||||
  styleUrls: ['./clock.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ClockComponent implements OnInit {
 | 
			
		||||
  @Input() mode: 'block' | 'mempool' = 'block';
 | 
			
		||||
  hideStats: boolean = false;
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  recommendedFees$: Observable<Recommendedfees>;
 | 
			
		||||
  mempoolInfo$: Observable<MempoolInfo>;
 | 
			
		||||
  block: BlockExtended;
 | 
			
		||||
  clockSize: number = 300;
 | 
			
		||||
  chainWidth: number = 384;
 | 
			
		||||
  chainHeight: number = 60;
 | 
			
		||||
  blockStyle;
 | 
			
		||||
  blockSizerStyle;
 | 
			
		||||
  wrapperStyle;
 | 
			
		||||
  limitWidth: number;
 | 
			
		||||
  limitHeight: number;
 | 
			
		||||
 | 
			
		||||
  gradientColors = {
 | 
			
		||||
    '': ['#9339f4', '#105fb0'],
 | 
			
		||||
    bisq: ['#9339f4', '#105fb0'],
 | 
			
		||||
    liquid: ['#116761', '#183550'],
 | 
			
		||||
    'liquidtestnet': ['#494a4a', '#272e46'],
 | 
			
		||||
    testnet: ['#1d486f', '#183550'],
 | 
			
		||||
    signet: ['#6f1d5d', '#471850'],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.route.queryParams.subscribe((params) => {
 | 
			
		||||
      this.hideStats = params && params.stats === 'false';
 | 
			
		||||
      this.limitWidth = Number.parseInt(params.width) || null;
 | 
			
		||||
      this.limitHeight = Number.parseInt(params.height) || null;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.resizeCanvas();
 | 
			
		||||
    this.websocketService.want(['blocks']);
 | 
			
		||||
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe(([block]) => {
 | 
			
		||||
        if (block) {
 | 
			
		||||
          this.block = block;
 | 
			
		||||
          this.blockStyle = this.getStyleForBlock(this.block);
 | 
			
		||||
          this.cd.markForCheck();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    this.recommendedFees$ = this.stateService.recommendedFees$;
 | 
			
		||||
    this.mempoolInfo$ = this.stateService.mempoolInfo$;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStyleForBlock(block: BlockExtended) {
 | 
			
		||||
    const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      background: `repeating-linear-gradient(
 | 
			
		||||
        #2d3348,
 | 
			
		||||
        #2d3348 ${greenBackgroundHeight}%,
 | 
			
		||||
        ${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
 | 
			
		||||
        ${this.gradientColors[''][1]} 100%
 | 
			
		||||
      )`,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
  resizeCanvas(): void {
 | 
			
		||||
    const windowWidth = this.limitWidth || window.innerWidth;
 | 
			
		||||
    const windowHeight = this.limitHeight || window.innerHeight;
 | 
			
		||||
    this.chainWidth = windowWidth;
 | 
			
		||||
    this.chainHeight = Math.max(60, windowHeight / 8);
 | 
			
		||||
    this.clockSize = Math.min(800, windowWidth, windowHeight - (1.4 * this.chainHeight));
 | 
			
		||||
    const size = Math.ceil(this.clockSize / 75) * 75;
 | 
			
		||||
    const margin = (this.clockSize - size) / 2;
 | 
			
		||||
    this.blockSizerStyle = {
 | 
			
		||||
      transform: `translate(${margin}px, ${margin}px)`,
 | 
			
		||||
      width: `${size}px`,
 | 
			
		||||
      height: `${size}px`,
 | 
			
		||||
    };
 | 
			
		||||
    this.wrapperStyle = {
 | 
			
		||||
      '--clock-width': `${this.clockSize}px`,
 | 
			
		||||
      '--chain-height': `${this.chainHeight}px`,
 | 
			
		||||
      'width': this.limitWidth ? `${this.limitWidth}px` : undefined,
 | 
			
		||||
      'height': this.limitHeight ? `${this.limitHeight}px` : undefined,
 | 
			
		||||
    };
 | 
			
		||||
    this.cd.markForCheck();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
<div
 | 
			
		||||
  class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" #container
 | 
			
		||||
  [class.ltr-transition]="ltrTransitionEnabled" [style.width]="width + 'px'" [style.height]="height + 'px'"
 | 
			
		||||
>
 | 
			
		||||
  <div class="position-container" [ngClass]="network ? network : ''" [style.top]="(height / 3) + 'px'">
 | 
			
		||||
    <span>
 | 
			
		||||
      <div class="blocks-wrapper">
 | 
			
		||||
        <app-mempool-blocks [minimal]="true" [count]="mempoolBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'mempool' ? 1 : 0"></app-mempool-blocks>
 | 
			
		||||
        <app-blockchain-blocks [minimal]="true" [count]="blockchainBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'block' ? -1 : 0"></app-blockchain-blocks>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="divider" [style.top]="-(height / 6) + 'px'">
 | 
			
		||||
        <svg
 | 
			
		||||
          viewBox="0 0 2 175"
 | 
			
		||||
          [style.width]="'2px'"
 | 
			
		||||
          [style.height]="(5 * height / 6) + 'px'"
 | 
			
		||||
        >
 | 
			
		||||
          <line
 | 
			
		||||
            class="divider-line"
 | 
			
		||||
            x0="0"
 | 
			
		||||
            x1="0"
 | 
			
		||||
            y0="0"
 | 
			
		||||
            y1="175px"
 | 
			
		||||
          ></line>
 | 
			
		||||
        </svg>
 | 
			
		||||
      </div>
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,94 @@
 | 
			
		||||
.divider {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: -0.5px;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  .divider-line {
 | 
			
		||||
    stroke: white;
 | 
			
		||||
    stroke-width: 4px;
 | 
			
		||||
    stroke-linecap: butt;
 | 
			
		||||
    stroke-dasharray: 25px 25px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE10+/Edge */
 | 
			
		||||
  user-select: none; /* Standard */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.position-container {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.black-background {
 | 
			
		||||
  background-color: #11131f;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scroll-spacer {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  width: 1px;
 | 
			
		||||
  height: 1px;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-block {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  margin: auto;
 | 
			
		||||
  width: 300px;
 | 
			
		||||
  left: -150px;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.time-toggle {
 | 
			
		||||
  color: white;
 | 
			
		||||
  font-size: 0.8rem;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: -1.8em;
 | 
			
		||||
  left: 1px;
 | 
			
		||||
  transform: translateX(-50%);
 | 
			
		||||
  background: none;
 | 
			
		||||
  border: none;
 | 
			
		||||
  outline: none;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.ltr-transition .blocks-wrapper,
 | 
			
		||||
.blockchain-wrapper.ltr-transition .position-container,
 | 
			
		||||
.blockchain-wrapper.ltr-transition .time-toggle {
 | 
			
		||||
  transition: transform 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.time-ltr {
 | 
			
		||||
  .blocks-wrapper {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .time-toggle {
 | 
			
		||||
    transform: translateX(-50%) scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(.ltr-layout) {
 | 
			
		||||
  .blockchain-wrapper.time-ltr .blocks-wrapper,
 | 
			
		||||
  .blockchain-wrapper .blocks-wrapper {
 | 
			
		||||
    direction: ltr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(.rtl-layout) {
 | 
			
		||||
  .blockchain-wrapper.time-ltr .blocks-wrapper,
 | 
			
		||||
  .blockchain-wrapper .blocks-wrapper {
 | 
			
		||||
    direction: rtl;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,73 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { firstValueFrom, Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-clockchain',
 | 
			
		||||
  templateUrl: './clockchain.component.html',
 | 
			
		||||
  styleUrls: ['./clockchain.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  @Input() width: number = 300;
 | 
			
		||||
  @Input() height: number = 60;
 | 
			
		||||
  @Input() mode: 'mempool' | 'block';
 | 
			
		||||
 | 
			
		||||
  mempoolBlocks: number = 3;
 | 
			
		||||
  blockchainBlocks: number = 6;
 | 
			
		||||
  blockWidth: number = 50;
 | 
			
		||||
  dividerStyle;
 | 
			
		||||
 | 
			
		||||
  network: string;
 | 
			
		||||
  timeLtrSubscription: Subscription;
 | 
			
		||||
  timeLtr: boolean = this.stateService.timeLtr.value;
 | 
			
		||||
  ltrTransitionEnabled = false;
 | 
			
		||||
  connectionStateSubscription: Subscription;
 | 
			
		||||
  loadingTip: boolean = true;
 | 
			
		||||
  connected: boolean = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.ngOnChanges();
 | 
			
		||||
 | 
			
		||||
    this.network = this.stateService.network;
 | 
			
		||||
    this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
 | 
			
		||||
      this.timeLtr = !!ltr;
 | 
			
		||||
    });
 | 
			
		||||
    this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
 | 
			
		||||
      this.connected = (state === 2);
 | 
			
		||||
    });
 | 
			
		||||
    firstValueFrom(this.stateService.chainTip$).then(() => {
 | 
			
		||||
      this.loadingTip = false;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
    this.blockWidth = Math.floor(7 * this.height / 12);
 | 
			
		||||
    this.mempoolBlocks = Math.floor(((this.width / 2) - (this.blockWidth * 0.32)) / (1.24 * this.blockWidth));
 | 
			
		||||
    this.blockchainBlocks = this.mempoolBlocks;
 | 
			
		||||
    this.dividerStyle = {
 | 
			
		||||
      width: '2px',
 | 
			
		||||
      height: `${this.height}px`,
 | 
			
		||||
    };
 | 
			
		||||
    this.cd.markForCheck();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.timeLtrSubscription.unsubscribe();
 | 
			
		||||
    this.connectionStateSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByPageFn(index: number, item: { index: number }) {
 | 
			
		||||
    return item.index;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleTimeDirection() {
 | 
			
		||||
    this.ltrTransitionEnabled = true;
 | 
			
		||||
    this.stateService.timeLtr.next(!this.timeLtr);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -5,5 +5,6 @@
 | 
			
		||||
  [blockLimit]="stateService.blockVSize"
 | 
			
		||||
  [orientation]="timeLtr ? 'right' : 'left'"
 | 
			
		||||
  [flip]="true"
 | 
			
		||||
  [pixelAlign]="pixelAlign"
 | 
			
		||||
  (txClickEvent)="onTxClick($event)"
 | 
			
		||||
></app-block-overview-graph>
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import { Router } from '@angular/router';
 | 
			
		||||
})
 | 
			
		||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
 | 
			
		||||
  @Input() index: number;
 | 
			
		||||
  @Input() pixelAlign: boolean = false;
 | 
			
		||||
  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,17 @@
 | 
			
		||||
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks">
 | 
			
		||||
  <div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
 | 
			
		||||
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
 | 
			
		||||
  <div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
 | 
			
		||||
    <div class="flashing">
 | 
			
		||||
      <ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
 | 
			
		||||
        <div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
 | 
			
		||||
        <div
 | 
			
		||||
          *ngIf="minimal && spotlight > 0 && spotlight === i + 1"
 | 
			
		||||
          class="spotlight-bottom"
 | 
			
		||||
          [style.right]="mempoolBlockStyles[i].right"
 | 
			
		||||
        ></div>
 | 
			
		||||
        <div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
 | 
			
		||||
          <a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
 | 
			
		||||
            class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
 | 
			
		||||
          <div class="block-body">
 | 
			
		||||
            <ng-container *ngIf="!minimal">
 | 
			
		||||
              <div [attr.data-cy]="'mempool-block-' + i + '-fees'" class="fees">
 | 
			
		||||
                ~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
              </div>
 | 
			
		||||
@ -35,6 +41,7 @@
 | 
			
		||||
                  <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
			
		||||
                </div>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </div>
 | 
			
		||||
          <span class="animated-border"></span>
 | 
			
		||||
        </div>
 | 
			
		||||
@ -45,10 +52,10 @@
 | 
			
		||||
</ng-container>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingBlocks>
 | 
			
		||||
  <div class="mempool-blocks-container" [class.time-ltr]="timeLtr">
 | 
			
		||||
  <div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'">
 | 
			
		||||
    <div class="flashing">
 | 
			
		||||
      <ng-template ngFor let-projectedBlock [ngForOf]="mempoolEmptyBlocks" let-i="index" [ngForTrackBy]="trackByFn">
 | 
			
		||||
        <div class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolEmptyBlockStyles[i]"></div>
 | 
			
		||||
        <div class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolEmptyBlockStyles[i]"></div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
.bitcoin-block {
 | 
			
		||||
  width: 125px;
 | 
			
		||||
  height: 125px;
 | 
			
		||||
  transition: background 2s, right 2s, transform 1s;
 | 
			
		||||
  width: var(--block-size);
 | 
			
		||||
  height: var(--block-size);
 | 
			
		||||
  transition: background 2s, right 2s, transform 1s, opacity 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-size {
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
  top: 0px;
 | 
			
		||||
  right: 0px;
 | 
			
		||||
  left: 0px;
 | 
			
		||||
  --block-size: 125px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flashing {
 | 
			
		||||
@ -66,11 +67,11 @@
 | 
			
		||||
 | 
			
		||||
.bitcoin-block::after {
 | 
			
		||||
  content: '';
 | 
			
		||||
  width: 125px;
 | 
			
		||||
  height: 24px;
 | 
			
		||||
  width: var(--block-size);
 | 
			
		||||
  height: calc(0.192 * var(--block-size));
 | 
			
		||||
  position:absolute;
 | 
			
		||||
  top: -24px;
 | 
			
		||||
  left: -20px;
 | 
			
		||||
  top: calc(-0.192 * var(--block-size));
 | 
			
		||||
  left: calc(-0.16 * var(--block-size));
 | 
			
		||||
  background-color: #232838;
 | 
			
		||||
  transform:skew(40deg);
 | 
			
		||||
  transform-origin:top;
 | 
			
		||||
@ -79,11 +80,11 @@
 | 
			
		||||
 | 
			
		||||
.bitcoin-block::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  width: 20px;
 | 
			
		||||
  height: 125px;
 | 
			
		||||
  width: calc(0.16 * var(--block-size));
 | 
			
		||||
  height: var(--block-size);
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -12px;
 | 
			
		||||
  left: -20px;
 | 
			
		||||
  top: calc(-0.096 * var(--block-size));
 | 
			
		||||
  left: calc(-0.16 * var(--block-size));
 | 
			
		||||
  background-color: #191c27;
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
 | 
			
		||||
@ -100,6 +101,10 @@
 | 
			
		||||
  background-color: #2d2825;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mempool-block.hide-block {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.black-background {
 | 
			
		||||
  background-color: #11131f;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
@ -141,7 +146,7 @@
 | 
			
		||||
 | 
			
		||||
  .bitcoin-block::before {
 | 
			
		||||
    transform: skewY(-50deg);
 | 
			
		||||
    left: 125px;
 | 
			
		||||
    left: var(--block-size);
 | 
			
		||||
  }
 | 
			
		||||
  .block-body {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
@ -153,3 +158,15 @@
 | 
			
		||||
    transform: translateX(70px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spotlight-bottom {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: calc(0.6 * var(--block-size));
 | 
			
		||||
  height: calc(0.25 * var(--block-size));
 | 
			
		||||
  border-left: solid calc(0.3 * var(--block-size)) transparent;
 | 
			
		||||
  border-bottom: solid calc(0.3 * var(--block-size)) white;
 | 
			
		||||
  border-right: solid calc(0.3 * var(--block-size)) transparent;
 | 
			
		||||
  transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener } from '@angular/core';
 | 
			
		||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core';
 | 
			
		||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
 | 
			
		||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
@ -23,7 +23,12 @@ import { animate, style, transition, trigger } from '@angular/animations';
 | 
			
		||||
  ])],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
  @Input() minimal: boolean = false;
 | 
			
		||||
  @Input() blockWidth: number = 125;
 | 
			
		||||
  @Input() count: number = null;
 | 
			
		||||
  @Input() spotlight: number = 0;
 | 
			
		||||
 | 
			
		||||
  specialBlocks = specialBlocks;
 | 
			
		||||
  mempoolBlocks: MempoolBlock[] = [];
 | 
			
		||||
  mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks();
 | 
			
		||||
@ -48,8 +53,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  timeLtr: boolean;
 | 
			
		||||
  animateEntry: boolean = false;
 | 
			
		||||
 | 
			
		||||
  blockWidth = 125;
 | 
			
		||||
  blockPadding = 30;
 | 
			
		||||
  blockOffset: number = 155;
 | 
			
		||||
  blockPadding: number = 30;
 | 
			
		||||
  containerOffset: number = 40;
 | 
			
		||||
  arrowVisible = false;
 | 
			
		||||
  tabHidden = false;
 | 
			
		||||
  feeRounding = '1.0-0';
 | 
			
		||||
@ -218,6 +224,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
    if (changes.blockWidth && this.blockWidth) {
 | 
			
		||||
      this.blockPadding = 0.24 * this.blockWidth;
 | 
			
		||||
      this.containerOffset = 0.32 * this.blockWidth;
 | 
			
		||||
      this.blockOffset = this.blockWidth + this.blockPadding;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.markBlocksSubscription.unsubscribe();
 | 
			
		||||
    this.blockSubscription.unsubscribe();
 | 
			
		||||
@ -238,9 +252,15 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
 | 
			
		||||
    const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
 | 
			
		||||
    const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
 | 
			
		||||
    let blocksAmount;
 | 
			
		||||
    if (this.count) {
 | 
			
		||||
      blocksAmount = 8;
 | 
			
		||||
    } else {
 | 
			
		||||
      blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
 | 
			
		||||
    }
 | 
			
		||||
    while (blocks.length > blocksAmount) {
 | 
			
		||||
      const block = blocks.pop();
 | 
			
		||||
      if (!this.count) {
 | 
			
		||||
        const lastBlock = blocks[blocks.length - 1];
 | 
			
		||||
        lastBlock.blockSize += block.blockSize;
 | 
			
		||||
        lastBlock.blockVSize += block.blockVSize;
 | 
			
		||||
@ -250,6 +270,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
        lastBlock.medianFee = this.median(lastBlock.feeRange);
 | 
			
		||||
        lastBlock.totalFees += block.totalFees;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (blocks.length) {
 | 
			
		||||
      blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
 | 
			
		||||
    }
 | 
			
		||||
@ -294,14 +315,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      'right': 40 + index * 155 + 'px',
 | 
			
		||||
      'right': this.containerOffset + index * this.blockOffset + 'px',
 | 
			
		||||
      'background': backgroundGradients.join(',') + ')'
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStyleForMempoolEmptyBlock(index: number) {
 | 
			
		||||
    return {
 | 
			
		||||
      'right': 40 + index * 155 + 'px',
 | 
			
		||||
      'right': this.containerOffset + index * this.blockOffset + 'px',
 | 
			
		||||
      'background': '#554b45',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-
 | 
			
		||||
import { GraphsComponent } from '../components/graphs/graphs.component';
 | 
			
		||||
import { StatisticsComponent } from '../components/statistics/statistics.component';
 | 
			
		||||
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
 | 
			
		||||
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
 | 
			
		||||
import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component';
 | 
			
		||||
import { PoolComponent } from '../components/pool/pool.component';
 | 
			
		||||
import { TelevisionComponent } from '../components/television/television.component';
 | 
			
		||||
@ -42,7 +41,6 @@ import { CommonModule } from '@angular/common';
 | 
			
		||||
    BlockFeeRatesGraphComponent,
 | 
			
		||||
    BlockSizesWeightsGraphComponent,
 | 
			
		||||
    FeeDistributionGraphComponent,
 | 
			
		||||
    MempoolBlockOverviewComponent,
 | 
			
		||||
    IncomingTransactionsGraphComponent,
 | 
			
		||||
    MempoolGraphComponent,
 | 
			
		||||
    LbtcPegsGraphComponent,
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,13 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
 | 
			
		||||
import { GlobalFooterComponent } from './components/global-footer/global-footer.component';
 | 
			
		||||
 | 
			
		||||
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
 | 
			
		||||
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
 | 
			
		||||
import { ClockFaceComponent } from '../components/clock-face/clock-face.component';
 | 
			
		||||
import { ClockComponent } from '../components/clock/clock.component';
 | 
			
		||||
import { ClockMinedComponent } from '../components/clock/clock-mined.component';
 | 
			
		||||
import { ClockMempoolComponent } from '../components/clock/clock-mempool.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    ClipboardComponent,
 | 
			
		||||
@ -172,6 +179,13 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer.
 | 
			
		||||
    GeolocationComponent,
 | 
			
		||||
    TestnetAlertComponent,
 | 
			
		||||
    GlobalFooterComponent,
 | 
			
		||||
 | 
			
		||||
    MempoolBlockOverviewComponent,
 | 
			
		||||
    ClockchainComponent,
 | 
			
		||||
    ClockComponent,
 | 
			
		||||
    ClockMinedComponent,
 | 
			
		||||
    ClockMempoolComponent,
 | 
			
		||||
    ClockFaceComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
@ -279,6 +293,13 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer.
 | 
			
		||||
    GeolocationComponent,
 | 
			
		||||
    PreviewTitleComponent,
 | 
			
		||||
    GlobalFooterComponent,
 | 
			
		||||
 | 
			
		||||
    MempoolBlockOverviewComponent,
 | 
			
		||||
    ClockchainComponent,
 | 
			
		||||
    ClockComponent,
 | 
			
		||||
    ClockMinedComponent,
 | 
			
		||||
    ClockMempoolComponent,
 | 
			
		||||
    ClockFaceComponent,
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class SharedModule {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								frontend/src/resources/clock/gradient.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/resources/clock/gradient.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 37 KiB  | 
@ -285,6 +285,10 @@ body {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.white-color {
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.green-color {
 | 
			
		||||
  color: #3bcc49;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user