| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  | import config from "../config"; | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  | import logger from "../logger"; | 
					
						
							| 
									
										
										
										
											2023-05-29 15:56:29 -04:00
										 |  |  | import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces"; | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  | import bitcoinApi from './bitcoin/bitcoin-api-factory'; | 
					
						
							| 
									
										
										
										
											2023-08-05 16:08:54 +09:00
										 |  |  | import { IEsploraApi } from "./bitcoin/esplora-api.interface"; | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  | import { Common } from "./common"; | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  | import redisCache from "./redis-cache"; | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  | export interface RbfTransaction extends TransactionStripped { | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  |   rbf?: boolean; | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |   mined?: boolean; | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |   fullRbf?: boolean; | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2022-12-09 10:32:58 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  | export interface RbfTree { | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |   tx: RbfTransaction; | 
					
						
							|  |  |  |   time: number; | 
					
						
							|  |  |  |   interval?: number; | 
					
						
							|  |  |  |   mined?: boolean; | 
					
						
							|  |  |  |   fullRbf: boolean; | 
					
						
							|  |  |  |   replaces: RbfTree[]; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  | export interface ReplacementInfo { | 
					
						
							|  |  |  |   mined: boolean; | 
					
						
							|  |  |  |   fullRbf: boolean; | 
					
						
							|  |  |  |   txid: string; | 
					
						
							|  |  |  |   oldFee: number; | 
					
						
							|  |  |  |   oldVsize: number; | 
					
						
							|  |  |  |   newFee: number; | 
					
						
							|  |  |  |   newVsize: number; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  | enum CacheOp { | 
					
						
							|  |  |  |   Remove = 0, | 
					
						
							|  |  |  |   Add = 1, | 
					
						
							|  |  |  |   Change = 2, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface CacheEvent { | 
					
						
							|  |  |  |   op: CacheOp; | 
					
						
							|  |  |  |   type: 'tx' | 'tree' | 'exp'; | 
					
						
							|  |  |  |   txid: string, | 
					
						
							|  |  |  |   value?: any, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  | class RbfCache { | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |   private replacedBy: Map<string, string> = new Map(); | 
					
						
							|  |  |  |   private replaces: Map<string, string[]> = new Map(); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |   private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
 | 
					
						
							|  |  |  |   private dirtyTrees: Set<string> = new Set(); | 
					
						
							|  |  |  |   private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
 | 
					
						
							| 
									
										
										
										
											2023-05-29 15:56:29 -04:00
										 |  |  |   private txs: Map<string, MempoolTransactionExtended> = new Map(); | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  |   private expiring: Map<string, number> = new Map(); | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |   private cacheQueue: CacheEvent[] = []; | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-16 01:00:18 +00:00
										 |  |  |   private evictionCount = 0; | 
					
						
							|  |  |  |   private staleCount = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  |   constructor() { | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  |     setInterval(this.cleanup.bind(this), 1000 * 60 * 10); | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |   private addTx(txid: string, tx: MempoolTransactionExtended): void { | 
					
						
							|  |  |  |     this.txs.set(txid, tx); | 
					
						
							|  |  |  |     this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private addTree(txid: string, tree: RbfTree): void { | 
					
						
							|  |  |  |     this.rbfTrees.set(txid, tree); | 
					
						
							|  |  |  |     this.dirtyTrees.add(txid); | 
					
						
							|  |  |  |     this.cacheQueue.push({ op: CacheOp.Add, type: 'tree', txid }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private addExpiration(txid: string, expiry: number): void { | 
					
						
							|  |  |  |     this.expiring.set(txid, expiry); | 
					
						
							|  |  |  |     this.cacheQueue.push({ op: CacheOp.Add, type: 'exp', txid, value: expiry }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private removeTx(txid: string): void { | 
					
						
							|  |  |  |     this.txs.delete(txid); | 
					
						
							|  |  |  |     this.cacheQueue.push({ op: CacheOp.Remove, type: 'tx', txid }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private removeTree(txid: string): void { | 
					
						
							|  |  |  |     this.rbfTrees.delete(txid); | 
					
						
							|  |  |  |     this.cacheQueue.push({ op: CacheOp.Remove, type: 'tree', txid }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private removeExpiration(txid: string): void { | 
					
						
							|  |  |  |     this.expiring.delete(txid); | 
					
						
							|  |  |  |     this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-29 15:56:29 -04:00
										 |  |  |   public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { | 
					
						
							| 
									
										
										
										
											2023-05-18 09:51:41 -04:00
										 |  |  |     if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-17 10:45:26 +00:00
										 |  |  |     newTxExtended.replacement = true; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  |     const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction; | 
					
						
							| 
									
										
										
										
											2023-05-11 08:57:12 -06:00
										 |  |  |     const newTime = newTxExtended.firstSeen || (Date.now() / 1000); | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  |     newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe); | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |     this.addTx(newTx.txid, newTxExtended); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // maintain rbf trees
 | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |     let txFullRbf = false; | 
					
						
							|  |  |  |     let treeFullRbf = false; | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     const replacedTrees: RbfTree[] = []; | 
					
						
							|  |  |  |     for (const replacedTxExtended of replaced) { | 
					
						
							|  |  |  |       const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction; | 
					
						
							|  |  |  |       replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe); | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |       if (!replacedTx.rbf) { | 
					
						
							|  |  |  |         txFullRbf = true; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |       this.replacedBy.set(replacedTx.txid, newTx.txid); | 
					
						
							|  |  |  |       if (this.treeMap.has(replacedTx.txid)) { | 
					
						
							|  |  |  |         const treeId = this.treeMap.get(replacedTx.txid); | 
					
						
							|  |  |  |         if (treeId) { | 
					
						
							|  |  |  |           const tree = this.rbfTrees.get(treeId); | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |           this.removeTree(treeId); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |           if (tree) { | 
					
						
							|  |  |  |             tree.interval = newTime - tree?.time; | 
					
						
							|  |  |  |             replacedTrees.push(tree); | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |             treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf; | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							| 
									
										
										
										
											2023-05-11 08:57:12 -06:00
										 |  |  |         const replacedTime = replacedTxExtended.firstSeen || (Date.now() / 1000); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |         replacedTrees.push({ | 
					
						
							|  |  |  |           tx: replacedTx, | 
					
						
							|  |  |  |           time: replacedTime, | 
					
						
							|  |  |  |           interval: newTime - replacedTime, | 
					
						
							|  |  |  |           fullRbf: !replacedTx.rbf, | 
					
						
							|  |  |  |           replaces: [], | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |         treeFullRbf = treeFullRbf || !replacedTx.rbf; | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |         this.addTx(replacedTx.txid, replacedTxExtended); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2022-12-09 10:32:58 -06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |     newTx.fullRbf = txFullRbf; | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     const treeId = replacedTrees[0].tx.txid; | 
					
						
							|  |  |  |     const newTree = { | 
					
						
							|  |  |  |       tx: newTx, | 
					
						
							| 
									
										
										
										
											2023-05-11 08:57:12 -06:00
										 |  |  |       time: newTime, | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |       fullRbf: treeFullRbf, | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |       replaces: replacedTrees | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |     this.addTree(treeId, newTree); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     this.updateTreeMap(treeId, newTree); | 
					
						
							|  |  |  |     this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid)); | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-25 14:00:17 +09:00
										 |  |  |   public has(txId: string): boolean { | 
					
						
							|  |  |  |     return this.txs.has(txId); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean { | 
					
						
							|  |  |  |     const tree = this.getRbfTree(txId); | 
					
						
							|  |  |  |     if (!tree) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const txs = this.getTransactionsInTree(tree); | 
					
						
							|  |  |  |     for (const tx of txs) { | 
					
						
							|  |  |  |       if (predicate(tx)) { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |   public getReplacedBy(txId: string): string | undefined { | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |     return this.replacedBy.get(txId); | 
					
						
							| 
									
										
										
										
											2022-12-09 10:32:58 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |   public getReplaces(txId: string): string[] | undefined { | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |     return this.replaces.get(txId); | 
					
						
							| 
									
										
										
										
											2022-12-09 10:32:58 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-29 15:56:29 -04:00
										 |  |  |   public getTx(txId: string): MempoolTransactionExtended | undefined { | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |     return this.txs.get(txId); | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |   public getRbfTree(txId: string): RbfTree | void { | 
					
						
							|  |  |  |     return this.rbfTrees.get(this.treeMap.get(txId) || ''); | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |   // get a paginated list of RbfTrees
 | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |   // ordered by most recent replacement time
 | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |   public getRbfTrees(onlyFullRbf: boolean, after?: string): RbfTree[] { | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |     const limit = 25; | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     const trees: RbfTree[] = []; | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |     const used = new Set<string>(); | 
					
						
							|  |  |  |     const replacements: string[][] = Array.from(this.replacedBy).reverse(); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     const afterTree = after ? this.treeMap.get(after) : null; | 
					
						
							|  |  |  |     let ready = !afterTree; | 
					
						
							|  |  |  |     for (let i = 0; i < replacements.length && trees.length <= limit - 1; i++) { | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |       const txid = replacements[i][1]; | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |       const treeId = this.treeMap.get(txid) || ''; | 
					
						
							|  |  |  |       if (treeId === afterTree) { | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |         ready = true; | 
					
						
							|  |  |  |       } else if (ready) { | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |         if (!used.has(treeId)) { | 
					
						
							|  |  |  |           const tree = this.rbfTrees.get(treeId); | 
					
						
							|  |  |  |           used.add(treeId); | 
					
						
							|  |  |  |           if (tree && (!onlyFullRbf || tree.fullRbf)) { | 
					
						
							|  |  |  |             trees.push(tree); | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     return trees; | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |   // get map of rbf trees that have been updated since the last call
 | 
					
						
							|  |  |  |   public getRbfChanges(): { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} { | 
					
						
							|  |  |  |     const changes: { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} = { | 
					
						
							|  |  |  |       trees: {}, | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |       map: {}, | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     this.dirtyTrees.forEach(id => { | 
					
						
							|  |  |  |       const tree = this.rbfTrees.get(id); | 
					
						
							|  |  |  |       if (tree) { | 
					
						
							|  |  |  |         changes.trees[id] = tree; | 
					
						
							|  |  |  |         this.getTransactionsInTree(tree).forEach(tx => { | 
					
						
							|  |  |  |           changes.map[tx.txid] = id; | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     this.dirtyTrees = new Set(); | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |     return changes; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |   public mined(txid): void { | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  |     if (!this.txs.has(txid)) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |     const treeId = this.treeMap.get(txid); | 
					
						
							|  |  |  |     if (treeId && this.rbfTrees.has(treeId)) { | 
					
						
							|  |  |  |       const tree = this.rbfTrees.get(treeId); | 
					
						
							|  |  |  |       if (tree) { | 
					
						
							|  |  |  |         this.setTreeMined(tree, txid); | 
					
						
							|  |  |  |         tree.mined = true; | 
					
						
							|  |  |  |         this.dirtyTrees.add(treeId); | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |         this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); | 
					
						
							| 
									
										
										
										
											2022-12-14 16:51:53 -06:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     this.evict(txid); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |   // flag a transaction as removed from the mempool
 | 
					
						
							| 
									
										
										
										
											2023-05-04 19:10:53 -04:00
										 |  |  |   public evict(txid: string, fast: boolean = false): void { | 
					
						
							| 
									
										
										
										
											2023-10-16 01:00:18 +00:00
										 |  |  |     this.evictionCount++; | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  |     if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |       const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
 | 
					
						
							|  |  |  |       this.addExpiration(txid, expiryTime); | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 18:14:09 -04:00
										 |  |  |   // is the transaction involved in a full rbf replacement?
 | 
					
						
							|  |  |  |   public isFullRbf(txid: string): boolean { | 
					
						
							|  |  |  |     const treeId = this.treeMap.get(txid); | 
					
						
							|  |  |  |     if (!treeId) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const tree = this.rbfTrees.get(treeId); | 
					
						
							|  |  |  |     if (!tree) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return tree?.fullRbf; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  |   private cleanup(): void { | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  |     const now = Date.now(); | 
					
						
							|  |  |  |     for (const txid of this.expiring.keys()) { | 
					
						
							|  |  |  |       if ((this.expiring.get(txid) || 0) < now) { | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |         this.removeExpiration(txid); | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |         this.remove(txid); | 
					
						
							| 
									
										
										
										
											2022-12-09 10:32:58 -06:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-10-16 01:00:18 +00:00
										 |  |  |     logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire (${this.evictionCount} newly expired)`); | 
					
						
							|  |  |  |     this.evictionCount = 0; | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // remove a transaction & all previous versions from the cache
 | 
					
						
							|  |  |  |   private remove(txid): void { | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  |     // don't remove a transaction if a newer version remains in the mempool
 | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |     if (!this.replacedBy.has(txid)) { | 
					
						
							| 
									
										
										
										
											2023-10-16 01:00:18 +00:00
										 |  |  |       const root = this.treeMap.get(txid); | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |       const replaces = this.replaces.get(txid); | 
					
						
							|  |  |  |       this.replaces.delete(txid); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |       this.treeMap.delete(txid); | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |       this.removeTx(txid); | 
					
						
							|  |  |  |       this.removeExpiration(txid); | 
					
						
							| 
									
										
										
										
											2023-10-16 01:00:18 +00:00
										 |  |  |       if (root === txid) { | 
					
						
							|  |  |  |         this.removeTree(txid); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |       for (const tx of (replaces || [])) { | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |         // recursively remove prior versions from the cache
 | 
					
						
							| 
									
										
										
										
											2022-12-14 08:49:35 -06:00
										 |  |  |         this.replacedBy.delete(tx); | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  |         // if this is the id of a tree, remove that too
 | 
					
						
							|  |  |  |         if (this.treeMap.get(tx) === tx) { | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |           this.removeTree(tx); | 
					
						
							| 
									
										
										
										
											2022-12-13 17:11:37 -06:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2022-12-09 14:35:51 -06:00
										 |  |  |         this.remove(tx); | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-12-17 09:39:06 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |   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); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |   public async updateCache(): Promise<void> { | 
					
						
							|  |  |  |     if (!config.REDIS.ENABLED) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     // Update the Redis cache by replaying queued events
 | 
					
						
							|  |  |  |     for (const e of this.cacheQueue) { | 
					
						
							|  |  |  |       if (e.op === CacheOp.Add || e.op === CacheOp.Change) { | 
					
						
							|  |  |  |         let value = e.value; | 
					
						
							|  |  |  |           switch(e.type) { | 
					
						
							|  |  |  |             case 'tx': { | 
					
						
							|  |  |  |               value = this.txs.get(e.txid); | 
					
						
							|  |  |  |             } break; | 
					
						
							|  |  |  |             case 'tree': { | 
					
						
							|  |  |  |               const tree = this.rbfTrees.get(e.txid); | 
					
						
							|  |  |  |               value = tree ? this.exportTree(tree) : null; | 
					
						
							|  |  |  |             } break; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           if (value != null) { | 
					
						
							|  |  |  |             await redisCache.$setRbfEntry(e.type, e.txid, value); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |       } else if (e.op === CacheOp.Remove) { | 
					
						
							|  |  |  |         await redisCache.$removeRbfEntry(e.type, e.txid); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     this.cacheQueue = []; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  |   public dump(): any { | 
					
						
							|  |  |  |     const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       txs: Array.from(this.txs.entries()), | 
					
						
							|  |  |  |       trees, | 
					
						
							|  |  |  |       expiring: Array.from(this.expiring.entries()), | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-17 10:45:26 +00:00
										 |  |  |   public async load({ txs, trees, expiring, mempool }): Promise<void> { | 
					
						
							| 
									
										
										
										
											2023-11-11 05:52:37 +00:00
										 |  |  |     try { | 
					
						
							|  |  |  |       txs.forEach(txEntry => { | 
					
						
							|  |  |  |         this.txs.set(txEntry.value.txid, txEntry.value); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       this.staleCount = 0; | 
					
						
							|  |  |  |       for (const deflatedTree of trees) { | 
					
						
							| 
									
										
										
										
											2023-12-17 10:45:26 +00:00
										 |  |  |         await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); | 
					
						
							| 
									
										
										
										
											2023-05-04 22:59:34 -04:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2023-11-11 05:52:37 +00:00
										 |  |  |       expiring.forEach(expiringEntry => { | 
					
						
							|  |  |  |         if (this.txs.has(expiringEntry.key)) { | 
					
						
							|  |  |  |           this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime()); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       this.staleCount = 0; | 
					
						
							| 
									
										
										
										
											2023-08-05 16:08:54 +09:00
										 |  |  |       await this.checkTrees(); | 
					
						
							| 
									
										
										
										
											2023-11-12 06:19:46 +00:00
										 |  |  |       logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); | 
					
						
							| 
									
										
										
										
											2023-11-11 05:52:37 +00:00
										 |  |  |       this.cleanup(); | 
					
						
							| 
									
										
										
										
											2023-11-12 06:19:46 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-11 05:52:37 +00:00
										 |  |  |     } catch (e) { | 
					
						
							|  |  |  |       logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e)); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   exportTree(tree: RbfTree, deflated: any = null) { | 
					
						
							|  |  |  |     if (!deflated) { | 
					
						
							|  |  |  |       deflated = { | 
					
						
							|  |  |  |         root: tree.tx.txid, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     deflated[tree.tx.txid] = { | 
					
						
							|  |  |  |       tx: tree.tx.txid, | 
					
						
							|  |  |  |       txMined: tree.tx.mined, | 
					
						
							|  |  |  |       time: tree.time, | 
					
						
							|  |  |  |       interval: tree.interval, | 
					
						
							|  |  |  |       mined: tree.mined, | 
					
						
							|  |  |  |       fullRbf: tree.fullRbf, | 
					
						
							|  |  |  |       replaces: tree.replaces.map(child => child.tx.txid), | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     tree.replaces.forEach(child => { | 
					
						
							|  |  |  |       this.exportTree(child, deflated); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     return deflated; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-17 10:45:26 +00:00
										 |  |  |   async importTree(mempool, root, txid, deflated, txs: Map<string, MempoolTransactionExtended>, mined: boolean = false): Promise<RbfTree | void> { | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  |     const treeInfo = deflated[txid]; | 
					
						
							|  |  |  |     const replaces: RbfTree[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-16 01:00:18 +00:00
										 |  |  |     // if the root tx is unknown, remove this tree and return early
 | 
					
						
							|  |  |  |     if (root === txid && !txs.has(txid)) { | 
					
						
							|  |  |  |       this.staleCount++; | 
					
						
							|  |  |  |       this.removeTree(deflated.key); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  |     // recursively reconstruct child trees
 | 
					
						
							|  |  |  |     for (const childId of treeInfo.replaces) { | 
					
						
							| 
									
										
										
										
											2023-12-17 10:45:26 +00:00
										 |  |  |       const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  |       if (replaced) { | 
					
						
							|  |  |  |         this.replacedBy.set(replaced.tx.txid, txid); | 
					
						
							| 
									
										
										
										
											2023-12-17 10:45:26 +00:00
										 |  |  |         if (mempool[replaced.tx.txid]) { | 
					
						
							|  |  |  |           mempool[replaced.tx.txid].replacement = true; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  |         replaces.push(replaced); | 
					
						
							|  |  |  |         if (replaced.mined) { | 
					
						
							|  |  |  |           mined = true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     this.replaces.set(txid, replaces.map(t => t.tx.txid)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const tx = txs.get(txid); | 
					
						
							|  |  |  |     if (!tx) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const strippedTx = Common.stripTransaction(tx) as RbfTransaction; | 
					
						
							|  |  |  |     strippedTx.rbf = tx.vin.some((v) => v.sequence < 0xfffffffe); | 
					
						
							|  |  |  |     strippedTx.mined = treeInfo.txMined; | 
					
						
							|  |  |  |     const tree = { | 
					
						
							|  |  |  |       tx: strippedTx, | 
					
						
							|  |  |  |       time: treeInfo.time, | 
					
						
							|  |  |  |       interval: treeInfo.interval, | 
					
						
							|  |  |  |       mined: mined, | 
					
						
							|  |  |  |       fullRbf: treeInfo.fullRbf, | 
					
						
							|  |  |  |       replaces, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     this.treeMap.set(txid, root); | 
					
						
							|  |  |  |     if (root === txid) { | 
					
						
							| 
									
										
										
										
											2023-05-12 16:31:01 -06:00
										 |  |  |       this.addTree(root, tree); | 
					
						
							| 
									
										
										
										
											2023-03-05 03:02:46 -06:00
										 |  |  |     } | 
					
						
							|  |  |  |     return tree; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-05 16:08:54 +09:00
										 |  |  |   private async checkTrees(): Promise<void> { | 
					
						
							|  |  |  |     const found: { [txid: string]: boolean } = {}; | 
					
						
							|  |  |  |     const txids = Array.from(this.txs.values()).map(tx => tx.txid).filter(txid => { | 
					
						
							|  |  |  |       return !this.expiring.has(txid) && !this.getRbfTree(txid)?.mined; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const processTxs = (txs: IEsploraApi.Transaction[]): void => { | 
					
						
							|  |  |  |       for (const tx of txs) { | 
					
						
							|  |  |  |         found[tx.txid] = true; | 
					
						
							|  |  |  |         if (tx.status?.confirmed) { | 
					
						
							|  |  |  |           const tree = this.getRbfTree(tx.txid); | 
					
						
							|  |  |  |           if (tree) { | 
					
						
							|  |  |  |             this.setTreeMined(tree, tx.txid); | 
					
						
							|  |  |  |             tree.mined = true; | 
					
						
							|  |  |  |             this.evict(tx.txid, false); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (config.MEMPOOL.BACKEND === 'esplora') { | 
					
						
							| 
									
										
										
										
											2023-11-17 07:06:44 +00:00
										 |  |  |       let processedCount = 0; | 
					
						
							| 
									
										
										
										
											2023-11-15 06:58:00 +00:00
										 |  |  |       const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40); | 
					
						
							| 
									
										
										
										
											2023-08-05 16:08:54 +09:00
										 |  |  |       for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) { | 
					
						
							|  |  |  |         const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength); | 
					
						
							| 
									
										
										
										
											2023-11-17 07:06:44 +00:00
										 |  |  |         processedCount += slice.length; | 
					
						
							| 
									
										
										
										
											2023-08-05 16:08:54 +09:00
										 |  |  |         try { | 
					
						
							|  |  |  |           const txs = await bitcoinApi.$getRawTransactions(slice); | 
					
						
							|  |  |  |           processTxs(txs); | 
					
						
							| 
									
										
										
										
											2023-11-17 07:06:44 +00:00
										 |  |  |           logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`); | 
					
						
							| 
									
										
										
										
											2023-08-05 16:08:54 +09:00
										 |  |  |         } catch (err) { | 
					
						
							| 
									
										
										
										
											2023-11-12 09:23:37 +00:00
										 |  |  |           logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`); | 
					
						
							| 
									
										
										
										
											2023-08-05 16:08:54 +09:00
										 |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       const txs: IEsploraApi.Transaction[] = []; | 
					
						
							|  |  |  |       for (const txid of txids) { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |           const tx = await bitcoinApi.$getRawTransaction(txid, true, false); | 
					
						
							|  |  |  |           txs.push(tx); | 
					
						
							|  |  |  |         } catch (err) { | 
					
						
							|  |  |  |           // some 404s are expected, so continue quietly
 | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       processTxs(txs); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for (const txid of txids) { | 
					
						
							|  |  |  |       if (!found[txid]) { | 
					
						
							|  |  |  |         this.evict(txid, false); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-14 16:08:57 +09:00
										 |  |  |   public getLatestRbfSummary(): ReplacementInfo[] { | 
					
						
							|  |  |  |     const rbfList = this.getRbfTrees(false); | 
					
						
							|  |  |  |     return rbfList.slice(0, 6).map(rbfTree => { | 
					
						
							|  |  |  |       let oldFee = 0; | 
					
						
							|  |  |  |       let oldVsize = 0; | 
					
						
							|  |  |  |       for (const replaced of rbfTree.replaces) { | 
					
						
							|  |  |  |         oldFee += replaced.tx.fee; | 
					
						
							|  |  |  |         oldVsize += replaced.tx.vsize; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         txid: rbfTree.tx.txid, | 
					
						
							|  |  |  |         mined: !!rbfTree.tx.mined, | 
					
						
							|  |  |  |         fullRbf: !!rbfTree.tx.fullRbf, | 
					
						
							|  |  |  |         oldFee, | 
					
						
							|  |  |  |         oldVsize, | 
					
						
							|  |  |  |         newFee: rbfTree.tx.fee, | 
					
						
							|  |  |  |         newVsize: rbfTree.tx.vsize, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-03-08 14:49:25 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default new RbfCache(); |