transactions-bundle.js
1 /*! 2 * tracker/transactions-bundle.js 3 * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. 4 */ 5 6 7 import util from '../lib/util.js' 8 import db from '../lib/db/mysql-db-wrapper.js' 9 import addrHelper from '../lib/bitcoin/addresses-helper.js' 10 import Transaction from './transaction.js' 11 import { TransactionsCache } from './transactions-cache.js' 12 13 /** 14 * @typedef {import('bitcoinjs-lib').Transaction} bitcoin.Transaction 15 */ 16 17 /** 18 * A base class defining a set of transactions (mempool, block) 19 */ 20 class TransactionsBundle { 21 22 /** 23 * Constructor 24 * @constructor 25 * @param {bitcoin.Transaction[]=} txs - array of bitcoin transaction objects 26 */ 27 constructor(txs) { 28 /** 29 * List of transactions 30 * @type Transaction[] 31 */ 32 this.transactions = (txs == null) ? [] : txs.map((tx) => new Transaction(tx)) 33 } 34 35 /** 36 * Adds a transaction 37 * @param {bitcoin.Transaction} tx - transaction object 38 */ 39 addTransaction(tx) { 40 if (tx) { 41 this.transactions.push(new Transaction(tx)) 42 } 43 } 44 45 /** 46 * Clear the bundle 47 */ 48 clear() { 49 this.transactions = [] 50 } 51 52 /** 53 * Return the bundle as an array of transactions 54 * @returns {Transaction[]} 55 */ 56 toArray() { 57 return [...this.transactions] 58 } 59 60 /** 61 * Get the size of the bundle 62 * @returns {number} return the number of transactions stored in the bundle 63 */ 64 size() { 65 return this.transactions.length 66 } 67 68 /** 69 * Find the transactions of interest 70 * based on theirs inputs 71 * @returns {Promise<Transaction[]>} returns an array of transactions objects 72 */ 73 async prefilterByInputs() { 74 // Process transactions by slices of 5000 transactions 75 const MAX_NB_TXS = 5000 76 const lists = util.splitList(this.transactions, MAX_NB_TXS) 77 const results = await util.parallelCall(lists, txs => this._prefilterByInputs(txs)) 78 return results.flat() 79 } 80 81 /** 82 * Find the transactions of interest 83 * based on theirs outputs 84 * @returns {Promise<Transaction[]>} returns an array of transactions objects 85 */ 86 async prefilterByOutputs() { 87 // Process transactions by slices of 5000 transactions 88 const MAX_NB_TXS = 5000 89 const lists = util.splitList(this.transactions, MAX_NB_TXS) 90 const results = await util.parallelCall(lists, txs => this._prefilterByOutputs(txs)) 91 return results.flat() 92 } 93 94 /** 95 * Find the transactions of interest 96 * based on theirs outputs (internal implementation) 97 * @params {Transaction[]} txs - array of transactions objects 98 * @returns {Awaited<Promise<Transaction[]>>} returns an array of transactions objects 99 */ 100 async _prefilterByOutputs(txs) { 101 /** 102 * @type {Transaction[]} 103 */ 104 const alreadySeenTXsOfInterest = [] 105 const addresses = [] 106 const filteredIndexTxs = [] 107 const indexedOutputs = {} 108 109 // Index the transaction outputs 110 for (const index in txs) { 111 const tx = txs[index] 112 const txid = tx.txid 113 114 /** 115 * Check if transaction has been checked in the past. 116 * If it has been, check for value: 117 * - true = is transaction of interest, save and skip processing 118 * - false = skip entirely 119 */ 120 if (TransactionsCache.has(txid)) { 121 if (TransactionsCache.get(txid)) { 122 alreadySeenTXsOfInterest.push(tx) 123 } 124 continue 125 } 126 127 for (const index_ in tx.tx.outs) { 128 const script = tx.tx.outs[index_].script 129 const address = addrHelper.outputScript2Address(script) 130 131 if (address) { 132 addresses.push(address) 133 if (!indexedOutputs[address]) 134 indexedOutputs[address] = [] 135 indexedOutputs[address].push(index) 136 } 137 } 138 } 139 140 // Prefilter 141 const outRes = await db.getUngroupedHDAccountsByAddresses(addresses) 142 for (const index in outRes) { 143 const key = outRes[index].addrAddress 144 const indexTxs = indexedOutputs[key] 145 if (indexTxs) { 146 for (const indexTx of indexTxs) 147 if (!filteredIndexTxs.includes(indexTx)) 148 filteredIndexTxs.push(indexTx) 149 } 150 } 151 152 return [...alreadySeenTXsOfInterest, ...filteredIndexTxs.map(x => txs[x])] 153 } 154 155 /** 156 * Find the transactions of interest 157 * based on theirs inputs (internal implementation) 158 * @params {Transaction[]} txs - array of transactions objects 159 * @returns {Awaited<Promise<Transaction[]>>} returns an array of transactions objects 160 */ 161 async _prefilterByInputs(txs) { 162 /** 163 * @type {Transaction[]} 164 */ 165 const alreadySeenTXsOfInterest = [] 166 const inputs = [] 167 const filteredIndexTxs = [] 168 const indexedInputs = {} 169 170 for (const index in txs) { 171 const tx = txs[index] 172 const txid = tx.txid 173 174 /** 175 * Check if transaction has been checked in the past. 176 * If it has been, check for value: 177 * - true = is transaction of interest, save and skip processing 178 * - false = skip entirely 179 */ 180 if (TransactionsCache.has(txid)) { 181 if (TransactionsCache.get(txid)) { 182 alreadySeenTXsOfInterest.push(tx) 183 } 184 continue 185 } 186 187 for (const index_ in tx.tx.ins) { 188 const spendHash = tx.tx.ins[index_].hash 189 const spendTxid = Buffer.from(spendHash).reverse().toString('hex') 190 const spendIndex = tx.tx.ins[index_].index 191 inputs.push({ txid: spendTxid, index: spendIndex }) 192 const key = `${spendTxid}-${spendIndex}` 193 if (!indexedInputs[key]) 194 indexedInputs[key] = [] 195 indexedInputs[key].push(index) 196 } 197 } 198 199 // Prefilter 200 const lists = util.splitList(inputs, 1000) 201 const results = await util.parallelCall(lists, list => db.getOutputSpends(list)) 202 const inRes = results.flat() 203 for (const index in inRes) { 204 const key = `${inRes[index].txnTxid}-${inRes[index].outIndex}` 205 const indexTxs = indexedInputs[key] 206 if (indexTxs) { 207 for (const indexTx of indexTxs) 208 if (!filteredIndexTxs.includes(indexTx)) 209 filteredIndexTxs.push(indexTx) 210 } 211 } 212 213 return [...alreadySeenTXsOfInterest, ...filteredIndexTxs.map(x => txs[x])] 214 } 215 216 } 217 218 export default TransactionsBundle