diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index fc952d6a8..ba06c53b3 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -77,6 +77,24 @@ export class Common { return matches; } + static findMinedRbfTransactions(minedTransactions: TransactionExtended[], spendMap: Map): { [txid: string]: { replaced: TransactionExtended[], replacedBy: TransactionExtended }} { + const matches: { [txid: string]: { replaced: TransactionExtended[], replacedBy: TransactionExtended }} = {}; + for (const tx of minedTransactions) { + const replaced: Set = new Set(); + for (let i = 0; i < tx.vin.length; i++) { + const vin = tx.vin[i]; + const match = spendMap.get(`${vin.txid}:${vin.vout}`); + if (match && match.txid !== tx.txid) { + replaced.add(match); + } + } + if (replaced.size) { + matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx }; + } + } + return matches; + } + static stripTransaction(tx: TransactionExtended): TransactionStripped { return { txid: tx.txid, diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 5746ca6d4..fe84fb8e4 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -14,6 +14,7 @@ class Mempool { private inSync: boolean = false; private mempoolCacheDelta: number = -1; private mempoolCache: { [txId: string]: TransactionExtended } = {}; + private spendMap = new Map(); private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], @@ -77,6 +78,10 @@ class Mempool { return this.mempoolCache; } + public getSpendMap(): Map { + return this.spendMap; + } + public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) { this.mempoolCache = mempoolData; if (this.mempoolChangedCallback) { @@ -85,6 +90,7 @@ class Mempool { if (this.$asyncMempoolChangedCallback) { await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []); } + this.addToSpendMap(Object.values(this.mempoolCache)); } public async $updateMemPoolInfo() { @@ -276,6 +282,34 @@ class Mempool { } } + public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: TransactionExtended[], replacedBy: TransactionExtended }}): void { + for (const rbfTransaction in rbfTransactions) { + if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { + // Store replaced transactions + rbfCache.add(rbfTransactions[rbfTransaction].replaced, rbfTransactions[rbfTransaction].replacedBy); + } + } + } + + public addToSpendMap(transactions: TransactionExtended[]): void { + for (const tx of transactions) { + for (const vin of tx.vin) { + this.spendMap.set(`${vin.txid}:${vin.vout}`, tx); + } + } + } + + public removeFromSpendMap(transactions: TransactionExtended[]): void { + for (const tx of transactions) { + for (const vin of tx.vin) { + const key = `${vin.txid}:${vin.vout}`; + if (this.spendMap.get(key)?.txid === tx.txid) { + this.spendMap.delete(key); + } + } + } + } + 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 index d8fb8656c..51f8ffeca 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -31,7 +31,7 @@ class RbfCache { } public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void { - if (!newTxExtended || !replaced?.length) { + if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { return; } diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 8b6604522..ca1bb01ff 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -332,6 +332,8 @@ class WebsocketHandler { for (const deletedTx of deletedTransactions) { rbfCache.evict(deletedTx.txid); } + memPool.removeFromSpendMap(deletedTransactions); + memPool.addToSpendMap(newTransactions); const recommendedFees = feeApi.getRecommendedFee(); // update init data @@ -600,6 +602,10 @@ class WebsocketHandler { } } + const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); + memPool.handleMinedRbfTransactions(rbfTransactions); + memPool.removeFromSpendMap(transactions); + // Update mempool to remove transactions included in the new block for (const txId of txIds) { delete _memPool[txId];