parent
							
								
									0bc078e662
								
							
						
					
					
						commit
						c7639b4768
					
				@ -8,6 +8,7 @@ import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
import bitcoinClient from './bitcoin/bitcoin-client';
 | 
			
		||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
 | 
			
		||||
import rbfCache from './rbf-cache';
 | 
			
		||||
 | 
			
		||||
class Mempool {
 | 
			
		||||
  private static WEBSOCKET_REFRESH_RATE_MS = 10000;
 | 
			
		||||
@ -200,6 +201,17 @@ class Mempool {
 | 
			
		||||
    logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
 | 
			
		||||
    for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
      if (this.mempoolCache[rbfTransaction]) {
 | 
			
		||||
        // Store replaced transactions
 | 
			
		||||
        rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
 | 
			
		||||
        // Erase the replaced transactions from the local mempool
 | 
			
		||||
        delete this.mempoolCache[rbfTransaction];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateTxPerSecond() {
 | 
			
		||||
    const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
 | 
			
		||||
    this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								backend/src/api/rbf-cache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								backend/src/api/rbf-cache.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
export interface CachedRbf {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  expires: Date;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class RbfCache {
 | 
			
		||||
  private cache: { [txid: string]: CachedRbf; } = {};
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public add(replacedTxId: string, newTxId: string): void {
 | 
			
		||||
    this.cache[replacedTxId] = {
 | 
			
		||||
      expires: new Date(Date.now() + 1000 * 604800), // 1 week
 | 
			
		||||
      txid: newTxId,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get(txId: string): CachedRbf | undefined {
 | 
			
		||||
    return this.cache[txId];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private cleanup(): void {
 | 
			
		||||
    const currentDate = new Date();
 | 
			
		||||
    for (const c in this.cache) {
 | 
			
		||||
      if (this.cache[c].expires < currentDate) {
 | 
			
		||||
        delete this.cache[c];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new RbfCache();
 | 
			
		||||
@ -11,6 +11,7 @@ import { Common } from './common';
 | 
			
		||||
import loadingIndicators from './loading-indicators';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
import rbfCache from './rbf-cache';
 | 
			
		||||
 | 
			
		||||
class WebsocketHandler {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -48,29 +49,38 @@ class WebsocketHandler {
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-tx']) {
 | 
			
		||||
            if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
 | 
			
		||||
              client['track-tx'] = parsedMessage['track-tx'];
 | 
			
		||||
              // Client is telling the transaction wasn't found but it might have appeared before we had the time to start watching for it
 | 
			
		||||
              // Client is telling the transaction wasn't found
 | 
			
		||||
              if (parsedMessage['watch-mempool']) {
 | 
			
		||||
                const tx = memPool.getMempool()[client['track-tx']];
 | 
			
		||||
                if (tx) {
 | 
			
		||||
                  if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
                    response['tx'] = tx;
 | 
			
		||||
                const rbfCacheTx = rbfCache.get(client['track-tx']);
 | 
			
		||||
                if (rbfCacheTx) {
 | 
			
		||||
                  response['txReplaced'] = {
 | 
			
		||||
                    txid: rbfCacheTx.txid,
 | 
			
		||||
                  };
 | 
			
		||||
                  client['track-tx'] = null;
 | 
			
		||||
                } else {
 | 
			
		||||
                  // It might have appeared before we had the time to start watching for it
 | 
			
		||||
                  const tx = memPool.getMempool()[client['track-tx']];
 | 
			
		||||
                  if (tx) {
 | 
			
		||||
                    if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
                      response['tx'] = tx;
 | 
			
		||||
                    } else {
 | 
			
		||||
                      // tx.prevout is missing from transactions when in bitcoind mode
 | 
			
		||||
                      try {
 | 
			
		||||
                        const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                        response['tx'] = fullTx;
 | 
			
		||||
                      } catch (e) {
 | 
			
		||||
                        logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  } else {
 | 
			
		||||
                    // tx.prevouts is missing from transactions when in bitcoind mode
 | 
			
		||||
                    try {
 | 
			
		||||
                      const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                      const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                      response['tx'] = fullTx;
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                      logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      client['track-mempool-tx'] = parsedMessage['track-tx'];
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                } else {
 | 
			
		||||
                  try {
 | 
			
		||||
                    const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                    response['tx'] = fullTx;
 | 
			
		||||
                  } catch (e) {
 | 
			
		||||
                    logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                    client['track-mempool-tx'] = parsedMessage['track-tx'];
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
@ -221,14 +231,10 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
    mempoolBlocks.updateMempoolBlocks(newMempool);
 | 
			
		||||
    const mBlocks = mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
    const mempool = memPool.getMempool();
 | 
			
		||||
    const mempoolInfo = memPool.getMempoolInfo();
 | 
			
		||||
    const vBytesPerSecond = memPool.getVBytesPerSecond();
 | 
			
		||||
    const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
 | 
			
		||||
 | 
			
		||||
    for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
      delete mempool[rbfTransaction];
 | 
			
		||||
    }
 | 
			
		||||
    memPool.handleRbfTransactions(rbfTransactions);
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach(async (client: WebSocket) => {
 | 
			
		||||
      if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,13 @@
 | 
			
		||||
  <div class="title-block">
 | 
			
		||||
    <div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
 | 
			
		||||
      <span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
 | 
			
		||||
      <a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction }">
 | 
			
		||||
      <a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction.size ? rbfTransaction : null }">
 | 
			
		||||
        <span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
 | 
			
		||||
        <span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <ng-container>
 | 
			
		||||
    <ng-container *ngIf="!rbfTransaction || rbfTransaction?.size">
 | 
			
		||||
      <h1 i18n="shared.transaction">Transaction</h1>
 | 
			
		||||
 | 
			
		||||
      <span class="tx-link float-left">
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
  transactionTime = -1;
 | 
			
		||||
  subscription: Subscription;
 | 
			
		||||
  fetchCpfpSubscription: Subscription;
 | 
			
		||||
  txReplacedSubscription: Subscription;
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  rbfTransaction: undefined | Transaction;
 | 
			
		||||
  cpfpInfo: CpfpInfo | null;
 | 
			
		||||
  showCpfpDetails = false;
 | 
			
		||||
@ -217,7 +219,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
 | 
			
		||||
      this.latestBlock = block;
 | 
			
		||||
 | 
			
		||||
      if (txConfirmed && this.tx) {
 | 
			
		||||
@ -232,9 +234,13 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.stateService.txReplaced$.subscribe(
 | 
			
		||||
      (rbfTransaction) => (this.rbfTransaction = rbfTransaction)
 | 
			
		||||
    );
 | 
			
		||||
    this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
 | 
			
		||||
      if (!rbfTransaction.size) {
 | 
			
		||||
        this.error = new Error();
 | 
			
		||||
        this.waitingForTransaction = false;
 | 
			
		||||
      }
 | 
			
		||||
      this.rbfTransaction = rbfTransaction;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadElectrsTransactionError(error: any): Observable<any> {
 | 
			
		||||
@ -302,6 +308,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.subscription.unsubscribe();
 | 
			
		||||
    this.fetchCpfpSubscription.unsubscribe();
 | 
			
		||||
    this.txReplacedSubscription.unsubscribe();
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.leaveTransaction();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,8 @@ export interface WebsocketResponse {
 | 
			
		||||
  action?: string;
 | 
			
		||||
  data?: string[];
 | 
			
		||||
  tx?: Transaction;
 | 
			
		||||
  rbfTransaction?: Transaction;
 | 
			
		||||
  rbfTransaction?: ReplacedTransaction;
 | 
			
		||||
  txReplaced?: ReplacedTransaction;
 | 
			
		||||
  utxoSpent?: object;
 | 
			
		||||
  transactions?: TransactionStripped[];
 | 
			
		||||
  loadingIndicators?: ILoadingIndicators;
 | 
			
		||||
@ -27,6 +28,9 @@ export interface WebsocketResponse {
 | 
			
		||||
  'track-bisq-market'?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReplacedTransaction extends Transaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
}
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
  blink?: boolean;
 | 
			
		||||
  height?: number;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
 | 
			
		||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
 | 
			
		||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
 | 
			
		||||
import { Router, NavigationStart } from '@angular/router';
 | 
			
		||||
import { isPlatformBrowser } from '@angular/common';
 | 
			
		||||
@ -80,7 +80,7 @@ export class StateService {
 | 
			
		||||
  bsqPrice$ = new ReplaySubject<number>(1);
 | 
			
		||||
  mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
 | 
			
		||||
  mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
 | 
			
		||||
  txReplaced$ = new Subject<Transaction>();
 | 
			
		||||
  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
			
		||||
  utxoSpent$ = new Subject<object>();
 | 
			
		||||
  mempoolTransactions$ = new Subject<Transaction>();
 | 
			
		||||
  blockTransactions$ = new Subject<Transaction>();
 | 
			
		||||
 | 
			
		||||
@ -239,6 +239,10 @@ export class WebsocketService {
 | 
			
		||||
      this.stateService.txReplaced$.next(response.rbfTransaction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.txReplaced) {
 | 
			
		||||
      this.stateService.txReplaced$.next(response.txReplaced);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response['mempool-blocks']) {
 | 
			
		||||
      this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user