/ lib / wallet / wallet-info.js
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