util.js

import * as bitcoin from '@oipwg/bitcoinjs-lib'
import { Buffer } from 'safe-buffer'
import createHash from 'create-hash'
import bs58check from 'bs58check'
import wif from 'wif'
import varuint from 'varuint-bitcoin'

/** @module util */

function ripemd160 (buffer) {
  return createHash('rmd160').update(buffer).digest()
}

function sha256 (buffer) {
  return createHash('sha256').update(buffer).digest()
}

function hash256 (buffer) {
  return sha256(sha256(buffer))
}

function hash160 (buffer) {
  return ripemd160(sha256(buffer))
}

function toBase58Check (hash, version) {
  const payload = Buffer.allocUnsafe(21)
  payload.writeUInt8(version, 0)
  hash.copy(payload, 1)

  return bs58check.encode(payload)
}

/**
 * @param  {Buffer} key - The buffer for the Private/Public Key to encode
 * @param  {number} version - The specific "version" byte to prepend
 * @return {string} Returns the Base58 encoded Key
 */
function toBase58 (key, version) {
  if (!key) {
    return console.log('KEY NULL!!!!')
  }

  return toBase58Check(hash160(key), version)
}

/**
 * Check if a WIF is valid for a specific CoinNetwork
 * @param  {string} key - Base58 WIF Private Key
 * @param  {CoinNetwork} network
 * @return {Boolean}
 */
function isValidWIF (key, network) {
  try {
    const dec = wif.decode(key)

    if (network) {
      return dec.version === network.wif
    } else {
      return true
    }
  } catch (e) {
    console.error(e)
    return false
  }
}

/**
 * Check if a Public Address is valid for a specific CoinNetwork
 * @param  {string} address - Base58 Public Address
 * @param  {CoinNetwork} network
 * @return {Boolean}
 */
function isValidPublicAddress (address, network) {
  try {
    const dec = bitcoin.address.fromBase58Check(address)
    if (network) {
      return dec.version === network.pubKeyHash || dec.version === network.scriptHash
    } else {
      return true
    }
  } catch (e) {
    return false
  }
}

// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery
async function discovery (chain, gapLimit, queryPromise, i, coin) {
  let gap = 0
  let checked = 0
  const allAddresses = []

  const cycle = async (myCoin) => {
    const batch = [chain.get()]
    checked++

    while (batch.length < gapLimit) {
      chain.next()
      batch.push(chain.get())

      checked++
    }

    let queryResultSet
    try {
      queryResultSet = await queryPromise(batch, myCoin)
    } catch (e) {
      throw new Error('discovery failed! \n' + e)
    }

    for (const adr of queryResultSet.addresses) { allAddresses.push(adr) }

    if (Array.isArray(queryResultSet.results)) { throw new TypeError('Expected query set, not Array') }

    // iterate batch, guarantees order agnostic of queryPromise result ordering
    batch.forEach(function (a) {
      if (queryResultSet.results[toBase58(a.address.publicKey, a.network.pubKeyHash)]) {
        gap = 0
      } else {
        gap += 1
      }
    })

    if (gap >= gapLimit) {
      const used = checked - gap

      return { used: used, checked: checked, chainIndex: i, addresses: allAddresses }
    } else {
      chain.next()
    }

    try {
      return await cycle(myCoin)
    } catch (e) { throw new Error(e) }
  }

  try {
    return await cycle(coin)
  } catch (e) { throw new Error(e) }
}

/**
 * Check if a given string is in a BIP39 Mnemonic format (is a string, and is at least 2 words long).
 * Please note that this does not validate if the Mnemonic is a valid BIP39 Mnemonic
 * (i.e. defined from Entropy vs a Brain Wallet)
 * @param  {string} mnemonic - BIP39 Mnemonic to check
 * @return {Boolean}
 */
function isMnemonic (mnemonic) {
  if (typeof mnemonic === 'string' && mnemonic.split(' ').length >= 2) { return true }

  return false
}

/**
 * Check if a given string is a BIP39 Entropy string.
 * @param  {string} entropy - The Entropy string to check
 * @return {Boolean}
 */
function isEntropy (entropy) {
  if (typeof entropy === 'string' && entropy.length >= 16 && entropy.length <= 32) { return true }

  return false
}

module.exports = {
  hash160,
  hash256,
  toBase58,
  isValidPublicAddress,
  isValidWIF,
  isMnemonic,
  isEntropy,
  discovery,
  varIntBuffer: varuint.encode
}