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()