/ lib / bitcoind-rpc / transactions.js
transactions.js
  1  /*!
  2   * lib/bitcoind-rpc/transactions.js
  3   * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
  4   */
  5  
  6  
  7  import QuickLRU from 'quick-lru'
  8  
  9  import errors from '../errors.js'
 10  import Logger from '../logger.js'
 11  import util from '../util.js'
 12  import { createRpcClient } from './rpc-client.js'
 13  import rpcLatestBlock from './latest-block.js'
 14  
 15  
 16  /**
 17   * A singleton providing information about transactions
 18   */
 19  class Transactions {
 20  
 21      constructor() {
 22          // Caches
 23          this.prevCache = new QuickLRU({
 24              // Maximum number of transactions to store
 25              maxSize: 20000,
 26              // Maximum age for items in the cache.
 27              maxAge: 1000 * 60 * 60 * 24 * 2 // two days
 28          })
 29  
 30  
 31          // Initialize the rpc client
 32          this.rpcClient = createRpcClient()
 33      }
 34  
 35      /**
 36       * Get the transactions for a given array of txids
 37       * @param {string[]} txids - txids of the transaction to be retrieved
 38       * @param {boolean} fees - true if fees must be computed, false otherwise
 39       * @returns {Promise<object[]>} return an array of transactions (object[])
 40       */
 41      async getTransactions(txids, fees) {
 42          try {
 43              const rpcCalls = txids.map((txid, index) => {
 44                  return {
 45                      method: 'getrawtransaction',
 46                      params: {
 47                          txid,
 48                          verbose: true
 49                      },
 50                      id: index
 51                  }
 52              })
 53  
 54              const txs = await this.rpcClient.batch(rpcCalls)
 55  
 56              return await util.parallelCall(txs, async tx => {
 57                  if (tx.result == null) {
 58                      Logger.info(`Bitcoind RPC :  got null for ${txids[tx.id]}`)
 59                      return null
 60                  } else {
 61                      return this._prepareTxResult(tx.result, fees)
 62                  }
 63              })
 64  
 65          } catch (error) {
 66              Logger.error(error, 'Bitcoind RPC : Transaction.getTransactions()')
 67              throw errors.generic.GEN
 68          }
 69      }
 70  
 71      /**
 72       * Get the transaction for a given txid
 73       * @param {string} txid - txid of the transaction to be retrieved
 74       * @param {boolean} fees - true if fees must be computed, false otherwise
 75       * @returns {Promise<object[]>}
 76       */
 77      async getTransaction(txid, fees) {
 78          try {
 79              const tx = await this.rpcClient.getrawtransaction({ txid, verbose: true })
 80              return this._prepareTxResult(tx, fees)
 81          } catch (error) {
 82              Logger.error(error, 'Bitcoind RPC : Transaction.getTransaction()')
 83              throw errors.generic.GEN
 84          }
 85      }
 86  
 87      /**
 88       * Get the raw transaction hex for a given txid
 89       * @param {string} txid - txid of the transaction to be retrieved
 90       * @returns {Promise<string>}
 91       */
 92      async getTransactionHex(txid) {
 93          try {
 94              const txHex = await this.rpcClient.getrawtransaction({ txid, verbose: false })
 95              return txHex
 96          } catch (error) {
 97              Logger.error(error, 'Bitcoind RPC : Transaction.getTransactionHex()')
 98              throw errors.generic.GEN
 99          }
100      }
101  
102      /**
103       * Formats a transaction object returned by the RPC API
104       * @param {object} tx - transaction
105       * @param {boolean} fees - true if fees must be computed, false otherwise
106       * @returns {Promise<object[]>} return an array of inputs (object[])
107       */
108      async _prepareTxResult(tx, fees) {
109          const returnValue = {
110              txid: tx.txid,
111              size: tx.size,
112              vsize: tx.vsize,
113              version: tx.version,
114              locktime: tx.locktime,
115              inputs: [],
116              outputs: []
117          }
118  
119          if (!returnValue.vsize)
120              delete returnValue.vsize
121  
122          if (tx.time)
123              returnValue.created = tx.time
124  
125          // Process block informations
126          if (tx.blockhash && tx.confirmations && tx.blocktime) {
127              returnValue.block = {
128                  height: rpcLatestBlock.height - tx.confirmations + 1,
129                  hash: tx.blockhash,
130                  time: tx.blocktime
131              }
132          }
133  
134          let inAmount = 0
135          let outAmount = 0
136  
137          // Process the inputs
138          returnValue.inputs = await this._getInputs(tx, fees)
139          inAmount = returnValue.inputs.reduce((previous, current) => previous + current.outpoint.value, 0)
140  
141          // Process the outputs
142          returnValue.outputs = this._getOutputs(tx)
143          outAmount = returnValue.outputs.reduce((previous, current) => previous + current.value, 0)
144  
145          // Process the fees (if needed)
146          if (fees) {
147              returnValue.fees = inAmount - outAmount
148              if (returnValue.fees > 0 && returnValue.size > 0)
149                  returnValue.feerate = Math.round(returnValue.fees / returnValue.size)
150              if (returnValue.fees > 0 && returnValue.vsize)
151                  returnValue.vfeerate = Math.round(returnValue.fees / returnValue.vsize)
152          }
153  
154          return returnValue
155      }
156  
157  
158      /**
159       * Extract information about the inputs of a transaction
160       * @param {object} tx - transaction
161       * @param {boolean} fees - true if fees must be computed, false otherwise
162       * @returns {Promise<object[]>} return an array of inputs (object[])
163       */
164      async _getInputs(tx, fees) {
165          const inputs = []
166          let n = 0
167  
168          await util.seriesCall(tx.vin, async input => {
169              const txin = {
170                  n,
171                  seq: input.sequence,
172              }
173  
174              if (input.coinbase) {
175                  txin.coinbase = input.coinbase
176              } else {
177                  txin.outpoint = {
178                      txid: input.txid,
179                      vout: input.vout
180                  }
181                  txin.sig = input.scriptSig.hex
182              }
183  
184              if (input.txinwitness)
185                  txin.witness = input.txinwitness
186  
187              if (fees && txin.outpoint) {
188                  const inTxid = txin.outpoint.txid
189                  let ptx
190  
191                  if (this.prevCache.has(inTxid)) {
192                      ptx = this.prevCache.get(inTxid)
193                  } else {
194                      ptx = await this.rpcClient.getrawtransaction({ txid: inTxid, verbose: true })
195                      this.prevCache.set(inTxid, ptx)
196                  }
197  
198                  const outpoint = ptx.vout[txin.outpoint.vout]
199                  txin.outpoint.value = Math.round(outpoint.value * 1e8)
200                  txin.outpoint.scriptpubkey = outpoint.scriptPubKey.hex
201                  inputs.push(txin)
202                  n++
203  
204              } else {
205                  inputs.push(txin)
206                  n++
207              }
208          })
209  
210          return inputs
211      }
212  
213      /**
214       * Extract information about the outputs of a transaction
215       * @param {object} tx - transaction
216       * @returns {object[]} return an array of outputs (object[])
217       */
218      _getOutputs(tx) {
219          const outputs = []
220          let n = 0
221  
222          for (let output of tx.vout) {
223              const pk = output.scriptPubKey
224              const amount = Math.round(output.value * 1e8)
225  
226              let o = {
227                  n,
228                  value: amount,
229                  scriptpubkey: pk.hex,
230                  type: pk.type
231              }
232  
233              if (pk.address) {
234                  o.address = pk.address
235              }
236  
237              outputs.push(o)
238              n++
239          }
240  
241          return outputs
242      }
243  
244  }
245  
246  export default new Transactions()