import * as bip32 from 'bip32'
import Account from './Account'
import TransactionBuilder from './TransactionBuilder'
const COIN_START = 0x80000000
/**
* Manage Accounts for a specific Coin
*/
class Coin {
/**
* Create a new Coin object to interact with Accounts and Chains for that coin. This spawns a BIP44 compatible wallet.
*
* ##### Examples
* Create a new Coin using a specified seed.
*```
*import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
*```
* Create a new Coin using a specified seed, don't auto discover.
*```
*import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin, false)
*```
* @param {string} node - BIP32 Node already derived to m/44'
* @param {CoinInfo} coin - The CoinInfo containing network & version variables
* @param {Object} [options] - The Options for spawning the Coin
* @param {boolean} [options.discover=true] - Should the Coin auto-discover Accounts and Chains
* @param {Object} [options.serializedData] - The Data to de-serialize from
* @return {Coin}
*/
constructor (node, coin, options) {
if (typeof node === 'string') { this.seed = node } else { this.seed = node.toBase58() }
this.coin = coin
this.discover = true
if (options && options.discover !== undefined) { this.discover = options.discover }
const purposeNode = bip32.fromBase58(this.seed)
purposeNode.network = this.coin.network
let bip44Num = this.coin.network.slip44
// Check if we need to convert the hexa to the index
if (bip44Num >= COIN_START) { bip44Num -= COIN_START }
this.root = purposeNode.derivePath(bip44Num + "'")
this.accounts = {}
if (options && options.serializedData) { this.deserialize(options.serializedData) }
if (this.discover) {
this.discoverAccounts()
}
}
serialize () {
const serializedAccounts = {}
for (const accountNumber in this.accounts) {
serializedAccounts[accountNumber] = this.accounts[accountNumber].serialize()
}
return {
name: this.coin.name,
network: this.coin.network,
seed: this.seed,
accounts: serializedAccounts
}
}
deserialize (serializedData) {
if (serializedData) {
if (serializedData.accounts) {
for (const accountNumber in serializedData.accounts) {
if (!Object.prototype.hasOwnProperty.call(serializedData.accounts, accountNumber)) continue
const accountMaster = bip32.fromBase58(serializedData.accounts[accountNumber].extendedPrivateKey, this.coin.network)
this.accounts[accountNumber] = new Account(accountMaster, this.coin, {
discover: false,
serializedData: serializedData.accounts[accountNumber]
})
}
}
}
}
/**
* Get the balance for the entire coin, or a specific address/array of addresses
* @param {Object} [options] - Specific options defining what balance to get back
* @param {Boolean} [options.discover=true] - Should the Coin discover Accounts
* @param {number|Array.<number>} [options.accounts=All Accounts in Coin] - Get Balance for defined Accounts
* @param {string|Array.<string>} [options.addresses=All Addresses in each Account in Coin] - Get Balance for defined Addresses
* @example <caption> Get Balance for entire Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* bitcoin.getBalance().then((balance) => {
* console.log(balance)
* })
* @return {Promise<number>} A Promise that will resolve to the balance of the entire Coin
*/
async getBalance (options) {
if (!options || (options && options.discover === undefined) || (options && options.discover === true)) {
try {
await this.discoverAccounts()
} catch (e) { throw new Error('Unable to Discover Coin Accounts for getBalance! \n' + e) }
}
const accountsToSearch = []
// Check if we are an array (ex. [0,1,2]) or just a number (ex. 1)
if (options && Array.isArray(options.accounts)) {
for (const accNum of options.accounts) {
if (!isNaN(accNum)) {
accountsToSearch.push(accNum)
}
}
} else if (options && !isNaN(options.accounts)) {
accountsToSearch.push(options.accounts)
} else {
for (const accNum in this.accounts) {
accountsToSearch.push(accNum)
}
}
let totalBalance = 0
let addrsToSearch
if (options && options.addresses && (typeof options.addresses === 'string' || Array.isArray(options.addresses))) {
addrsToSearch = options.addresses
}
for (const accNum of accountsToSearch) {
if (this.accounts[accNum]) {
try {
const balanceRes = await this.accounts[accNum].getBalance({
discover: false,
addresses: addrsToSearch,
id: accNum
})
totalBalance += balanceRes.balance
} catch (e) { throw new Error('Unable to get Coin balance! \n' + e) }
}
}
return totalBalance
}
/**
* Get a specific Address
* @param {number} [accountNumber=0] - Number of the account you wish to get the Address from
* @param {number} [chainNumber=0] - Number of the Chain you wish to get the Address from
* @param {number} [addressIndex=0] - Index of the Address you wish to get
* @return {Address}
*/
getAddress (accountNumber, chainNumber, addressIndex) {
return this.getAccount(accountNumber || 0).getAddress(chainNumber, addressIndex)
}
/**
* Get the Main Address for a specific Account number.
* This is the Address at index 0 on the External Chain of the Account.
* @param {number} [accountNumber=0] - Number of the Account you wish to get
* @example <caption>Get Main Address for Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let mainAddress = bitcoin.getMainAddress()
* @example <caption>Get Main Address for Account #1 on Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let mainAddress = bitcoin.getMainAddress(1)
* @return {Address}
*/
getMainAddress (accountNumber) {
return this.getAccount(accountNumber || 0).getMainAddress()
}
/**
* Send a Payment to specified Addresses and Amounts
* @param {Object} options - the options for the specific transaction being sent
* @param {OutputAddress|Array.<OutputAddress>} options.to - Define outputs for the Payment
* @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 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
*/
sendPayment (options) {
return new Promise((resolve, reject) => {
if (!options) { reject(new Error('You must define your payment options!')) }
const processPayment = () => {
let sendFrom = []
let allAddresses = []
// Add all Addresses from selected accounts to array
for (const account in this.accounts) {
// Check if we are defining what accounts to send the payment from
if (options.fromAccounts) {
// Check if it is a single account number, or an array of account numbers
if (typeof options.fromAccounts === 'number') {
// If we match the passed account number, set the grabbed addresses
if (options.fromAccounts === parseInt(account)) {
allAddresses = this.accounts[account].getAddresses()
}
} else if (Array.isArray(options.fromAccounts)) {
// If we are an array, iterate through
for (const acs of options.fromAccounts) {
if (acs === parseInt(account)) {
allAddresses = allAddresses.concat(this.accounts[account].getAddresses())
}
}
}
} else {
allAddresses = allAddresses.concat(this.accounts[account].getAddresses())
}
}
// Check if we define what address we wish to send from
if (options.from) {
// Check if it is a single from address or an array
if (typeof options.from === 'string') {
for (const address of allAddresses) {
if (address.getPublicAddress() === options.from) {
sendFrom.push(address)
}
}
} else if (Array.isArray(options.from)) {
for (const adr of options.from) {
for (const address of allAddresses) {
if (address.getPublicAddress() === adr) {
sendFrom.push(address)
}
}
}
}
// else add all the addresses on the Account that have received any txs
} else {
sendFrom = allAddresses
}
if (sendFrom.length === 0) {
reject(new Error('No Addresses match defined options.from Addresses!'))
return
}
const newOpts = options
newOpts.from = sendFrom
const txb = new TransactionBuilder(this.coin, newOpts)
txb.sendTX().then(resolve).catch(reject)
}
if (options.discover === false) {
processPayment()
} else {
this.discoverAccounts().then(processPayment).catch(reject)
}
})
}
/**
* Get the Extended Private Key for the root path. This is derived at m/44'/coinType'
* @example <caption>Get the Extended Private Key for the entire Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let extPrivateKey = bitcoin.getExtendedPrivateKey()
* // extPrivateKey = xprv9x8MQtHNRrGgrnWPkUxjUC57DWKgkjobwAYUFedxVa2FAA5qaQuGqLkJnVcszqomTar51PCR8JiKnGGgzK9eJKGjbpUirKPVHxH2PU2Rc93
* @return {string} The Extended Private Key
*/
getExtendedPrivateKey () {
return this.root.toBase58()
}
/**
* Get the Neutered Extended Public Key for the root path. This is derived at m/44'/coinType'
* @example <caption>Get the Extended Private Key for the entire Coin</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let extPublicKey = bitcoin.getExtendedPrivateKey()
* // extPublicKey = xpub6B7hpPpGGDpz5GarrWVjqL1qmYABACXTJPU5433a3uZE2xQz7xDXP94ndkjrxogjordTDSDaHY4i5G4HqRH6E9FJZk2F4ED4cbnprW2Vm9v
* @return {string} The Extended Public Key
*/
getExtendedPublicKey () {
return this.root.neutered().toBase58()
}
/**
* Get the Account at the specified number
* @param {number} [accountNumber=0]
* @example <caption>Get Default Account</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let account = bitcoin.getAccount()
* @example <caption>Get Account #1</caption>
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let account = bitcoin.getAccount(1)
* @return {Account}
*/
getAccount (accountNumber) {
let num = accountNumber || 0
if (typeof accountNumber === 'string' && !isNaN(parseInt(accountNumber))) { num = parseInt(accountNumber) }
if (!this.accounts[num]) { return this.addAccount(num) }
return this.accounts[num]
}
/**
* Get all Accounts on the Coin
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let accounts = bitcoin.getAccounts()
* // accounts = {
* // 0: Account,
* // 1: Account
* // }
* @return {Object.<number, Account>} Returns a JSON object with accounts
*/
getAccounts () {
return this.accounts
}
/**
* Add the Account at the specified number, if it already exists, it returns the Account.
* If the Account does not exist, it will create it and then return it.
* @param {number} [accountNumber=0]
* @param {Boolean} [discover=discover Set in Coin Constructor] - Should the Account start auto-discovery.
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let account = bitcoin.addAccount(1)
* @return {Account}
*/
addAccount (accountNumber, discover) {
let num = accountNumber || 0
if (typeof accountNumber === 'string' && !isNaN(parseInt(accountNumber))) { num = parseInt(accountNumber) }
// if the account has already been added, just return
if (this.accounts[num]) { return this.getAccount(num) }
const accountMaster = this.root.deriveHardened(num)
let shouldDiscover
if (discover !== undefined) { shouldDiscover = discover } else { shouldDiscover = this.discover }
this.accounts[num] = new Account(accountMaster, this.coin, { discover: shouldDiscover })
return this.getAccount(num)
}
/**
* Get the CoinInfo for the Coin
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin)
* let coinInfo = bitcoin.getCoinInfo()
* // coinInfo = Networks.bitcoin
* @return {CoinInfo}
*/
getCoinInfo () {
return this.coin
}
getHighestAccountNumber () {
let highestAccountNumber = 0
for (const accNum in this.accounts) {
if (accNum > highestAccountNumber) { highestAccountNumber = accNum }
}
return parseInt(highestAccountNumber)
}
/**
* Discover all Accounts for the Coin
* @example
* import { Coin, Networks } from '@oipwg/hdmw'
*
* let bitcoin = new Coin('00000000000000000000000000000000', Networks.bitcoin, false)
* bitcoin.discoverAccounts().then((accounts) => {
* console.log(accounts.length)
* })
* @return {Promise<Array.<Account>>} Returns a Promise that will resolve to an Array of Accounts once complete
*/
async discoverAccounts () {
// Reset the internal accounts
this.accounts = {}
// Get the Account #0 and start discovery there.
try {
await this.getAccount(0).discoverChains()
} catch (e) { throw new Error('Unable to discoverAccounts! \n' + e) }
while (this.accounts[this.getHighestAccountNumber()].getUsedAddresses().length > 0) {
try {
await this.getAccount(this.getHighestAccountNumber() + 1).discoverChains()
} catch (e) { throw new Error('Unable to discover account #' + (this.getHighestAccountNumber() + 1) + '\n' + e) }
}
const discoveredAccounts = []
for (const accNum in this.accounts) {
discoveredAccounts.push(this.accounts[accNum])
}
return discoveredAccounts
}
}
module.exports = Coin