support trees of RBF replacements
This commit is contained in:
		
							parent
							
								
									c064ef6ace
								
							
						
					
					
						commit
						086b41d958
					
				@ -644,7 +644,7 @@ class BitcoinRoutes {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async getRbfHistory(req: Request, res: Response) {
 | 
					  private async getRbfHistory(req: Request, res: Response) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const replacements = rbfCache.getRbfChain(req.params.txId) || [];
 | 
					      const replacements = rbfCache.getRbfTree(req.params.txId) || null;
 | 
				
			||||||
      const replaces = rbfCache.getReplaces(req.params.txId) || null;
 | 
					      const replaces = rbfCache.getReplaces(req.params.txId) || null;
 | 
				
			||||||
      res.json({
 | 
					      res.json({
 | 
				
			||||||
        replacements,
 | 
					        replacements,
 | 
				
			||||||
@ -657,7 +657,7 @@ class BitcoinRoutes {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async getRbfReplacements(req: Request, res: Response) {
 | 
					  private async getRbfReplacements(req: Request, res: Response) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = rbfCache.getRbfChains(false);
 | 
					      const result = rbfCache.getRbfTrees(false);
 | 
				
			||||||
      res.json(result);
 | 
					      res.json(result);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
					      res.status(500).send(e instanceof Error ? e.message : e);
 | 
				
			||||||
@ -666,7 +666,7 @@ class BitcoinRoutes {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async getFullRbfReplacements(req: Request, res: Response) {
 | 
					  private async getFullRbfReplacements(req: Request, res: Response) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = rbfCache.getRbfChains(true);
 | 
					      const result = rbfCache.getRbfTrees(true);
 | 
				
			||||||
      res.json(result);
 | 
					      res.json(result);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
					      res.status(500).send(e instanceof Error ? e.message : e);
 | 
				
			||||||
 | 
				
			|||||||
@ -57,11 +57,11 @@ export class Common {
 | 
				
			|||||||
    return arr;
 | 
					    return arr;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
 | 
					  static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended[] } {
 | 
				
			||||||
    const matches: { [txid: string]: TransactionExtended } = {};
 | 
					    const matches: { [txid: string]: TransactionExtended[] } = {};
 | 
				
			||||||
    deleted
 | 
					    added
 | 
				
			||||||
      .forEach((deletedTx) => {
 | 
					      .forEach((addedTx) => {
 | 
				
			||||||
        const foundMatches = added.find((addedTx) => {
 | 
					        const foundMatches = deleted.filter((deletedTx) => {
 | 
				
			||||||
          // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
 | 
					          // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
 | 
				
			||||||
          return addedTx.fee > deletedTx.fee
 | 
					          return addedTx.fee > deletedTx.fee
 | 
				
			||||||
            // The new transaction must pay more fee per kB than the replaced tx.
 | 
					            // The new transaction must pay more fee per kB than the replaced tx.
 | 
				
			||||||
@ -70,8 +70,8 @@ export class Common {
 | 
				
			|||||||
            && deletedTx.vin.some((deletedVin) =>
 | 
					            && deletedTx.vin.some((deletedVin) =>
 | 
				
			||||||
              addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
 | 
					              addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        if (foundMatches) {
 | 
					        if (foundMatches?.length) {
 | 
				
			||||||
          matches[deletedTx.txid] = foundMatches;
 | 
					          matches[addedTx.txid] = foundMatches;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    return matches;
 | 
					    return matches;
 | 
				
			||||||
 | 
				
			|||||||
@ -265,13 +265,15 @@ class Mempool {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
 | 
					  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended[]; }): void {
 | 
				
			||||||
    for (const rbfTransaction in rbfTransactions) {
 | 
					    for (const rbfTransaction in rbfTransactions) {
 | 
				
			||||||
      if (this.mempoolCache[rbfTransaction]) {
 | 
					      if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
 | 
				
			||||||
        // Store replaced transactions
 | 
					        // Store replaced transactions
 | 
				
			||||||
        rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction]);
 | 
					        rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
 | 
				
			||||||
        // Erase the replaced transactions from the local mempool
 | 
					        // Erase the replaced transactions from the local mempool
 | 
				
			||||||
        delete this.mempoolCache[rbfTransaction];
 | 
					        for (const replaced of rbfTransactions[rbfTransaction]) {
 | 
				
			||||||
 | 
					          delete this.mempoolCache[replaced.txid];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,22 +1,27 @@
 | 
				
			|||||||
 | 
					import { runInNewContext } from "vm";
 | 
				
			||||||
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
 | 
					import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
 | 
				
			||||||
import { Common } from "./common";
 | 
					import { Common } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface RbfTransaction extends TransactionStripped {
 | 
					interface RbfTransaction extends TransactionStripped {
 | 
				
			||||||
  rbf?: boolean;
 | 
					  rbf?: boolean;
 | 
				
			||||||
 | 
					  mined?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RbfChain = {
 | 
					interface RbfTree {
 | 
				
			||||||
  tx: RbfTransaction,
 | 
					  tx: RbfTransaction;
 | 
				
			||||||
  time: number,
 | 
					  time: number;
 | 
				
			||||||
  mined?: boolean,
 | 
					  interval?: number;
 | 
				
			||||||
}[];
 | 
					  mined?: boolean;
 | 
				
			||||||
 | 
					  fullRbf: boolean;
 | 
				
			||||||
 | 
					  replaces: RbfTree[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RbfCache {
 | 
					class RbfCache {
 | 
				
			||||||
  private replacedBy: Map<string, string> = new Map();
 | 
					  private replacedBy: Map<string, string> = new Map();
 | 
				
			||||||
  private replaces: Map<string, string[]> = new Map();
 | 
					  private replaces: Map<string, string[]> = new Map();
 | 
				
			||||||
  private rbfChains: Map<string, RbfChain> = new Map(); // sequences of consecutive replacements
 | 
					  private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
 | 
				
			||||||
  private dirtyChains: Set<string> = new Set();
 | 
					  private dirtyTrees: Set<string> = new Set();
 | 
				
			||||||
  private chainMap: Map<string, string> = new Map(); // map of txids to sequence ids
 | 
					  private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
 | 
				
			||||||
  private txs: Map<string, TransactionExtended> = new Map();
 | 
					  private txs: Map<string, TransactionExtended> = new Map();
 | 
				
			||||||
  private expiring: Map<string, Date> = new Map();
 | 
					  private expiring: Map<string, Date> = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,37 +29,58 @@ class RbfCache {
 | 
				
			|||||||
    setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
 | 
					    setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public add(replacedTxExtended: TransactionExtended, newTxExtended: TransactionExtended): void {
 | 
					  public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void {
 | 
				
			||||||
    const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
 | 
					    if (!newTxExtended || !replaced?.length) {
 | 
				
			||||||
    replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
 | 
					    const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
 | 
				
			||||||
 | 
					    const newTime = newTxExtended.firstSeen || Date.now();
 | 
				
			||||||
    newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
					    newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.replacedBy.set(replacedTx.txid, newTx.txid);
 | 
					 | 
				
			||||||
    this.txs.set(replacedTx.txid, replacedTxExtended);
 | 
					 | 
				
			||||||
    this.txs.set(newTx.txid, newTxExtended);
 | 
					    this.txs.set(newTx.txid, newTxExtended);
 | 
				
			||||||
    if (!this.replaces.has(newTx.txid)) {
 | 
					 | 
				
			||||||
      this.replaces.set(newTx.txid, []);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this.replaces.get(newTx.txid)?.push(replacedTx.txid);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // maintain rbf chains
 | 
					    // maintain rbf trees
 | 
				
			||||||
    if (this.chainMap.has(replacedTx.txid)) {
 | 
					    let fullRbf = false;
 | 
				
			||||||
      // add to an existing chain
 | 
					    const replacedTrees: RbfTree[] = [];
 | 
				
			||||||
      const chainRoot = this.chainMap.get(replacedTx.txid) || '';
 | 
					    for (const replacedTxExtended of replaced) {
 | 
				
			||||||
      this.rbfChains.get(chainRoot)?.push({ tx: newTx, time: newTxExtended.firstSeen || Date.now() });
 | 
					      const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
 | 
				
			||||||
      this.chainMap.set(newTx.txid, chainRoot);
 | 
					      replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
				
			||||||
      this.dirtyChains.add(chainRoot);
 | 
					      this.replacedBy.set(replacedTx.txid, newTx.txid);
 | 
				
			||||||
    } else {
 | 
					      if (this.treeMap.has(replacedTx.txid)) {
 | 
				
			||||||
      // start a new chain
 | 
					        const treeId = this.treeMap.get(replacedTx.txid);
 | 
				
			||||||
      this.rbfChains.set(replacedTx.txid, [
 | 
					        if (treeId) {
 | 
				
			||||||
        { tx: replacedTx, time: replacedTxExtended.firstSeen || Date.now() },
 | 
					          const tree = this.rbfTrees.get(treeId);
 | 
				
			||||||
        { tx: newTx, time: newTxExtended.firstSeen || Date.now() },
 | 
					          this.rbfTrees.delete(treeId);
 | 
				
			||||||
      ]);
 | 
					          if (tree) {
 | 
				
			||||||
      this.chainMap.set(replacedTx.txid, replacedTx.txid);
 | 
					            tree.interval = newTime - tree?.time;
 | 
				
			||||||
      this.chainMap.set(newTx.txid, replacedTx.txid);
 | 
					            replacedTrees.push(tree);
 | 
				
			||||||
      this.dirtyChains.add(replacedTx.txid);
 | 
					            fullRbf = fullRbf || tree.fullRbf;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const replacedTime = replacedTxExtended.firstSeen || Date.now();
 | 
				
			||||||
 | 
					        replacedTrees.push({
 | 
				
			||||||
 | 
					          tx: replacedTx,
 | 
				
			||||||
 | 
					          time: replacedTime,
 | 
				
			||||||
 | 
					          interval: newTime - replacedTime,
 | 
				
			||||||
 | 
					          fullRbf: !replacedTx.rbf,
 | 
				
			||||||
 | 
					          replaces: [],
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        fullRbf = fullRbf || !replacedTx.rbf;
 | 
				
			||||||
 | 
					        this.txs.set(replacedTx.txid, replacedTxExtended);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    const treeId = replacedTrees[0].tx.txid;
 | 
				
			||||||
 | 
					    const newTree = {
 | 
				
			||||||
 | 
					      tx: newTx,
 | 
				
			||||||
 | 
					      time: newTxExtended.firstSeen || Date.now(),
 | 
				
			||||||
 | 
					      fullRbf,
 | 
				
			||||||
 | 
					      replaces: replacedTrees
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    this.rbfTrees.set(treeId, newTree);
 | 
				
			||||||
 | 
					    this.updateTreeMap(treeId, newTree);
 | 
				
			||||||
 | 
					    this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
 | 
				
			||||||
 | 
					    this.dirtyTrees.add(treeId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public getReplacedBy(txId: string): string | undefined {
 | 
					  public getReplacedBy(txId: string): string | undefined {
 | 
				
			||||||
@ -69,66 +95,64 @@ class RbfCache {
 | 
				
			|||||||
    return this.txs.get(txId);
 | 
					    return this.txs.get(txId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public getRbfChain(txId: string): RbfChain {
 | 
					  public getRbfTree(txId: string): RbfTree | void {
 | 
				
			||||||
    return this.rbfChains.get(this.chainMap.get(txId) || '') || [];
 | 
					    return this.rbfTrees.get(this.treeMap.get(txId) || '');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // get a paginated list of RbfChains
 | 
					  // get a paginated list of RbfTrees
 | 
				
			||||||
  // ordered by most recent replacement time
 | 
					  // ordered by most recent replacement time
 | 
				
			||||||
  public getRbfChains(onlyFullRbf: boolean, after?: string): RbfChain[] {
 | 
					  public getRbfTrees(onlyFullRbf: boolean, after?: string): RbfTree[] {
 | 
				
			||||||
    const limit = 25;
 | 
					    const limit = 25;
 | 
				
			||||||
    const chains: RbfChain[] = [];
 | 
					    const trees: RbfTree[] = [];
 | 
				
			||||||
    const used = new Set<string>();
 | 
					    const used = new Set<string>();
 | 
				
			||||||
    const replacements: string[][] = Array.from(this.replacedBy).reverse();
 | 
					    const replacements: string[][] = Array.from(this.replacedBy).reverse();
 | 
				
			||||||
    const afterChain = after ? this.chainMap.get(after) : null;
 | 
					    const afterTree = after ? this.treeMap.get(after) : null;
 | 
				
			||||||
    let ready = !afterChain;
 | 
					    let ready = !afterTree;
 | 
				
			||||||
    for (let i = 0; i < replacements.length && chains.length <= limit - 1; i++) {
 | 
					    for (let i = 0; i < replacements.length && trees.length <= limit - 1; i++) {
 | 
				
			||||||
      const txid = replacements[i][1];
 | 
					      const txid = replacements[i][1];
 | 
				
			||||||
      const chainRoot = this.chainMap.get(txid) || '';
 | 
					      const treeId = this.treeMap.get(txid) || '';
 | 
				
			||||||
      if (chainRoot === afterChain) {
 | 
					      if (treeId === afterTree) {
 | 
				
			||||||
        ready = true;
 | 
					        ready = true;
 | 
				
			||||||
      } else if (ready) {
 | 
					      } else if (ready) {
 | 
				
			||||||
        if (!used.has(chainRoot)) {
 | 
					        if (!used.has(treeId)) {
 | 
				
			||||||
          const chain = this.rbfChains.get(chainRoot);
 | 
					          const tree = this.rbfTrees.get(treeId);
 | 
				
			||||||
          used.add(chainRoot);
 | 
					          used.add(treeId);
 | 
				
			||||||
          if (chain && (!onlyFullRbf || chain.slice(0, -1).some(entry => !entry.tx.rbf))) {
 | 
					          if (tree && (!onlyFullRbf || tree.fullRbf)) {
 | 
				
			||||||
            chains.push(chain);
 | 
					            trees.push(tree);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return chains;
 | 
					    return trees;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // get map of rbf chains that have been updated since the last call
 | 
					  // get map of rbf trees that have been updated since the last call
 | 
				
			||||||
  public getRbfChanges(): { chains: {[root: string]: RbfChain }, map: { [txid: string]: string }} {
 | 
					  public getRbfChanges(): { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} {
 | 
				
			||||||
    const changes: { chains: {[root: string]: RbfChain }, map: { [txid: string]: string }} = {
 | 
					    const changes: { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} = {
 | 
				
			||||||
      chains: {},
 | 
					      trees: {},
 | 
				
			||||||
      map: {},
 | 
					      map: {},
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    this.dirtyChains.forEach(root => {
 | 
					    this.dirtyTrees.forEach(id => {
 | 
				
			||||||
      const chain = this.rbfChains.get(root);
 | 
					      const tree = this.rbfTrees.get(id);
 | 
				
			||||||
      if (chain) {
 | 
					      if (tree) {
 | 
				
			||||||
        changes.chains[root] = chain;
 | 
					        changes.trees[id] = tree;
 | 
				
			||||||
        chain.forEach(entry => {
 | 
					        this.getTransactionsInTree(tree).forEach(tx => {
 | 
				
			||||||
          changes.map[entry.tx.txid] = root;
 | 
					          changes.map[tx.txid] = id;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.dirtyChains = new Set();
 | 
					    this.dirtyTrees = new Set();
 | 
				
			||||||
    return changes;
 | 
					    return changes;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public mined(txid): void {
 | 
					  public mined(txid): void {
 | 
				
			||||||
    const chainRoot = this.chainMap.get(txid)
 | 
					    const treeId = this.treeMap.get(txid);
 | 
				
			||||||
    if (chainRoot && this.rbfChains.has(chainRoot)) {
 | 
					    if (treeId && this.rbfTrees.has(treeId)) {
 | 
				
			||||||
      const chain = this.rbfChains.get(chainRoot);
 | 
					      const tree = this.rbfTrees.get(treeId);
 | 
				
			||||||
      if (chain) {
 | 
					      if (tree) {
 | 
				
			||||||
        const chainEntry = chain.find(entry => entry.tx.txid === txid);
 | 
					        this.setTreeMined(tree, txid);
 | 
				
			||||||
        if (chainEntry) {
 | 
					        tree.mined = true;
 | 
				
			||||||
          chainEntry.mined = true;
 | 
					        this.dirtyTrees.add(treeId);
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.dirtyChains.add(chainRoot);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.evict(txid);
 | 
					    this.evict(txid);
 | 
				
			||||||
@ -155,20 +179,45 @@ class RbfCache {
 | 
				
			|||||||
    if (!this.replacedBy.has(txid)) {
 | 
					    if (!this.replacedBy.has(txid)) {
 | 
				
			||||||
      const replaces = this.replaces.get(txid);
 | 
					      const replaces = this.replaces.get(txid);
 | 
				
			||||||
      this.replaces.delete(txid);
 | 
					      this.replaces.delete(txid);
 | 
				
			||||||
      this.chainMap.delete(txid);
 | 
					      this.treeMap.delete(txid);
 | 
				
			||||||
      this.txs.delete(txid);
 | 
					      this.txs.delete(txid);
 | 
				
			||||||
      this.expiring.delete(txid);
 | 
					      this.expiring.delete(txid);
 | 
				
			||||||
      for (const tx of (replaces || [])) {
 | 
					      for (const tx of (replaces || [])) {
 | 
				
			||||||
        // recursively remove prior versions from the cache
 | 
					        // recursively remove prior versions from the cache
 | 
				
			||||||
        this.replacedBy.delete(tx);
 | 
					        this.replacedBy.delete(tx);
 | 
				
			||||||
        // if this is the root of a chain, remove that too
 | 
					        // if this is the id of a tree, remove that too
 | 
				
			||||||
        if (this.chainMap.get(tx) === tx) {
 | 
					        if (this.treeMap.get(tx) === tx) {
 | 
				
			||||||
          this.rbfChains.delete(tx);
 | 
					          this.rbfTrees.delete(tx);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.remove(tx);
 | 
					        this.remove(tx);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private updateTreeMap(newId: string, tree: RbfTree): void {
 | 
				
			||||||
 | 
					    this.treeMap.set(tree.tx.txid, newId);
 | 
				
			||||||
 | 
					    tree.replaces.forEach(subtree => {
 | 
				
			||||||
 | 
					      this.updateTreeMap(newId, subtree);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getTransactionsInTree(tree: RbfTree, txs: RbfTransaction[] = []): RbfTransaction[] {
 | 
				
			||||||
 | 
					    txs.push(tree.tx);
 | 
				
			||||||
 | 
					    tree.replaces.forEach(subtree => {
 | 
				
			||||||
 | 
					      this.getTransactionsInTree(subtree, txs);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return txs;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private setTreeMined(tree: RbfTree, txid: string): void {
 | 
				
			||||||
 | 
					    if (tree.tx.txid === txid) {
 | 
				
			||||||
 | 
					      tree.tx.mined = true;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      tree.replaces.forEach(subtree => {
 | 
				
			||||||
 | 
					        this.setTreeMined(subtree, txid);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default new RbfCache();
 | 
					export default new RbfCache();
 | 
				
			||||||
 | 
				
			|||||||
@ -289,9 +289,9 @@ class WebsocketHandler {
 | 
				
			|||||||
    const rbfChanges = rbfCache.getRbfChanges();
 | 
					    const rbfChanges = rbfCache.getRbfChanges();
 | 
				
			||||||
    let rbfReplacements;
 | 
					    let rbfReplacements;
 | 
				
			||||||
    let fullRbfReplacements;
 | 
					    let fullRbfReplacements;
 | 
				
			||||||
    if (Object.keys(rbfChanges.chains).length) {
 | 
					    if (Object.keys(rbfChanges.trees).length) {
 | 
				
			||||||
      rbfReplacements = rbfCache.getRbfChains(false);
 | 
					      rbfReplacements = rbfCache.getRbfTrees(false);
 | 
				
			||||||
      fullRbfReplacements = rbfCache.getRbfChains(true);
 | 
					      fullRbfReplacements = rbfCache.getRbfTrees(true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const recommendedFees = feeApi.getRecommendedFee();
 | 
					    const recommendedFees = feeApi.getRecommendedFee();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -415,20 +415,16 @@ class WebsocketHandler {
 | 
				
			|||||||
          response['utxoSpent'] = outspends;
 | 
					          response['utxoSpent'] = outspends;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (rbfTransactions[client['track-tx']]) {
 | 
					        const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']);
 | 
				
			||||||
          for (const rbfTransaction in rbfTransactions) {
 | 
					        if (rbfReplacedBy) {
 | 
				
			||||||
            if (client['track-tx'] === rbfTransaction) {
 | 
					          response['rbfTransaction'] = {
 | 
				
			||||||
              response['rbfTransaction'] = {
 | 
					            txid: rbfReplacedBy,
 | 
				
			||||||
                txid: rbfTransactions[rbfTransaction].txid,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
              break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const rbfChange = rbfChanges.map[client['track-tx']];
 | 
					        const rbfChange = rbfChanges.map[client['track-tx']];
 | 
				
			||||||
        if (rbfChange) {
 | 
					        if (rbfChange) {
 | 
				
			||||||
          response['rbfInfo'] = rbfChanges.chains[rbfChange];
 | 
					          response['rbfInfo'] = rbfChanges.trees[rbfChange];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -17,37 +17,22 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="clearfix"></div>
 | 
					  <div class="clearfix"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="rbf-chains" style="min-height: 295px">
 | 
					  <div class="rbf-trees" style="min-height: 295px">
 | 
				
			||||||
    <ng-container *ngIf="rbfChains$ | async as chains">
 | 
					    <ng-container *ngIf="rbfTrees$ | async as trees">
 | 
				
			||||||
      <div *ngFor="let chain of chains" class="chain">
 | 
					      <div *ngFor="let tree of trees" class="tree">
 | 
				
			||||||
        <p class="info">
 | 
					        <p class="info">
 | 
				
			||||||
          <app-time kind="since" [time]="chain[chain.length - 1].time"></app-time>
 | 
					 | 
				
			||||||
          <span class="type">
 | 
					          <span class="type">
 | 
				
			||||||
            <span *ngIf="isMined(chain)" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
 | 
					            <span *ngIf="isMined(tree)" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
 | 
				
			||||||
            <span *ngIf="isFullRbf(chain)" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
 | 
					            <span *ngIf="isFullRbf(tree)" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
 | 
					          <app-time kind="since" [time]="tree.time"></app-time>
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
        <div class="txids">
 | 
					        <div class="timeline-wrapper" [class.mined]="isMined(tree)">
 | 
				
			||||||
          <span class="txid">
 | 
					          <app-rbf-timeline [replacements]="tree"></app-rbf-timeline>
 | 
				
			||||||
            <a class="rbf-link" [routerLink]="['/tx/' | relativeUrl, chain[0].tx.txid]">
 | 
					 | 
				
			||||||
              <span class="d-inline">{{ chain[0].tx.txid | shortenString : 24 }}</span>
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <span class="arrow">
 | 
					 | 
				
			||||||
            <fa-icon [icon]="['fas', 'arrow-right']" [fixedWidth]="true"></fa-icon>
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <span class="txid right">
 | 
					 | 
				
			||||||
            <a class="rbf-link" [routerLink]="['/tx/' | relativeUrl, chain[chain.length - 1].tx.txid]">
 | 
					 | 
				
			||||||
              <span class="d-inline">{{ chain[chain.length - 1].tx.txid | shortenString : 24 }}</span>
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div class="timeline-wrapper" [class.mined]="isMined(chain)">
 | 
					 | 
				
			||||||
          <app-rbf-timeline [replacements]="chain"></app-rbf-timeline>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="no-replacements" *ngIf="!chains?.length">
 | 
					      <div class="no-replacements" *ngIf="!trees?.length">
 | 
				
			||||||
        <p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
 | 
					        <p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </ng-container>
 | 
					    </ng-container>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,13 +4,14 @@
 | 
				
			|||||||
  margin-top: 13px;
 | 
					  margin-top: 13px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.rbf-chains {
 | 
					.rbf-trees {
 | 
				
			||||||
  .info {
 | 
					  .info {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: row;
 | 
					    flex-direction: row;
 | 
				
			||||||
    justify-content: space-between;
 | 
					    justify-content: space-between;
 | 
				
			||||||
    align-items: baseline;
 | 
					    align-items: baseline;
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    margin-bottom: 0.5em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .type {
 | 
					    .type {
 | 
				
			||||||
      .badge {
 | 
					      .badge {
 | 
				
			||||||
@ -19,27 +20,10 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chain {
 | 
					  .tree {
 | 
				
			||||||
    margin-bottom: 1em;
 | 
					    margin-bottom: 1em;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .txids {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: row;
 | 
					 | 
				
			||||||
    align-items: baseline;
 | 
					 | 
				
			||||||
    justify-content: space-between;
 | 
					 | 
				
			||||||
    margin-bottom: 2px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .txid {
 | 
					 | 
				
			||||||
      flex-basis: 0;
 | 
					 | 
				
			||||||
      flex-grow: 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &.right {
 | 
					 | 
				
			||||||
        text-align: right;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .timeline-wrapper.mined {
 | 
					  .timeline-wrapper.mined {
 | 
				
			||||||
    border: solid 4px #1a9436;
 | 
					    border: solid 4px #1a9436;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';
 | 
				
			|||||||
import { BehaviorSubject, EMPTY, merge, Observable, Subscription } from 'rxjs';
 | 
					import { BehaviorSubject, EMPTY, merge, Observable, Subscription } from 'rxjs';
 | 
				
			||||||
import { catchError, switchMap, tap } from 'rxjs/operators';
 | 
					import { catchError, switchMap, tap } from 'rxjs/operators';
 | 
				
			||||||
import { WebsocketService } from 'src/app/services/websocket.service';
 | 
					import { WebsocketService } from 'src/app/services/websocket.service';
 | 
				
			||||||
import { RbfInfo } from '../../interfaces/node-api.interface';
 | 
					import { RbfTree } from '../../interfaces/node-api.interface';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
import { StateService } from '../../services/state.service';
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,14 +14,12 @@ import { StateService } from '../../services/state.service';
 | 
				
			|||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class RbfList implements OnInit, OnDestroy {
 | 
					export class RbfList implements OnInit, OnDestroy {
 | 
				
			||||||
  rbfChains$: Observable<RbfInfo[][]>;
 | 
					  rbfTrees$: Observable<RbfTree[]>;
 | 
				
			||||||
  fromChainSubject = new BehaviorSubject(null);
 | 
					  nextRbfSubject = new BehaviorSubject(null);
 | 
				
			||||||
  urlFragmentSubscription: Subscription;
 | 
					  urlFragmentSubscription: Subscription;
 | 
				
			||||||
  fullRbfEnabled: boolean;
 | 
					  fullRbfEnabled: boolean;
 | 
				
			||||||
  fullRbf: boolean;
 | 
					  fullRbf: boolean;
 | 
				
			||||||
  isLoading = true;
 | 
					  isLoading = true;
 | 
				
			||||||
  firstChainId: string;
 | 
					 | 
				
			||||||
  lastChainId: string;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private route: ActivatedRoute,
 | 
					    private route: ActivatedRoute,
 | 
				
			||||||
@ -37,13 +35,13 @@ export class RbfList implements OnInit, OnDestroy {
 | 
				
			|||||||
    this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
 | 
					    this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
 | 
				
			||||||
      this.fullRbf = (fragment === 'fullrbf');
 | 
					      this.fullRbf = (fragment === 'fullrbf');
 | 
				
			||||||
      this.websocketService.startTrackRbf(this.fullRbf ? 'fullRbf' : 'all');
 | 
					      this.websocketService.startTrackRbf(this.fullRbf ? 'fullRbf' : 'all');
 | 
				
			||||||
      this.fromChainSubject.next(this.firstChainId);
 | 
					      this.nextRbfSubject.next(null);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.rbfChains$ = merge(
 | 
					    this.rbfTrees$ = merge(
 | 
				
			||||||
      this.fromChainSubject.pipe(
 | 
					      this.nextRbfSubject.pipe(
 | 
				
			||||||
        switchMap((fromChainId) => {
 | 
					        switchMap(() => {
 | 
				
			||||||
          return this.apiService.getRbfList$(this.fullRbf, fromChainId || undefined)
 | 
					          return this.apiService.getRbfList$(this.fullRbf);
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        catchError((e) => {
 | 
					        catchError((e) => {
 | 
				
			||||||
          return EMPTY;
 | 
					          return EMPTY;
 | 
				
			||||||
@ -52,11 +50,8 @@ export class RbfList implements OnInit, OnDestroy {
 | 
				
			|||||||
      this.stateService.rbfLatest$
 | 
					      this.stateService.rbfLatest$
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .pipe(
 | 
					    .pipe(
 | 
				
			||||||
      tap((result: RbfInfo[][]) => {
 | 
					      tap(() => {
 | 
				
			||||||
        this.isLoading = false;
 | 
					        this.isLoading = false;
 | 
				
			||||||
        if (result && result.length && result[0].length) {
 | 
					 | 
				
			||||||
          this.lastChainId = result[result.length - 1][0].tx.txid;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -68,16 +63,16 @@ export class RbfList implements OnInit, OnDestroy {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isFullRbf(chain: RbfInfo[]): boolean {
 | 
					  isFullRbf(tree: RbfTree): boolean {
 | 
				
			||||||
    return chain.slice(0, -1).some(entry => !entry.tx.rbf);
 | 
					    return tree.fullRbf;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isMined(chain: RbfInfo[]): boolean {
 | 
					  isMined(tree: RbfTree): boolean {
 | 
				
			||||||
    return chain.some(entry => entry.mined);
 | 
					    return tree.mined;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // pageChange(page: number) {
 | 
					  // pageChange(page: number) {
 | 
				
			||||||
  //   this.fromChainSubject.next(this.lastChainId);
 | 
					  //   this.fromTreeSubject.next(this.lastTreeId);
 | 
				
			||||||
  // }
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnDestroy(): void {
 | 
					  ngOnDestroy(): void {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,31 +1,54 @@
 | 
				
			|||||||
<div class="rbf-timeline box" [class.mined]="mined">
 | 
					<div class="rbf-timeline box" [class.mined]="replacements.mined">
 | 
				
			||||||
  <div class="timeline">
 | 
					  <div class="timeline-wrapper">
 | 
				
			||||||
    <div class="intervals">
 | 
					    <div class="timeline" *ngFor="let timeline of rows">
 | 
				
			||||||
      <ng-container *ngFor="let replacement of replacements; let i = index;">
 | 
					      <div class="intervals">
 | 
				
			||||||
        <div class="interval" *ngIf="i > 0">
 | 
					        <ng-container *ngFor="let cell of timeline; let i = index;">
 | 
				
			||||||
          <div class="interval-time">
 | 
					          <div class="node-spacer"></div>
 | 
				
			||||||
            <app-time [time]="replacement.time - replacements[i-1].time" [relative]="false"></app-time>
 | 
					          <ng-container *ngIf="i < timeline.length - 1">
 | 
				
			||||||
          </div>
 | 
					            <div class="interval" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
 | 
				
			||||||
        </div>
 | 
					              <div class="interval-time">
 | 
				
			||||||
        <div class="node-spacer"></div>
 | 
					                <app-time [time]="cell.replacement.interval" [relative]="false"></app-time>
 | 
				
			||||||
      </ng-container>
 | 
					              </div>
 | 
				
			||||||
    </div>
 | 
					            </div>
 | 
				
			||||||
    <div class="nodes">
 | 
					          </ng-container>
 | 
				
			||||||
      <ng-container *ngFor="let replacement of replacements; let i = index;">
 | 
					        </ng-container>
 | 
				
			||||||
        <div class="interval-spacer" *ngIf="i > 0">
 | 
					      </div>
 | 
				
			||||||
          <div class="track"></div>
 | 
					      <div class="nodes">
 | 
				
			||||||
        </div>
 | 
					        <ng-container *ngFor="let cell of timeline; let i = index;">
 | 
				
			||||||
        <div class="node" [class.selected]="txid === replacement.tx.txid" [class.mined]="replacement.mined">
 | 
					          <ng-container *ngIf="cell.replacement; else nonNode">
 | 
				
			||||||
          <div class="track"></div>
 | 
					            <div class="node" [class.selected]="txid === cell.replacement.tx.txid" [class.mined]="cell.replacement.tx.mined" [class.first-node]="cell.first">
 | 
				
			||||||
          <a class="shape-border" [class.rbf]="replacement.tx.rbf" [routerLink]="['/tx/' | relativeUrl, replacement.tx.txid]" [title]="replacement.tx.txid">
 | 
					              <div class="track"></div>
 | 
				
			||||||
            <div class="shape"></div>
 | 
					              <a class="shape-border" [class.rbf]="cell.replacement.tx.rbf" [routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]" [title]="cell.replacement.tx.txid">
 | 
				
			||||||
          </a>
 | 
					                <div class="shape"></div>
 | 
				
			||||||
          <span class="fee-rate">{{ replacement.tx.fee / (replacement.tx.vsize) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span>
 | 
					              </a>
 | 
				
			||||||
        </div>
 | 
					              <span class="fee-rate">{{ cell.replacement.tx.fee / (cell.replacement.tx.vsize) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span>
 | 
				
			||||||
      </ng-container>
 | 
					            </div>
 | 
				
			||||||
 | 
					          </ng-container>
 | 
				
			||||||
 | 
					          <ng-template #nonNode>
 | 
				
			||||||
 | 
					            <ng-container [ngSwitch]="cell.connector">
 | 
				
			||||||
 | 
					              <div class="connector" *ngSwitchCase="'pipe'"><div class="pipe"></div></div>
 | 
				
			||||||
 | 
					              <div class="connector" *ngSwitchCase="'corner'"><div class="corner"></div></div>
 | 
				
			||||||
 | 
					              <div class="node-spacer" *ngSwitchDefault></div>
 | 
				
			||||||
 | 
					            </ng-container>
 | 
				
			||||||
 | 
					          </ng-template>
 | 
				
			||||||
 | 
					          <ng-container *ngIf="i < timeline.length - 1">
 | 
				
			||||||
 | 
					            <div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
 | 
				
			||||||
 | 
					              <div class="track"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </ng-container>
 | 
				
			||||||
 | 
					        </ng-container>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ng-template #nodeSpacer>
 | 
				
			||||||
 | 
					    <div class="node-spacer"></div>
 | 
				
			||||||
 | 
					  </ng-template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ng-template #intervalSpacer>
 | 
				
			||||||
 | 
					    <div class="interval-spacer"></div>
 | 
				
			||||||
 | 
					  </ng-template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <!-- <app-rbf-timeline-tooltip
 | 
					  <!-- <app-rbf-timeline-tooltip
 | 
				
			||||||
    *ngIf=[tooltip]
 | 
					    *ngIf=[tooltip]
 | 
				
			||||||
    [line]="hoverLine"
 | 
					    [line]="hoverLine"
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,7 @@
 | 
				
			|||||||
    background: linear-gradient(to left, #24273e, #24273e, transparent);
 | 
					    background: linear-gradient(to left, #24273e, #24273e, transparent);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .timeline {
 | 
					  .timeline-wrapper {
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
    width: calc(100% - 2em);
 | 
					    width: calc(100% - 2em);
 | 
				
			||||||
    margin: auto;
 | 
					    margin: auto;
 | 
				
			||||||
@ -44,20 +44,27 @@
 | 
				
			|||||||
    align-items: flex-start;
 | 
					    align-items: flex-start;
 | 
				
			||||||
    text-align: center;
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .node, .node-spacer {
 | 
					    .node, .node-spacer, .connector {
 | 
				
			||||||
      width: 4em;
 | 
					      width: 6em;
 | 
				
			||||||
      min-width: 4em;
 | 
					      min-width: 6em;
 | 
				
			||||||
      flex-grow: 1;
 | 
					      flex-grow: 1;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .interval, .interval-spacer {
 | 
					    .interval, .interval-spacer {
 | 
				
			||||||
      width: 8em;
 | 
					      width: 8em;
 | 
				
			||||||
      min-width: 4em;
 | 
					      min-width: 5em;
 | 
				
			||||||
      max-width: 8em;
 | 
					      max-width: 8em;
 | 
				
			||||||
 | 
					      height: 32px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: row;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      align-items: flex-end;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .interval-time {
 | 
					    .interval-time {
 | 
				
			||||||
      font-size: 12px;
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					      line-height: 16px;
 | 
				
			||||||
 | 
					      padding: 0 10px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -73,7 +80,7 @@
 | 
				
			|||||||
      background: #105fb0;
 | 
					      background: #105fb0;
 | 
				
			||||||
      border-radius: 5px;
 | 
					      border-radius: 5px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    &:first-child {
 | 
					    &.first-node {
 | 
				
			||||||
      .track {
 | 
					      .track {
 | 
				
			||||||
        left: 50%;
 | 
					        left: 50%;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -139,5 +146,24 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .connector {
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					      height: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .corner, .pipe {
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        left: -10px;
 | 
				
			||||||
 | 
					        width: 20px;
 | 
				
			||||||
 | 
					        height: 108px;
 | 
				
			||||||
 | 
					        bottom: 50%;
 | 
				
			||||||
 | 
					        border-right: solid 10px #105fb0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .corner {
 | 
				
			||||||
 | 
					        border-bottom: solid 10px #105fb0;
 | 
				
			||||||
 | 
					        border-bottom-right-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,18 +1,26 @@
 | 
				
			|||||||
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
 | 
					import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
 | 
				
			||||||
import { Router } from '@angular/router';
 | 
					import { Router } from '@angular/router';
 | 
				
			||||||
import { RbfInfo } from '../../interfaces/node-api.interface';
 | 
					import { RbfInfo, RbfTree } from '../../interfaces/node-api.interface';
 | 
				
			||||||
import { StateService } from '../../services/state.service';
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Connector = 'pipe' | 'corner';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TimelineCell {
 | 
				
			||||||
 | 
					  replacement?: RbfInfo,
 | 
				
			||||||
 | 
					  connector?: Connector,
 | 
				
			||||||
 | 
					  first?: boolean,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-rbf-timeline',
 | 
					  selector: 'app-rbf-timeline',
 | 
				
			||||||
  templateUrl: './rbf-timeline.component.html',
 | 
					  templateUrl: './rbf-timeline.component.html',
 | 
				
			||||||
  styleUrls: ['./rbf-timeline.component.scss'],
 | 
					  styleUrls: ['./rbf-timeline.component.scss'],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
					export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
				
			||||||
  @Input() replacements: RbfInfo[];
 | 
					  @Input() replacements: RbfTree;
 | 
				
			||||||
  @Input() txid: string;
 | 
					  @Input() txid: string;
 | 
				
			||||||
  mined: boolean;
 | 
					  rows: TimelineCell[][] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dir: 'rtl' | 'ltr' = 'ltr';
 | 
					  dir: 'rtl' | 'ltr' = 'ltr';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,10 +36,130 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit(): void {
 | 
					  ngOnInit(): void {
 | 
				
			||||||
    this.mined = this.replacements.some(entry => entry.mined);
 | 
					    this.rows = this.buildTimelines(this.replacements);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnChanges(): void {
 | 
					  ngOnChanges(): void {
 | 
				
			||||||
    this.mined = this.replacements.some(entry => entry.mined);
 | 
					    this.rows = this.buildTimelines(this.replacements);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // converts a tree of RBF events into a format that can be more easily rendered in HTML
 | 
				
			||||||
 | 
					  buildTimelines(tree: RbfTree): TimelineCell[][] {
 | 
				
			||||||
 | 
					    if (!tree) return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const split = this.splitTimelines(tree);
 | 
				
			||||||
 | 
					    const timelines = this.prepareTimelines(split);
 | 
				
			||||||
 | 
					    return this.connectTimelines(timelines);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // splits a tree into N leaf-to-root paths
 | 
				
			||||||
 | 
					  splitTimelines(tree: RbfTree, tail: RbfInfo[] = []): RbfInfo[][] {
 | 
				
			||||||
 | 
					    const replacements = [...tail, tree];
 | 
				
			||||||
 | 
					    if (tree.replaces.length) {
 | 
				
			||||||
 | 
					      return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements)));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return [[...replacements]];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // merges separate leaf-to-root paths into a coherent forking timeline
 | 
				
			||||||
 | 
					  // represented as a 2D array of Rbf events
 | 
				
			||||||
 | 
					  prepareTimelines(lines: RbfInfo[][]): RbfInfo[][] {
 | 
				
			||||||
 | 
					    lines.sort((a, b) => b.length - a.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rows = lines.map(() => []);
 | 
				
			||||||
 | 
					    let lineGroups = [lines];
 | 
				
			||||||
 | 
					    let done = false;
 | 
				
			||||||
 | 
					    let column = 0; // sanity check for while loop stopping condition
 | 
				
			||||||
 | 
					    while (!done && column < 100) {
 | 
				
			||||||
 | 
					      // iterate over timelines element-by-element
 | 
				
			||||||
 | 
					      // at each step, group lines which share a common transaction at their head
 | 
				
			||||||
 | 
					      // (i.e. lines terminating in the same replacement event)
 | 
				
			||||||
 | 
					      let index = 0;
 | 
				
			||||||
 | 
					      let emptyCount = 0;
 | 
				
			||||||
 | 
					      const nextGroups = [];
 | 
				
			||||||
 | 
					      for (const group of lineGroups) {
 | 
				
			||||||
 | 
					        const toMerge: { [txid: string]: RbfInfo[][] } = {};
 | 
				
			||||||
 | 
					        let emptyInGroup = 0;
 | 
				
			||||||
 | 
					        let first = true;
 | 
				
			||||||
 | 
					        for (const line of group) {
 | 
				
			||||||
 | 
					          const head = line.shift() || null;
 | 
				
			||||||
 | 
					          if (first) {
 | 
				
			||||||
 | 
					            // only insert the first instance of the replacement node
 | 
				
			||||||
 | 
					            rows[index].unshift(head);
 | 
				
			||||||
 | 
					            first = false;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            // substitute duplicates with empty cells
 | 
				
			||||||
 | 
					            // (we'll fill these in with connecting lines later)
 | 
				
			||||||
 | 
					            rows[index].unshift(null);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          // group the tails of the remaining lines for the next iteration
 | 
				
			||||||
 | 
					          if (line.length) {
 | 
				
			||||||
 | 
					            const nextId = line[0].tx.txid;
 | 
				
			||||||
 | 
					            if (!toMerge[nextId]) {
 | 
				
			||||||
 | 
					              toMerge[nextId] = [];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            toMerge[nextId].push(line);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            emptyInGroup++;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          index++;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for (const merged of Object.values(toMerge).sort((a, b) => b.length - a.length)) {
 | 
				
			||||||
 | 
					          nextGroups.push(merged);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for (let i = 0; i < emptyInGroup; i++) {
 | 
				
			||||||
 | 
					          nextGroups.push([[]]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        emptyCount += emptyInGroup;
 | 
				
			||||||
 | 
					        lineGroups = nextGroups;
 | 
				
			||||||
 | 
					        done = (emptyCount >= rows.length);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      column++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return rows;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements
 | 
				
			||||||
 | 
					    connectTimelines(timelines: RbfInfo[][]): TimelineCell[][] {
 | 
				
			||||||
 | 
					      const rows: TimelineCell[][] = [];
 | 
				
			||||||
 | 
					      timelines.forEach((lines, row) => {
 | 
				
			||||||
 | 
					        rows.push([]);
 | 
				
			||||||
 | 
					        let started = false;
 | 
				
			||||||
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        lines.forEach((replacement, column) => {
 | 
				
			||||||
 | 
					          const cell: TimelineCell = {};
 | 
				
			||||||
 | 
					          if (replacement) {
 | 
				
			||||||
 | 
					            cell.replacement = replacement;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          rows[row].push(cell);
 | 
				
			||||||
 | 
					          if (replacement) {
 | 
				
			||||||
 | 
					            if (!started) {
 | 
				
			||||||
 | 
					              cell.first = true;
 | 
				
			||||||
 | 
					              started = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else if (started && !finished) {
 | 
				
			||||||
 | 
					            if (column < timelines[row].length) {
 | 
				
			||||||
 | 
					              let matched = false;
 | 
				
			||||||
 | 
					              for (let i = row; i >= 0 && !matched; i--) {
 | 
				
			||||||
 | 
					                const nextCell = rows[i][column];
 | 
				
			||||||
 | 
					                if (nextCell.replacement) {
 | 
				
			||||||
 | 
					                  matched = true;
 | 
				
			||||||
 | 
					                } else if (i === row) {
 | 
				
			||||||
 | 
					                  rows[i][column] = {
 | 
				
			||||||
 | 
					                    connector: 'corner'
 | 
				
			||||||
 | 
					                  };
 | 
				
			||||||
 | 
					                } else if (nextCell.connector !== 'corner') {
 | 
				
			||||||
 | 
					                  rows[i][column] = {
 | 
				
			||||||
 | 
					                    connector: 'pipe'
 | 
				
			||||||
 | 
					                  };
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return rows;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -190,7 +190,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <br>
 | 
					    <br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ng-container *ngIf="rbfInfo?.length">
 | 
					    <ng-container *ngIf="rbfInfo">
 | 
				
			||||||
      <div class="title float-left">
 | 
					      <div class="title float-left">
 | 
				
			||||||
        <h2 id="rbf" i18n="transaction.replacements|Replacements">Replacements</h2>
 | 
					        <h2 id="rbf" i18n="transaction.replacements|Replacements">Replacements</h2>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,7 @@ import { WebsocketService } from '../../services/websocket.service';
 | 
				
			|||||||
import { AudioService } from '../../services/audio.service';
 | 
					import { AudioService } from '../../services/audio.service';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
import { SeoService } from '../../services/seo.service';
 | 
					import { SeoService } from '../../services/seo.service';
 | 
				
			||||||
import { BlockExtended, CpfpInfo, RbfInfo } from '../../interfaces/node-api.interface';
 | 
					import { BlockExtended, CpfpInfo, RbfTree } from '../../interfaces/node-api.interface';
 | 
				
			||||||
import { LiquidUnblinding } from './liquid-ublinding';
 | 
					import { LiquidUnblinding } from './liquid-ublinding';
 | 
				
			||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
					import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
				
			||||||
import { Price, PriceService } from '../../services/price.service';
 | 
					import { Price, PriceService } from '../../services/price.service';
 | 
				
			||||||
@ -54,7 +54,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
  rbfTransaction: undefined | Transaction;
 | 
					  rbfTransaction: undefined | Transaction;
 | 
				
			||||||
  replaced: boolean = false;
 | 
					  replaced: boolean = false;
 | 
				
			||||||
  rbfReplaces: string[];
 | 
					  rbfReplaces: string[];
 | 
				
			||||||
  rbfInfo: RbfInfo[];
 | 
					  rbfInfo: RbfTree;
 | 
				
			||||||
  cpfpInfo: CpfpInfo | null;
 | 
					  cpfpInfo: CpfpInfo | null;
 | 
				
			||||||
  showCpfpDetails = false;
 | 
					  showCpfpDetails = false;
 | 
				
			||||||
  fetchCpfp$ = new Subject<string>();
 | 
					  fetchCpfp$ = new Subject<string>();
 | 
				
			||||||
@ -188,7 +188,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
        return of(null);
 | 
					        return of(null);
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    ).subscribe((rbfResponse) => {
 | 
					    ).subscribe((rbfResponse) => {
 | 
				
			||||||
      this.rbfInfo = rbfResponse?.replacements || [];
 | 
					      this.rbfInfo = rbfResponse?.replacements;
 | 
				
			||||||
      this.rbfReplaces = rbfResponse?.replaces || null;
 | 
					      this.rbfReplaces = rbfResponse?.replaces || null;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -476,7 +476,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
    this.replaced = false;
 | 
					    this.replaced = false;
 | 
				
			||||||
    this.transactionTime = -1;
 | 
					    this.transactionTime = -1;
 | 
				
			||||||
    this.cpfpInfo = null;
 | 
					    this.cpfpInfo = null;
 | 
				
			||||||
    this.rbfInfo = [];
 | 
					    this.rbfInfo = null;
 | 
				
			||||||
    this.rbfReplaces = [];
 | 
					    this.rbfReplaces = [];
 | 
				
			||||||
    this.showCpfpDetails = false;
 | 
					    this.showCpfpDetails = false;
 | 
				
			||||||
    document.body.scrollTo(0, 0);
 | 
					    document.body.scrollTo(0, 0);
 | 
				
			||||||
 | 
				
			|||||||
@ -27,9 +27,15 @@ export interface CpfpInfo {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface RbfInfo {
 | 
					export interface RbfInfo {
 | 
				
			||||||
  tx: RbfTransaction,
 | 
					  tx: RbfTransaction;
 | 
				
			||||||
  time: number,
 | 
					  time: number;
 | 
				
			||||||
  mined?: boolean,
 | 
					  interval?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RbfTree extends RbfInfo {
 | 
				
			||||||
 | 
					  mined?: boolean;
 | 
				
			||||||
 | 
					  fullRbf: boolean;
 | 
				
			||||||
 | 
					  replaces: RbfTree[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DifficultyAdjustment {
 | 
					export interface DifficultyAdjustment {
 | 
				
			||||||
@ -154,6 +160,7 @@ export interface TransactionStripped {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface RbfTransaction extends TransactionStripped {
 | 
					interface RbfTransaction extends TransactionStripped {
 | 
				
			||||||
  rbf?: boolean;
 | 
					  rbf?: boolean;
 | 
				
			||||||
 | 
					  mined?: boolean,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface RewardStats {
 | 
					export interface RewardStats {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { ILoadingIndicators } from '../services/state.service';
 | 
					import { ILoadingIndicators } from '../services/state.service';
 | 
				
			||||||
import { Transaction } from './electrs.interface';
 | 
					import { Transaction } from './electrs.interface';
 | 
				
			||||||
import { BlockExtended, DifficultyAdjustment, RbfInfo } from './node-api.interface';
 | 
					import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface WebsocketResponse {
 | 
					export interface WebsocketResponse {
 | 
				
			||||||
  block?: BlockExtended;
 | 
					  block?: BlockExtended;
 | 
				
			||||||
@ -16,8 +16,8 @@ export interface WebsocketResponse {
 | 
				
			|||||||
  tx?: Transaction;
 | 
					  tx?: Transaction;
 | 
				
			||||||
  rbfTransaction?: ReplacedTransaction;
 | 
					  rbfTransaction?: ReplacedTransaction;
 | 
				
			||||||
  txReplaced?: ReplacedTransaction;
 | 
					  txReplaced?: ReplacedTransaction;
 | 
				
			||||||
  rbfInfo?: RbfInfo[];
 | 
					  rbfInfo?: RbfTree;
 | 
				
			||||||
  rbfLatest?: RbfInfo[][];
 | 
					  rbfLatest?: RbfTree[];
 | 
				
			||||||
  utxoSpent?: object;
 | 
					  utxoSpent?: object;
 | 
				
			||||||
  transactions?: TransactionStripped[];
 | 
					  transactions?: TransactionStripped[];
 | 
				
			||||||
  loadingIndicators?: ILoadingIndicators;
 | 
					  loadingIndicators?: ILoadingIndicators;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Injectable } from '@angular/core';
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
 | 
					import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
 | 
				
			||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
 | 
					import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
 | 
				
			||||||
  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfInfo } from '../interfaces/node-api.interface';
 | 
					  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree } from '../interfaces/node-api.interface';
 | 
				
			||||||
import { Observable } from 'rxjs';
 | 
					import { Observable } from 'rxjs';
 | 
				
			||||||
import { StateService } from './state.service';
 | 
					import { StateService } from './state.service';
 | 
				
			||||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
					import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
				
			||||||
@ -124,16 +124,16 @@ export class ApiService {
 | 
				
			|||||||
    return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
 | 
					    return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getRbfHistory$(txid: string): Observable<{ replacements: RbfInfo[], replaces: string[] }> {
 | 
					  getRbfHistory$(txid: string): Observable<{ replacements: RbfTree, replaces: string[] }> {
 | 
				
			||||||
    return this.httpClient.get<{ replacements: RbfInfo[], replaces: string[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/rbf');
 | 
					    return this.httpClient.get<{ replacements: RbfTree, replaces: string[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/rbf');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getRbfCachedTx$(txid: string): Observable<Transaction> {
 | 
					  getRbfCachedTx$(txid: string): Observable<Transaction> {
 | 
				
			||||||
    return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/cached');
 | 
					    return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/cached');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getRbfList$(fullRbf: boolean, after?: string): Observable<RbfInfo[][]> {
 | 
					  getRbfList$(fullRbf: boolean, after?: string): Observable<RbfTree[]> {
 | 
				
			||||||
    return this.httpClient.get<RbfInfo[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/' + (fullRbf ? 'fullrbf/' : '') + 'replacements/' + (after || ''));
 | 
					    return this.httpClient.get<RbfTree[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/' + (fullRbf ? 'fullrbf/' : '') + 'replacements/' + (after || ''));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
 | 
					  listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
 | 
				
			|||||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
 | 
					import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
 | 
				
			||||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
					import { Transaction } from '../interfaces/electrs.interface';
 | 
				
			||||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
					import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
				
			||||||
import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats, RbfInfo } from '../interfaces/node-api.interface';
 | 
					import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
 | 
				
			||||||
import { Router, NavigationStart } from '@angular/router';
 | 
					import { Router, NavigationStart } from '@angular/router';
 | 
				
			||||||
import { isPlatformBrowser } from '@angular/common';
 | 
					import { isPlatformBrowser } from '@angular/common';
 | 
				
			||||||
import { map, shareReplay } from 'rxjs/operators';
 | 
					import { map, shareReplay } from 'rxjs/operators';
 | 
				
			||||||
@ -98,8 +98,8 @@ export class StateService {
 | 
				
			|||||||
  mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
 | 
					  mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
 | 
				
			||||||
  mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
 | 
					  mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
 | 
				
			||||||
  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
					  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
				
			||||||
  txRbfInfo$ = new Subject<RbfInfo[]>();
 | 
					  txRbfInfo$ = new Subject<RbfTree>();
 | 
				
			||||||
  rbfLatest$ = new Subject<RbfInfo[][]>();
 | 
					  rbfLatest$ = new Subject<RbfTree[]>();
 | 
				
			||||||
  utxoSpent$ = new Subject<object>();
 | 
					  utxoSpent$ = new Subject<object>();
 | 
				
			||||||
  difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
 | 
					  difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
 | 
				
			||||||
  mempoolTransactions$ = new Subject<Transaction>();
 | 
					  mempoolTransactions$ = new Subject<Transaction>();
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
 | 
				
			|||||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
 | 
					import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
 | 
				
			||||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
 | 
					import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
 | 
				
			||||||
  faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
 | 
					  faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
 | 
				
			||||||
  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowRight, faArrowsRotate, faCircleLeft } from '@fortawesome/free-solid-svg-icons';
 | 
					  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
 | 
					import { InfiniteScrollModule } from 'ngx-infinite-scroll';
 | 
				
			||||||
import { MasterPageComponent } from '../components/master-page/master-page.component';
 | 
					import { MasterPageComponent } from '../components/master-page/master-page.component';
 | 
				
			||||||
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
 | 
					import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
 | 
				
			||||||
@ -315,7 +315,6 @@ export class SharedModule {
 | 
				
			|||||||
    library.addIcons(faDownload);
 | 
					    library.addIcons(faDownload);
 | 
				
			||||||
    library.addIcons(faQrcode);
 | 
					    library.addIcons(faQrcode);
 | 
				
			||||||
    library.addIcons(faArrowRightArrowLeft);
 | 
					    library.addIcons(faArrowRightArrowLeft);
 | 
				
			||||||
    library.addIcons(faArrowRight);
 | 
					 | 
				
			||||||
    library.addIcons(faExchangeAlt);
 | 
					    library.addIcons(faExchangeAlt);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user