diff --git a/class/abstract-hd-electrum-wallet.js b/class/abstract-hd-electrum-wallet.js new file mode 100644 index 000000000..7e1bca835 --- /dev/null +++ b/class/abstract-hd-electrum-wallet.js @@ -0,0 +1,891 @@ +import { NativeModules } from 'react-native'; +import bip39 from 'bip39'; +import BigNumber from 'bignumber.js'; +import b58 from 'bs58check'; +import { AbstractHDWallet } from './abstract-hd-wallet'; +const bitcoin = require('bitcoinjs-lib'); +const BlueElectrum = require('../BlueElectrum'); +const HDNode = require('bip32'); +const coinSelectAccumulative = require('coinselect/accumulative'); +const coinSelectSplit = require('coinselect/split'); + +const { RNRandomBytes } = NativeModules; + +export class AbstractHDElectrumWallet extends AbstractHDWallet { + static type = 'abstract'; + static typeReadable = 'abstract'; + static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68 + static finalRBFSequence = 4294967295; // 0xFFFFFFFF + + constructor() { + super(); + this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed + this._balances_by_internal_index = {}; + + this._txs_by_external_index = {}; + this._txs_by_internal_index = {}; + + this._utxo = []; + } + + /** + * @inheritDoc + */ + getBalance() { + let ret = 0; + for (let bal of Object.values(this._balances_by_external_index)) { + ret += bal.c; + } + for (let bal of Object.values(this._balances_by_internal_index)) { + ret += bal.c; + } + return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); + } + + /** + * @inheritDoc + */ + timeToRefreshTransaction() { + for (let tx of this.getTransactions()) { + if (tx.confirmations < 7) return true; + } + return false; + } + + getUnconfirmedBalance() { + let ret = 0; + for (let bal of Object.values(this._balances_by_external_index)) { + ret += bal.u; + } + for (let bal of Object.values(this._balances_by_internal_index)) { + ret += bal.u; + } + return ret; + } + + async generate() { + let that = this; + return new Promise(function(resolve) { + if (typeof RNRandomBytes === 'undefined') { + // CLI/CI environment + // crypto should be provided globally by test launcher + return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line + if (err) throw err; + that.secret = bip39.entropyToMnemonic(buf.toString('hex')); + resolve(); + }); + } + + // RN environment + RNRandomBytes.randomBytes(32, (err, bytes) => { + if (err) throw new Error(err); + let b = Buffer.from(bytes, 'base64').toString('hex'); + that.secret = bip39.entropyToMnemonic(b); + resolve(); + }); + }); + } + + _getExternalWIFByIndex(index) { + return this._getWIFByIndex(false, index); + } + + _getInternalWIFByIndex(index) { + return this._getWIFByIndex(true, index); + } + + /** + * Get internal/external WIF by wallet index + * @param {Boolean} internal + * @param {Number} index + * @returns {string|false} Either string WIF or FALSE if error happened + * @private + */ + _getWIFByIndex(internal, index) { + if (!this.secret) return false; + const mnemonic = this.secret; + const seed = bip39.mnemonicToSeed(mnemonic); + const root = HDNode.fromSeed(seed); + const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; + const child = root.derivePath(path); + + return child.toWIF(); + } + + _getNodeAddressByIndex(node, index) { + index = index * 1; // cast to int + if (node === 0) { + if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit + } + + if (node === 1) { + if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit + } + + if (node === 0 && !this._node0) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node0 = hdNode.derive(node); + } + + if (node === 1 && !this._node1) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node1 = hdNode.derive(node); + } + + let address; + if (node === 0) { + address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index)); + } + + if (node === 1) { + address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index)); + } + + if (node === 0) { + return (this.external_addresses_cache[index] = address); + } + + if (node === 1) { + return (this.internal_addresses_cache[index] = address); + } + } + + _getNodePubkeyByIndex(node, index) { + index = index * 1; // cast to int + + if (node === 0 && !this._node0) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node0 = hdNode.derive(node); + } + + if (node === 1 && !this._node1) { + const xpub = this.constructor._zpubToXpub(this.getXpub()); + const hdNode = HDNode.fromBase58(xpub); + this._node1 = hdNode.derive(node); + } + + if (node === 0) { + return this._node0.derive(index).publicKey; + } + + if (node === 1) { + return this._node1.derive(index).publicKey; + } + } + + _getExternalAddressByIndex(index) { + return this._getNodeAddressByIndex(0, index); + } + + _getInternalAddressByIndex(index) { + return this._getNodeAddressByIndex(1, index); + } + + /** + * Returning zpub actually, not xpub. Keeping same method name + * for compatibility. + * + * @return {String} zpub + */ + getXpub() { + if (this._xpub) { + return this._xpub; // cache hit + } + // first, getting xpub + const mnemonic = this.secret; + const seed = bip39.mnemonicToSeed(mnemonic); + const root = HDNode.fromSeed(seed); + + const path = "m/84'/0'/0'"; + const child = root.derivePath(path).neutered(); + const xpub = child.toBase58(); + + // bitcoinjs does not support zpub yet, so we just convert it from xpub + let data = b58.decode(xpub); + data = data.slice(4); + data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]); + this._xpub = b58.encode(data); + + return this._xpub; + } + + /** + * @inheritDoc + */ + async fetchTransactions() { + // if txs are absent for some internal address in hierarchy - this is a sign + // we should fetch txs for that address + // OR if some address has unconfirmed balance - should fetch it's txs + // OR some tx for address is unconfirmed + // OR some tx has < 7 confirmations + + // fetching transactions in batch: first, getting batch history for all addresses, + // then batch fetching all involved txids + // finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs) + // then we combine it all together + + let addresses2fetch = []; + + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + // external addresses first + let hasUnconfirmed = false; + this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; + for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; + + if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { + addresses2fetch.push(this._getExternalAddressByIndex(c)); + } + } + + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + // next, internal addresses + let hasUnconfirmed = false; + this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; + for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; + + if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { + addresses2fetch.push(this._getInternalAddressByIndex(c)); + } + } + + // first: batch fetch for all addresses histories + let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch); + let txs = {}; + for (let history of Object.values(histories)) { + for (let tx of history) { + txs[tx.tx_hash] = tx; + } + } + + // next, batch fetching each txid we got + let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs)); + + // now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too. + // then we combine all this data (we need inputs to see source addresses and amounts) + let vinTxids = []; + for (let txdata of Object.values(txdatas)) { + for (let vin of txdata.vin) { + vinTxids.push(vin.txid); + } + } + let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids); + + // fetched all transactions from our inputs. now we need to combine it. + // iterating all _our_ transactions: + for (let txid of Object.keys(txdatas)) { + // iterating all inputs our our single transaction: + for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) { + let inpTxid = txdatas[txid].vin[inpNum].txid; + let inpVout = txdatas[txid].vin[inpNum].vout; + // got txid and output number of _previous_ transaction we shoud look into + if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) { + // extracting amount & addresses from previous output and adding it to _our_ input: + txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses; + txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value; + } + } + } + + // now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid + // or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations); + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations); + } + + // now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index + + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + for (let tx of Object.values(txdatas)) { + for (let vin of tx.vin) { + if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { + if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_external_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_external_index[c].push(clonedTx); + } + } + for (let vout of tx.vout) { + if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { + if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_external_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_external_index[c].push(clonedTx); + } + } + } + } + + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + for (let tx of Object.values(txdatas)) { + for (let vin of tx.vin) { + if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { + if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_internal_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_internal_index[c].push(clonedTx); + } + } + for (let vout of tx.vout) { + if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { + // this TX is related to our address + this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + // trying to replace tx if it exists already (because it has lower confirmations, for example) + let replaced = false; + for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { + if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { + replaced = true; + this._txs_by_internal_index[c][cc] = clonedTx; + } + } + if (!replaced) this._txs_by_internal_index[c].push(clonedTx); + } + } + } + } + + this._lastTxFetch = +new Date(); + } + + getTransactions() { + let txs = []; + + for (let addressTxs of Object.values(this._txs_by_external_index)) { + txs = txs.concat(addressTxs); + } + for (let addressTxs of Object.values(this._txs_by_internal_index)) { + txs = txs.concat(addressTxs); + } + + let ret = []; + for (let tx of txs) { + tx.received = tx.blocktime * 1000; + if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed + tx.confirmations = tx.confirmations || 0; // unconfirmed + tx.hash = tx.txid; + tx.value = 0; + + for (let vin of tx.inputs) { + // if input (spending) goes from our address - we are loosing! + if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) { + tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); + } + } + + for (let vout of tx.outputs) { + // when output goes to our address - this means we are gaining! + if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) { + tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber(); + } + } + ret.push(tx); + } + + // now, deduplication: + let usedTxIds = {}; + let ret2 = []; + for (let tx of ret) { + if (!usedTxIds[tx.txid]) ret2.push(tx); + usedTxIds[tx.txid] = 1; + } + + return ret2.sort(function(a, b) { + return b.received - a.received; + }); + } + + async _binarySearchIterationForInternalAddress(index) { + const gerenateChunkAddresses = chunkNum => { + let ret = []; + for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { + ret.push(this._getInternalAddressByIndex(c)); + } + return ret; + }; + + let lastChunkWithUsedAddressesNum = null; + let lastHistoriesWithUsedAddresses = null; + for (let c = 0; c < Math.round(index / this.gap_limit); c++) { + let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); + if (this.constructor._getTransactionsFromHistories(histories).length > 0) { + // in this particular chunk we have used addresses + lastChunkWithUsedAddressesNum = c; + lastHistoriesWithUsedAddresses = histories; + } else { + // empty chunk. no sense searching more chunks + break; + } + } + + let lastUsedIndex = 0; + + if (lastHistoriesWithUsedAddresses) { + // now searching for last used address in batch lastChunkWithUsedAddressesNum + for ( + let c = lastChunkWithUsedAddressesNum * this.gap_limit; + c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; + c++ + ) { + let address = this._getInternalAddressByIndex(c); + if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { + lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued + } + } + } + + return lastUsedIndex; + } + + async _binarySearchIterationForExternalAddress(index) { + const gerenateChunkAddresses = chunkNum => { + let ret = []; + for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { + ret.push(this._getExternalAddressByIndex(c)); + } + return ret; + }; + + let lastChunkWithUsedAddressesNum = null; + let lastHistoriesWithUsedAddresses = null; + for (let c = 0; c < Math.round(index / this.gap_limit); c++) { + let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); + if (this.constructor._getTransactionsFromHistories(histories).length > 0) { + // in this particular chunk we have used addresses + lastChunkWithUsedAddressesNum = c; + lastHistoriesWithUsedAddresses = histories; + } else { + // empty chunk. no sense searching more chunks + break; + } + } + + let lastUsedIndex = 0; + + if (lastHistoriesWithUsedAddresses) { + // now searching for last used address in batch lastChunkWithUsedAddressesNum + for ( + let c = lastChunkWithUsedAddressesNum * this.gap_limit; + c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; + c++ + ) { + let address = this._getExternalAddressByIndex(c); + if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { + lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued + } + } + } + + return lastUsedIndex; + } + + async fetchBalance() { + try { + if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { + // doing binary search for last used address: + this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000); + this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000); + } // end rescanning fresh wallet + + // finally fetching balance + await this._fetchBalance(); + } catch (err) { + console.warn(err); + } + } + + async _fetchBalance() { + // probing future addressess in hierarchy whether they have any transactions, in case + // our 'next free addr' pointers are lagging behind + let tryAgain = false; + let txs = await BlueElectrum.getTransactionsByAddress( + this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), + ); + if (txs.length > 0) { + // whoa, someone uses our wallet outside! better catch up + this.next_free_address_index += this.gap_limit; + tryAgain = true; + } + + txs = await BlueElectrum.getTransactionsByAddress( + this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), + ); + if (txs.length > 0) { + this.next_free_change_address_index += this.gap_limit; + tryAgain = true; + } + + // FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ] + + if (tryAgain) return this._fetchBalance(); + + // next, business as usuall. fetch balances + + let addresses2fetch = []; + + // generating all involved addresses. + // basically, refetch all from index zero to maximum. doesnt matter + // since we batch them 100 per call + + // external + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + addresses2fetch.push(this._getExternalAddressByIndex(c)); + } + + // internal + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + addresses2fetch.push(this._getInternalAddressByIndex(c)); + } + + let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch); + + // converting to a more compact internal format + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + let addr = this._getExternalAddressByIndex(c); + if (balances.addresses[addr]) { + // first, if balances differ from what we store - we delete transactions for that + // address so next fetchTransactions() will refetch everything + if (this._balances_by_external_index[c]) { + if ( + this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || + this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed + ) { + delete this._txs_by_external_index[c]; + } + } + // update local representation of balances on that address: + this._balances_by_external_index[c] = { + c: balances.addresses[addr].confirmed, + u: balances.addresses[addr].unconfirmed, + }; + } + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + let addr = this._getInternalAddressByIndex(c); + if (balances.addresses[addr]) { + // first, if balances differ from what we store - we delete transactions for that + // address so next fetchTransactions() will refetch everything + if (this._balances_by_internal_index[c]) { + if ( + this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || + this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed + ) { + delete this._txs_by_internal_index[c]; + } + } + // update local representation of balances on that address: + this._balances_by_internal_index[c] = { + c: balances.addresses[addr].confirmed, + u: balances.addresses[addr].unconfirmed, + }; + } + } + + this._lastBalanceFetch = +new Date(); + } + + async fetchUtxo() { + // considering only confirmed balance + let addressess = []; + + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { + addressess.push(this._getExternalAddressByIndex(c)); + } + } + + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { + addressess.push(this._getInternalAddressByIndex(c)); + } + } + + this._utxo = []; + for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { + this._utxo = this._utxo.concat(arr); + } + + // backward compatibility TODO: remove when we make sure `.utxo` is not used + this.utxo = this._utxo; + // this belongs in `.getUtxo()` + for (let u of this.utxo) { + u.txid = u.txId; + u.amount = u.value; + u.wif = this._getWifForAddress(u.address); + u.confirmations = u.height ? 1 : 0; + } + } + + getUtxo() { + return this._utxo; + } + + _getDerivationPathByAddress(address) { + const path = "m/84'/0'/0'"; + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c; + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c; + } + + return false; + } + + _getPubkeyByAddress(address) { + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c); + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c); + } + + return false; + } + + weOwnAddress(address) { + for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { + if (this._getExternalAddressByIndex(c) === address) return true; + } + for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { + if (this._getInternalAddressByIndex(c) === address) return true; + } + return false; + } + + /** + * @deprecated + */ + createTx(utxos, amount, fee, address) { + throw new Error('Deprecated'); + } + + /** + * + * @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos + * @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) + * @param feeRate {Number} satoshi per byte + * @param changeAddress {String} Excessive coins will go back to that address + * @param sequence {Number} Used in RBF + * @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case + * @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} + */ + createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) { + if (!changeAddress) throw new Error('No change address provided'); + sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence; + + let algo = coinSelectAccumulative; + if (targets.length === 1 && targets[0] && !targets[0].value) { + // we want to send MAX + algo = coinSelectSplit; + } + + let { inputs, outputs, fee } = algo(utxos, targets, feeRate); + + // .inputs and .outputs will be undefined if no solution was found + if (!inputs || !outputs) { + throw new Error('Not enough balance. Try sending smaller amount'); + } + + let psbt = new bitcoin.Psbt(); + + let c = 0; + let keypairs = {}; + let values = {}; + + inputs.forEach(input => { + let keyPair; + if (!skipSigning) { + // skiping signing related stuff + keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address)); + keypairs[c] = keyPair; + } + values[c] = input.value; + c++; + if (!skipSigning) { + // skiping signing related stuff + if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); + } + let pubkey = this._getPubkeyByAddress(input.address); + let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); + // this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting + // should be from root. basically, fingerprint should be provided from outside by user when importing zpub + let path = this._getDerivationPathByAddress(input.address); + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + psbt.addInput({ + hash: input.txId, + index: input.vout, + sequence, + bip32Derivation: [ + { + masterFingerprint, + path, + pubkey, + }, + ], + witnessUtxo: { + script: p2wpkh.output, + value: input.value, + }, + }); + }); + + outputs.forEach(output => { + // if output has no address - this is change output + let change = false; + if (!output.address) { + change = true; + output.address = changeAddress; + } + + let path = this._getDerivationPathByAddress(output.address); + let pubkey = this._getPubkeyByAddress(output.address); + let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); + // this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting + // should be from root. basically, fingerprint should be provided from outside by user when importing zpub + + let outputData = { + address: output.address, + value: output.value, + }; + + if (change) { + outputData['bip32Derivation'] = [ + { + masterFingerprint, + path, + pubkey, + }, + ]; + } + + psbt.addOutput(outputData); + }); + + if (!skipSigning) { + // skiping signing related stuff + for (let cc = 0; cc < c; cc++) { + psbt.signInput(cc, keypairs[cc]); + } + } + + let tx; + if (!skipSigning) { + tx = psbt.finalizeAllInputs().extractTransaction(); + } + return { tx, inputs, outputs, fee, psbt }; + } + + /** + * Combines 2 PSBTs into final transaction from which you can + * get HEX and broadcast + * + * @param base64one {string} + * @param base64two {string} + * @returns {Transaction} + */ + combinePsbt(base64one, base64two) { + const final1 = bitcoin.Psbt.fromBase64(base64one); + const final2 = bitcoin.Psbt.fromBase64(base64two); + final1.combine(final2); + return final1.finalizeAllInputs().extractTransaction(); + } + + /** + * Creates Segwit Bech32 Bitcoin address + * + * @param hdNode + * @returns {String} + */ + static _nodeToBech32SegwitAddress(hdNode) { + return bitcoin.payments.p2wpkh({ + pubkey: hdNode.publicKey, + }).address; + } + + /** + * Converts zpub to xpub + * + * @param {String} zpub + * @returns {String} xpub + */ + static _zpubToXpub(zpub) { + let data = b58.decode(zpub); + data = data.slice(4); + data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); + + return b58.encode(data); + } + + static _getTransactionsFromHistories(histories) { + let txs = []; + for (let history of Object.values(histories)) { + for (let tx of history) { + txs.push(tx); + } + } + return txs; + } + + /** + * Broadcast txhex. Can throw an exception if failed + * + * @param {String} txhex + * @returns {Promise} + */ + async broadcastTx(txhex) { + let broadcast = await BlueElectrum.broadcastV2(txhex); + console.log({ broadcast }); + if (broadcast.indexOf('successfully') !== -1) return true; + return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok + } +} diff --git a/class/hd-segwit-bech32-wallet.js b/class/hd-segwit-bech32-wallet.js index 00b3eb2dd..fe2776870 100644 --- a/class/hd-segwit-bech32-wallet.js +++ b/class/hd-segwit-bech32-wallet.js @@ -1,898 +1,23 @@ -import { AbstractHDWallet } from './abstract-hd-wallet'; -import { NativeModules } from 'react-native'; -import bip39 from 'bip39'; -import BigNumber from 'bignumber.js'; -import b58 from 'bs58check'; -const bitcoin = require('bitcoinjs-lib'); -const BlueElectrum = require('../BlueElectrum'); -const HDNode = require('bip32'); -const coinSelectAccumulative = require('coinselect/accumulative'); -const coinSelectSplit = require('coinselect/split'); - -const { RNRandomBytes } = NativeModules; +import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; /** * HD Wallet (BIP39). * In particular, BIP84 (Bech32 Native Segwit) * @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki */ -export class HDSegwitBech32Wallet extends AbstractHDWallet { +export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet { static type = 'HDsegwitBech32'; static typeReadable = 'HD SegWit (BIP84 Bech32 Native)'; - static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68 - static finalRBFSequence = 4294967295; // 0xFFFFFFFF - constructor() { - super(); - this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed - this._balances_by_internal_index = {}; - - this._txs_by_external_index = {}; - this._txs_by_internal_index = {}; - - this._utxo = []; + allowSend() { + return true; } allowBatchSend() { return true; } - allowSendMax(): boolean { + allowSendMax() { return true; } - - /** - * @inheritDoc - */ - getBalance() { - let ret = 0; - for (let bal of Object.values(this._balances_by_external_index)) { - ret += bal.c; - } - for (let bal of Object.values(this._balances_by_internal_index)) { - ret += bal.c; - } - return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); - } - - /** - * @inheritDoc - */ - timeToRefreshTransaction() { - for (let tx of this.getTransactions()) { - if (tx.confirmations < 7) return true; - } - return false; - } - - getUnconfirmedBalance() { - let ret = 0; - for (let bal of Object.values(this._balances_by_external_index)) { - ret += bal.u; - } - for (let bal of Object.values(this._balances_by_internal_index)) { - ret += bal.u; - } - return ret; - } - - allowSend() { - return true; - } - - async generate() { - let that = this; - return new Promise(function(resolve) { - if (typeof RNRandomBytes === 'undefined') { - // CLI/CI environment - // crypto should be provided globally by test launcher - return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line - if (err) throw err; - that.secret = bip39.entropyToMnemonic(buf.toString('hex')); - resolve(); - }); - } - - // RN environment - RNRandomBytes.randomBytes(32, (err, bytes) => { - if (err) throw new Error(err); - let b = Buffer.from(bytes, 'base64').toString('hex'); - that.secret = bip39.entropyToMnemonic(b); - resolve(); - }); - }); - } - - _getExternalWIFByIndex(index) { - return this._getWIFByIndex(false, index); - } - - _getInternalWIFByIndex(index) { - return this._getWIFByIndex(true, index); - } - - /** - * Get internal/external WIF by wallet index - * @param {Boolean} internal - * @param {Number} index - * @returns {string|false} Either string WIF or FALSE if error happened - * @private - */ - _getWIFByIndex(internal, index) { - if (!this.secret) return false; - const mnemonic = this.secret; - const seed = bip39.mnemonicToSeed(mnemonic); - const root = HDNode.fromSeed(seed); - const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; - const child = root.derivePath(path); - - return child.toWIF(); - } - - _getNodeAddressByIndex(node, index) { - index = index * 1; // cast to int - if (node === 0) { - if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit - } - - if (node === 1) { - if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit - } - - if (node === 0 && !this._node0) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node0 = hdNode.derive(node); - } - - if (node === 1 && !this._node1) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node1 = hdNode.derive(node); - } - - let address; - if (node === 0) { - address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index)); - } - - if (node === 1) { - address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index)); - } - - if (node === 0) { - return (this.external_addresses_cache[index] = address); - } - - if (node === 1) { - return (this.internal_addresses_cache[index] = address); - } - } - - _getNodePubkeyByIndex(node, index) { - index = index * 1; // cast to int - - if (node === 0 && !this._node0) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node0 = hdNode.derive(node); - } - - if (node === 1 && !this._node1) { - const xpub = this.constructor._zpubToXpub(this.getXpub()); - const hdNode = HDNode.fromBase58(xpub); - this._node1 = hdNode.derive(node); - } - - if (node === 0) { - return this._node0.derive(index).publicKey; - } - - if (node === 1) { - return this._node1.derive(index).publicKey; - } - } - - _getExternalAddressByIndex(index) { - return this._getNodeAddressByIndex(0, index); - } - - _getInternalAddressByIndex(index) { - return this._getNodeAddressByIndex(1, index); - } - - /** - * Returning zpub actually, not xpub. Keeping same method name - * for compatibility. - * - * @return {String} zpub - */ - getXpub() { - if (this._xpub) { - return this._xpub; // cache hit - } - // first, getting xpub - const mnemonic = this.secret; - const seed = bip39.mnemonicToSeed(mnemonic); - const root = HDNode.fromSeed(seed); - - const path = "m/84'/0'/0'"; - const child = root.derivePath(path).neutered(); - const xpub = child.toBase58(); - - // bitcoinjs does not support zpub yet, so we just convert it from xpub - let data = b58.decode(xpub); - data = data.slice(4); - data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]); - this._xpub = b58.encode(data); - - return this._xpub; - } - - /** - * @inheritDoc - */ - async fetchTransactions() { - // if txs are absent for some internal address in hierarchy - this is a sign - // we should fetch txs for that address - // OR if some address has unconfirmed balance - should fetch it's txs - // OR some tx for address is unconfirmed - // OR some tx has < 7 confirmations - - // fetching transactions in batch: first, getting batch history for all addresses, - // then batch fetching all involved txids - // finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs) - // then we combine it all together - - let addresses2fetch = []; - - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - // external addresses first - let hasUnconfirmed = false; - this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; - for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; - - if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { - addresses2fetch.push(this._getExternalAddressByIndex(c)); - } - } - - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - // next, internal addresses - let hasUnconfirmed = false; - this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; - for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; - - if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { - addresses2fetch.push(this._getInternalAddressByIndex(c)); - } - } - - // first: batch fetch for all addresses histories - let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch); - let txs = {}; - for (let history of Object.values(histories)) { - for (let tx of history) { - txs[tx.tx_hash] = tx; - } - } - - // next, batch fetching each txid we got - let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs)); - - // now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too. - // then we combine all this data (we need inputs to see source addresses and amounts) - let vinTxids = []; - for (let txdata of Object.values(txdatas)) { - for (let vin of txdata.vin) { - vinTxids.push(vin.txid); - } - } - let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids); - - // fetched all transactions from our inputs. now we need to combine it. - // iterating all _our_ transactions: - for (let txid of Object.keys(txdatas)) { - // iterating all inputs our our single transaction: - for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) { - let inpTxid = txdatas[txid].vin[inpNum].txid; - let inpVout = txdatas[txid].vin[inpNum].vout; - // got txid and output number of _previous_ transaction we shoud look into - if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) { - // extracting amount & addresses from previous output and adding it to _our_ input: - txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses; - txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value; - } - } - } - - // now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid - // or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations); - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations); - } - - // now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index - - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - for (let tx of Object.values(txdatas)) { - for (let vin of tx.vin) { - if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { - if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_external_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_external_index[c].push(clonedTx); - } - } - for (let vout of tx.vout) { - if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { - if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_external_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_external_index[c].push(clonedTx); - } - } - } - } - - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - for (let tx of Object.values(txdatas)) { - for (let vin of tx.vin) { - if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { - if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_internal_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_internal_index[c].push(clonedTx); - } - } - for (let vout of tx.vout) { - if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { - // this TX is related to our address - this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; - let clonedTx = Object.assign({}, tx); - clonedTx.inputs = tx.vin.slice(0); - clonedTx.outputs = tx.vout.slice(0); - delete clonedTx.vin; - delete clonedTx.vout; - - // trying to replace tx if it exists already (because it has lower confirmations, for example) - let replaced = false; - for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { - if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { - replaced = true; - this._txs_by_internal_index[c][cc] = clonedTx; - } - } - if (!replaced) this._txs_by_internal_index[c].push(clonedTx); - } - } - } - } - - this._lastTxFetch = +new Date(); - } - - getTransactions() { - let txs = []; - - for (let addressTxs of Object.values(this._txs_by_external_index)) { - txs = txs.concat(addressTxs); - } - for (let addressTxs of Object.values(this._txs_by_internal_index)) { - txs = txs.concat(addressTxs); - } - - let ret = []; - for (let tx of txs) { - tx.received = tx.blocktime * 1000; - if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed - tx.confirmations = tx.confirmations || 0; // unconfirmed - tx.hash = tx.txid; - tx.value = 0; - - for (let vin of tx.inputs) { - // if input (spending) goes from our address - we are loosing! - if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) { - tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); - } - } - - for (let vout of tx.outputs) { - // when output goes to our address - this means we are gaining! - if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) { - tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber(); - } - } - ret.push(tx); - } - - // now, deduplication: - let usedTxIds = {}; - let ret2 = []; - for (let tx of ret) { - if (!usedTxIds[tx.txid]) ret2.push(tx); - usedTxIds[tx.txid] = 1; - } - - return ret2.sort(function(a, b) { - return b.received - a.received; - }); - } - - async _binarySearchIterationForInternalAddress(index) { - const gerenateChunkAddresses = chunkNum => { - let ret = []; - for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { - ret.push(this._getInternalAddressByIndex(c)); - } - return ret; - }; - - let lastChunkWithUsedAddressesNum = null; - let lastHistoriesWithUsedAddresses = null; - for (let c = 0; c < Math.round(index / this.gap_limit); c++) { - let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); - if (this.constructor._getTransactionsFromHistories(histories).length > 0) { - // in this particular chunk we have used addresses - lastChunkWithUsedAddressesNum = c; - lastHistoriesWithUsedAddresses = histories; - } else { - // empty chunk. no sense searching more chunks - break; - } - } - - let lastUsedIndex = 0; - - if (lastHistoriesWithUsedAddresses) { - // now searching for last used address in batch lastChunkWithUsedAddressesNum - for ( - let c = lastChunkWithUsedAddressesNum * this.gap_limit; - c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; - c++ - ) { - let address = this._getInternalAddressByIndex(c); - if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { - lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued - } - } - } - - return lastUsedIndex; - } - - async _binarySearchIterationForExternalAddress(index) { - const gerenateChunkAddresses = chunkNum => { - let ret = []; - for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { - ret.push(this._getExternalAddressByIndex(c)); - } - return ret; - }; - - let lastChunkWithUsedAddressesNum = null; - let lastHistoriesWithUsedAddresses = null; - for (let c = 0; c < Math.round(index / this.gap_limit); c++) { - let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); - if (this.constructor._getTransactionsFromHistories(histories).length > 0) { - // in this particular chunk we have used addresses - lastChunkWithUsedAddressesNum = c; - lastHistoriesWithUsedAddresses = histories; - } else { - // empty chunk. no sense searching more chunks - break; - } - } - - let lastUsedIndex = 0; - - if (lastHistoriesWithUsedAddresses) { - // now searching for last used address in batch lastChunkWithUsedAddressesNum - for ( - let c = lastChunkWithUsedAddressesNum * this.gap_limit; - c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; - c++ - ) { - let address = this._getExternalAddressByIndex(c); - if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { - lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued - } - } - } - - return lastUsedIndex; - } - - async fetchBalance() { - try { - if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { - // doing binary search for last used address: - this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000); - this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000); - } // end rescanning fresh wallet - - // finally fetching balance - await this._fetchBalance(); - } catch (err) { - console.warn(err); - } - } - - async _fetchBalance() { - // probing future addressess in hierarchy whether they have any transactions, in case - // our 'next free addr' pointers are lagging behind - let tryAgain = false; - let txs = await BlueElectrum.getTransactionsByAddress( - this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), - ); - if (txs.length > 0) { - // whoa, someone uses our wallet outside! better catch up - this.next_free_address_index += this.gap_limit; - tryAgain = true; - } - - txs = await BlueElectrum.getTransactionsByAddress( - this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), - ); - if (txs.length > 0) { - this.next_free_change_address_index += this.gap_limit; - tryAgain = true; - } - - // FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ] - - if (tryAgain) return this._fetchBalance(); - - // next, business as usuall. fetch balances - - let addresses2fetch = []; - - // generating all involved addresses. - // basically, refetch all from index zero to maximum. doesnt matter - // since we batch them 100 per call - - // external - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - addresses2fetch.push(this._getExternalAddressByIndex(c)); - } - - // internal - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - addresses2fetch.push(this._getInternalAddressByIndex(c)); - } - - let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch); - - // converting to a more compact internal format - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - let addr = this._getExternalAddressByIndex(c); - if (balances.addresses[addr]) { - // first, if balances differ from what we store - we delete transactions for that - // address so next fetchTransactions() will refetch everything - if (this._balances_by_external_index[c]) { - if ( - this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || - this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed - ) { - delete this._txs_by_external_index[c]; - } - } - // update local representation of balances on that address: - this._balances_by_external_index[c] = { - c: balances.addresses[addr].confirmed, - u: balances.addresses[addr].unconfirmed, - }; - } - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - let addr = this._getInternalAddressByIndex(c); - if (balances.addresses[addr]) { - // first, if balances differ from what we store - we delete transactions for that - // address so next fetchTransactions() will refetch everything - if (this._balances_by_internal_index[c]) { - if ( - this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || - this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed - ) { - delete this._txs_by_internal_index[c]; - } - } - // update local representation of balances on that address: - this._balances_by_internal_index[c] = { - c: balances.addresses[addr].confirmed, - u: balances.addresses[addr].unconfirmed, - }; - } - } - - this._lastBalanceFetch = +new Date(); - } - - async fetchUtxo() { - // considering only confirmed balance - let addressess = []; - - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { - addressess.push(this._getExternalAddressByIndex(c)); - } - } - - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { - addressess.push(this._getInternalAddressByIndex(c)); - } - } - - this._utxo = []; - for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { - this._utxo = this._utxo.concat(arr); - } - } - - getUtxo() { - return this._utxo; - } - - _getDerivationPathByAddress(address) { - const path = "m/84'/0'/0'"; - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c; - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c; - } - - return false; - } - - _getPubkeyByAddress(address) { - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c); - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c); - } - - return false; - } - - weOwnAddress(address) { - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._getExternalAddressByIndex(c) === address) return true; - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._getInternalAddressByIndex(c) === address) return true; - } - return false; - } - - /** - * @deprecated - */ - createTx(utxos, amount, fee, address) { - throw new Error('Deprecated'); - } - - /** - * - * @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos - * @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) - * @param feeRate {Number} satoshi per byte - * @param changeAddress {String} Excessive coins will go back to that address - * @param sequence {Number} Used in RBF - * @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case - * @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} - */ - createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) { - if (!changeAddress) throw new Error('No change address provided'); - sequence = sequence || HDSegwitBech32Wallet.defaultRBFSequence; - - let algo = coinSelectAccumulative; - if (targets.length === 1 && targets[0] && !targets[0].value) { - // we want to send MAX - algo = coinSelectSplit; - } - - let { inputs, outputs, fee } = algo(utxos, targets, feeRate); - - // .inputs and .outputs will be undefined if no solution was found - if (!inputs || !outputs) { - throw new Error('Not enough balance. Try sending smaller amount'); - } - - let psbt = new bitcoin.Psbt(); - - let c = 0; - let keypairs = {}; - let values = {}; - - inputs.forEach(input => { - let keyPair; - if (!skipSigning) { - // skiping signing related stuff - keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address)); - keypairs[c] = keyPair; - } - values[c] = input.value; - c++; - if (!skipSigning) { - // skiping signing related stuff - if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); - } - let pubkey = this._getPubkeyByAddress(input.address); - let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); - // this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting - // should be from root. basically, fingerprint should be provided from outside by user when importing zpub - let path = this._getDerivationPathByAddress(input.address); - const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); - psbt.addInput({ - hash: input.txId, - index: input.vout, - sequence, - bip32Derivation: [ - { - masterFingerprint, - path, - pubkey, - }, - ], - witnessUtxo: { - script: p2wpkh.output, - value: input.value, - }, - }); - }); - - outputs.forEach(output => { - // if output has no address - this is change output - let change = false; - if (!output.address) { - change = true; - output.address = changeAddress; - } - - let path = this._getDerivationPathByAddress(output.address); - let pubkey = this._getPubkeyByAddress(output.address); - let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); - // this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting - // should be from root. basically, fingerprint should be provided from outside by user when importing zpub - - let outputData = { - address: output.address, - value: output.value, - }; - - if (change) { - outputData['bip32Derivation'] = [ - { - masterFingerprint, - path, - pubkey, - }, - ]; - } - - psbt.addOutput(outputData); - }); - - if (!skipSigning) { - // skiping signing related stuff - for (let cc = 0; cc < c; cc++) { - psbt.signInput(cc, keypairs[cc]); - } - } - - let tx; - if (!skipSigning) { - tx = psbt.finalizeAllInputs().extractTransaction(); - } - return { tx, inputs, outputs, fee, psbt }; - } - - /** - * Combines 2 PSBTs into final transaction from which you can - * get HEX and broadcast - * - * @param base64one {string} - * @param base64two {string} - * @returns {Transaction} - */ - combinePsbt(base64one, base64two) { - const final1 = bitcoin.Psbt.fromBase64(base64one); - const final2 = bitcoin.Psbt.fromBase64(base64two); - final1.combine(final2); - return final1.finalizeAllInputs().extractTransaction(); - } - - /** - * Creates Segwit Bech32 Bitcoin address - * - * @param hdNode - * @returns {String} - */ - static _nodeToBech32SegwitAddress(hdNode) { - return bitcoin.payments.p2wpkh({ - pubkey: hdNode.publicKey, - }).address; - } - - /** - * Converts zpub to xpub - * - * @param {String} zpub - * @returns {String} xpub - */ - static _zpubToXpub(zpub) { - let data = b58.decode(zpub); - data = data.slice(4); - data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); - - return b58.encode(data); - } - - static _getTransactionsFromHistories(histories) { - let txs = []; - for (let history of Object.values(histories)) { - for (let tx of history) { - txs.push(tx); - } - } - return txs; - } - - /** - * Broadcast txhex. Can throw an exception if failed - * - * @param {String} txhex - * @returns {Promise} - */ - async broadcastTx(txhex) { - let broadcast = await BlueElectrum.broadcastV2(txhex); - console.log({ broadcast }); - if (broadcast.indexOf('successfully') !== -1) return true; - return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok - } } diff --git a/class/hd-segwit-p2sh-wallet.js b/class/hd-segwit-p2sh-wallet.js index 0205c1d37..32f8e3b6c 100644 --- a/class/hd-segwit-p2sh-wallet.js +++ b/class/hd-segwit-p2sh-wallet.js @@ -1,48 +1,18 @@ -import { AbstractHDWallet } from './abstract-hd-wallet'; -import Frisbee from 'frisbee'; -import { NativeModules } from 'react-native'; import bip39 from 'bip39'; import BigNumber from 'bignumber.js'; import b58 from 'bs58check'; import signer from '../models/signer'; import { BitcoinUnit } from '../models/bitcoinUnits'; +import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; const bitcoin = require('bitcoinjs-lib'); const HDNode = require('bip32'); -const { RNRandomBytes } = NativeModules; - -/** - * Converts ypub to xpub - * @param {String} ypub - wallet ypub - * @returns {*} - */ -function ypubToXpub(ypub) { - let data = b58.decode(ypub); - if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!'); - data = data.slice(4); - data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); - - return b58.encode(data); -} - -/** - * Creates Segwit P2SH Bitcoin address - * @param hdNode - * @returns {String} - */ -function nodeToP2shSegwitAddress(hdNode) { - const { address } = bitcoin.payments.p2sh({ - redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }), - }); - return address; -} - /** * HD Wallet (BIP39). * In particular, BIP49 (P2SH Segwit) * @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki */ -export class HDSegwitP2SHWallet extends AbstractHDWallet { +export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet { static type = 'HDsegwitP2SH'; static typeReadable = 'HD SegWit (BIP49 P2SH)'; @@ -54,37 +24,6 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { return true; } - async generate() { - let that = this; - return new Promise(function(resolve) { - if (typeof RNRandomBytes === 'undefined') { - // CLI/CI environment - // crypto should be provided globally by test launcher - return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line - if (err) throw err; - that.secret = bip39.entropyToMnemonic(buf.toString('hex')); - resolve(); - }); - } - - // RN environment - RNRandomBytes.randomBytes(32, (err, bytes) => { - if (err) throw new Error(err); - let b = Buffer.from(bytes, 'base64').toString('hex'); - that.secret = bip39.entropyToMnemonic(b); - resolve(); - }); - }); - } - - _getExternalWIFByIndex(index) { - return this._getWIFByIndex(false, index); - } - - _getInternalWIFByIndex(index) { - return this._getWIFByIndex(true, index); - } - /** * Get internal/external WIF by wallet index * @param {Boolean} internal @@ -107,11 +46,11 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit if (!this._node0) { - const xpub = ypubToXpub(this.getXpub()); + const xpub = this.constructor._ypubToXpub(this.getXpub()); const hdNode = HDNode.fromBase58(xpub); this._node0 = hdNode.derive(0); } - const address = nodeToP2shSegwitAddress(this._node0.derive(index)); + const address = this.constructor._nodeToP2shSegwitAddress(this._node0.derive(index)); return (this.external_addresses_cache[index] = address); } @@ -121,11 +60,11 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit if (!this._node1) { - const xpub = ypubToXpub(this.getXpub()); + const xpub = this.constructor._ypubToXpub(this.getXpub()); const hdNode = HDNode.fromBase58(xpub); this._node1 = hdNode.derive(1); } - const address = nodeToP2shSegwitAddress(this._node1.derive(index)); + const address = this.constructor._nodeToP2shSegwitAddress(this._node1.derive(index)); return (this.internal_addresses_cache[index] = address); } @@ -158,108 +97,6 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { return this._xpub; } - async _getTransactionsBatch(addresses) { - const api = new Frisbee({ baseURI: 'https://blockchain.info' }); - let transactions = []; - let offset = 0; - - while (1) { - let response = await api.get('/multiaddr?active=' + addresses + '&n=100&offset=' + offset); - - if (response && response.body) { - if (response.body.txs && response.body.txs.length === 0) { - break; - } - - this._lastTxFetch = +new Date(); - - // processing TXs and adding to internal memory - if (response.body.txs) { - for (let tx of response.body.txs) { - let value = 0; - - for (let input of tx.inputs) { - // ----- INPUTS - - if (input.prev_out && input.prev_out.addr && this.weOwnAddress(input.prev_out.addr)) { - // this is outgoing from us - value -= input.prev_out.value; - } - } - - for (let output of tx.out) { - // ----- OUTPUTS - - if (output.addr && this.weOwnAddress(output.addr)) { - // this is incoming to us - value += output.value; - } - } - - tx.value = value; // new BigNumber(value).div(100000000).toString() * 1; - if (response.body.hasOwnProperty('info')) { - if (response.body.info.latest_block.height && tx.block_height) { - tx.confirmations = response.body.info.latest_block.height - tx.block_height + 1; - } else { - tx.confirmations = 0; - } - } else { - tx.confirmations = 0; - } - transactions.push(tx); - } - - if (response.body.txs.length < 100) { - // this fetch yilded less than page size, thus requesting next batch makes no sense - break; - } - } else { - break; // error ? - } - } else { - throw new Error('Could not fetch transactions from API: ' + response.err); // breaks here - } - - offset += 100; - } - return transactions; - } - - /** - * @inheritDoc - */ - async fetchTransactions() { - try { - if (this.usedAddresses.length === 0) { - // just for any case, refresh balance (it refreshes internal `this.usedAddresses`) - await this.fetchBalance(); - } - - this.transactions = []; - - let addresses4batch = []; - for (let addr of this.usedAddresses) { - addresses4batch.push(addr); - if (addresses4batch.length >= 45) { - let addresses = addresses4batch.join('|'); - let transactions = await this._getTransactionsBatch(addresses); - this.transactions = this.transactions.concat(transactions); - addresses4batch = []; - } - } - // final batch - for (let c = 0; c <= this.gap_limit; c++) { - addresses4batch.push(this._getExternalAddressByIndex(this.next_free_address_index + c)); - addresses4batch.push(this._getInternalAddressByIndex(this.next_free_change_address_index + c)); - } - let addresses = addresses4batch.join('|'); - let transactions = await this._getTransactionsBatch(addresses); - this.transactions = this.transactions.concat(transactions); - } catch (err) { - console.warn(err); - } - } - /** * * @param utxos @@ -291,4 +128,30 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { this._getInternalAddressByIndex(this.next_free_change_address_index), ); } + + /** + * Converts ypub to xpub + * @param {String} ypub - wallet ypub + * @returns {*} + */ + static _ypubToXpub(ypub) { + let data = b58.decode(ypub); + if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!'); + data = data.slice(4); + data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); + + return b58.encode(data); + } + + /** + * Creates Segwit P2SH Bitcoin address + * @param hdNode + * @returns {String} + */ + static _nodeToP2shSegwitAddress(hdNode) { + const { address } = bitcoin.payments.p2sh({ + redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }), + }); + return address; + } } diff --git a/tests/integration/HDWallet.test.js b/tests/integration/HDWallet.test.js index 582e50fbe..e98c83a36 100644 --- a/tests/integration/HDWallet.test.js +++ b/tests/integration/HDWallet.test.js @@ -55,7 +55,7 @@ it('can create a Segwit HD (BIP49)', async function() { assert.ok(hd._lastTxFetch === 0); await hd.fetchTransactions(); assert.ok(hd._lastTxFetch > 0); - assert.strictEqual(hd.transactions.length, 4); + assert.strictEqual(hd.getTransactions().length, 4); assert.strictEqual('L4MqtwJm6hkbACLG4ho5DF8GhcXdLEbbvpJnbzA9abfD6RDpbr2m', hd._getExternalWIFByIndex(0)); assert.strictEqual( @@ -84,7 +84,7 @@ it('HD (BIP49) can work with a gap', async function() { // console.log('external', c, hd._getExternalAddressByIndex(c)); // } await hd.fetchTransactions(); - assert.ok(hd.transactions.length >= 3); + assert.ok(hd.getTransactions().length >= 3); }); it('Segwit HD (BIP49) can batch fetch many txs', async function() { @@ -93,7 +93,7 @@ it('Segwit HD (BIP49) can batch fetch many txs', async function() { hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ'; await hd.fetchBalance(); await hd.fetchTransactions(); - assert.ok(hd.getTransactions().length === 153); + assert.strictEqual(hd.getTransactions().length, 152); }); it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagging behind', async function() { @@ -104,7 +104,7 @@ it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagg hd.next_free_address_index = 50; await hd.fetchBalance(); await hd.fetchTransactions(); - assert.strictEqual(hd.getTransactions().length, 153); + assert.strictEqual(hd.getTransactions().length, 152); }); it('Segwit HD (BIP49) can generate addressess only via ypub', function() { @@ -143,9 +143,14 @@ it('HD (BIP49) can create TX', async () => { hd.setSecret(process.env.HD_MNEMONIC_BIP49); assert.ok(hd.validateMnemonic()); + await hd.fetchBalance(); await hd.fetchUtxo(); - await hd.getChangeAddressAsync(); // to refresh internal pointer to next free address - await hd.getAddressAsync(); // to refresh internal pointer to next free address + assert.ok(typeof hd.utxo[0].confirmations === 'number'); + assert.ok(hd.utxo[0].txid); + assert.ok(hd.utxo[0].vout !== undefined); + assert.ok(hd.utxo[0].amount); + assert.ok(hd.utxo[0].address); + assert.ok(hd.utxo[0].wif); let txhex = hd.createTx(hd.utxo, 0.000014, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); assert.strictEqual( txhex, @@ -209,21 +214,6 @@ it('HD (BIP49) can create TX', async () => { assert.strictEqual(tx.outs[0].value, 75000); }); -it('Segwit HD (BIP49) can fetch UTXO', async function() { - let hd = new HDSegwitP2SHWallet(); - hd.usedAddresses = ['1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55', '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV']; // hacking internals - await hd.fetchUtxo(); - assert.ok(hd.utxo.length >= 12); - assert.ok(typeof hd.utxo[0].confirmations === 'number'); - assert.ok(hd.utxo[0].txid); - assert.ok(hd.utxo[0].vout); - assert.ok(hd.utxo[0].amount); - assert.ok( - hd.utxo[0].address && - (hd.utxo[0].address === '1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55' || hd.utxo[0].address === '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV'), - ); -}); - it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', async function() { if (!process.env.HD_MNEMONIC_BIP49_MANY_TX) { console.error('process.env.HD_MNEMONIC_BIP49_MANY_TX not set, skipped'); @@ -302,7 +292,7 @@ it('can create a Legacy HD (BIP44)', async function() { assert.ok(hd._lastTxFetch === 0); await hd.fetchTransactions(); assert.ok(hd._lastTxFetch > 0); - assert.strictEqual(hd.transactions.length, 4); + assert.strictEqual(hd.getTransactions().length, 4); assert.strictEqual(hd.next_free_address_index, 1); assert.strictEqual(hd.next_free_change_address_index, 1); @@ -403,7 +393,7 @@ it.skip('HD breadwallet works', async function() { assert.ok(hdBread._lastTxFetch === 0); await hdBread.fetchTransactions(); assert.ok(hdBread._lastTxFetch > 0); - assert.strictEqual(hdBread.transactions.length, 177); + assert.strictEqual(hdBread.getTransactions().length, 177); for (let tx of hdBread.getTransactions()) { assert.ok(tx.confirmations); }