text: /*
    oclif command to update the balances stored in the wallet.json file.
  
    TODO:
  */
  
  "use strict"
  
  const AppUtils = require("../util")
  const appUtils = new AppUtils()
  
  const config = require("../../config")
  
  // Mainnet by default
  const BITBOX = new config.BCHLIB({ restURL: config.MAINNET_REST })
  
  // Used for debugging and error reporting.
  const util = require("util")
  util.inspect.defaultOptions = { depth: 2 }
  
  const { Command, flags } = require("@oclif/command")
  
  class UpdateBalances extends Command {
    constructor(argv, config) {
      super(argv, config)
  
      this.BITBOX = BITBOX
    }
  
    async run() {
      try {
        const { flags } = this.parse(UpdateBalances)
  
        this.validateFlags(flags)
  
        const name = flags.name
  
        // Open the wallet data file.
        const filename = `${__dirname}/../../wallets/${name}.json`
        let walletInfo = appUtils.openWallet(filename)
        walletInfo.name = name
  
        console.log(`Existing balance: ${walletInfo.balance} BCH`)
  
        // Determine if this is a testnet wallet or a mainnet wallet.
        if (walletInfo.network === "testnet")
          this.BITBOX = new config.BCHLIB({ restURL: config.TESTNET_REST })
  
        // Update the balances in the wallet.
        walletInfo = await this.updateBalances(filename, walletInfo)
  
        console.log(`Updated balance: ${walletInfo.balance} BCH`)
      } catch (err) {
        //if (err.message) console.log(err.message)
        console.log(`Error in UpdateBalances.run: `, err)
      }
    }
  
    // Validate the proper flags are passed in.
    validateFlags(flags) {
      // Exit if wallet not specified.
      const name = flags.name
      if (!name || name === "")
        throw new Error(`You must specify a wallet with the -n flag.`)
  
      return true
    }
  
    // Update the balances in the wallet.
    async updateBalances(filename, walletInfo) {
      console.log(`debug: walletInfo.nextAddress = ${walletInfo.nextAddress}`)
  
      // Query data on each address that has been generated by the wallet.
      //const addressData = await this.getAddressData(walletInfo)
      const addressData = await this.getAllAddressData(walletInfo)
      //console.log(`addressData: ${util.inspect(addressData)}`)
  
      // Update hasBalance array with non-zero balances.
      const hasBalance = this.generateHasBalance(addressData.addressData)
  
      // Sum all the balances in hasBalance to calculate total balance.
      const balance = this.sumConfirmedBalances(hasBalance, true)
  
      // Save the data to the wallet JSON file.
      walletInfo.balance = appUtils.eightDecimals(
        balance.totalConfirmed + balance.totalUnconfirmed
      )
      walletInfo.balanceConfirmed = balance.totalConfirmed
      walletInfo.balanceUnconfirmed = balance.totalUnconfirmed
      walletInfo.hasBalance = hasBalance
      walletInfo.SLPUtxos = addressData.slpUtxoData
      await appUtils.saveWallet(filename, walletInfo)
  
      return walletInfo
    }
  
    // Retrieves data for every address generated by the wallet.
    // Returns an array of address data for every address generated by the wallet.
    async getAllAddressData(walletInfo) {
      try {
        let addressData = [] // Accumulates address data.
        let slpUtxoData = [] // Accumulates SLP token UTXOs.
        let currentIndex = 0 // tracks the current HD index.
        let batchHasBalance = true // Flag to signal when last address found.
  
        while (batchHasBalance) {
          // Get a 20-address batch of data.
          const thisDataBatch = await this.getAddressData(
            walletInfo,
            currentIndex,
            20
          )
          //console.log(`thisDataBatch: ${util.inspect(thisDataBatch)}`)
  
          // Increment the index by 20 (addresses).
          currentIndex += 20
  
          // Check if data has no balance. no balance == last address.
          batchHasBalance = this.detectBalance(thisDataBatch.balances)
          //console.log(`batchHasBalance: ${batchHasBalance}`)
  
          // Add data to the array, unless this last batch has no balances.
          if (batchHasBalance) {
            addressData = addressData.concat(thisDataBatch.balances)
            slpUtxoData = slpUtxoData.concat(thisDataBatch.slpUtxos)
          }
  
          //console.log(`addressData: ${util.inspect(addressData)}`)
  
          // Protect against run-away while loop.
          if (currentIndex > 10000) break
        }
  
        // Add the HD index to the SLP UTXO data, so the wallet knows which HD
        // address the SLP UTXO belongs to.
        slpUtxoData = this.addSLPIndex(addressData, slpUtxoData)
  
        return { addressData, slpUtxoData }
      } catch (err) {
        console.log(`Error in update-balances.js/getAllAddressData()`)
        throw err
      }
    }
  
    // All addresses in the wallet are generated by the HD node. For each SLP
    // UTXO, this function identifies which index of the HD node the UTXO belongs
    // to. Each index corresponds to an address generated by the HD node. This
    // information is needed when spending the SLP UTXO.
    addSLPIndex(addressData, slpUtxoData) {
      try {
        // Loop through each SLP UTXO
        for (let i = 0; i < slpUtxoData.length; i++) {
          const slpUtxo = slpUtxoData[i]
  
          // Loop through the array of address data
          for (let j = 0; j < addressData.length; j++) {
            const bchAddr = addressData[j]
  
            // If the cash addresses match, record the HD index in the SLP UTXO.
            if (bchAddr.cashAddress === slpUtxo.cashAddr) slpUtxo.hdIndex = j
          }
        }
  
        return slpUtxoData
      } catch (err) {
        console.log(`Error in update-balances.js/addSLPIndex()`)
        throw err
      }
    }
  
    // Returns true if any of the address data has a balance.
    // dataBatch is expected to be an array of address data.
    detectBalance(dataBatch) {
      try {
        // Loop through the address data in the dataBatch array.
        for (let i = 0; i < dataBatch.length; i++) {
          const thisAddr = dataBatch[i]
  
          // Exit if a balance is detected in any of the addresses.
          if (thisAddr.totalReceivedSat > 0 || thisAddr.unconfirmedBalanceSat > 0)
            return true
        }
  
        // If the loop completes without finding a balance, return false.
        return false
      } catch (err) {
        console.log(`Error in update-balances.js/detectBalance()`)
        throw err
      }
    }
  
    // Retrieves details data (objects) on addresses in an HD wallet from REST server.
    // A max of 20 addresses can be retrieved at a time.
    // Addresses start at the index and the number of address data retrieved is
    // set by the limit (up to 20). Data is returned as an array of objects.
    async getAddressData(walletInfo, index, limit) {
      try {
        if (isNaN(index)) throw new Error(`index must be supplied as a number.`)
  
        if (!limit || isNaN(limit))
          throw new Error(`limit must be supplied as a non-zero number.`)
  
        if (limit > 20) throw new Error(`limit must be 20 or less.`)
  
        console.log(
          `Getting address data at index ${index} up to index ${index + limit}`
        )
  
        // Get the list of addresses.
        const addresses = await this.generateAddress(walletInfo, index, limit)
        //console.log(`addresses: ${util.inspect(addresses)}`)
  
        // get BCH balance and details for each address.
        const balances = await this.BITBOX.Address.details(addresses)
  
        // get SLP utxo information for the addresses
        const slpUtxos = await this.getSlpUtxos(addresses)
        //console.log(`slpUtxos: ${JSON.stringify(slpUtxos, null, 2)}`)
  
        return { balances, slpUtxos }
      } catch (err) {
        console.log(`Error in update-balances.js/getAddressData()`)
        throw err
      }
    }
  
    // This function expects an array of up to 20 addresses as input.
    // Retrieve SLP Utxo information that will be saved to the wallet file.
    async getSlpUtxos(addresses) {
      try {
        // Validate input.
        if (!Array.isArray(addresses))
          throw new Error(`addresses must be an array`)
        if (addresses.length > 20)
          throw new Error(`addresses array must be 20 or fewer elements.`)
  
        // Check addresses to see if they contain any SLP tokens.
        const slpBalances = await this.BITBOX.Util.balancesForAddress(addresses)
        //console.log(`slpBalances: ${JSON.stringify(slpBalances, null, 2)}`)
  
        // Remove empty arrays (addresses that have no tokens).
        const consolidatedBalances = slpBalances.filter(x => {
          if (x.length > 0) return x
        })
  
        // Loop through each address that has SLP tokens.
        let slpUtxos = []
        for (let i = 0; i < consolidatedBalances.length; i++) {
          const thisAddress = consolidatedBalances[i][0].slpAddress
          //console.log(`thisAddress: ${JSON.stringify(thisAddress, null, 2)}`)
  
          // Get all SLP token UTXOs associated with this address.
          const tokenUtxos = await this.findSlpUtxos(thisAddress)
          //console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
  
          // Merge any new UTXOs in with the list.
          slpUtxos = slpUtxos.concat(tokenUtxos)
        }
  
        return slpUtxos
      } catch (err) {
        console.log(`Error in update-balances.js/getSlpUtxos().`)
        throw err
      }
    }
  
    // Retrieves SLP Utxos for an address that has been identified to be holding
    // SLP tokens.
    async findSlpUtxos(slpAddr) {
      try {
        // console.log(`slpAddr: ${slpAddr}`)
  
        // Convert the slpAddr to a cashAddr.
        const cashAddr = this.BITBOX.Address.toCashAddress(slpAddr)
        // console.log(`cashAddr: ${cashAddr}`)
  
        // Get utxos associated with this address.
        const u = await this.BITBOX.Address.utxo(cashAddr)
        //console.log(`u = ${JSON.stringify(u, null, 2)}`)
  
        const utxos = u.utxos
        //console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
  
        // Figure out which UTXOs are associated with SLP tokens.
        const isTokenUtxo = await this.BITBOX.Util.tokenUtxoDetails(utxos)
        //console.log(`isTokenUtxo: ${JSON.stringify(isTokenUtxo, null, 2)}`)
  
        // Filter out just the UTXOs that belong to SLP tokens.
        const tokenUtxos = []
        for (let i = 0; i < isTokenUtxo.length; i++)
          if (isTokenUtxo[i]) tokenUtxos.push(utxos[i])
  
        // Add address data to each UTXO.
        for (let i = 0; i < tokenUtxos.length; i++) {
          tokenUtxos[i].cashAddr = cashAddr
          tokenUtxos[i].slpAddr = slpAddr
        }
  
        return tokenUtxos
      } catch (err) {
        console.log(`Error in update-balances.js/findSlpUtxo().`)
        throw err
      }
    }
  
    // Generates an array of HD addresses.
    // Address are generated from index to limit.
    // e.g. generateAddress(walletInfo, 20, 10)
    // will generate a 20-element array of addresses from index 20 to 29
    async generateAddress(walletInfo, index, limit) {
      // root seed buffer
      let rootSeed
      if (config.RESTAPI === "bitcoin.com")
        rootSeed = this.BITBOX.Mnemonic.toSeed(walletInfo.mnemonic)
      else rootSeed = await this.BITBOX.Mnemonic.toSeed(walletInfo.mnemonic)
  
      // master HDNode
      if (walletInfo.network === "testnet")
        var masterHDNode = this.BITBOX.HDNode.fromSeed(rootSeed, "testnet")
      else var masterHDNode = this.BITBOX.HDNode.fromSeed(rootSeed)
  
      // HDNode of BIP44 account
      const account = this.BITBOX.HDNode.derivePath(masterHDNode, "m/44'/245'/0'")
  
      // Empty array for collecting generated addresses
      const bulkAddresses = []
  
      // Generate the addresses.
      for (let i = index; i < index + limit; i++) {
        // derive an external change address HDNode
        const change = this.BITBOX.HDNode.derivePath(account, `0/${i}`)
  
        // get the cash address
        const newAddress = this.BITBOX.HDNode.toCashAddress(change)
        //const legacy = this.BITBOX.HDNode.toLegacyAddress(change)
  
        //push address into array
        bulkAddresses.push(newAddress)
      }
  
      return bulkAddresses
    }
  
    // Generates the data that will be stored in the hasBalance array of the
    // wallet JSON file.
    generateHasBalance(addressData) {
      const hasBalance = []
  
      // Loop through each HD address index
      for (var i = 0; i < addressData.length; i++) {
        const thisAddr = addressData[i]
  
        // If the address has a balance, add it to the hasBalance array.
        if (thisAddr.balance > 0 || thisAddr.unconfirmedBalance > 0) {
          const thisObj = {
            index: i,
            balance: thisAddr.balance,
            balanceSat: thisAddr.balanceSat,
            unconfirmedBalance: thisAddr.unconfirmedBalance,
            unconfirmedBalanceSat: thisAddr.unconfirmedBalanceSat,
            cashAddress: thisAddr.cashAddress
          }
  
          hasBalance.push(thisObj)
        }
      }
  
      return hasBalance
    }
  
    // Sums the confirmed balances in the hasBalance array to create a single,
    // aggrigate balance
    sumConfirmedBalances(hasBalance, verbose) {
      let total = 0
      let totalConfirmed = 0
      let totalUnconfirmed = 0
  
      for (var i = 0; i < hasBalance.length; i++) {
        const thisHasBalance = hasBalance[i]
  
        total += thisHasBalance.balance + thisHasBalance.unconfirmedBalance
        totalConfirmed += thisHasBalance.balance
        totalUnconfirmed += thisHasBalance.unconfirmedBalance
      }
  
      // The amount of satoshis in one coin.
      const ONE_COIN = 100000000
  
      // Convert to satoshis
      const totalSatoshis = Math.floor(total * ONE_COIN)
      const totalConfirmedSatoshis = Math.floor(totalConfirmed * ONE_COIN)
      const totalUnconfirmedSatoshis = Math.floor(totalUnconfirmed * ONE_COIN)
  
      // Convert back to BCH
      total = totalSatoshis / ONE_COIN
      totalConfirmed = totalConfirmedSatoshis / ONE_COIN
      totalUnconfirmed = totalUnconfirmedSatoshis / ONE_COIN
  
      if (verbose) return { totalConfirmed, totalUnconfirmed }
  
      return total
    }
  }
  
  UpdateBalances.description = `Poll the network and update the balances of the wallet.`
  
  UpdateBalances.flags = {
    name: flags.string({ char: "n", description: "Name of wallet" })
  }
  
  module.exports = UpdateBalances
  