import bip32utils from '@oipwg/bip32-utils'
import Address from './Address'
import TransactionBuilder from './TransactionBuilder'
import { discovery } from './util'
// Class Constants
const GAP_LIMIT = 20
const CUSTOM_ADDRESS_FUNCTION = (node, network) => {
return { address: node, network: network }
}
/**
* A BIP32 Node that manages Derivation of Chains and Addresses. This is created from the [`bip32` npm package managed by `bitcoinjs`](https://github.com/bitcoinjs/bip32).
* @typedef {Object} bip32
* @example <caption>Spawn a Bitcoin bip32 Node</caption>
* import * as bip32 from 'bip32';
*
* let bip32Node = bip32.fromBase58("xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb")
* @example <caption>Spawn a Flo bip32 Node</caption>
* import * as bip32 from 'bip32';
* import { Networks } from '@oipwg/hdmw';
*
* let bip32Node = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*/
/**
* A BIP32 Chain manager. This is created from the [`@oipwg/bip32-utils` npm package managed by `oipwg`](https://github.com/oipwg/bip32-utils).
* @typedef {Object} bip32utilschain
* @example
* import * as bip32 from 'bip32';
* import bip32utils from '@oipwg/bip32-utils';
*
* let bip32Node = bip32.fromBase58("xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb")
* let chain = new bip32utils.Chain(bip32Node)
*/
/**
* Manages Chains and Addresses for a specific BIP32/BIP44 Account
*/
class Account {
/**
* Create a new Account to manage Chains and Addresses for based on a BIP32 Node
*
* ##### Examples
* Create a Bitcoin Account
* ```
* import { Account, Networks } from '@oipwg/hdmw';
*
* let accountMaster = bip32.fromBase58("xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb")
*
* let account = new Account(accountMaster, Networks.bitcoin);
* ```
* Create a Flo Account
* ```
* import { Account, Networks } from '@oipwg/hdmw';
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo);
* ```
* @param {bip32} accountMaster - The BIP32 Node to derive Chains and Addresses from.
* @param {CoinInfo} coin - The CoinInfo for the Account
* @param {Object} [options] - The Options of the Account
* @param {boolean} [options.discover=true] - Should the Account auto-discover Chains and Addresses
* @param {Object} [options.serializedData] - Serialized data to load the Account from
* @return {Account}
*/
constructor (accountMaster, coin, options) {
this.accountMaster = accountMaster
this.coin = coin || {}
const external = this.accountMaster.derive(0)
const internal = this.accountMaster.derive(1)
this.account = new bip32utils.Account([
new bip32utils.Chain(external, undefined, CUSTOM_ADDRESS_FUNCTION),
new bip32utils.Chain(internal, undefined, CUSTOM_ADDRESS_FUNCTION)
])
this.addresses = {}
this.chains = {
0: {
index: 0,
lastUpdate: 0
},
1: {
index: 1,
lastUpdate: 0
}
}
this.discover = true
if (options && options.discover !== undefined) { this.discover = options.discover }
// Discover both External and Internal chains
if (options && options.serializedData) { this.deserialize(options.serializedData) }
if (this.discover) {
this.discoverChains()
}
}
serialize () {
const addresses = this.getAddresses()
const serializedAddresses = addresses.map((address) => {
return address.serialize()
})
return {
extendedPrivateKey: this.getExtendedPrivateKey(),
addresses: serializedAddresses,
chains: this.chains
}
}
deserialize (serializedData) {
if (serializedData) {
// Rehydrate Addresses
if (serializedData.addresses) {
const rehydratedAddresses = []
for (const address of serializedData.addresses) {
rehydratedAddresses.push(new Address(address.wif, this.coin, address))
}
for (const address of rehydratedAddresses) {
this.addresses[address.getPublicAddress()] = address
}
}
// Rehydrate Chain info
if (serializedData.chains) {
this.chains = serializedData.chains
}
}
}
/**
* Get the Main Address for a specified Chain and Index on the Chain.
* @param {number} [chainNumber=0] - Number of the specific chain you want to get the Main Address for
* @param {number} [mainAddressNumber=0] - Index of the Main Address on the specified chain
* @example
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let address = account.getMainAddress()
* // address.getPublicAddress() = FPznv9i9iHX5vt4VMbH9x2LgUcrjtSn4cW
* @return {Address}
*/
getMainAddress (chainNumber, mainAddressNumber) {
return this.getAddress(chainNumber, mainAddressNumber)
}
/**
* Get the Address for a specified Chain and Index on the Chain.
* @param {number} [chainNumber=0] - Number of the specific chain you want to get the Address from
* @param {number} [addressNumber=0] - Index of the Address on the specified chain
* @example <caption>Get the address on Chain `0` at Index `10`</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let address = account.getAddress(0, 10)
* // address.getPublicAddress() = F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp
* @return {Address}
*/
getAddress (chainNumber, addressNumber) {
const addr = CUSTOM_ADDRESS_FUNCTION(this.account.getChain(chainNumber || 0).__parent.derive(addressNumber || 0), this.coin.network)
const tmpHydratedAddr = new Address(addr, this.coin, false)
// Attempt to match to address that we already have
if (this.addresses[tmpHydratedAddr.getPublicAddress()]) { return this.addresses[tmpHydratedAddr.getPublicAddress()] } else { this.addresses[tmpHydratedAddr.getPublicAddress()] = tmpHydratedAddr }
return tmpHydratedAddr
}
/**
* Get all derived Addresses for the entire Account, or just for a specific Chain.
* @param {number} [chainNumber] - Number of the specific chain you want to get the Addresses from
* @example <caption>Get all Addresses on the Account</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let addresses = account.getAddresses()
* // addresses = [Address, Address, Address]
* @example <caption>Get the addresses on Chain `0`</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let addresses = account.getAddresses(0)
* // addresses = [Address, Address, Address]
* @return {Array.<Address>}
*/
getAddresses (chainNumber) {
const addrs = []
if (chainNumber && typeof chainNumber === 'number') {
for (const addr in this.addresses) {
const chain = this.account.getChain(chainNumber)
const addresses = chain.addresses.map((ad) => {
return new Address(ad, this.coin, false)
})
for (const adr of addresses) {
if (adr.getPublicAddress() === this.addresses[addr].getPublicAddress()) {
addrs.push(this.addresses[addr])
}
}
}
} else {
for (const addr in this.addresses) {
addrs.push(this.addresses[addr])
}
}
return addrs
}
/**
* Get all Used Addresses (addresses that have received at least 1 tx) for the entire Account, or just for a specific Chain.
* @param {number} [chainNumber] - Number of the specific chain you want to get the Addresses from
* @example <caption>Get all Used Addresses on the Account</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let addresses = account.getUsedAddresses()
* // addresses = [Address, Address, Address]
* @example <caption>Get the addresses on Chain `0`</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let addresses = account.getUsedAddresses(0)
* // addresses = [Address, Address, Address]
* @return {Array.<Address>}
*/
getUsedAddresses (chainNumber) {
const usedAddresses = []
const allAddresses = this.getAddresses()
for (const address of allAddresses) {
if (address.getTotalReceived() > 0) { usedAddresses.push(address) }
}
return usedAddresses
}
/**
* Get the Balance for the entire Account
* @example
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* account.getBalance({ discover: true }).then((balance) => {
* console.log(balance);
* })
* @param {Object} [options] Specific options defining what balance to get back
* @param {Boolean} [options.discover=true] - Should the Account discover Chains and Addresses
* @param {string|Array.<string>} [options.addresses] - Address, or Addresses to get the balance of
* @param {number} [options.id] - The ID number to return when the Promise resolves
* @return {Promise<number>} - Returns a Promise that will resolve to the total balance.
*/
async getBalance (options) {
let discover = this.discover
if (options && options.discover !== undefined) { discover = options.discover }
if (discover) {
try {
await this.discoverChains()
} catch (e) {
throw new Error('Unable to discover Account Chains in Account getBalance! \n' + e)
}
}
let totalBal = 0
// Iterate through each of the addresses we have found
for (const addr in this.addresses) {
// Are we searching only for a single addresses balance?
if (options && options.addresses && typeof options.addresses === 'string') {
if (addr === options.addresses) {
totalBal += this.addresses[addr].getBalance()
}
// Are we searching for only the addresses in an array?
} else if (options && options.addresses && Array.isArray(options.addresses)) {
for (const ad of options.addresses) {
if (addr === ad) {
totalBal += this.addresses[addr].getBalance()
}
}
// If not the first two, then just add them all up :)
} else {
totalBal += this.addresses[addr].getBalance()
}
}
const balanceData = {
balance: totalBal
}
if (options && options.id) { balanceData.id = options.id }
return balanceData
}
/**
* Get the Next Chain Address for a specified chain
* @param {number} [chainNumber=0] - The specific chain that you want to get the next address from
* @example <caption>Get the next Chain Address on Chain #1</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let address = account.getNextChainAddress(1)
* @return {Address}
*/
getNextChainAddress (chainNumber) {
return new Address(this.account.getChain(chainNumber || 0).next(), this.coin, false)
}
/**
* Get the Next Change Address from the "Internal" chain
* @example
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let address = account.getNextChangeAddress()
* @return {Address}
*/
getNextChangeAddress () {
// We use Chain 1 since that is the "Internal" chain used for generating change addresses.
return this.getNextChainAddress(1)
}
/**
* 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 Account] - Define what public address(es) 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 = () => {
const sendFrom = []
const allAddresses = this.getAddresses()
// Check if we define what address we wish to send from
if (options.from) {
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 {
for (const address of allAddresses) {
if (address.getBalance() >= 0) {
sendFrom.push(address)
}
}
}
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)
}
if (options.discover === false) {
processPayment()
} else {
this.discoverChains().then(processPayment)
}
})
}
/**
* Get the Extended Private Key for the Account
* @example
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let extPrivateKey = account.getExtendedPrivateKey()
* // extPrivateKey = Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC
* @return {string}
*/
getExtendedPrivateKey () {
return this.accountMaster.toBase58()
}
/**
* Get the Extended Public Key for the Account
* @example
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let extPublicKey = account.getExtendedPublicKey()
* // extPublicKey = Fpub1BPo8vEQqDkoDQmDqcJ8WFHD331AMpd7VU7atCJsix8xbHwN6K9wfDLjZKnW9fUw5uJg8UJMLhQ5W7gTxv6DbkfPoeJbBpMaUHrULxzVnSy
* @return {string}
*/
getExtendedPublicKey () {
return this.accountMaster.neutered().toBase58()
}
/**
* Get the specified Chain number
* @param {number} chainNumber - The number of the chain you are requesting
* @example <caption>Get Chain 0</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* let chain = account.getChain(0)
* @return {bip32utils.Chain}
*/
getChain (chainNumber) {
return this.account.getChain(chainNumber)
}
async DiscoverChain (chainNumber, gapLimit) {
const chains = this.account.getChains()
const chain = chains[chainNumber].clone()
let discovered
try {
discovered = await discovery(chain, gapLimit, this.ChainPromise, chainNumber, this.coin)
} catch (e) {
throw new Error('Discovery error in DiscoverChain #' + chainNumber + ' \n' + e)
}
// throw away EACH unused address AFTER the last unused address
const unused = discovered.checked - discovered.used
for (let j = 1; j < unused; ++j) chain.pop()
// override the internal chain
this.account.chains[discovered.chainIndex] = chain
for (const address of discovered.addresses) { this.addresses[address.getPublicAddress()] = address }
return discovered
}
async ChainPromise (addresses, coin) {
const results = {}
const allAddresses = []
const addressPromises = []
for (const addr of addresses) {
const address = new Address(addr, coin, false)
const addressUpdatePromise = address.updateState()
// This will only be called for any rejections AFTER the first one,
// please take a look at the comment below for more info.
addressUpdatePromise.catch((e) => { console.warn(`An Address Discovery Promise failed during Account Discovery! ${e}\n${e.stack}`) })
addressPromises.push(addressUpdatePromise)
}
let promiseResponses = []
try {
promiseResponses = await Promise.all(addressPromises)
} catch (e) {
// This will still be called even though we use prom.catch() above.
// The first promise rejection will be caught here, all other promises
// that reject AFTER the first, will be caught in the above prom.catch() function.
throw new Error(`Account Discovery failure in ChainPromise! ${e}\n${e.stack}`)
}
for (const address of promiseResponses) {
results[address.getPublicAddress()] = address.getTotalReceived() > 0
// Store all addresses
allAddresses.push(address)
}
return { results: results, addresses: allAddresses }
}
/**
* Discover Used and Unused addresses for a specified Chain number
* @param {number} chainNumber - The number of the chain you wish to discover
* @example <caption>Discover Chain 0</caption>
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* account.discoverChain(0).then((acc) => {
* console.log(acc.getChain(0).addresses)
* })
* @return {Promise<Account>} - A Promise that once finished will resolve to the Account (now with discovery done)
*/
async discoverChain (chainNumber) {
try {
await this.DiscoverChain(chainNumber, GAP_LIMIT)
} catch (e) {
throw new Error('Unable to discoverChain #' + chainNumber + '! \n' + e)
}
this.chains[chainNumber] = { lastUpdate: Date.now() }
return this
}
/**
* Discover all Chains
* @example
* import * as bip32 from 'bip32'
* import { Account, Networks } from '@oipwg/hdmw'
*
* let accountMaster = bip32.fromBase58("Fprv4xQSjQhWzrCVzvgkjam897LUV1AfxMuG8FBz5ouGAcbyiVcDYmqh7R2Fi22wjA56GQdmoU1AzfxsEmVnc5RfjGrWmAiqvfzmj4cCL3fJiiC", Networks.flo.network)
*
* let account = new Account(accountMaster, Networks.flo, false);
* account.discoverChains().then((acc) => {
* console.log(acc.getChain(0).addresses)
* console.log(acc.getChain(1).addresses)
* })
* @return {Promise<Account>} - A Promise that once finished will resolve to the Account (now with discovery done)
*/
async discoverChains () {
const chainsToDiscover = [0, 1]
let account
// Do each chain one at a time in case it crashes and errors out.
for (const c of chainsToDiscover) {
try {
account = await this.discoverChain(c)
} catch (e) {
throw new Error('Unable to discoverChains! \n' + e)
}
}
return account
}
}
module.exports = Account