import * as bip32 from 'bip32'
import * as bip39 from 'bip39'
import Exchange from '@oipwg/exchange-rate'
import { Insight } from '@oipwg/insight-explorer'
import axios from 'axios'
import Coin from './Coin'
import networks from './networks'
import networkConfig from './networks/config'
import { isEntropy, isMnemonic, isValidPublicAddress } from './util'
const DEFAULT_SUPPORTED_COINS = ['bitcoin', 'litecoin', 'flo', 'raven']
const DEFAULT_SUPPORTED_TESTNET_COINS = ['bitcoinTestnet', 'floTestnet', 'litecoinTestnet', 'ravenTestnet']
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
/** Full Service [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) Multi-Coin Wallet supporting both sending and receiving payments */
class Wallet {
/**
* Create a new [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) wallet with the supplied settings
*
* ##### Examples
* Create wallet with Random Mnemonic
* ```
* let wallet = new Wallet()
* ```
* Create wallet from [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) Mnemonic
* ```
* let wallet = new Wallet("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
* ```
* Create wallet from [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) Entropy
* ```
* let wallet = new Wallet('00000000000000000000000000000000')
* ```
* Create wallet from Seed Hex
* ```
* let wallet = new Wallet("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4")
* ```
* Create wallet from Seed Buffer
* ```
* let wallet = new Wallet(Buffer.from("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4", "hex"))
* ```
*
* @param {string|Buffer} [seed] - [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) Mnemonic, [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) Entropy, or Seed Hex/Buffer
* @param {Object} [options] - Wallet settings
* @param {boolean} [options.discover=false] - Defines if the Wallet should "auto-discover" Coin Account chains or not
* @param {Array.<string>} [options.supportedCoins=['bitcoin', 'litecoin', 'flo']] - An Array of coins that the Wallet should support
* @param {Array.<CoinInfo>} [options.networks] - An array containing a custom coins network info
* @param {Object} [options.serializedData] - A previous Wallet state to reload from
*
* @example <caption>Create wallet using Mnemonic</caption>
* let wallet = new Wallet("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
*
* @return {Wallet}
*/
constructor (seed, options) {
// Check if seed is a string or buffer, if not, create a new BIP39 Mnemonic
if (isMnemonic(seed)) {
this.fromMnemonic(seed)
} else if (isEntropy(seed)) {
this.fromEntropy(seed)
} else if (seed) {
this.fromSeed(seed)
} else {
this.fromMnemonic(bip39.generateMnemonic())
}
// Derive the "m" level of the BIP44 wallet
this.masterNode = bip32.fromSeed(Buffer.from(this.seed, 'hex'))
// Set the networks to the imported defaults
this.networks = networks
// Check for custom coins/networks
if (options && typeof options === 'object') {
// Check if the user has defined their own supported coins for the wallet
if (options.supportedCoins) {
if (typeof options.supportedCoins === 'string') {
this.supportedCoins = [options.supportedCoins]
} else if (Array.isArray(options.supportedCoins)) {
this.supportedCoins = options.supportedCoins
}
}
// Check if the user has defined any custom networks that should be imported
if (options.networks && typeof options.networks === 'object') {
// Attach each passed in network, overwrite if needed
for (const node in options.networks) {
if (!Object.prototype.hasOwnProperty.call(options.networks, node)) continue
this.networks[node] = options.networks[node]
}
}
}
// If we were not passed in a supported coin array by the options, then set it to the default options.
if (!this.supportedCoins || !Array.isArray(this.supportedCoins)) { this.supportedCoins = DEFAULT_SUPPORTED_COINS }
// The array to hold the live coin objects
this.coins = {}
// An optional variable to say if we should auto run address discovery on Account Chains
if (options && (options.discover || options.discover === false)) { this.discover = options.discover } else { this.discover = true }
// Attempt to deserialize if we were passed serialized data
if (options && options.serializedData) { this.deserialize(options.serializedData) }
// Add all coins
for (const coinName of this.supportedCoins) { this.addCoin(coinName) }
}
serialize () {
const serializedCoins = {}
for (const name in this.coins) {
serializedCoins[name] = this.coins[name].serialize()
}
return {
masterNode: this.masterNode.toBase58(),
seed: this.getMnemonic() ? this.getMnemonic() : this.seed,
coins: serializedCoins
}
}
deserialize (serializedData) {
if (serializedData) {
if (serializedData.coins) {
for (const name in serializedData.coins) {
if (!Object.prototype.hasOwnProperty.call(serializedData.coins, name)) continue
this.addCoin(name, { serializedData: serializedData.coins[name] })
}
}
}
}
/**
* Add a Coin to the Wallet
* @param {String} name - The coin "name" as defined in CoinInfo.name
* @param {Object} [options] - Options you want passed to the coin being added
*/
addCoin (name, options) {
const opts = options || {}
if (!opts.discover) { opts.discover = this.discover }
// If the coin isn't already added AND we have access to a valid network,
// then add the coin.
if (!this.coins[name] && this.networks[name]) {
this.coins[name] = new Coin(this.masterNode.derivePath('44\''), this.networks[name], opts)
}
}
/**
* Get a specific Coin
* @param {string} coin - The coin "name" as defined in CoinInfo.name
* @example
* let wallet = new Wallet();
* let coin = wallet.getCoin("bitcoin")
* @return {Coin} Returns the requested Coin
*/
getCoin (coin) {
for (const c in this.coins) {
if (c === coin) { return this.coins[c] }
}
}
/**
* Get all Coins running inside the Wallet
* @example
* let wallet = new Wallet();
* let coins = wallet.getCoins();
* // coins = {
* // "bitcoin": Coin,
* // "litecoin": Coin,
* // "flo": Coin
* // }
* @return {Object.<number, Coin>} Object containing all coins
*/
getCoins () {
return this.coins
}
async GetCoinBalance (coin, options) {
// This is a helper function to catch errors thrown by coin.getBalance() and return them
let balance
try {
balance = await coin.getBalance(options)
} catch (e) {
return {
error: new Error('Unable to get individual Coin Balance \n' + e)
}
}
return {
balance
}
}
/**
* Get Coin Balances
* @param {Object} [options] - The options for searching the Balance of coins
* @param {Array} [options.coins=["bitcoin", "litecoin", "flo"]] - An array of coin names you want to get the balances for. If no coins are given, an array of all available coins will be used.
* @param {Boolean} [options.discover=true] - Should we attempt a new discovery, or just grab the available balances
* @param {Boolean} [options.testnet=true] - Should we attempt to get balances for testnet coins as well (coins ending with 'Testnet')
*
* @return {Promise<Object>} Returns a Promise that will resolve to an Object containing info about each coins balance, along with errors if there are any
*
* @example
* let wallet = new Wallet(...)
* wallet.getCoinBalances(["bitcoin", "litecoin", "flo"])
*
* //example return
* {
* "flo": 2.16216,
* "bitcoin": "error fetching balance",
* "litecoin": 3.32211
* }
*/
async getCoinBalances (options = { discover: true, testnet: true }) {
const coinnames = options.coins || Object.keys(this.getCoins())
// when passing in custom options object, it's easy to forget to set the defaults, so just in case
if (options.discover === undefined) {
options.discover = true
}
// checking if false so that if undefined, it will proceed normally
if (options.testnet === false) {
for (let i = coinnames.length - 1; i >= 0; i--) {
if (coinnames[i].includes('Testnet')) {
coinnames.splice(i, 1)
}
}
}
const coinPromises = {}
for (const name of coinnames) {
coinPromises[name] = this.GetCoinBalance(this.getCoin(name), options)
}
const coinBalances = {}
for (const coin in coinPromises) {
const response = await coinPromises[coin]
if (typeof response.balance === 'number') { coinBalances[coin] = response.balance } else { coinBalances[coin] = `error fetching balance: ${JSON.stringify(response)}` }
}
return coinBalances
}
/**
* Calculate Exchange Rates for supported coins
* @param {Object} [options] - The options for getting the exchange rates
* @param {Array} [options.coins=["bitcoin", "litecoin", "flo"]] - An array of coin names you want to get the balances for. If no coins are given, an array of all available coins will be used.
* @param {String} [options.fiat="usd"] - The fiat type for which you wish to get the exchange rate for
*
* @return {Promise<Object>} Returns a Promise that will resolve to an Object containing info about each coins exchange rate, along with errors if there are any
*
* @example
* let wallet = new Wallet(...)
* wallet.getExchangeRates(["flo", "bitcoin", "litecoin"], "usd")
*
* //returns
* {
* "flo": expect.any(Number) || "error",
* "bitcoin": expect.any(Number) || "error",
* "litecoin": expect.any(Number) || "error"
* }
*/
async getExchangeRates (options = { fiat: 'usd' }) {
const coins = options.coins || Object.keys(this.getCoins())
if (!coins) throw new Error('No coins found to fetch exchange rates')
if (!options.fiat) { options.fiat = 'usd' }
// Initialize an Exchange object
if (!this.Exchange) { this.Exchange = new Exchange() }
const promiseArray = {}
for (const coinname of coins) {
if (coinname.includes('Testnet')) { break }
promiseArray[coinname] = this.Exchange.getExchangeRate(coinname, options.fiat)
}
const rates = {}
for (const coinname in promiseArray) {
try {
rates[coinname] = await promiseArray[coinname]
} catch (err) {
rates[coinname] = 'error fetching rate'
}
}
return rates
}
/**
* Calculate Balance of coins after exchange rate conversion
* @param {Object} [options] - The options for getting the exchange rates
* @param {Array} [options.coins=["bitcoin", "litecoin", "flo"]] - An array of coin names you want to get the balances for. If no coins are given, an array of all available coins will be used.
* @param {String} [options.fiat="usd"] - The fiat type for which you wish to get the exchange rate for
* @param {Boolean} [options.discover=true] - Should we attempt a new discovery, or just grab the available balances
* @param {Boolean} [options.testnet=true] - should we include testnet coins?
* @return {Promise<Object>} Returns a Promise that will resolve to the fiat balances for each coin
* @example
* let wallet = new Wallet(...)
* wallet.getFiatBalances(["flo", "bitcoin", "litecoin"], "usd")
*
* //returns
* {
* "flo": expect.any(Number) || "error",
* "bitcoin": expect.any(Number) || "error",
* "litecoin": expect.any(Number) || "error"
* }
*/
async getFiatBalances (options) {
const fiatBalances = {}
let balances = {}
let xrates = {}
try {
balances = await this.getCoinBalances(options)
} catch (err) {
throw new Error(`Failed to get coin balances: ${JSON.stringify(err)}`)
}
try {
xrates = await this.getExchangeRates(options)
} catch (err) {
throw new Error(`Failed to get exchange rates: ${JSON.stringify(err)}`)
}
for (const coinB in balances) {
for (const coinX in xrates) {
if (coinB === coinX) {
// Both have been grabbed with no errors
if (!isNaN(balances[coinB]) && !isNaN(xrates[coinX])) {
fiatBalances[coinB] = balances[coinB] * xrates[coinX]
}
}
}
}
// Set the error state for coins not properly returned
for (const coinName of this.supportedCoins) {
if (!fiatBalances[coinName]) {
fiatBalances[coinName] = 'error'
}
}
return fiatBalances
}
/**
* Init Wallet from BIP39 Mnemonic
* @param {string} mnemonic - A BIP39 Mnemonic String
* @example
* let wallet = new Wallet();
* wallet.fromMnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
* @return {Boolean} Returns if the operation was successful
*/
fromMnemonic (mnemonic) {
if (isMnemonic(mnemonic)) {
this.mnemonic = mnemonic
this.entropy = bip39.mnemonicToEntropy(this.mnemonic)
this.seed = bip39.mnemonicToSeedSync(this.mnemonic).toString('hex')
return true
}
return false
}
/**
* Get the [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) Mnemonic, if defined
* @example
* let wallet = new Wallet('00000000000000000000000000000000');
* let mnemonic = wallet.getMnemonic()
* // mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
* @return {string}
*/
getMnemonic () {
return this.mnemonic
}
/**
* Init Wallet from BIP39 Entropy
* @param {string} entropy - A BIP39 Entropy String
* @example
* let wallet = new Wallet();
* wallet.fromEntropy('00000000000000000000000000000000')
* @return {Boolean} Returns if the operation was successful
*/
fromEntropy (entropy) {
if (isEntropy(entropy)) {
this.entropy = entropy
this.mnemonic = bip39.entropyToMnemonic(this.entropy)
return this.fromMnemonic(this.mnemonic)
}
return false
}
/**
* Get the Entropy value used to generate the [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) Mnemonic.
* Note that the Entropy will only be defined if we are creating
* a wallet from Entropy or a Mnemonic, not off of just the Seed Hex
*
* @example
* let wallet = new Wallet("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about");
* let entropy = wallet.getEntropy()
* // entropy = '00000000000000000000000000000000'
* @return {string}
*/
getEntropy () {
return this.entropy
}
/**
* Init Wallet from a Seed
* @param {string|Buffer} seed
* @example
* let wallet = new Wallet();
* wallet.fromSeed("example-seed");
* @return {Boolean} Returns if the operation was successful
*/
fromSeed (seed) {
if (seed instanceof Buffer) {
this.seed = seed.toString('hex')
return true
} else if (typeof seed === 'string') {
this.seed = seed
return true
}
return false
}
/**
* Get the Encoded Seed hex string
* @example
* let wallet = new Wallet('00000000000000000000000000000000');
* let seedHex = wallet.getSeed()
* // seedHex = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4'
* @return {string} The hex string of the seed buffer
*/
getSeed () {
return this.seed
}
/**
* @param {Object} options - Options about the payment you wish to send
* @param {OutputAddress|Array.<OutputAddress>} options.to - Define outputs for the Payment
* @param {string|Array.<string>} [options.coin] - Define which coin you would like to send from
* @param {string|Array.<string>} [options.from=All Addresses in Coin] - Define what public address(es) you wish to send from
* @param {number|Array.<number>} [options.fromAccounts=All Accounts in Coin] - Define what Accounts on the Coin you wish to send from
* @param {Boolean} [options.discover=true] - Should discovery happen before sending payment
* @param {string} [options.floData=""] - Flo data to attach to the transaction
* @return {Promise<string>} Returns a promise that will resolve to the success TXID
*/
async sendPayment (options) {
if (!options) { throw new Error('You must define payment options!') }
if (!options.to) { throw new Error('You must define your payment outputs!') }
// Check if the user defined a coin name to send from
if (options.coin) {
if (typeof options.coin !== 'string') { throw new Error('Send From Coin option must be the string name of the Coin!') }
if (this.getCoin(options.coin)) {
try {
return this.getCoin(options.coin).sendPayment(options)
} catch (err) { throw new Error(err) }
}
} else {
// If coin name is not passed, attempt to match addresses to a Coin!
let coinMatch = ''
if (Array.isArray(options.to)) {
for (const coin in this.networks) {
let allMatchCoin = true
for (const toAdr of options.to) {
for (const adr in toAdr) {
if (!Object.prototype.hasOwnProperty.call(toAdr, adr)) continue
if (isValidPublicAddress(adr, this.networks[coin].network)) {
coinMatch = this.networks[coin].name
} else {
allMatchCoin = false
}
}
}
// If not all addresses are valid, don't match to coin
if (!allMatchCoin && coinMatch === this.networks[coin].name) { coinMatch = '' }
}
} else {
for (const coin in this.networks) {
for (const adr in options.to) {
if (!Object.prototype.hasOwnProperty.call(options.to, adr)) continue
if (isValidPublicAddress(adr, this.networks[coin].network)) {
coinMatch = this.networks[coin].name
}
}
}
}
if (coinMatch !== '') {
if (this.getCoin(coinMatch)) {
try {
return this.getCoin(coinMatch).sendPayment(options)
} catch (err) { throw new Error(err) }
} else { throw new Error('Cannot get Coin for matched network! ' + coinMatch) }
} else {
throw new Error('Not all to addresses match any Coin network! Please check your outputs.')
}
}
}
/**
* Returns the network information for the coins available
* @return Array.<CoinInfo>
*/
getNetworks () {
return this.networks
}
setNetworks (networks) {
this.networks = networks
}
/**
* Add default SUPPORTED testnet coins to wallet
* @param {Boolean} [bool=true] - if true, add testnet coins, is false, remove them
*/
addTestnetCoins (bool = true) {
if (bool) {
for (const coinName of DEFAULT_SUPPORTED_TESTNET_COINS) { this.addCoin(coinName) }
} else {
for (const coinName of DEFAULT_SUPPORTED_TESTNET_COINS) { delete this.coins[coinName] }
}
}
/**
* Set the urls for the insight api explorers
* @param options
* @param {string} options.flo - flo api
* @param {string} options.floTestnet - floTestnet api
* @param {string} options.bitcoin - bitcoin api
* @param {string} options.bitcoinTestnet - bitcoinTestnet api
* @param {string} options.litecoin - litecoin api
* @param {string} options.litecoinTestnet - litecoinTestnet api
* @example
* let options = {
* flo: 'myFloSiteApi.com/yadayada,
* bitcoin: 'myBitcoinApi.superApi/AyePeeEye',
* litecoin: 'superLightCoin.hero'
* }
* new Wallet(mnemonic, {discover: false}).setNetworkApi(options)
*/
setExplorerUrls (options) {
const networks = this.getNetworks()
for (const networkCoin in networks) {
for (const coin in options) {
if (networkCoin === coin) {
networks[coin].explorer = new Insight(options[coin])
}
}
}
this.setNetworks(networks)
}
/**
* Get back the network explorer apis for supported coins
*/
getExplorerUrls () {
const networks = this.getNetworks()
const networkObject = {}
for (const walletCoin of Object.keys(this.getCoins())) {
for (const networkCoin in networks) {
if (walletCoin === networkCoin) {
networkObject[walletCoin] = networks[walletCoin].explorer.url
}
}
}
return networkObject
}
resetExplorerUrls () {
this.setExplorerUrls(networkConfig.defaultApiUrls)
}
static getDefaultExplorerUrls () {
return networkConfig.defaultApiUrls
}
async purchaseRecord ({ txid, terms }) {
try {
// lookup a record at txid & terms
const response = await axios.get(`https://api.oip.io/oip/o5/location/request?id=${txid}&terms=${terms}`)
const { valid_until: validUntil, pre_image: preImage } = response.data
const res = await axios.get(`https://api.oip.io/oip/o5/record/get/${txid}`)
//! ask Bits about hard coding template.
const { amount, destination } = res.data.results[0].record.details.tmpl_DE84D583
const account = this.getCoin('flo').getAccount()
const mainAddress = account.getMainAddress() // address with a balance
const publicAddress = mainAddress.getPublicAddress()
// send desired amount to to address
const paymentTxid = await account.sendPayment({
to: { [destination]: amount },
from: publicAddress
})
await sleep(10000)
// proof constructed by sining the request pre_image with address (that sent FLO)
const signature = mainAddress.signMessage(preImage)
const body = {
valid_until: validUntil,
id: txid,
term: terms,
pre_image: preImage,
signature,
payment_txid: paymentTxid,
signing_address: publicAddress
}
// location/request needs to be submitted
const res2 = await axios.post(`https://api.oip.io/oip/o5/location/proof?id=${txid}&terms=${terms}`, body)
return res2.data
} catch (error) {
console.log(error)
}
}
}
module.exports = Wallet