diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 64505ba2b..43aea6059 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -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); diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts new file mode 100644 index 000000000..3162ad263 --- /dev/null +++ b/backend/src/api/rbf-cache.ts @@ -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(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 92abaab6d..fe43a725f 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -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) { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 381c59d29..a0c92cbb4 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -3,13 +3,13 @@