wallet-info.js
1 /*! 2 * lib/wallet/wallet-info.js 3 * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. 4 */ 5 6 7 import db from '../db/mysql-db-wrapper.js' 8 import util from '../util.js' 9 import rpcLatestBlock from '../bitcoind-rpc/latest-block.js' 10 import rpcFees from '../bitcoind-rpc/fees.js' 11 import addrService from '../bitcoin/addresses-service.js' 12 import HdAccountInfo from './hd-account-info.js' 13 import AddressInfo from './address-info.js' 14 15 16 /** 17 * A class storing information about a (full|partial) wallet 18 * Provides a set of methods allowing to retrieve specific information 19 * @class WalletInfo 20 */ 21 class WalletInfo { 22 23 /** 24 * Constructor 25 * @constructor 26 * @param {object=} entities - wallet entities (hdaccounts, addresses, pubkeys) 27 */ 28 constructor(entities) { 29 // Initializes wallet properties 30 this.entities = entities 31 32 this.wallet = { 33 finalBalance: 0 34 } 35 36 this.info = { 37 fees: {}, 38 estimatorFees: {}, 39 latestBlock: { 40 height: rpcLatestBlock.height, 41 hash: rpcLatestBlock.hash, 42 time: rpcLatestBlock.time, 43 } 44 } 45 46 this.addresses = [] 47 this.txs = [] 48 this.unspentOutputs = [] 49 this.nTx = 0 50 } 51 52 53 /** 54 * Ensure hd accounts exist in database 55 * @returns {Promise<Array<any>>} 56 */ 57 async ensureHdAccounts() { 58 return util.parallelCall(this.entities.xpubs, async xpub => { 59 const hdaInfo = new HdAccountInfo(xpub) 60 return hdaInfo.ensureHdAccount() 61 }) 62 } 63 64 /** 65 * Load information about the hd accounts 66 * @returns {Promise<Array<any>>} 67 */ 68 async loadHdAccountsInfo() { 69 return util.parallelCall(this.entities.xpubs, async xpub => { 70 const hdaInfo = new HdAccountInfo(xpub) 71 await hdaInfo.loadInfo() 72 this.wallet.finalBalance += hdaInfo.finalBalance 73 this.addresses.push(hdaInfo) 74 }) 75 } 76 77 /** 78 * Ensure addresses exist in database 79 * @returns {Promise} 80 */ 81 async ensureAddresses() { 82 const importAddrs = [] 83 84 const addrIdMap = await db.getAddressesIds(this.entities.addrs) 85 86 for (let addr of this.entities.addrs) { 87 if (!addrIdMap[addr]) 88 importAddrs.push(addr) 89 } 90 91 // Import new addresses 92 return addrService.restoreAddresses(importAddrs, true) 93 } 94 95 /** 96 * Filter addresses that belong to an active hd account 97 * @returns {Promise<void>} 98 */ 99 async filterAddresses() { 100 const res = await db.getXpubByAddresses(this.entities.addrs) 101 102 for (let addr in res) { 103 let xpub = res[addr] 104 if (this.entities.xpubs.includes(xpub)) { 105 let index = this.entities.addrs.indexOf(addr) 106 if (index > -1) { 107 this.entities.addrs.splice(index, 1) 108 this.entities.pubkeys.splice(index, 1) 109 } 110 } 111 } 112 } 113 114 /** 115 * Load information about the addresses 116 * @returns {Promise<Array<void>>} 117 */ 118 async loadAddressesInfo() { 119 return util.parallelCall(this.entities.addrs, async address => { 120 const addrInfo = new AddressInfo(address) 121 await addrInfo.loadInfo() 122 this.wallet.finalBalance += addrInfo.finalBalance 123 this.addresses.push(addrInfo) 124 }) 125 } 126 127 /** 128 * Loads a partial list of transactions for this wallet 129 * @param {number} page - page index 130 * @param {number} count - number of transactions per page 131 * @param {boolean=} txBalance - True if past wallet balance 132 * should be computed for each transaction 133 * @returns {Promise<void>} 134 */ 135 async loadTransactions(page, count, txBalance) { 136 this.txs = await db.getTxsByAddrAndXpubs( 137 this.entities.addrs, 138 this.entities.xpubs, 139 page, 140 count 141 ) 142 143 if (txBalance) { 144 // Computes wallet balance after each transaction 145 let balance = this.wallet.finalBalance 146 for (let index = 0; index < this.txs.length; index++) { 147 this.txs[index].balance = balance 148 balance -= this.txs[index].result 149 } 150 } 151 } 152 153 /** 154 * Loads the number of transactions for this wallet 155 * @returns {Promise<void>} 156 */ 157 async loadNbTransactions() { 158 const nbTxs = await db.getAddrAndXpubsNbTransactions( 159 this.entities.addrs, 160 this.entities.xpubs 161 ) 162 163 if (nbTxs != null) 164 this.nTx = nbTxs 165 } 166 167 /** 168 * Loads tinfo about the fee rates 169 * @returns {Promise<void>} 170 */ 171 async loadFeesInfo() { 172 this.info.fees = await rpcFees.getFees() 173 174 try { 175 this.info.estimatorFees = rpcFees.getEstimatorFees() 176 } catch { 177 this.info.estimatorFees = null 178 } 179 } 180 181 /** 182 * Loads the list of unspent outputs for this wallet 183 * @returns {Promise<void>} 184 */ 185 async loadUtxos() { 186 // Load the utxos for the hd accounts 187 await util.parallelCall(this.entities.xpubs, async xpub => { 188 const hdaInfo = new HdAccountInfo(xpub) 189 const utxos = await hdaInfo.loadUtxos() 190 for (let utxo of utxos) 191 this.unspentOutputs.push(utxo) 192 }) 193 194 // Load the utxos for the addresses 195 const utxos = await db.getUnspentOutputs(this.entities.addrs) 196 197 for (let utxo of utxos) { 198 const config = 199 (utxo.blockHeight == null) 200 ? 0 201 : (rpcLatestBlock.height - utxo.blockHeight + 1) 202 203 const entry = { 204 tx_hash: utxo.txnTxid, 205 tx_output_n: utxo.outIndex, 206 tx_version: utxo.txnVersion, 207 tx_locktime: utxo.txnLocktime, 208 value: utxo.outAmount, 209 script: utxo.outScript, 210 addr: utxo.addrAddress, 211 confirmations: config 212 } 213 214 this.unspentOutputs.push(entry) 215 } 216 217 // Order the utxos 218 this.unspentOutputs.sort((a, b) => b.confirmations - a.confirmations) 219 } 220 221 /** 222 * Post process addresses and public keys 223 */ 224 async postProcessAddresses() { 225 for (let b = 0; b < this.entities.pubkeys.length; b++) { 226 const pk = this.entities.pubkeys[b] 227 228 if (pk) { 229 const address = this.entities.addrs[b] 230 231 // Add pubkeys in this.addresses 232 for (let c = 0; c < this.addresses.length; c++) { 233 if (address === this.addresses[c].address) 234 this.addresses[c].pubkey = pk 235 } 236 237 // Add pubkeys in this.txs 238 for (let d = 0; d < this.txs.length; d++) { 239 // inputs 240 for (let e = 0; e < this.txs[d].inputs.length; e++) { 241 if (address === this.txs[d].inputs[e].prev_out.addr) 242 this.txs[d].inputs[e].prev_out.pubkey = pk 243 } 244 // outputs 245 for (let e = 0; e < this.txs[d].out.length; e++) { 246 if (address === this.txs[d].out[e].addr) 247 this.txs[d].out[e].pubkey = pk 248 } 249 } 250 251 // Add pubkeys in this.unspentOutputs 252 for (let f = 0; f < this.unspentOutputs.length; f++) { 253 if (address === this.unspentOutputs[f].addr) { 254 this.unspentOutputs[f].pubkey = pk 255 } 256 } 257 } 258 } 259 } 260 261 /** 262 * Post process hd accounts (xpubs translations) 263 */ 264 async postProcessHdAccounts() { 265 for (let b = 0; b < this.entities.xpubs.length; b++) { 266 const entityXPub = this.entities.xpubs[b] 267 const entityYPub = this.entities.ypubs[b] 268 const entityZPub = this.entities.zpubs[b] 269 270 if (entityYPub || entityZPub) { 271 const tgtXPub = entityYPub || entityZPub 272 273 // Translate xpub => ypub/zpub in this.addresses 274 for (let c = 0; c < this.addresses.length; c++) { 275 if (entityXPub === this.addresses[c].address) 276 this.addresses[c].address = tgtXPub 277 } 278 279 // Translate xpub => ypub/zpub in this.txs 280 for (let d = 0; d < this.txs.length; d++) { 281 // inputs 282 for (let e = 0; e < this.txs[d].inputs.length; e++) { 283 const xpub = this.txs[d].inputs[e].prev_out.xpub 284 if (xpub && (xpub.m === entityXPub)) 285 this.txs[d].inputs[e].prev_out.xpub.m = tgtXPub 286 } 287 288 // outputs 289 for (let e = 0; e < this.txs[d].out.length; e++) { 290 const xpub = this.txs[d].out[e].xpub 291 if (xpub && (xpub.m === entityXPub)) 292 this.txs[d].out[e].xpub.m = tgtXPub 293 } 294 } 295 296 // Translate xpub => ypub/zpub in this.unspentOutputs 297 for (let f = 0; f < this.unspentOutputs.length; f++) { 298 const xpub = this.unspentOutputs[f].xpub 299 if (xpub && (xpub.m === entityXPub)) { 300 this.unspentOutputs[f].xpub.m = tgtXPub 301 } 302 } 303 } 304 } 305 } 306 307 /** 308 * Return a plain old js object with wallet properties 309 * @returns {object} 310 */ 311 toPojo() { 312 return { 313 wallet: { 314 final_balance: this.wallet.finalBalance 315 }, 316 info: { 317 fees: this.info.fees, 318 estimatorFees: this.info.estimatorFees, 319 latest_block: this.info.latestBlock 320 }, 321 addresses: this.addresses.map(a => a.toPojo()), 322 txs: this.txs, 323 unspent_outputs: this.unspentOutputs, 324 n_tx: this.nTx 325 } 326 } 327 328 } 329 330 export default WalletInfo