/ tracker / transactions-bundle.js
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