Timeline of replacements for RBF-d transactions
This commit is contained in:
		
							parent
							
								
									8db7326a5a
								
							
						
					
					
						commit
						1b843da785
					
				@ -32,7 +32,7 @@ class BitcoinRoutes {
 | 
				
			|||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
 | 
				
			||||||
      .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
 | 
					      .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
 | 
				
			||||||
@ -642,8 +642,12 @@ class BitcoinRoutes {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async getRbfHistory(req: Request, res: Response) {
 | 
					  private async getRbfHistory(req: Request, res: Response) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = rbfCache.getReplaces(req.params.txId);
 | 
					      const replacements = rbfCache.getRbfChain(req.params.txId) || [];
 | 
				
			||||||
      res.json(result || []);
 | 
					      const replaces = rbfCache.getReplaces(req.params.txId) || null;
 | 
				
			||||||
 | 
					      res.json({
 | 
				
			||||||
 | 
					        replacements,
 | 
				
			||||||
 | 
					        replaces
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
					      res.status(500).send(e instanceof Error ? e.message : e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -269,7 +269,7 @@ class Mempool {
 | 
				
			|||||||
    for (const rbfTransaction in rbfTransactions) {
 | 
					    for (const rbfTransaction in rbfTransactions) {
 | 
				
			||||||
      if (this.mempoolCache[rbfTransaction]) {
 | 
					      if (this.mempoolCache[rbfTransaction]) {
 | 
				
			||||||
        // Store replaced transactions
 | 
					        // Store replaced transactions
 | 
				
			||||||
        rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
 | 
					        rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction]);
 | 
				
			||||||
        // Erase the replaced transactions from the local mempool
 | 
					        // Erase the replaced transactions from the local mempool
 | 
				
			||||||
        delete this.mempoolCache[rbfTransaction];
 | 
					        delete this.mempoolCache[rbfTransaction];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,15 @@
 | 
				
			|||||||
import { TransactionExtended } from "../mempool.interfaces";
 | 
					import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
 | 
				
			||||||
 | 
					import { Common } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RbfTransaction extends TransactionStripped {
 | 
				
			||||||
 | 
					  rbf?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RbfCache {
 | 
					class RbfCache {
 | 
				
			||||||
  private replacedBy: { [txid: string]: string; } = {};
 | 
					  private replacedBy: { [txid: string]: string; } = {};
 | 
				
			||||||
  private replaces: { [txid: string]: string[] } = {};
 | 
					  private replaces: { [txid: string]: string[] } = {};
 | 
				
			||||||
 | 
					  private rbfChains: { [root: string]: { tx: TransactionStripped, time: number, mined?: boolean }[] } = {}; // sequences of consecutive replacements
 | 
				
			||||||
 | 
					  private chainMap: { [txid: string]: string } = {}; // map of txids to sequence ids
 | 
				
			||||||
  private txs: { [txid: string]: TransactionExtended } = {};
 | 
					  private txs: { [txid: string]: TransactionExtended } = {};
 | 
				
			||||||
  private expiring: { [txid: string]: Date } = {};
 | 
					  private expiring: { [txid: string]: Date } = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,13 +17,34 @@ class RbfCache {
 | 
				
			|||||||
    setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
 | 
					    setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public add(replacedTx: TransactionExtended, newTxId: string): void {
 | 
					  public add(replacedTxExtended: TransactionExtended, newTxExtended: TransactionExtended): void {
 | 
				
			||||||
    this.replacedBy[replacedTx.txid] = newTxId;
 | 
					    const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
 | 
				
			||||||
    this.txs[replacedTx.txid] = replacedTx;
 | 
					    replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
				
			||||||
    if (!this.replaces[newTxId]) {
 | 
					    const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
 | 
				
			||||||
      this.replaces[newTxId] = [];
 | 
					    newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.replacedBy[replacedTx.txid] = newTx.txid;
 | 
				
			||||||
 | 
					    this.txs[replacedTx.txid] = replacedTxExtended;
 | 
				
			||||||
 | 
					    if (!this.replaces[newTx.txid]) {
 | 
				
			||||||
 | 
					      this.replaces[newTx.txid] = [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.replaces[newTx.txid].push(replacedTx.txid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // maintain rbf chains
 | 
				
			||||||
 | 
					    if (this.chainMap[replacedTx.txid]) {
 | 
				
			||||||
 | 
					      // add to an existing chain
 | 
				
			||||||
 | 
					      const chainRoot = this.chainMap[replacedTx.txid];
 | 
				
			||||||
 | 
					      this.rbfChains[chainRoot].push({ tx: newTx, time: newTxExtended.firstSeen || Date.now() });
 | 
				
			||||||
 | 
					      this.chainMap[newTx.txid] = chainRoot;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // start a new chain
 | 
				
			||||||
 | 
					      this.rbfChains[replacedTx.txid] = [
 | 
				
			||||||
 | 
					        { tx: replacedTx, time: replacedTxExtended.firstSeen || Date.now() },
 | 
				
			||||||
 | 
					        { tx: newTx, time: newTxExtended.firstSeen || Date.now() },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      this.chainMap[replacedTx.txid] = replacedTx.txid;
 | 
				
			||||||
 | 
					      this.chainMap[newTx.txid] = replacedTx.txid;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.replaces[newTxId].push(replacedTx.txid);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public getReplacedBy(txId: string): string | undefined {
 | 
					  public getReplacedBy(txId: string): string | undefined {
 | 
				
			||||||
@ -31,6 +59,10 @@ class RbfCache {
 | 
				
			|||||||
    return this.txs[txId];
 | 
					    return this.txs[txId];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public getRbfChain(txId: string): { tx: TransactionStripped, time: number }[] {
 | 
				
			||||||
 | 
					    return this.rbfChains[this.chainMap[txId]] || [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // flag a transaction as removed from the mempool
 | 
					  // flag a transaction as removed from the mempool
 | 
				
			||||||
  public evict(txid): void {
 | 
					  public evict(txid): void {
 | 
				
			||||||
    this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
 | 
					    this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
 | 
				
			||||||
@ -48,14 +80,20 @@ class RbfCache {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // remove a transaction & all previous versions from the cache
 | 
					  // remove a transaction & all previous versions from the cache
 | 
				
			||||||
  private remove(txid): void {
 | 
					  private remove(txid): void {
 | 
				
			||||||
    // don't remove a transaction while a newer version remains in the mempool
 | 
					    // don't remove a transaction if a newer version remains in the mempool
 | 
				
			||||||
    if (this.replaces[txid] && !this.replacedBy[txid]) {
 | 
					    if (!this.replacedBy[txid]) {
 | 
				
			||||||
      const replaces = this.replaces[txid];
 | 
					      const replaces = this.replaces[txid];
 | 
				
			||||||
      delete this.replaces[txid];
 | 
					      delete this.replaces[txid];
 | 
				
			||||||
 | 
					      delete this.chainMap[txid];
 | 
				
			||||||
 | 
					      delete this.txs[txid];
 | 
				
			||||||
 | 
					      delete this.expiring[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
 | 
				
			||||||
        delete this.replacedBy[tx];
 | 
					        delete this.replacedBy[tx];
 | 
				
			||||||
        delete this.txs[tx];
 | 
					        // if this is the root of a chain, remove that too
 | 
				
			||||||
 | 
					        if (this.chainMap[tx] === tx) {
 | 
				
			||||||
 | 
					          delete this.rbfChains[tx];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        this.remove(tx);
 | 
					        this.remove(tx);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					<div class="rbf-timeline box">
 | 
				
			||||||
 | 
					  <div class="timeline">
 | 
				
			||||||
 | 
					    <div class="intervals">
 | 
				
			||||||
 | 
					      <ng-container *ngFor="let replacement of replacements; let i = index;">
 | 
				
			||||||
 | 
					        <div class="interval" *ngIf="i > 0">
 | 
				
			||||||
 | 
					          <div class="interval-time">
 | 
				
			||||||
 | 
					            <app-time [time]="replacement.time - replacements[i-1].time" [relative]="false"></app-time>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="node-spacer"></div>
 | 
				
			||||||
 | 
					      </ng-container>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="nodes">
 | 
				
			||||||
 | 
					      <ng-container *ngFor="let replacement of replacements; let i = index;">
 | 
				
			||||||
 | 
					        <div class="interval-spacer" *ngIf="i > 0">
 | 
				
			||||||
 | 
					          <div class="track"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="node" [class.selected]="txid === replacement.tx.txid">
 | 
				
			||||||
 | 
					          <div class="track"></div>
 | 
				
			||||||
 | 
					          <a class="shape-border" [class.rbf]="replacement.tx.rbf" [routerLink]="['/tx/' | relativeUrl, replacement.tx.txid]" [title]="replacement.tx.txid">
 | 
				
			||||||
 | 
					            <div class="shape"></div>
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					          <span class="fee-rate">{{ replacement.tx.fee / (replacement.tx.vsize) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </ng-container>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- <app-rbf-timeline-tooltip
 | 
				
			||||||
 | 
					    *ngIf=[tooltip]
 | 
				
			||||||
 | 
					    [line]="hoverLine"
 | 
				
			||||||
 | 
					    [cursorPosition]="tooltipPosition"
 | 
				
			||||||
 | 
					    [isConnector]="hoverConnector"
 | 
				
			||||||
 | 
					  ></app-rbf-timeline-tooltip> -->
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -0,0 +1,137 @@
 | 
				
			|||||||
 | 
					.rbf-timeline {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 1em 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::after, &::before {
 | 
				
			||||||
 | 
					    content: '';
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    width: 2em;
 | 
				
			||||||
 | 
					    z-index: 2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::before {
 | 
				
			||||||
 | 
					    left: 0;
 | 
				
			||||||
 | 
					    background: linear-gradient(to right, #24273e, #24273e, transparent);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::after {
 | 
				
			||||||
 | 
					    right: 0;
 | 
				
			||||||
 | 
					    background: linear-gradient(to left, #24273e, #24273e, transparent);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .timeline {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    width: calc(100% - 2em);
 | 
				
			||||||
 | 
					    margin: auto;
 | 
				
			||||||
 | 
					    overflow-x: auto;
 | 
				
			||||||
 | 
					    -ms-overflow-style: none;
 | 
				
			||||||
 | 
					    scrollbar-width: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::-webkit-scrollbar {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .intervals, .nodes {
 | 
				
			||||||
 | 
					    min-width: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    align-items: flex-start;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .node, .node-spacer {
 | 
				
			||||||
 | 
					      width: 4em;
 | 
				
			||||||
 | 
					      min-width: 4em;
 | 
				
			||||||
 | 
					      flex-grow: 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .interval, .interval-spacer {
 | 
				
			||||||
 | 
					      width: 8em;
 | 
				
			||||||
 | 
					      min-width: 4em;
 | 
				
			||||||
 | 
					      max-width: 8em;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .interval-time {
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .node, .interval-spacer {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    .track {
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      height: 10px;
 | 
				
			||||||
 | 
					      left: -5px;
 | 
				
			||||||
 | 
					      right: -5px;
 | 
				
			||||||
 | 
					      top: 0;
 | 
				
			||||||
 | 
					      transform: translateY(-50%);
 | 
				
			||||||
 | 
					      background: #105fb0;
 | 
				
			||||||
 | 
					      border-radius: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    &:first-child {
 | 
				
			||||||
 | 
					      .track {
 | 
				
			||||||
 | 
					        left: 50%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    &:last-child {
 | 
				
			||||||
 | 
					      .track {
 | 
				
			||||||
 | 
					        right: 50%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .nodes {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    margin-top: 1em;
 | 
				
			||||||
 | 
					    .node {
 | 
				
			||||||
 | 
					      .shape-border {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					        margin: auto;
 | 
				
			||||||
 | 
					        height: calc(1em + 8px);
 | 
				
			||||||
 | 
					        width: calc(1em + 8px);
 | 
				
			||||||
 | 
					        margin-bottom: -8px;
 | 
				
			||||||
 | 
					        transform: translateY(-50%);
 | 
				
			||||||
 | 
					        border-radius: 10%;
 | 
				
			||||||
 | 
					        cursor: pointer;
 | 
				
			||||||
 | 
					        padding: 4px;
 | 
				
			||||||
 | 
					        background: transparent;
 | 
				
			||||||
 | 
					        transition: background-color 300ms, padding 300ms;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .shape {
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          height: 100%;
 | 
				
			||||||
 | 
					          border-radius: 10%;
 | 
				
			||||||
 | 
					          background: white;
 | 
				
			||||||
 | 
					          transition: background-color 300ms;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.rbf, &.rbf .shape {
 | 
				
			||||||
 | 
					          border-radius: 50%;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .symbol::ng-deep {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					        margin-top: -0.5em;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.selected {
 | 
				
			||||||
 | 
					        .shape-border {
 | 
				
			||||||
 | 
					          background: #9339f4;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .shape-border:hover {
 | 
				
			||||||
 | 
					        padding: 0px;
 | 
				
			||||||
 | 
					        .shape {
 | 
				
			||||||
 | 
					          background: #1bd8f4;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
 | 
				
			||||||
 | 
					import { Router } from '@angular/router';
 | 
				
			||||||
 | 
					import { RbfInfo } from '../../interfaces/node-api.interface';
 | 
				
			||||||
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-rbf-timeline',
 | 
				
			||||||
 | 
					  templateUrl: './rbf-timeline.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./rbf-timeline.component.scss'],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
				
			||||||
 | 
					  @Input() replacements: RbfInfo[];
 | 
				
			||||||
 | 
					  @Input() txid: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dir: 'rtl' | 'ltr' = 'ltr';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private router: Router,
 | 
				
			||||||
 | 
					    private stateService: StateService,
 | 
				
			||||||
 | 
					    private apiService: ApiService,
 | 
				
			||||||
 | 
					    @Inject(LOCALE_ID) private locale: string,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
 | 
				
			||||||
 | 
					      this.dir = 'rtl';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnInit(): void {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnChanges(): void {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -197,6 +197,15 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <br>
 | 
					    <br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ng-container *ngIf="rbfInfo?.length">
 | 
				
			||||||
 | 
					      <div class="title float-left">
 | 
				
			||||||
 | 
					        <h2 id="rbf" i18n="transaction.replacements|Replacements">Replacements</h2>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="clearfix"></div>
 | 
				
			||||||
 | 
					      <app-rbf-timeline [txid]="txId" [replacements]="rbfInfo"></app-rbf-timeline>
 | 
				
			||||||
 | 
					      <br>
 | 
				
			||||||
 | 
					    </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ng-container *ngIf="flowEnabled; else flowPlaceholder">
 | 
					    <ng-container *ngIf="flowEnabled; else flowPlaceholder">
 | 
				
			||||||
      <div class="title float-left">
 | 
					      <div class="title float-left">
 | 
				
			||||||
        <h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
 | 
					        <h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
 | 
				
			||||||
 | 
				
			|||||||
@ -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 } from '../../interfaces/node-api.interface';
 | 
					import { BlockExtended, CpfpInfo, RbfInfo } 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';
 | 
				
			||||||
@ -53,6 +53,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[];
 | 
				
			||||||
  cpfpInfo: CpfpInfo | null;
 | 
					  cpfpInfo: CpfpInfo | null;
 | 
				
			||||||
  showCpfpDetails = false;
 | 
					  showCpfpDetails = false;
 | 
				
			||||||
  fetchCpfp$ = new Subject<string>();
 | 
					  fetchCpfp$ = new Subject<string>();
 | 
				
			||||||
@ -183,10 +184,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
          .getRbfHistory$(txId)
 | 
					          .getRbfHistory$(txId)
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      catchError(() => {
 | 
					      catchError(() => {
 | 
				
			||||||
        return of([]);
 | 
					        return of(null);
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    ).subscribe((replaces) => {
 | 
					    ).subscribe((rbfResponse) => {
 | 
				
			||||||
      this.rbfReplaces = replaces;
 | 
					      this.rbfInfo = rbfResponse?.replacements || [];
 | 
				
			||||||
 | 
					      this.rbfReplaces = rbfResponse?.replaces || null;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.fetchCachedTxSubscription = this.fetchCachedTx$
 | 
					    this.fetchCachedTxSubscription = this.fetchCachedTx$
 | 
				
			||||||
@ -460,6 +462,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.rbfReplaces = [];
 | 
					    this.rbfReplaces = [];
 | 
				
			||||||
    this.showCpfpDetails = false;
 | 
					    this.showCpfpDetails = false;
 | 
				
			||||||
    document.body.scrollTo(0, 0);
 | 
					    document.body.scrollTo(0, 0);
 | 
				
			||||||
 | 
				
			|||||||
@ -26,6 +26,11 @@ export interface CpfpInfo {
 | 
				
			|||||||
  bestDescendant?: BestDescendant | null;
 | 
					  bestDescendant?: BestDescendant | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RbfInfo {
 | 
				
			||||||
 | 
					  tx: RbfTransaction,
 | 
				
			||||||
 | 
					  time: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DifficultyAdjustment {
 | 
					export interface DifficultyAdjustment {
 | 
				
			||||||
  progressPercent: number;
 | 
					  progressPercent: number;
 | 
				
			||||||
  difficultyChange: number;
 | 
					  difficultyChange: number;
 | 
				
			||||||
@ -146,6 +151,10 @@ export interface TransactionStripped {
 | 
				
			|||||||
  status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
 | 
					  status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RbfTransaction extends TransactionStripped {
 | 
				
			||||||
 | 
					  rbf?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface RewardStats {
 | 
					export interface RewardStats {
 | 
				
			||||||
  startBlock: number;
 | 
					  startBlock: number;
 | 
				
			||||||
  endBlock: number;
 | 
					  endBlock: number;
 | 
				
			||||||
 | 
				
			|||||||
@ -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 } from '../interfaces/node-api.interface';
 | 
					  PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfInfo } 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,8 +124,8 @@ 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<string[]> {
 | 
					  getRbfHistory$(txid: string): Observable<{ replacements: RbfInfo[], replaces: string[] }> {
 | 
				
			||||||
    return this.httpClient.get<string[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/replaces');
 | 
					    return this.httpClient.get<{ replacements: RbfInfo[], replaces: string[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/rbf');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getRbfCachedTx$(txid: string): Observable<Transaction> {
 | 
					  getRbfCachedTx$(txid: string): Observable<Transaction> {
 | 
				
			||||||
 | 
				
			|||||||
@ -61,6 +61,7 @@ import { DifficultyComponent } from '../components/difficulty/difficulty.compone
 | 
				
			|||||||
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
 | 
					import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
 | 
				
			||||||
import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
 | 
					import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
 | 
				
			||||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
 | 
					import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
 | 
				
			||||||
 | 
					import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
 | 
				
			||||||
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
 | 
					import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
 | 
				
			||||||
import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component';
 | 
					import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component';
 | 
				
			||||||
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
 | 
					import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
 | 
				
			||||||
@ -138,6 +139,7 @@ import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.
 | 
				
			|||||||
    DifficultyComponent,
 | 
					    DifficultyComponent,
 | 
				
			||||||
    DifficultyMiningComponent,
 | 
					    DifficultyMiningComponent,
 | 
				
			||||||
    DifficultyTooltipComponent,
 | 
					    DifficultyTooltipComponent,
 | 
				
			||||||
 | 
					    RbfTimelineComponent,
 | 
				
			||||||
    TxBowtieGraphComponent,
 | 
					    TxBowtieGraphComponent,
 | 
				
			||||||
    TxBowtieGraphTooltipComponent,
 | 
					    TxBowtieGraphTooltipComponent,
 | 
				
			||||||
    TermsOfServiceComponent,
 | 
					    TermsOfServiceComponent,
 | 
				
			||||||
@ -242,6 +244,7 @@ import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.
 | 
				
			|||||||
    DifficultyComponent,
 | 
					    DifficultyComponent,
 | 
				
			||||||
    DifficultyMiningComponent,
 | 
					    DifficultyMiningComponent,
 | 
				
			||||||
    DifficultyTooltipComponent,
 | 
					    DifficultyTooltipComponent,
 | 
				
			||||||
 | 
					    RbfTimelineComponent,
 | 
				
			||||||
    TxBowtieGraphComponent,
 | 
					    TxBowtieGraphComponent,
 | 
				
			||||||
    TxBowtieGraphTooltipComponent,
 | 
					    TxBowtieGraphTooltipComponent,
 | 
				
			||||||
    TermsOfServiceComponent,
 | 
					    TermsOfServiceComponent,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user