Merge branch 'master' into mononaut/more-fee-bands
This commit is contained in:
		
						commit
						42d5650bc0
					
				@ -1,5 +1,5 @@
 | 
				
			|||||||
import logger from '../logger';
 | 
					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 { Common } from './common';
 | 
				
			||||||
import config from '../config';
 | 
					import config from '../config';
 | 
				
			||||||
import { Worker } from 'worker_threads';
 | 
					import { Worker } from 'worker_threads';
 | 
				
			||||||
@ -104,8 +104,12 @@ class MempoolBlocks {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
 | 
					  private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
 | 
				
			||||||
    const mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
					    const mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
				
			||||||
 | 
					    let blockSize = 0;
 | 
				
			||||||
    let blockWeight = 0;
 | 
					    let blockWeight = 0;
 | 
				
			||||||
    let blockVsize = 0;
 | 
					    let blockVsize = 0;
 | 
				
			||||||
 | 
					    let blockFees = 0;
 | 
				
			||||||
 | 
					    const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
 | 
				
			||||||
 | 
					    let transactionIds: string[] = [];
 | 
				
			||||||
    let transactions: TransactionExtended[] = [];
 | 
					    let transactions: TransactionExtended[] = [];
 | 
				
			||||||
    transactionsSorted.forEach((tx) => {
 | 
					    transactionsSorted.forEach((tx) => {
 | 
				
			||||||
      if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
 | 
					      if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
 | 
				
			||||||
@ -116,9 +120,14 @@ class MempoolBlocks {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
        blockWeight += tx.weight;
 | 
					        blockWeight += tx.weight;
 | 
				
			||||||
        blockVsize += tx.vsize;
 | 
					        blockVsize += tx.vsize;
 | 
				
			||||||
        transactions.push(tx);
 | 
					        blockSize += tx.size;
 | 
				
			||||||
 | 
					        blockFees += tx.fee;
 | 
				
			||||||
 | 
					        if (blockVsize <= sizeLimit) {
 | 
				
			||||||
 | 
					          transactions.push(tx);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        transactionIds.push(tx.txid);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
 | 
					        mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
 | 
				
			||||||
        blockVsize = 0;
 | 
					        blockVsize = 0;
 | 
				
			||||||
        tx.position = {
 | 
					        tx.position = {
 | 
				
			||||||
          block: mempoolBlocks.length,
 | 
					          block: mempoolBlocks.length,
 | 
				
			||||||
@ -126,11 +135,14 @@ class MempoolBlocks {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
        blockVsize += tx.vsize;
 | 
					        blockVsize += tx.vsize;
 | 
				
			||||||
        blockWeight = tx.weight;
 | 
					        blockWeight = tx.weight;
 | 
				
			||||||
 | 
					        blockSize = tx.size;
 | 
				
			||||||
 | 
					        blockFees = tx.fee;
 | 
				
			||||||
 | 
					        transactionIds = [tx.txid];
 | 
				
			||||||
        transactions = [tx];
 | 
					        transactions = [tx];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    if (transactions.length) {
 | 
					    if (transactions.length) {
 | 
				
			||||||
      mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
 | 
					      mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return mempoolBlocks;
 | 
					    return mempoolBlocks;
 | 
				
			||||||
@ -178,6 +190,8 @@ class MempoolBlocks {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
 | 
					  public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
 | 
				
			||||||
 | 
					    const start = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // reset mempool short ids
 | 
					    // reset mempool short ids
 | 
				
			||||||
    this.resetUids();
 | 
					    this.resetUids();
 | 
				
			||||||
    for (const tx of Object.values(newMempool)) {
 | 
					    for (const tx of Object.values(newMempool)) {
 | 
				
			||||||
@ -194,7 +208,7 @@ class MempoolBlocks {
 | 
				
			|||||||
          fee: entry.fee,
 | 
					          fee: entry.fee,
 | 
				
			||||||
          weight: entry.weight,
 | 
					          weight: entry.weight,
 | 
				
			||||||
          feePerVsize: entry.fee / (entry.weight / 4),
 | 
					          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[],
 | 
					          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
 | 
					    // run the block construction algorithm in a separate thread, and wait for a result
 | 
				
			||||||
    let threadErrorListener;
 | 
					    let threadErrorListener;
 | 
				
			||||||
    try {
 | 
					    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;
 | 
					        threadErrorListener = reject;
 | 
				
			||||||
        this.txSelectionWorker?.once('message', (result): void => {
 | 
					        this.txSelectionWorker?.once('message', (result): void => {
 | 
				
			||||||
          resolve(result);
 | 
					          resolve(result);
 | 
				
			||||||
@ -224,19 +238,14 @@ class MempoolBlocks {
 | 
				
			|||||||
        this.txSelectionWorker?.once('error', reject);
 | 
					        this.txSelectionWorker?.once('error', reject);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
 | 
					      this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
 | 
				
			||||||
      let { blocks, clusters } = this.convertResultTxids(await workerResultPromise);
 | 
					      const { blocks, rates, 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`);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // clean up thread error listener
 | 
					      // clean up thread error listener
 | 
				
			||||||
      this.txSelectionWorker?.removeListener('error', threadErrorListener);
 | 
					      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) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
					      logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -250,6 +259,8 @@ class MempoolBlocks {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const start = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const tx of Object.values(added)) {
 | 
					    for (const tx of Object.values(added)) {
 | 
				
			||||||
      this.setUid(tx);
 | 
					      this.setUid(tx);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -262,7 +273,7 @@ class MempoolBlocks {
 | 
				
			|||||||
        fee: entry.fee,
 | 
					        fee: entry.fee,
 | 
				
			||||||
        weight: entry.weight,
 | 
					        weight: entry.weight,
 | 
				
			||||||
        feePerVsize: entry.fee / (entry.weight / 4),
 | 
					        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[],
 | 
					        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
 | 
					    // run the block construction algorithm in a separate thread, and wait for a result
 | 
				
			||||||
    let threadErrorListener;
 | 
					    let threadErrorListener;
 | 
				
			||||||
    try {
 | 
					    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;
 | 
					        threadErrorListener = reject;
 | 
				
			||||||
        this.txSelectionWorker?.once('message', (result): void => {
 | 
					        this.txSelectionWorker?.once('message', (result): void => {
 | 
				
			||||||
          resolve(result);
 | 
					          resolve(result);
 | 
				
			||||||
@ -278,84 +289,100 @@ class MempoolBlocks {
 | 
				
			|||||||
        this.txSelectionWorker?.once('error', reject);
 | 
					        this.txSelectionWorker?.once('error', reject);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
 | 
					      this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
 | 
				
			||||||
      let { blocks, clusters } = this.convertResultTxids(await workerResultPromise);
 | 
					      const { blocks, rates, 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`);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.removeUids(removedUids);
 | 
					      this.removeUids(removedUids);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // clean up thread error listener
 | 
					      // clean up thread error listener
 | 
				
			||||||
      this.txSelectionWorker?.removeListener('error', threadErrorListener);
 | 
					      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) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
					      logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private processBlockTemplates(mempool, blocks: ThreadTransaction[][], clusters, saveResults): MempoolBlockWithTransactions[] {
 | 
					  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];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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
 | 
					    // update this thread's mempool with the results
 | 
				
			||||||
    blocks.forEach((block, blockIndex) => {
 | 
					    for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
 | 
				
			||||||
      let runningVsize = 0;
 | 
					      const block: string[] = blocks[blockIndex];
 | 
				
			||||||
      block.forEach(tx => {
 | 
					      let txid: string;
 | 
				
			||||||
        if (tx.txid && tx.txid in mempool) {
 | 
					      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
 | 
					          // save position in projected blocks
 | 
				
			||||||
          mempool[tx.txid].position = {
 | 
					          mempoolTx.position = {
 | 
				
			||||||
            block: blockIndex,
 | 
					            block: blockIndex,
 | 
				
			||||||
            vsize: runningVsize + (mempool[tx.txid].vsize / 2),
 | 
					            vsize: totalVsize + (mempoolTx.vsize / 2),
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          runningVsize += mempool[tx.txid].vsize;
 | 
					          mempoolTx.cpfpChecked = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (tx.effectiveFeePerVsize != null) {
 | 
					          totalSize += mempoolTx.size;
 | 
				
			||||||
            mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
 | 
					          totalVsize += mempoolTx.vsize;
 | 
				
			||||||
 | 
					          totalWeight += mempoolTx.weight;
 | 
				
			||||||
 | 
					          totalFees += mempoolTx.fee;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (totalVsize <= sizeLimit) {
 | 
				
			||||||
 | 
					            transactions.push(mempoolTx);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
 | 
					 | 
				
			||||||
            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) {
 | 
					 | 
				
			||||||
                matched = true;
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                const relative = {
 | 
					 | 
				
			||||||
                  txid: txid,
 | 
					 | 
				
			||||||
                  fee: mempool[txid].fee,
 | 
					 | 
				
			||||||
                  weight: mempool[txid].weight,
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                if (matched) {
 | 
					 | 
				
			||||||
                  descendants.push(relative);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                  ancestors.push(relative);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            mempool[tx.txid].ancestors = ancestors;
 | 
					 | 
				
			||||||
            mempool[tx.txid].descendants = descendants;
 | 
					 | 
				
			||||||
            mempool[tx.txid].bestDescendant = null;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          logger.warn('projected transaction missing from mempool cache');
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      readyBlocks.push({
 | 
				
			||||||
 | 
					        transactionIds: block,
 | 
				
			||||||
 | 
					        transactions,
 | 
				
			||||||
 | 
					        totalSize,
 | 
				
			||||||
 | 
					        totalWeight,
 | 
				
			||||||
 | 
					        totalFees
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // unpack the condensed blocks into proper mempool blocks
 | 
					    for (const cluster of Object.values(clusters)) {
 | 
				
			||||||
    const mempoolBlocks = blocks.map((transactions) => {
 | 
					      for (const memberTxid of cluster) {
 | 
				
			||||||
      return this.dataToMempoolBlocks(transactions.map(tx => {
 | 
					        if (memberTxid in mempool) {
 | 
				
			||||||
        return mempool[tx.txid] || null;
 | 
					          const mempoolTx = mempool[memberTxid];
 | 
				
			||||||
      }).filter(tx => !!tx));
 | 
					          const ancestors: Ancestor[] = [];
 | 
				
			||||||
    });
 | 
					          const descendants: Ancestor[] = [];
 | 
				
			||||||
 | 
					          let matched = false;
 | 
				
			||||||
 | 
					          cluster.forEach(txid => {
 | 
				
			||||||
 | 
					            if (txid === memberTxid) {
 | 
				
			||||||
 | 
					              matched = true;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              const relative = {
 | 
				
			||||||
 | 
					                txid: txid,
 | 
				
			||||||
 | 
					                fee: mempool[txid].fee,
 | 
				
			||||||
 | 
					                weight: mempool[txid].weight,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					              if (matched) {
 | 
				
			||||||
 | 
					                descendants.push(relative);
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                ancestors.push(relative);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          mempoolTx.ancestors = ancestors;
 | 
				
			||||||
 | 
					          mempoolTx.descendants = descendants;
 | 
				
			||||||
 | 
					          mempoolTx.bestDescendant = null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mempoolBlocks = readyBlocks.map(b => this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (saveResults) {
 | 
					    if (saveResults) {
 | 
				
			||||||
      const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
 | 
					      const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
 | 
				
			||||||
@ -366,27 +393,17 @@ class MempoolBlocks {
 | 
				
			|||||||
    return mempoolBlocks;
 | 
					    return mempoolBlocks;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private dataToMempoolBlocks(transactions: TransactionExtended[]): MempoolBlockWithTransactions {
 | 
					  private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number): 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);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const feeStats = Common.calcEffectiveFeeStatistics(transactions);
 | 
					    const feeStats = Common.calcEffectiveFeeStatistics(transactions);
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      blockSize: totalSize,
 | 
					      blockSize: totalSize,
 | 
				
			||||||
      blockVSize: totalWeight / 4,
 | 
					      blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
 | 
				
			||||||
      nTx: transactions.length,
 | 
					      nTx: transactionIds.length,
 | 
				
			||||||
      totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
 | 
					      totalFees: totalFees,
 | 
				
			||||||
      medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
 | 
					      medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
 | 
				
			||||||
      feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
 | 
					      feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
 | 
				
			||||||
      transactionIds: transactions.map((tx) => tx.txid),
 | 
					      transactionIds: transactionIds,
 | 
				
			||||||
      transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
 | 
					      transactions: transactions.map((tx) => Common.stripTransaction(tx)),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -415,14 +432,16 @@ class MempoolBlocks {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private convertResultTxids({ blocks, clusters }: { blocks: any[][], clusters: Map<number, number[]>})
 | 
					  private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
 | 
				
			||||||
    : { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] }} {
 | 
					    : { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
 | 
				
			||||||
    for (const block of blocks) {
 | 
					    const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
 | 
				
			||||||
      for (const tx of block) {
 | 
					      return this.uidMap.get(uid) || '';
 | 
				
			||||||
        tx.txid = this.uidMap.get(tx.uid);
 | 
					    }));
 | 
				
			||||||
        if (tx.cpfpRoot) {
 | 
					    const convertedRates = {};
 | 
				
			||||||
          tx.cpfpRoot = this.uidMap.get(tx.cpfpRoot);
 | 
					    for (const rateUid of rates.keys()) {
 | 
				
			||||||
        }
 | 
					      const rateTxid = this.uidMap.get(rateUid);
 | 
				
			||||||
 | 
					      if (rateTxid) {
 | 
				
			||||||
 | 
					        convertedRates[rateTxid] = rates.get(rateUid);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const convertedClusters = {};
 | 
					    const convertedClusters = {};
 | 
				
			||||||
@ -435,7 +454,7 @@ class MempoolBlocks {
 | 
				
			|||||||
        convertedClusters[rootTxid] = members;
 | 
					        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 config from '../config';
 | 
				
			||||||
import logger from '../logger';
 | 
					import logger from '../logger';
 | 
				
			||||||
import { CompactThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
 | 
					import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces';
 | 
				
			||||||
import { PairingHeap } from '../utils/pairing-heap';
 | 
					import { PairingHeap } from '../utils/pairing-heap';
 | 
				
			||||||
import { parentPort } from 'worker_threads';
 | 
					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.
 | 
					    // return the result to main thread.
 | 
				
			||||||
    if (parentPort) {
 | 
					    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)
 | 
					* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
					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 start = Date.now();
 | 
				
			||||||
  const auditPool: Map<number, AuditTransaction> = new Map();
 | 
					  const auditPool: Map<number, AuditTransaction> = new Map();
 | 
				
			||||||
  const mempoolArray: AuditTransaction[] = [];
 | 
					  const mempoolArray: AuditTransaction[] = [];
 | 
				
			||||||
  const restOfArray: CompactThreadTransaction[] = [];
 | 
					 | 
				
			||||||
  const cpfpClusters: Map<number, number[]> = new Map();
 | 
					  const cpfpClusters: Map<number, number[]> = new Map();
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  mempool.forEach(tx => {
 | 
					  mempool.forEach(tx => {
 | 
				
			||||||
 | 
					    tx.dirty = false;
 | 
				
			||||||
    // initializing everything up front helps V8 optimize property access later
 | 
					    // initializing everything up front helps V8 optimize property access later
 | 
				
			||||||
    auditPool.set(tx.uid, {
 | 
					    auditPool.set(tx.uid, {
 | 
				
			||||||
      uid: tx.uid,
 | 
					      uid: tx.uid,
 | 
				
			||||||
@ -81,9 +81,8 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Build blocks by greedily choosing the highest feerate package
 | 
					  // Build blocks by greedily choosing the highest feerate package
 | 
				
			||||||
  // (i.e. the package rooted in the transaction with the best ancestor score)
 | 
					  // (i.e. the package rooted in the transaction with the best ancestor score)
 | 
				
			||||||
  const blocks: CompactThreadTransaction[][] = [];
 | 
					  const blocks: number[][] = [];
 | 
				
			||||||
  let blockWeight = 4000;
 | 
					  let blockWeight = 4000;
 | 
				
			||||||
  let blockSize = 0;
 | 
					 | 
				
			||||||
  let transactions: AuditTransaction[] = [];
 | 
					  let transactions: AuditTransaction[] = [];
 | 
				
			||||||
  const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
 | 
					  const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
 | 
				
			||||||
    if (a.score === b.score) {
 | 
					    if (a.score === b.score) {
 | 
				
			||||||
@ -139,13 +138,16 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
				
			|||||||
          ancestor.used = true;
 | 
					          ancestor.used = true;
 | 
				
			||||||
          ancestor.usedBy = nextTx.uid;
 | 
					          ancestor.usedBy = nextTx.uid;
 | 
				
			||||||
          // update original copy of this tx with effective fee rate & relatives data
 | 
					          // update original copy of this tx with effective fee rate & relatives data
 | 
				
			||||||
          mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
 | 
					          if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) {
 | 
				
			||||||
          if (isCluster) {
 | 
					            mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
 | 
				
			||||||
            mempoolTx.cpfpRoot = nextTx.uid;
 | 
					            mempoolTx.dirty = true;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (mempoolTx.cpfpRoot !== nextTx.uid) {
 | 
				
			||||||
 | 
					            mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
 | 
				
			||||||
 | 
					            mempoolTx.dirty;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          mempoolTx.cpfpChecked = true;
 | 
					          mempoolTx.cpfpChecked = true;
 | 
				
			||||||
          transactions.push(ancestor);
 | 
					          transactions.push(ancestor);
 | 
				
			||||||
          blockSize += ancestor.size;
 | 
					 | 
				
			||||||
          blockWeight += ancestor.weight;
 | 
					          blockWeight += ancestor.weight;
 | 
				
			||||||
          used.push(ancestor);
 | 
					          used.push(ancestor);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -171,11 +173,10 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
				
			|||||||
    if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
 | 
					    if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
 | 
				
			||||||
      // construct this block
 | 
					      // construct this block
 | 
				
			||||||
      if (transactions.length) {
 | 
					      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
 | 
					      // reset for the next block
 | 
				
			||||||
      transactions = [];
 | 
					      transactions = [];
 | 
				
			||||||
      blockSize = 0;
 | 
					 | 
				
			||||||
      blockWeight = 4000;
 | 
					      blockWeight = 4000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // 'overflow' packages didn't fit in this block, but are valid candidates for the next
 | 
					      // '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
 | 
					  // add the final unbounded block if it contains any transactions
 | 
				
			||||||
  if (transactions.length > 0) {
 | 
					  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 end = Date.now();
 | 
				
			||||||
  const time = end - start;
 | 
					  const time = end - start;
 | 
				
			||||||
  logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
 | 
					  logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return { blocks, clusters: cpfpClusters };
 | 
					  return { blocks, rates, clusters: cpfpClusters };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// traverse in-mempool ancestors
 | 
					// traverse in-mempool ancestors
 | 
				
			||||||
 | 
				
			|||||||
@ -114,6 +114,7 @@ export interface CompactThreadTransaction {
 | 
				
			|||||||
  inputs: number[];
 | 
					  inputs: number[];
 | 
				
			||||||
  cpfpRoot?: string;
 | 
					  cpfpRoot?: string;
 | 
				
			||||||
  cpfpChecked?: boolean;
 | 
					  cpfpChecked?: boolean;
 | 
				
			||||||
 | 
					  dirty?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ThreadTransaction {
 | 
					export interface ThreadTransaction {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,8 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
 | 
				
			|||||||
import { StartComponent } from './components/start/start.component';
 | 
					import { StartComponent } from './components/start/start.component';
 | 
				
			||||||
import { TransactionComponent } from './components/transaction/transaction.component';
 | 
					import { TransactionComponent } from './components/transaction/transaction.component';
 | 
				
			||||||
import { BlockComponent } from './components/block/block.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 { AddressComponent } from './components/address/address.component';
 | 
				
			||||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
					import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
				
			||||||
import { AboutComponent } from './components/about/about.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',
 | 
					    path: 'status',
 | 
				
			||||||
    data: { networks: ['bitcoin', 'liquid'] },
 | 
					    data: { networks: ['bitcoin', 'liquid'] },
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
  @Input() unavailable: boolean = false;
 | 
					  @Input() unavailable: boolean = false;
 | 
				
			||||||
  @Input() auditHighlighting: boolean = false;
 | 
					  @Input() auditHighlighting: boolean = false;
 | 
				
			||||||
  @Input() blockConversion: Price;
 | 
					  @Input() blockConversion: Price;
 | 
				
			||||||
 | 
					  @Input() pixelAlign: boolean = false;
 | 
				
			||||||
  @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
 | 
					  @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
 | 
				
			||||||
  @Output() txHoverEvent = new EventEmitter<string>();
 | 
					  @Output() txHoverEvent = new EventEmitter<string>();
 | 
				
			||||||
  @Output() readyEvent = new EventEmitter();
 | 
					  @Output() readyEvent = new EventEmitter();
 | 
				
			||||||
@ -201,7 +202,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
				
			|||||||
      this.start();
 | 
					      this.start();
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
 | 
					      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();
 | 
					      this.start();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ export default class BlockScene {
 | 
				
			|||||||
  gridWidth: number;
 | 
					  gridWidth: number;
 | 
				
			||||||
  gridHeight: number;
 | 
					  gridHeight: number;
 | 
				
			||||||
  gridSize: number;
 | 
					  gridSize: number;
 | 
				
			||||||
 | 
					  pixelAlign: boolean;
 | 
				
			||||||
  vbytesPerUnit: number;
 | 
					  vbytesPerUnit: number;
 | 
				
			||||||
  unitPadding: number;
 | 
					  unitPadding: number;
 | 
				
			||||||
  unitWidth: number;
 | 
					  unitWidth: number;
 | 
				
			||||||
@ -23,19 +24,24 @@ export default class BlockScene {
 | 
				
			|||||||
  animateUntil = 0;
 | 
					  animateUntil = 0;
 | 
				
			||||||
  dirty: boolean;
 | 
					  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,
 | 
					      { 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 {
 | 
					  resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
 | 
				
			||||||
    this.width = width;
 | 
					    this.width = width;
 | 
				
			||||||
    this.height = height;
 | 
					    this.height = height;
 | 
				
			||||||
    this.gridSize = this.width / this.gridWidth;
 | 
					    this.gridSize = this.width / this.gridWidth;
 | 
				
			||||||
    this.unitPadding =  width / 500;
 | 
					    if (this.pixelAlign) {
 | 
				
			||||||
    this.unitWidth = this.gridSize - (this.unitPadding * 2);
 | 
					      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;
 | 
					    this.dirty = true;
 | 
				
			||||||
    if (this.initialised && this.scene) {
 | 
					    if (this.initialised && this.scene) {
 | 
				
			||||||
@ -209,14 +215,15 @@ export default class BlockScene {
 | 
				
			|||||||
    this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
 | 
					    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,
 | 
					      { 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 {
 | 
					  ): void {
 | 
				
			||||||
    this.orientation = orientation;
 | 
					    this.orientation = orientation;
 | 
				
			||||||
    this.flip = flip;
 | 
					    this.flip = flip;
 | 
				
			||||||
    this.vertexArray = vertexArray;
 | 
					    this.vertexArray = vertexArray;
 | 
				
			||||||
    this.highlightingEnabled = highlighting;
 | 
					    this.highlightingEnabled = highlighting;
 | 
				
			||||||
 | 
					    this.pixelAlign = pixelAlign;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.scene = {
 | 
					    this.scene = {
 | 
				
			||||||
      count: 0,
 | 
					      count: 0,
 | 
				
			||||||
@ -342,7 +349,12 @@ export default class BlockScene {
 | 
				
			|||||||
  private gridToScreen(position: Square | void): Square {
 | 
					  private gridToScreen(position: Square | void): Square {
 | 
				
			||||||
    if (position) {
 | 
					    if (position) {
 | 
				
			||||||
      const slotSize = (position.s * this.gridSize);
 | 
					      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,
 | 
					      // 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.
 | 
					      // so we rotate and/or flip the y axis to match the target configuration.
 | 
				
			||||||
 | 
				
			|||||||
@ -1,53 +1,61 @@
 | 
				
			|||||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr"
 | 
					<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [class.minimal]="minimal"
 | 
				
			||||||
  [style.left]="static ? (offset || 0) + 'px' : null"
 | 
					  [style.left]="static ? (offset || 0) + 'px' : null" [style.--block-size]="blockWidth+'px'"
 | 
				
			||||||
  *ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate">
 | 
					  *ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate">
 | 
				
			||||||
  <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
 | 
					  <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
 | 
				
			||||||
    <ng-container *ngIf="connected && block && !block.loading && !block.placeholder; else placeholderBlock">
 | 
					    <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"
 | 
					      <div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
 | 
				
			||||||
        class="text-center bitcoin-block mined-block blockchain-blocks-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]"
 | 
					        id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
 | 
				
			||||||
        [class.blink-bg]="isSpecial(block.height)">
 | 
					        [class.blink-bg]="isSpecial(block.height)">
 | 
				
			||||||
        <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
 | 
					        <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
 | 
				
			||||||
          class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
 | 
					          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 [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
 | 
				
			||||||
            }}</a>
 | 
					            }}</a>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="block-body">
 | 
					        <div class="block-body">
 | 
				
			||||||
          <div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
 | 
					          <ng-container *ngIf="!minimal">
 | 
				
			||||||
            ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
 | 
					            <div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
 | 
				
			||||||
              i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
					              ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
 | 
				
			||||||
          </div>
 | 
					                i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
				
			||||||
          <ng-template #emptyfees>
 | 
					 | 
				
			||||||
            <div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
 | 
					 | 
				
			||||||
               
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </ng-template>
 | 
					            <ng-template #emptyfees>
 | 
				
			||||||
          <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
 | 
					              <div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
 | 
				
			||||||
            *ngIf="block?.extras?.feeRange; else emptyfeespan">
 | 
					                 
 | 
				
			||||||
            {{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{
 | 
					              </div>
 | 
				
			||||||
            block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
 | 
					            </ng-template>
 | 
				
			||||||
              i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
					            <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
 | 
				
			||||||
          </div>
 | 
					              *ngIf="block?.extras?.feeRange; else emptyfeespan">
 | 
				
			||||||
          <ng-template #emptyfeespan>
 | 
					              {{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{
 | 
				
			||||||
            <div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
 | 
					              block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
 | 
				
			||||||
               
 | 
					                i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </ng-template>
 | 
					            <ng-template #emptyfeespan>
 | 
				
			||||||
          <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
 | 
					              <div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
 | 
				
			||||||
            class="block-size">
 | 
					                 
 | 
				
			||||||
            <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
					              </div>
 | 
				
			||||||
          </div>
 | 
					            </ng-template>
 | 
				
			||||||
          <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
 | 
					            <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
 | 
				
			||||||
            class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
 | 
					              class="block-size">
 | 
				
			||||||
          <div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
 | 
					              <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
				
			||||||
            <ng-container
 | 
					            </div>
 | 
				
			||||||
              *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
 | 
					            <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
 | 
				
			||||||
            <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
 | 
					              class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
 | 
				
			||||||
            <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
 | 
					            <div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
 | 
				
			||||||
          </div>
 | 
					              <ng-container
 | 
				
			||||||
          <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
 | 
					                *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
 | 
				
			||||||
            <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
 | 
					              <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
 | 
				
			||||||
 | 
					              <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
 | 
				
			||||||
 | 
					            </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>
 | 
				
			||||||
        <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
 | 
					        <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"
 | 
					          <a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
 | 
				
			||||||
@ -79,11 +87,11 @@
 | 
				
			|||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<ng-template #loadingBlocksTemplate>
 | 
					<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 class="flashing">
 | 
				
			||||||
      <div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn">
 | 
					      <div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn">
 | 
				
			||||||
        <div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}"
 | 
					        <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>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
.bitcoin-block {
 | 
					.bitcoin-block {
 | 
				
			||||||
  width: 125px;
 | 
					  width: var(--block-size);
 | 
				
			||||||
  height: 125px;
 | 
					  height: var(--block-size);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.blockLink {
 | 
					.blockLink {
 | 
				
			||||||
@ -22,7 +22,11 @@
 | 
				
			|||||||
.mined-block {
 | 
					.mined-block {
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: 0px;
 | 
					  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 {
 | 
					.mined-block.placeholder-block {
 | 
				
			||||||
@ -35,9 +39,11 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.blocks-container {
 | 
					.blocks-container {
 | 
				
			||||||
 | 
					  --block-size: 125px;
 | 
				
			||||||
 | 
					  --block-offset: calc(0.32 * var(--block-size));
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: 0px;
 | 
					  top: 0px;
 | 
				
			||||||
  left: 40px;
 | 
					  left: var(--block-offset);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.block-body {
 | 
					.block-body {
 | 
				
			||||||
@ -77,11 +83,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.bitcoin-block::after {
 | 
					.bitcoin-block::after {
 | 
				
			||||||
  content: '';
 | 
					  content: '';
 | 
				
			||||||
  width: 125px;
 | 
					  width: var(--block-size);
 | 
				
			||||||
  height: 24px;
 | 
					  height: calc(0.192 * var(--block-size));
 | 
				
			||||||
  position:absolute;
 | 
					  position:absolute;
 | 
				
			||||||
  top: -24px;
 | 
					  top: calc(-0.192 * var(--block-size));
 | 
				
			||||||
  left: -20px;
 | 
					  left: calc(-0.16 * var(--block-size));
 | 
				
			||||||
  background-color: #232838;
 | 
					  background-color: #232838;
 | 
				
			||||||
  transform:skew(40deg);
 | 
					  transform:skew(40deg);
 | 
				
			||||||
  transform-origin:top;
 | 
					  transform-origin:top;
 | 
				
			||||||
@ -89,11 +95,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.bitcoin-block::before {
 | 
					.bitcoin-block::before {
 | 
				
			||||||
  content: '';
 | 
					  content: '';
 | 
				
			||||||
  width: 20px;
 | 
					  width: calc(0.16 * var(--block-size));
 | 
				
			||||||
  height: 125px;
 | 
					  height: var(--block-size);
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: -12px;
 | 
					  top: calc(-0.096 * var(--block-size));
 | 
				
			||||||
  left: -20px;
 | 
					  left: calc(-0.16 * var(--block-size));
 | 
				
			||||||
  background-color: #191c27;
 | 
					  background-color: #191c27;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  transform: skewY(50deg);
 | 
					  transform: skewY(50deg);
 | 
				
			||||||
@ -168,4 +174,16 @@
 | 
				
			|||||||
  .bitcoin-block {
 | 
					  .bitcoin-block {
 | 
				
			||||||
    transform: scaleX(-1);
 | 
					    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() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
 | 
				
			||||||
  @Input() loadingTip: boolean = false;
 | 
					  @Input() loadingTip: boolean = false;
 | 
				
			||||||
  @Input() connected: boolean = true;
 | 
					  @Input() connected: boolean = true;
 | 
				
			||||||
 | 
					  @Input() minimal: boolean = false;
 | 
				
			||||||
 | 
					  @Input() blockWidth: number = 125;
 | 
				
			||||||
 | 
					  @Input() spotlight: number = 0;
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  specialBlocks = specialBlocks;
 | 
					  specialBlocks = specialBlocks;
 | 
				
			||||||
  network = '';
 | 
					  network = '';
 | 
				
			||||||
@ -51,6 +54,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
  timeLtrSubscription: Subscription;
 | 
					  timeLtrSubscription: Subscription;
 | 
				
			||||||
  timeLtr: boolean;
 | 
					  timeLtr: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  blockOffset: number = 155;
 | 
				
			||||||
 | 
					  dividerBlockOffset: number = 205;
 | 
				
			||||||
 | 
					  blockPadding: number = 30;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  gradientColors = {
 | 
					  gradientColors = {
 | 
				
			||||||
    '': ['#9339f4', '#105fb0'],
 | 
					    '': ['#9339f4', '#105fb0'],
 | 
				
			||||||
    bisq: ['#9339f4', '#105fb0'],
 | 
					    bisq: ['#9339f4', '#105fb0'],
 | 
				
			||||||
@ -118,7 +125,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          this.blockStyles = [];
 | 
					          this.blockStyles = [];
 | 
				
			||||||
          if (this.blocksFilled && block.height > this.chainTip) {
 | 
					          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(() => {
 | 
					            setTimeout(() => {
 | 
				
			||||||
              this.blockStyles = [];
 | 
					              this.blockStyles = [];
 | 
				
			||||||
              this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
 | 
					              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 {
 | 
					  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) {
 | 
					    if (this.static) {
 | 
				
			||||||
      const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
 | 
					      const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
 | 
				
			||||||
      this.updateStaticBlocks(animateSlide);
 | 
					      this.updateStaticBlocks(animateSlide);
 | 
				
			||||||
@ -191,14 +205,14 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      this.arrowVisible = true;
 | 
					      this.arrowVisible = true;
 | 
				
			||||||
      if (newBlockFromLeft) {
 | 
					      if (newBlockFromLeft) {
 | 
				
			||||||
        this.arrowLeftPx = blockindex * 155 + 30 - 205;
 | 
					        this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding - this.dividerBlockOffset;
 | 
				
			||||||
        setTimeout(() => {
 | 
					        setTimeout(() => {
 | 
				
			||||||
          this.arrowTransition = '2s';
 | 
					          this.arrowTransition = '2s';
 | 
				
			||||||
          this.arrowLeftPx = blockindex * 155 + 30;
 | 
					          this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
 | 
				
			||||||
          this.cd.markForCheck();
 | 
					          this.cd.markForCheck();
 | 
				
			||||||
        }, 50);
 | 
					        }, 50);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        this.arrowLeftPx = blockindex * 155 + 30;
 | 
					        this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
 | 
				
			||||||
        if (!animate) {
 | 
					        if (!animate) {
 | 
				
			||||||
          setTimeout(() => {
 | 
					          setTimeout(() => {
 | 
				
			||||||
            this.arrowTransition = '2s';
 | 
					            this.arrowTransition = '2s';
 | 
				
			||||||
@ -245,7 +259,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    this.blocks = this.blocks.slice(0, this.count);
 | 
					    this.blocks = this.blocks.slice(0, this.count);
 | 
				
			||||||
    this.blockStyles = [];
 | 
					    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();
 | 
					    this.cd.markForCheck();
 | 
				
			||||||
    if (animateSlide) {
 | 
					    if (animateSlide) {
 | 
				
			||||||
      // animate blocks slide right
 | 
					      // animate blocks slide right
 | 
				
			||||||
@ -287,7 +301,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      left: addLeft + 155 * index + 'px',
 | 
					      left: addLeft + this.blockOffset * index + 'px',
 | 
				
			||||||
      background: `repeating-linear-gradient(
 | 
					      background: `repeating-linear-gradient(
 | 
				
			||||||
        #2d3348,
 | 
					        #2d3348,
 | 
				
			||||||
        #2d3348 ${greenBackgroundHeight}%,
 | 
					        #2d3348 ${greenBackgroundHeight}%,
 | 
				
			||||||
@ -309,7 +323,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
    const addLeft = animateEnterFrom || 0;
 | 
					    const addLeft = animateEnterFrom || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      left: addLeft + (155 * index) + 'px',
 | 
					      left: addLeft + (this.blockOffset * index) + 'px',
 | 
				
			||||||
      background: "#2d3348",
 | 
					      background: "#2d3348",
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -317,7 +331,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
  getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) {
 | 
					  getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) {
 | 
				
			||||||
    const addLeft = animateEnterFrom || 0;
 | 
					    const addLeft = animateEnterFrom || 0;
 | 
				
			||||||
    return {
 | 
					    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;
 | 
					    const addLeft = animateEnterFrom || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
 | 
					      left: addLeft + this.blockOffset * this.emptyBlocks.indexOf(block) + 'px',
 | 
				
			||||||
      background: "#2d3348",
 | 
					      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"
 | 
					  [blockLimit]="stateService.blockVSize"
 | 
				
			||||||
  [orientation]="timeLtr ? 'right' : 'left'"
 | 
					  [orientation]="timeLtr ? 'right' : 'left'"
 | 
				
			||||||
  [flip]="true"
 | 
					  [flip]="true"
 | 
				
			||||||
 | 
					  [pixelAlign]="pixelAlign"
 | 
				
			||||||
  (txClickEvent)="onTxClick($event)"
 | 
					  (txClickEvent)="onTxClick($event)"
 | 
				
			||||||
></app-block-overview-graph>
 | 
					></app-block-overview-graph>
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ import { Router } from '@angular/router';
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
 | 
					export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
 | 
				
			||||||
  @Input() index: number;
 | 
					  @Input() index: number;
 | 
				
			||||||
 | 
					  @Input() pixelAlign: boolean = false;
 | 
				
			||||||
  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
					  @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
					  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,40 +1,47 @@
 | 
				
			|||||||
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks">
 | 
					<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
 | 
				
			||||||
  <div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
 | 
					  <div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
 | 
				
			||||||
    <div class="flashing">
 | 
					    <div class="flashing">
 | 
				
			||||||
      <ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
 | 
					      <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]"
 | 
					          <a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
 | 
				
			||||||
            class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
 | 
					            class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
 | 
				
			||||||
          <div class="block-body">
 | 
					          <div class="block-body">
 | 
				
			||||||
            <div [attr.data-cy]="'mempool-block-' + i + '-fees'" class="fees">
 | 
					            <ng-container *ngIf="!minimal">
 | 
				
			||||||
              ~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
					              <div [attr.data-cy]="'mempool-block-' + i + '-fees'" class="fees">
 | 
				
			||||||
            </div>
 | 
					                ~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
				
			||||||
            <div [attr.data-cy]="'mempool-block-' + i + '-fee-span'" class="fee-span">
 | 
					 | 
				
			||||||
              {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div *ngIf="showMiningInfo" class="block-size">
 | 
					 | 
				
			||||||
              <app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
 | 
					 | 
				
			||||||
            <div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
 | 
					 | 
				
			||||||
              <ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
 | 
					 | 
				
			||||||
              <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
 | 
					 | 
				
			||||||
              <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
 | 
					 | 
				
			||||||
              <ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
 | 
					 | 
				
			||||||
                <app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
 | 
					 | 
				
			||||||
              </ng-template>
 | 
					 | 
				
			||||||
              <ng-template #timeDiffMainnet>
 | 
					 | 
				
			||||||
                <app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
 | 
					 | 
				
			||||||
              </ng-template>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <ng-template #mergedBlock>
 | 
					 | 
				
			||||||
              <div [attr.data-cy]="'mempool-block-' + i + '-blocks'" class="time-difference">
 | 
					 | 
				
			||||||
                <b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / stateService.blockVSize | ceil }"></ng-container>)</b>
 | 
					 | 
				
			||||||
                <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
					 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </ng-template>
 | 
					              <div [attr.data-cy]="'mempool-block-' + i + '-fee-span'" class="fee-span">
 | 
				
			||||||
 | 
					                {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div *ngIf="showMiningInfo" class="block-size">
 | 
				
			||||||
 | 
					                <app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
 | 
				
			||||||
 | 
					              <div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
 | 
				
			||||||
 | 
					                <ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
 | 
				
			||||||
 | 
					                <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
 | 
				
			||||||
 | 
					                <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
 | 
				
			||||||
 | 
					                <ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
 | 
				
			||||||
 | 
					                  <app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
 | 
				
			||||||
 | 
					                </ng-template>
 | 
				
			||||||
 | 
					                <ng-template #timeDiffMainnet>
 | 
				
			||||||
 | 
					                  <app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
 | 
				
			||||||
 | 
					                </ng-template>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <ng-template #mergedBlock>
 | 
				
			||||||
 | 
					                <div [attr.data-cy]="'mempool-block-' + i + '-blocks'" class="time-difference">
 | 
				
			||||||
 | 
					                  <b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / stateService.blockVSize | ceil }"></ng-container>)</b>
 | 
				
			||||||
 | 
					                  <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </ng-template>
 | 
				
			||||||
 | 
					            </ng-container>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <span class="animated-border"></span>
 | 
					          <span class="animated-border"></span>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@ -45,10 +52,10 @@
 | 
				
			|||||||
</ng-container>
 | 
					</ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<ng-template #loadingBlocks>
 | 
					<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">
 | 
					    <div class="flashing">
 | 
				
			||||||
      <ng-template ngFor let-projectedBlock [ngForOf]="mempoolEmptyBlocks" let-i="index" [ngForTrackBy]="trackByFn">
 | 
					      <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>
 | 
					      </ng-template>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
.bitcoin-block {
 | 
					.bitcoin-block {
 | 
				
			||||||
  width: 125px;
 | 
					  width: var(--block-size);
 | 
				
			||||||
  height: 125px;
 | 
					  height: var(--block-size);
 | 
				
			||||||
  transition: background 2s, right 2s, transform 1s;
 | 
					  transition: background 2s, right 2s, transform 1s, opacity 1s;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.block-size {
 | 
					.block-size {
 | 
				
			||||||
@ -14,6 +14,7 @@
 | 
				
			|||||||
  top: 0px;
 | 
					  top: 0px;
 | 
				
			||||||
  right: 0px;
 | 
					  right: 0px;
 | 
				
			||||||
  left: 0px;
 | 
					  left: 0px;
 | 
				
			||||||
 | 
					  --block-size: 125px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.flashing {
 | 
					.flashing {
 | 
				
			||||||
@ -66,11 +67,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.bitcoin-block::after {
 | 
					.bitcoin-block::after {
 | 
				
			||||||
  content: '';
 | 
					  content: '';
 | 
				
			||||||
  width: 125px;
 | 
					  width: var(--block-size);
 | 
				
			||||||
  height: 24px;
 | 
					  height: calc(0.192 * var(--block-size));
 | 
				
			||||||
  position:absolute;
 | 
					  position:absolute;
 | 
				
			||||||
  top: -24px;
 | 
					  top: calc(-0.192 * var(--block-size));
 | 
				
			||||||
  left: -20px;
 | 
					  left: calc(-0.16 * var(--block-size));
 | 
				
			||||||
  background-color: #232838;
 | 
					  background-color: #232838;
 | 
				
			||||||
  transform:skew(40deg);
 | 
					  transform:skew(40deg);
 | 
				
			||||||
  transform-origin:top;
 | 
					  transform-origin:top;
 | 
				
			||||||
@ -79,11 +80,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.bitcoin-block::before {
 | 
					.bitcoin-block::before {
 | 
				
			||||||
  content: '';
 | 
					  content: '';
 | 
				
			||||||
  width: 20px;
 | 
					  width: calc(0.16 * var(--block-size));
 | 
				
			||||||
  height: 125px;
 | 
					  height: var(--block-size);
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: -12px;
 | 
					  top: calc(-0.096 * var(--block-size));
 | 
				
			||||||
  left: -20px;
 | 
					  left: calc(-0.16 * var(--block-size));
 | 
				
			||||||
  background-color: #191c27;
 | 
					  background-color: #191c27;
 | 
				
			||||||
  z-index: -1;
 | 
					  z-index: -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -100,6 +101,10 @@
 | 
				
			|||||||
  background-color: #2d2825;
 | 
					  background-color: #2d2825;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mempool-block.hide-block {
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.black-background {
 | 
					.black-background {
 | 
				
			||||||
  background-color: #11131f;
 | 
					  background-color: #11131f;
 | 
				
			||||||
  z-index: 100;
 | 
					  z-index: 100;
 | 
				
			||||||
@ -141,7 +146,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  .bitcoin-block::before {
 | 
					  .bitcoin-block::before {
 | 
				
			||||||
    transform: skewY(-50deg);
 | 
					    transform: skewY(-50deg);
 | 
				
			||||||
    left: 125px;
 | 
					    left: var(--block-size);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .block-body {
 | 
					  .block-body {
 | 
				
			||||||
    transform: scaleX(-1);
 | 
					    transform: scaleX(-1);
 | 
				
			||||||
@ -152,4 +157,16 @@
 | 
				
			|||||||
  #arrow-up {
 | 
					  #arrow-up {
 | 
				
			||||||
    transform: translateX(70px);
 | 
					    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 { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
 | 
				
			||||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
 | 
					import { MempoolBlock } from '../../interfaces/websocket.interface';
 | 
				
			||||||
import { StateService } from '../../services/state.service';
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
@ -23,7 +23,12 @@ import { animate, style, transition, trigger } from '@angular/animations';
 | 
				
			|||||||
  ])],
 | 
					  ])],
 | 
				
			||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  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;
 | 
					  specialBlocks = specialBlocks;
 | 
				
			||||||
  mempoolBlocks: MempoolBlock[] = [];
 | 
					  mempoolBlocks: MempoolBlock[] = [];
 | 
				
			||||||
  mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks();
 | 
					  mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks();
 | 
				
			||||||
@ -48,8 +53,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
  timeLtr: boolean;
 | 
					  timeLtr: boolean;
 | 
				
			||||||
  animateEntry: boolean = false;
 | 
					  animateEntry: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  blockWidth = 125;
 | 
					  blockOffset: number = 155;
 | 
				
			||||||
  blockPadding = 30;
 | 
					  blockPadding: number = 30;
 | 
				
			||||||
 | 
					  containerOffset: number = 40;
 | 
				
			||||||
  arrowVisible = false;
 | 
					  arrowVisible = false;
 | 
				
			||||||
  tabHidden = false;
 | 
					  tabHidden = false;
 | 
				
			||||||
  feeRounding = '1.0-0';
 | 
					  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() {
 | 
					  ngOnDestroy() {
 | 
				
			||||||
    this.markBlocksSubscription.unsubscribe();
 | 
					    this.markBlocksSubscription.unsubscribe();
 | 
				
			||||||
    this.blockSubscription.unsubscribe();
 | 
					    this.blockSubscription.unsubscribe();
 | 
				
			||||||
@ -238,17 +252,24 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
 | 
					  reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
 | 
				
			||||||
    const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
 | 
					    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) {
 | 
					    while (blocks.length > blocksAmount) {
 | 
				
			||||||
      const block = blocks.pop();
 | 
					      const block = blocks.pop();
 | 
				
			||||||
      const lastBlock = blocks[blocks.length - 1];
 | 
					      if (!this.count) {
 | 
				
			||||||
      lastBlock.blockSize += block.blockSize;
 | 
					        const lastBlock = blocks[blocks.length - 1];
 | 
				
			||||||
      lastBlock.blockVSize += block.blockVSize;
 | 
					        lastBlock.blockSize += block.blockSize;
 | 
				
			||||||
      lastBlock.nTx += block.nTx;
 | 
					        lastBlock.blockVSize += block.blockVSize;
 | 
				
			||||||
      lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
 | 
					        lastBlock.nTx += block.nTx;
 | 
				
			||||||
      lastBlock.feeRange.sort((a, b) => a - b);
 | 
					        lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
 | 
				
			||||||
      lastBlock.medianFee = this.median(lastBlock.feeRange);
 | 
					        lastBlock.feeRange.sort((a, b) => a - b);
 | 
				
			||||||
      lastBlock.totalFees += block.totalFees;
 | 
					        lastBlock.medianFee = this.median(lastBlock.feeRange);
 | 
				
			||||||
 | 
					        lastBlock.totalFees += block.totalFees;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (blocks.length) {
 | 
					    if (blocks.length) {
 | 
				
			||||||
      blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
 | 
					      blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
 | 
				
			||||||
@ -294,14 +315,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      'right': 40 + index * 155 + 'px',
 | 
					      'right': this.containerOffset + index * this.blockOffset + 'px',
 | 
				
			||||||
      'background': backgroundGradients.join(',') + ')'
 | 
					      'background': backgroundGradients.join(',') + ')'
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getStyleForMempoolEmptyBlock(index: number) {
 | 
					  getStyleForMempoolEmptyBlock(index: number) {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      'right': 40 + index * 155 + 'px',
 | 
					      'right': this.containerOffset + index * this.blockOffset + 'px',
 | 
				
			||||||
      'background': '#554b45',
 | 
					      'background': '#554b45',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,6 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-
 | 
				
			|||||||
import { GraphsComponent } from '../components/graphs/graphs.component';
 | 
					import { GraphsComponent } from '../components/graphs/graphs.component';
 | 
				
			||||||
import { StatisticsComponent } from '../components/statistics/statistics.component';
 | 
					import { StatisticsComponent } from '../components/statistics/statistics.component';
 | 
				
			||||||
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.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 { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component';
 | 
				
			||||||
import { PoolComponent } from '../components/pool/pool.component';
 | 
					import { PoolComponent } from '../components/pool/pool.component';
 | 
				
			||||||
import { TelevisionComponent } from '../components/television/television.component';
 | 
					import { TelevisionComponent } from '../components/television/television.component';
 | 
				
			||||||
@ -42,7 +41,6 @@ import { CommonModule } from '@angular/common';
 | 
				
			|||||||
    BlockFeeRatesGraphComponent,
 | 
					    BlockFeeRatesGraphComponent,
 | 
				
			||||||
    BlockSizesWeightsGraphComponent,
 | 
					    BlockSizesWeightsGraphComponent,
 | 
				
			||||||
    FeeDistributionGraphComponent,
 | 
					    FeeDistributionGraphComponent,
 | 
				
			||||||
    MempoolBlockOverviewComponent,
 | 
					 | 
				
			||||||
    IncomingTransactionsGraphComponent,
 | 
					    IncomingTransactionsGraphComponent,
 | 
				
			||||||
    MempoolGraphComponent,
 | 
					    MempoolGraphComponent,
 | 
				
			||||||
    LbtcPegsGraphComponent,
 | 
					    LbtcPegsGraphComponent,
 | 
				
			||||||
 | 
				
			|||||||
@ -90,6 +90,13 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
				
			|||||||
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
 | 
					import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
 | 
				
			||||||
import { GlobalFooterComponent } from './components/global-footer/global-footer.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({
 | 
					@NgModule({
 | 
				
			||||||
  declarations: [
 | 
					  declarations: [
 | 
				
			||||||
    ClipboardComponent,
 | 
					    ClipboardComponent,
 | 
				
			||||||
@ -172,6 +179,13 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer.
 | 
				
			|||||||
    GeolocationComponent,
 | 
					    GeolocationComponent,
 | 
				
			||||||
    TestnetAlertComponent,
 | 
					    TestnetAlertComponent,
 | 
				
			||||||
    GlobalFooterComponent,
 | 
					    GlobalFooterComponent,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    MempoolBlockOverviewComponent,
 | 
				
			||||||
 | 
					    ClockchainComponent,
 | 
				
			||||||
 | 
					    ClockComponent,
 | 
				
			||||||
 | 
					    ClockMinedComponent,
 | 
				
			||||||
 | 
					    ClockMempoolComponent,
 | 
				
			||||||
 | 
					    ClockFaceComponent,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    CommonModule,
 | 
					    CommonModule,
 | 
				
			||||||
@ -279,6 +293,13 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer.
 | 
				
			|||||||
    GeolocationComponent,
 | 
					    GeolocationComponent,
 | 
				
			||||||
    PreviewTitleComponent,
 | 
					    PreviewTitleComponent,
 | 
				
			||||||
    GlobalFooterComponent,
 | 
					    GlobalFooterComponent,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    MempoolBlockOverviewComponent,
 | 
				
			||||||
 | 
					    ClockchainComponent,
 | 
				
			||||||
 | 
					    ClockComponent,
 | 
				
			||||||
 | 
					    ClockMinedComponent,
 | 
				
			||||||
 | 
					    ClockMempoolComponent,
 | 
				
			||||||
 | 
					    ClockFaceComponent,
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class SharedModule {
 | 
					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;
 | 
					  color: #fff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.white-color {
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.green-color {
 | 
					.green-color {
 | 
				
			||||||
  color: #3bcc49;
 | 
					  color: #3bcc49;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user