diff --git a/package.json b/package.json index cd6d1407b..e9501ce78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockchain-wallet-client", - "version": "3.37.3", + "version": "3.38.5", "description": "Blockchain.info JavaScript Wallet", "homepage": "https://github.com/blockchain/my-wallet-v3", "bugs": { @@ -51,6 +51,7 @@ "bitcoin-exchange-client": "^0.4.6", "bitcoin-sfox-client": "^0.1.11", "bitcoin-unocoin-client": "^0.3.4", + "bitcoincashjs-lib": "https://github.com/bitcoinjs/bitcoinjs-lib#9ac221c80dbc3462d7a2392cec2045ae2b590461", "bitcoinjs-lib": "2.1.*", "bs58": "2.0.*", "core-js": "^2.4.1", @@ -61,8 +62,8 @@ "isomorphic-fetch": "^2.2.1", "pbkdf2": "^3.0.12", "ramda": "^0.22.1", + "ramda-lens": "^0.1.2", "randombytes": "^2.0.1", - "synk": "^0.0.2", "unorm": "^1.4.1", "ws": "2.0.*" }, diff --git a/src/bch/bch-account.js b/src/bch/bch-account.js new file mode 100644 index 000000000..e4f97b3ff --- /dev/null +++ b/src/bch/bch-account.js @@ -0,0 +1,55 @@ +/* eslint-disable semi */ +const BchSpendable = require('./bch-spendable') + +const ACCOUNT_LABEL_PREFIX = 'Bitcoin Cash - ' + +class BchAccount extends BchSpendable { + constructor (bchWallet, wallet, btcAccount) { + super(bchWallet, wallet) + this._btcAccount = btcAccount + } + + get index () { + return this._btcAccount.index + } + + get xpub () { + return this._btcAccount.extendedPublicKey + } + + get archived () { + return this._btcAccount.archived + } + + get label () { + return ACCOUNT_LABEL_PREFIX + this._btcAccount.label + } + + get balance () { + return super.getAddressBalance(this.xpub) + } + + get receiveAddress () { + let { receive } = this._bchWallet.getAccountIndexes(this.xpub) + return this._btcAccount.receiveAddressAtIndex(receive) + } + + get changeAddress () { + let { change } = this._bchWallet.getAccountIndexes(this.xpub) + return this._btcAccount.changeAddressAtIndex(change) + } + + get coinCode () { + return 'bch' + } + + getAvailableBalance (feePerByte) { + return super.getAvailableBalance(this.index, feePerByte) + } + + createPayment () { + return super.createPayment().from(this.index, this.changeAddress) + } +} + +module.exports = BchAccount diff --git a/src/bch/bch-api.js b/src/bch/bch-api.js new file mode 100644 index 000000000..ff7b720ba --- /dev/null +++ b/src/bch/bch-api.js @@ -0,0 +1,76 @@ +/* eslint-disable semi */ +const { curry, is, prop, lensProp, compose, assoc, over, map } = require('ramda'); +const { mapped } = require('ramda-lens'); +const API = require('../api'); +const Coin = require('./coin.js'); +const Bitcoin = require('bitcoincashjs-lib'); +const constants = require('../constants'); +const Helpers = require('../helpers'); + +const scriptToAddress = coin => { + const scriptBuffer = Buffer.from(coin.script, 'hex'); + let network = constants.getNetwork(Bitcoin); + const address = Bitcoin.address.fromOutputScript(scriptBuffer, network).toString(); + return assoc('priv', address, coin) +} + +const pushTx = (tx) => { + const format = 'plain' + return fetch(`${API.API_ROOT_URL}bch/pushtx`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: API.encodeFormData({ tx, format }) + }).then(r => + r.status === 200 ? r.text() : r.text().then(e => Promise.reject(e)) + ).then(r => + r.indexOf('Transaction Submitted') > -1 ? true : Promise.reject(r) + ) +}; + +const apiGetUnspents = (as, conf) => { + const active = as.join('|'); + const confirmations = Helpers.isPositiveNumber(conf) ? conf : -1 + const format = 'json' + return fetch(`${API.API_ROOT_URL}bch/unspent`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: API.encodeFormData({ active, confirmations, format }) + }).then(r => + r.status === 200 ? r.json() : r.json().then(e => Promise.reject(e)) + ); +} + +const multiaddr = (addresses, n = 1) => { + const active = Helpers.toArrayFormat(addresses).join('|') + const data = { active, format: 'json', offset: 0, no_compact: true, n, language: 'en', no_buttons: true }; + return fetch(`${API.API_ROOT_URL}bch/multiaddr`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: API.encodeFormData(data) + }).then(r => r.status === 200 ? r.json() : r.json().then(e => Promise.reject(e))); +}; + +// source can be a list of legacy addresses or a single integer for account index +const getUnspents = curry((wallet, source) => { + switch (true) { + case is(Number, source): + const accIdx = wallet.hdwallet.accounts[source].extendedPublicKey + return apiGetUnspents([accIdx]) + .then(prop('unspent_outputs')) + .then(over(compose(mapped, lensProp('xpub')), assoc('index', source))) + .then(map(Coin.fromJS)); + case is(Array, source): + return apiGetUnspents(source) + .then(prop('unspent_outputs')) + .then(over(mapped, scriptToAddress)) + .then(map(Coin.fromJS)); + default: + return Promise.reject('WRONG_SOURCE_FOR_UNSPENTS'); + } +}) + +module.exports = { + getUnspents, + pushTx, + multiaddr +}; diff --git a/src/bch/bch-imported.js b/src/bch/bch-imported.js new file mode 100644 index 000000000..ab4b4d3b6 --- /dev/null +++ b/src/bch/bch-imported.js @@ -0,0 +1,34 @@ +/* eslint-disable semi */ +const BchSpendable = require('./bch-spendable') +const { compose, reduce, filter, add } = require('ramda') + +const sumNonNull = compose(reduce(add, 0), filter(x => x != null)) + +class BchImported extends BchSpendable { + get addresses () { + return this._wallet.spendableActiveAddresses + } + + get label () { + return 'Imported Addresses' + } + + get balance () { + let balances = this.addresses.map(a => super.getAddressBalance(a)) + return balances.every(x => x == null) ? null : sumNonNull(balances) + } + + get coinCode () { + return 'bch' + } + + getAvailableBalance (feePerByte) { + return super.getAvailableBalance(this.addresses, feePerByte) + } + + createPayment () { + return super.createPayment().from(this.addresses, this.addresses[0]) + } +} + +module.exports = BchImported diff --git a/src/bch/bch-payment.js b/src/bch/bch-payment.js new file mode 100644 index 000000000..d43995fb1 --- /dev/null +++ b/src/bch/bch-payment.js @@ -0,0 +1,160 @@ +/* eslint-disable semi */ +const { compose, clone, assoc, is, all } = require('ramda') +const Coin = require('./coin') +const BchApi = require('./bch-api') +const { isBitcoinAddress, isPositiveInteger } = require('../helpers') +const { selectAll, descentDraw } = require('./coin-selection') +const { sign } = require('./signer') + +const isValidFrom = (from) => ( + is(Number, from) || + (is(Array, from) && all(isBitcoinAddress, from)) +) + +class PaymentError extends Error { + constructor (message, state) { + super(message) + this.recover = () => Promise.resolve(state) + } +} + +class BchPayment { + constructor (wallet) { + this._wallet = wallet + this._payment = BchPayment.defaultStateP() + } + + map (f) { + this._payment = this._payment.then(f) + return this + } + + handleError (f) { + this._payment = this._payment.catch(paymentError => { + f(paymentError) + return is(Function, paymentError.recover) + ? paymentError.recover() + : BchPayment.defaultStateP() + }) + return this + } + + sideEffect (f) { + this._payment.then(clone).then(f) + return this + } + + from (from, change) { + if (!isValidFrom(from)) { + throw new Error('must provide a valid payment source') + } + if (!isBitcoinAddress(change)) { + throw new Error('must provide a valid change address') + } + return this.map(payment => + BchApi.getUnspents(this._wallet, from).then(coins => { + let setData = compose(assoc('coins', coins), assoc('change', change)) + return setData(payment) + }) + ) + } + + to (to) { + if (!isBitcoinAddress(to)) { + throw new Error('must provide a valid destination address') + } + return this.clean().map(assoc('to', to)) + } + + amount (amount) { + if (!isPositiveInteger(amount)) { + throw new Error('must provide a valid amount') + } + return this.clean().map(assoc('amount', amount)) + } + + feePerByte (feePerByte) { + if (!isPositiveInteger(feePerByte)) { + throw new Error('must provide a valid fee-per-byte value') + } + return this.clean().map(assoc('feePerByte', feePerByte)) + } + + clean () { + return this.map(compose( + assoc('selection', null), + assoc('hash', null), + assoc('rawTx', null) + )) + } + + build () { + return this.map(payment => { + if (payment.to == null) { + throw new PaymentError('must set a destination address', payment) + } + if (payment.amount == null) { + throw new PaymentError('must set an amount', payment) + } + if (payment.feePerByte == null) { + throw new PaymentError('must set a fee-per-byte value', payment) + } + let targets = [new Coin({ address: payment.to, value: payment.amount })] + let selection = descentDraw(targets, payment.feePerByte, payment.coins, payment.change) + return assoc('selection', selection, payment) + }) + } + + buildSweep () { + return this.map(payment => { + if (payment.to == null) { + throw new PaymentError('must set a destination address', payment) + } + if (payment.feePerByte == null) { + throw new PaymentError('must set a fee-per-byte value', payment) + } + let selection = selectAll(payment.feePerByte, payment.coins, payment.to) + return assoc('selection', selection, payment) + }) + } + + sign (secPass) { + return this.map(payment => { + if (payment.selection == null) { + throw new PaymentError('cannot sign an unbuilt transaction', payment) + } + let tx = sign(secPass, this._wallet, payment.selection) + let setData = compose(assoc('hash', tx.getId()), assoc('rawTx', tx.toHex())) + return setData(payment) + }) + } + + publish () { + /* return Promise, not BchPayment instance */ + return this._payment.then(payment => { + if (payment.rawTx == null) { + throw new PaymentError('cannot publish an unsigned transaction', payment) + } + return BchApi.pushTx(payment.rawTx) + .then(() => ({ hash: payment.hash })) + }) + } + + static defaultStateP () { + return Promise.resolve(BchPayment.defaultState()) + } + + static defaultState () { + return { + coins: [], + to: null, + amount: null, + feePerByte: null, + selection: null, + hash: null, + rawTx: null + } + } +} + +module.exports = BchPayment diff --git a/src/bch/bch-spendable.js b/src/bch/bch-spendable.js new file mode 100644 index 000000000..50efd5dc6 --- /dev/null +++ b/src/bch/bch-spendable.js @@ -0,0 +1,27 @@ +/* eslint-disable semi */ +const BchApi = require('./bch-api') +const { selectAll } = require('./coin-selection') + +class BchSpendable { + constructor (bchWallet, wallet) { + this._bchWallet = bchWallet + this._wallet = wallet + } + + getAddressBalance (source) { + return this._bchWallet.getAddressBalance(source) + } + + getAvailableBalance (source, feePerByte) { + return BchApi.getUnspents(this._wallet, source).then(coins => { + let { fee, outputs } = selectAll(feePerByte, coins, null) + return { fee: feePerByte, sweepFee: fee, amount: outputs[0].value } + }); + } + + createPayment () { + return this._bchWallet.createPayment() + } +} + +module.exports = BchSpendable diff --git a/src/bch/coin-selection.js b/src/bch/coin-selection.js new file mode 100644 index 000000000..6cf89c520 --- /dev/null +++ b/src/bch/coin-selection.js @@ -0,0 +1,85 @@ +const { curry, unfold, reduce, last, filter, head, map, isNil, isEmpty, tail, clamp, sort } = require('ramda'); +const Coin = require('./coin.js'); + +const fold = curry((empty, xs) => reduce((acc, x) => acc.concat(x), empty, xs)); +const foldCoins = fold(Coin.empty); + +const dustThreshold = (feeRate) => (Coin.inputBytes({}) + Coin.outputBytes({})) * feeRate; + +const transactionBytes = (inputs, outputs) => + Coin.TX_EMPTY_SIZE + inputs.reduce((a, c) => a + Coin.inputBytes(c), 0) + outputs.reduce((a, c) => a + Coin.outputBytes(c), 0); + +const effectiveBalance = curry((feePerByte, inputs, outputs = [{}]) => + foldCoins(inputs).map(v => + clamp(0, Infinity, v - transactionBytes(inputs, outputs) * feePerByte)) +); + +// findTarget :: [Coin] -> Number -> [Coin] -> String -> Selection +const findTarget = (targets, feePerByte, coins, changeAddress) => { + let target = foldCoins(targets).value; + let _findTarget = seed => { + let acc = seed[0]; + let newCoin = head(seed[2]); + if (isNil(newCoin) || acc > target + seed[1]) { return false; } + let partialFee = seed[1] + Coin.inputBytes(newCoin) * feePerByte; + let restCoins = tail(seed[2]); + let nextAcc = acc + newCoin.value; + return acc > target + partialFee ? false : [[nextAcc, partialFee, newCoin], [nextAcc, partialFee, restCoins]]; + }; + let partialFee = transactionBytes([], targets) * feePerByte; + let effectiveCoins = filter(c => Coin.effectiveValue(feePerByte, c) > 0, coins); + let selection = unfold(_findTarget, [0, partialFee, effectiveCoins]); + if (isEmpty(selection)) { + // no coins to select + return { fee: 0, inputs: [], outputs: [] }; + } else { + let maxBalance = last(selection)[0]; + let fee = last(selection)[1]; + let selectedCoins = map(e => e[2], selection); + if (maxBalance < target + fee) { + // not enough money to satisfy target + return { fee: fee, inputs: [], outputs: targets }; + } else { + let extra = maxBalance - target - fee; + if (extra >= dustThreshold(feePerByte)) { + // add change + let change = Coin.fromJS({ value: extra, address: changeAddress, change: true }); + return { fee: fee, inputs: selectedCoins, outputs: [...targets, change] }; + } else { + // burn change + return { fee: fee + extra, inputs: selectedCoins, outputs: targets }; + } + } + } +}; + +// selectAll :: Number -> [Coin] -> String -> Selection +const selectAll = (feePerByte, coins, outAddress) => { + let effectiveCoins = filter(c => Coin.effectiveValue(feePerByte, c) > 0, coins); + let effBalance = effectiveBalance(feePerByte, effectiveCoins).value; + let balance = foldCoins(effectiveCoins).value; + let fee = balance - effBalance; + return { + fee, + inputs: effectiveCoins, + outputs: [Coin.fromJS({ value: effBalance, address: outAddress })] + }; +}; + +// descentDraw :: [Coin] -> Number -> [Coin] -> String -> Selection +const descentDraw = (targets, feePerByte, coins, changeAddress) => + findTarget(targets, feePerByte, sort(Coin.descentSort, coins), changeAddress); + +// ascentDraw :: [Coin] -> Number -> [Coin] -> String -> Selection +const ascentDraw = (targets, feePerByte, coins, changeAddress) => + findTarget(targets, feePerByte, sort(Coin.ascentSort, coins), changeAddress); + +module.exports = { + dustThreshold, + transactionBytes, + effectiveBalance, + findTarget, + selectAll, + descentDraw, + ascentDraw +}; diff --git a/src/bch/coin.js b/src/bch/coin.js new file mode 100644 index 000000000..2731042e9 --- /dev/null +++ b/src/bch/coin.js @@ -0,0 +1,87 @@ +const { curry, clamp, split, length } = require('ramda'); + +class Coin { + constructor (obj) { + this.value = obj.value; + this.script = obj.script; + this.txHash = obj.txHash; + this.index = obj.index; + this.address = obj.address; + this.priv = obj.priv; + this.change = obj.change; + } + + toString () { + return `Coin(${this.value})`; + } + + concat (coin) { + return Coin.of(this.value + coin.value); + } + + equals (coin) { + return this.value === coin.value; + } + + lte (coin) { + return this.value <= coin.value; + } + + ge (coin) { + return this.value >= coin.value; + } + + map (f) { + return Coin.of(f(this.value)); + } + + isFromAccount () { + return length(split('/', this.priv)) > 1; + } + + isFromLegacy () { + return !this.isFromAccount(); + } + + static descentSort (coinA, coinB) { + return coinB.value - coinA.value; + } + + static ascentSort (coinA, coinB) { + return coinA.value - coinB.value; + } + + static fromJS (o) { + return new Coin({ + value: o.value, + script: o.script, + txHash: o.tx_hash_big_endian, + index: o.tx_output_n, + change: o.change || false, + priv: o.priv || (o.xpub ? `${o.xpub.index}-${o.xpub.path}` : undefined), + address: o.address + }); + } + + static of (value) { + return new Coin({ value }); + } +} + +Coin.TX_EMPTY_SIZE = 4 + 1 + 1 + 4; +Coin.TX_INPUT_BASE = 32 + 4 + 1 + 4; +Coin.TX_INPUT_PUBKEYHASH = 106; +Coin.TX_OUTPUT_BASE = 8 + 1; +Coin.TX_OUTPUT_PUBKEYHASH = 25; + +Coin.empty = Coin.of(0); + +Coin.inputBytes = (_input) => Coin.TX_INPUT_BASE + Coin.TX_INPUT_PUBKEYHASH; + +Coin.outputBytes = (_output) => Coin.TX_OUTPUT_BASE + Coin.TX_OUTPUT_PUBKEYHASH; + +Coin.effectiveValue = curry((feePerByte, coin) => + clamp(0, Infinity, coin.value - feePerByte * Coin.inputBytes(coin)) +); + +module.exports = Coin; diff --git a/src/bch/index.js b/src/bch/index.js new file mode 100644 index 000000000..dc64ac40e --- /dev/null +++ b/src/bch/index.js @@ -0,0 +1,78 @@ +/* eslint-disable semi */ +const { map, fromPairs } = require('ramda') +const BchApi = require('./bch-api') +const BchPayment = require('./bch-payment') +const Tx = require('../wallet-transaction') +const BchAccount = require('./bch-account') +const BchImported = require('./bch-imported') + +const BCH_FORK_HEIGHT = 478558 + +class BitcoinCashWallet { + constructor (wallet) { + this._wallet = wallet + this._balance = null + this._addressInfo = {} + this._txs = [] + + let imported = new BchImported(this, this._wallet) + this.importedAddresses = imported.addresses.length > 0 ? imported : null + + this.accounts = wallet.hdwallet.accounts.map(account => + new BchAccount(this, this._wallet, account) + ) + } + + get balance () { + return this._balance + } + + get txs () { + return this._txs + } + + get defaultAccount () { + return this.accounts[this._wallet.hdwallet.defaultAccountIndex] + } + + getAddressBalance (xpubOrAddress) { + let info = this._addressInfo[xpubOrAddress] + let balance = info && info.final_balance + return balance == null ? null : balance + } + + getAccountIndexes (xpub) { + let defaults = { account_index: 0, change_index: 0 } + let info = this._addressInfo[xpub] || defaults + return { receive: info.account_index, change: info.change_index } + } + + getHistory () { + let addrs = this.importedAddresses == null ? [] : this.importedAddresses.addresses + let xpubs = this.accounts.map(a => a.xpub) + return BchApi.multiaddr(addrs.concat(xpubs), 50).then(result => { + let { wallet, addresses, txs, info } = result + + this._balance = wallet.final_balance + this._addressInfo = fromPairs(map(a => [a.address, a], addresses)) + + this._txs = txs + .filter(tx => !tx.block_height || tx.block_height >= BCH_FORK_HEIGHT) + .map(tx => Tx.factory(tx, 'bch')) + + this._txs.forEach(tx => { + tx.confirmations = Tx.setConfirmations(tx.block_height, info.latest_block) + }) + }) + } + + createPayment () { + return new BchPayment(this._wallet) + } + + static fromBlockchainWallet (wallet) { + return new BitcoinCashWallet(wallet) + } +} + +module.exports = BitcoinCashWallet diff --git a/src/bch/signer.js b/src/bch/signer.js new file mode 100644 index 000000000..d1dbce998 --- /dev/null +++ b/src/bch/signer.js @@ -0,0 +1,76 @@ +const { curry, forEach, addIndex, lensProp, compose, over } = require('ramda'); +const { mapped } = require('ramda-lens'); +const Bitcoin = require('bitcoincashjs-lib'); +const constants = require('../constants'); +const WalletCrypto = require('../wallet-crypto'); +const Helpers = require('../helpers'); +const KeyRing = require('../keyring'); + +const getKey = (priv, addr) => { + let format = Helpers.detectPrivateKeyFormat(priv); + let key = Helpers.privateKeyStringToKey(priv, format, Bitcoin); + let network = constants.getNetwork(Bitcoin); + let ckey = new Bitcoin.ECPair(key.d, null, { compressed: true, network: network }); + let ukey = new Bitcoin.ECPair(key.d, null, { compressed: false, network: network }); + if (ckey.getAddress() === addr) { + return ckey; + } else if (ukey.getAddress() === addr) { + return ukey; + } + return key; +}; + +const getKeyForAddress = (wallet, password, addr) => { + const k = wallet.key(addr).priv; + const privateKeyBase58 = password == null ? k + : WalletCrypto.decryptSecretWithSecondPassword(k, password, wallet.sharedKey, wallet.pbkdf2_iterations); + return getKey(privateKeyBase58, addr); +}; + +const getXPRIV = (wallet, password, accountIndex) => { + const account = wallet.hdwallet.accounts[accountIndex]; + return account.extendedPrivateKey == null || password == null + ? account.extendedPrivateKey + : WalletCrypto.decryptSecretWithSecondPassword(account.extendedPrivateKey, password, wallet.sharedKey, wallet.pbkdf2_iterations); +}; + +const pathToKey = (wallet, password, fullpath) => { + const [idx, path] = fullpath.split('-'); + const xpriv = getXPRIV(wallet, password, idx); + const keyring = new KeyRing(xpriv, undefined, Bitcoin); + return keyring.privateKeyFromPath(path).keyPair; +}; + +const isFromAccount = (selection) => { + return selection.inputs[0] ? selection.inputs[0].isFromAccount() : false; +}; + +const signSelection = selection => { + let network = constants.getNetwork(Bitcoin); + const hashType = Bitcoin.Transaction.SIGHASH_ALL | Bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143; + + let tx = new Bitcoin.TransactionBuilder(network); + tx.enableBitcoinCash(true); + + let addInput = coin => tx.addInput(coin.txHash, coin.index, Bitcoin.Transaction.DEFAULT_SEQUENCE, new Buffer(coin.script, 'hex')); + let addOutput = coin => tx.addOutput(coin.address, coin.value); + let sign = (coin, i) => tx.sign(i, coin.priv, null, hashType, coin.value); + + forEach(addInput, selection.inputs); + forEach(addOutput, selection.outputs); + addIndex(forEach)(sign, selection.inputs); + + return tx.build(); +}; + +const sign = curry((password, wallet, selection) => { + const getPrivAcc = keypath => pathToKey(wallet, password, keypath); + const getPrivAddr = address => getKeyForAddress(wallet, password, address); + const getKeys = isFromAccount(selection) ? getPrivAcc : getPrivAddr; + const selectionWithKeys = over(compose(lensProp('inputs'), mapped, lensProp('priv')), getKeys, selection); + return signSelection(selectionWithKeys); +}); + +module.exports = { + sign +}; diff --git a/src/blockchain-wallet.js b/src/blockchain-wallet.js index a3b9c0678..1bfeaf366 100644 --- a/src/blockchain-wallet.js +++ b/src/blockchain-wallet.js @@ -25,6 +25,7 @@ var Labels = require('./labels'); var EthWallet = require('./eth/eth-wallet'); var ShapeShift = require('./shift'); var Bitcoin = require('bitcoinjs-lib'); +var BitcoinCash = require('./bch'); // Wallet @@ -354,6 +355,12 @@ Object.defineProperties(Wallet.prototype, { get: function () { return this._shapeshift; } + }, + 'bch': { + configurable: false, + get: function () { + return this._bch; + } } }); @@ -896,6 +903,10 @@ Wallet.prototype.loadMetadata = function (optionalPayloads, magicHashes) { return this._shapeshift.fetch(); }; + var loadBch = function () { + this._bch = BitcoinCash.fromBlockchainWallet(this); + }; + let promises = []; if (this.isMetadataReady) { @@ -909,6 +920,7 @@ Wallet.prototype.loadMetadata = function (optionalPayloads, magicHashes) { if (this.isUpgradedToHD) { // Labels currently don't use the KV Store, so this should never fail. promises.push(fetchLabels.call(this)); + promises.push(loadBch.call(this)); } return Promise.all(promises); diff --git a/src/constants.js b/src/constants.js index 5587d5855..83c2bb27a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,3 @@ - var Bitcoin = require('bitcoinjs-lib'); module.exports = { @@ -6,8 +5,12 @@ module.exports = { APP_NAME: 'javascript_web', APP_VERSION: '3.0', SHAPE_SHIFT_KEY: void 0, - getNetwork: function () { - return Bitcoin.networks[this.NETWORK]; + getNetwork: function (bitcoinjs) { + if (bitcoinjs) { + return bitcoinjs.networks[this.NETWORK]; + } else { + return Bitcoin.networks[this.NETWORK]; + } }, getDefaultWalletOptions: function () { return { diff --git a/src/eth/eth-account.js b/src/eth/eth-account.js index 0f9efa33f..ee93d0ea8 100644 --- a/src/eth/eth-account.js +++ b/src/eth/eth-account.js @@ -46,6 +46,10 @@ class EthAccount { return this._nonce; } + get coinCode () { + return 'eth'; + } + markAsCorrect () { this._correct = true; } diff --git a/src/eth/eth-wallet-tx.js b/src/eth/eth-wallet-tx.js index 09b9cb27b..75af16a00 100644 --- a/src/eth/eth-wallet-tx.js +++ b/src/eth/eth-wallet-tx.js @@ -58,6 +58,10 @@ class EthWalletTx { return this._note; } + get coinCode () { + return 'eth'; + } + getTxType (accounts) { accounts = toArrayFormat(accounts); let incoming = accounts.some(a => this.isToAccount(a)); diff --git a/src/hd-account.js b/src/hd-account.js index c351e03ce..afaab145b 100644 --- a/src/hd-account.js +++ b/src/hd-account.js @@ -135,11 +135,11 @@ Object.defineProperties(HDAccount.prototype, { }, 'receiveAddress': { configurable: false, - get: function () { return this._keyRing.receive.getAddress(this.receiveIndex); } + get: function () { return this.receiveAddressAtIndex(this.receiveIndex); } }, 'changeAddress': { configurable: false, - get: function () { return this._keyRing.change.getAddress(this._changeIndex); } + get: function () { return this.changeAddressAtIndex(this.changeIndex); } }, 'isEncrypted': { configurable: false, @@ -152,6 +152,10 @@ Object.defineProperties(HDAccount.prototype, { 'index': { configurable: false, get: function () { return this._index; } + }, + 'coinCode': { + configurable: false, + get: function () { return 'btc'; } } }); @@ -230,6 +234,11 @@ HDAccount.prototype.receiveAddressAtIndex = function (index) { return this._keyRing.receive.getAddress(index); }; +HDAccount.prototype.changeAddressAtIndex = function (index) { + assert(Helpers.isPositiveInteger(index), 'Error: change index must be a positive integer'); + return this._keyRing.change.getAddress(index); +}; + HDAccount.prototype.encrypt = function (cipher) { if (!this._xpriv) return this; var xpriv = cipher ? cipher(this._xpriv) : this._xpriv; diff --git a/src/helpers.js b/src/helpers.js index 843e464db..6251ac2a7 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -348,9 +348,10 @@ function parseMiniKey (miniKey) { return Bitcoin.crypto.sha256(miniKey); } -Helpers.privateKeyStringToKey = function (value, format) { +Helpers.privateKeyStringToKey = function (value, format, bitcoinjs) { + var bitcoinLib = bitcoinjs || Bitcoin; if (format === 'sipa' || format === 'compsipa') { - return Bitcoin.ECPair.fromWIF(value, constants.getNetwork()); + return bitcoinLib.ECPair.fromWIF(value, constants.getNetwork(bitcoinLib)); } else { var keyBuffer = null; @@ -372,7 +373,7 @@ Helpers.privateKeyStringToKey = function (value, format) { } var d = BigInteger.fromBuffer(keyBuffer); - return new Bitcoin.ECPair(d, null, { network: constants.getNetwork() }); + return new bitcoinLib.ECPair(d, null, { network: constants.getNetwork(bitcoinLib) }); } }; diff --git a/src/keychain.js b/src/keychain.js index e8165e81a..cf14ff59e 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -8,7 +8,8 @@ var Helpers = require('./helpers'); var constants = require('./constants'); // keychain -function KeyChain (extendedKey, index, cache) { +function KeyChain (extendedKey, index, cache, bitcoinjs) { + this._Bitcoin = bitcoinjs || Bitcoin; this._chainRoot = null; this.init(extendedKey, index, cache); @@ -41,10 +42,10 @@ KeyChain.prototype.init = function (extendedKey, index, cache) { // if cache is defined we use it to recreate the chain // otherwise we generate it using extendedKey and index if (cache) { - this._chainRoot = Bitcoin.HDNode.fromBase58(cache, constants.getNetwork()); + this._chainRoot = this._Bitcoin.HDNode.fromBase58(cache, constants.getNetwork(this._Bitcoin)); } else { this._chainRoot = extendedKey && Helpers.isPositiveInteger(index) && index >= 0 - ? Bitcoin.HDNode.fromBase58(extendedKey, constants.getNetwork()).derive(index) : undefined; + ? this._Bitcoin.HDNode.fromBase58(extendedKey, constants.getNetwork(this._Bitcoin)).derive(index) : undefined; } return this; }; diff --git a/src/keyring.js b/src/keyring.js index fb6f9c4b0..1fb8096b5 100644 --- a/src/keyring.js +++ b/src/keyring.js @@ -3,11 +3,13 @@ module.exports = KeyRing; var assert = require('assert'); +var Bitcoin = require('bitcoinjs-lib'); var KeyChain = require('./keychain'); // keyring: A collection of keychains -function KeyRing (extendedKey, cache) { +function KeyRing (extendedKey, cache, bitcoinjs) { + this._bitcoinjs = bitcoinjs || Bitcoin; this._receiveChain = null; this._changeChain = null; this.init(extendedKey, cache); @@ -29,9 +31,9 @@ KeyRing.prototype.init = function (extendedKey, cache) { if (this._receiveChain && this._changeChain) return this; if (extendedKey || cache.receiveAccount && cache.changeAccount) { this._receiveChain = cache.receiveAccount - ? new KeyChain(null, null, cache.receiveAccount) : new KeyChain(extendedKey, 0); + ? new KeyChain(null, null, cache.receiveAccount, this._bitcoinjs) : new KeyChain(extendedKey, 0, undefined, this._bitcoinjs); this._changeChain = cache.changeAccount - ? new KeyChain(null, null, cache.changeAccount) : new KeyChain(extendedKey, 1); + ? new KeyChain(null, null, cache.changeAccount, this._bitcoinjs) : new KeyChain(extendedKey, 1, undefined, this._bitcoinjs); } return this; }; diff --git a/src/shift/bch-payment.js b/src/shift/bch-payment.js new file mode 100644 index 000000000..7fcf32ead --- /dev/null +++ b/src/shift/bch-payment.js @@ -0,0 +1,32 @@ +/* eslint-disable semi */ +const ShiftPayment = require('./shift-payment') + +class BchPayment extends ShiftPayment { + constructor (wallet, account) { + super() + this._wallet = wallet + this._payment = account.createPayment() + } + + setFromQuote (quote, feePerByte) { + super.setFromQuote(quote) + this._payment.to(quote.depositAddress) + this._payment.amount(Math.round(parseFloat(quote.depositAmount) * 1e8)) + this._payment.feePerByte(feePerByte) + this._payment.build() + return this + } + + getFee () { + return new Promise(resolve => { + this._payment.sideEffect(payment => resolve(payment.selection.fee)) + }) + } + + publish (secPass) { + this._payment.sign(secPass) + return this._payment.publish() + } +} + +module.exports = BchPayment diff --git a/src/shift/btc-payment.js b/src/shift/btc-payment.js index a8ad052ac..b0ddf3c02 100644 --- a/src/shift/btc-payment.js +++ b/src/shift/btc-payment.js @@ -2,10 +2,10 @@ const ShiftPayment = require('./shift-payment') class BtcPayment extends ShiftPayment { - constructor (wallet) { + constructor (wallet, account) { super() this._payment = wallet.createPayment() - this._payment.from(wallet.hdwallet.defaultAccountIndex) + this._payment.from(account.index) } setFromQuote (quote, fee = 'priority') { diff --git a/src/shift/eth-payment.js b/src/shift/eth-payment.js index f51639bfe..da4eaa2bf 100644 --- a/src/shift/eth-payment.js +++ b/src/shift/eth-payment.js @@ -2,11 +2,11 @@ const ShiftPayment = require('./shift-payment') class EthPayment extends ShiftPayment { - constructor (wallet) { + constructor (wallet, account) { super() this._wallet = wallet this._eth = wallet.eth - this._payment = this._eth.defaultAccount.createPayment() + this._payment = account.createPayment() } setFromQuote (quote) { @@ -24,12 +24,6 @@ class EthPayment extends ShiftPayment { }) } - saveWithdrawalLabel () { - let label = `ShapeShift order #${this.quote.orderId}` - let account = this._wallet.hdwallet.defaultAccount - account.setLabel(account.receiveIndex, label) - } - publish (secPass) { let privateKey = this._eth.getPrivateKeyForAccount(this._eth.defaultAccount, secPass) this._payment.sign(privateKey) diff --git a/src/shift/index.js b/src/shift/index.js index 164a32197..581815607 100644 --- a/src/shift/index.js +++ b/src/shift/index.js @@ -5,6 +5,7 @@ const Trade = require('./trade') const Quote = require('./quote') const BtcPayment = require('./btc-payment') const EthPayment = require('./eth-payment') +const BchPayment = require('./bch-payment') const METADATA_TYPE_SHAPE_SHIFT = 6; @@ -47,17 +48,26 @@ class ShapeShift { .then(Quote.fromApiResponse) } - buildPayment (quote, fee) { + buildPayment (quote, fee, fromAccount) { trace('building payment') let payment if (quote.depositAddress == null) { throw new Error('Quote is missing deposit address') } + if (fromAccount != null && fromAccount.coinCode !== quote.fromCurrency) { + throw new Error('Sending account currency does not match quote deposit currency') + } if (quote.fromCurrency === 'btc') { - payment = BtcPayment.fromWallet(this._wallet) + let account = fromAccount || this._wallet.hdwallet.defaultAccount + payment = BtcPayment.fromWallet(this._wallet, account) } if (quote.fromCurrency === 'eth') { - payment = EthPayment.fromWallet(this._wallet) + let account = fromAccount || this._wallet.eth.defaultAccount + payment = EthPayment.fromWallet(this._wallet, account) + } + if (quote.fromCurrency === 'bch') { + let account = fromAccount || this._wallet.bch.defaultAccount + payment = BchPayment.fromWallet(this._wallet, account) } if (payment == null) { throw new Error(`Tried to build for unsupported currency ${quote.fromCurrency}`) @@ -69,7 +79,9 @@ class ShapeShift { trace('starting shift') return payment.publish(secPass).then(({ hash }) => { trace('finished shift') - payment.saveWithdrawalLabel() + if (payment.quote.toCurrency === 'btc') { + this.saveBtcWithdrawalLabel(payment.quote) + } let trade = Trade.fromQuote(payment.quote) trade.setDepositHash(hash) this._trades.unshift(trade) @@ -116,9 +128,18 @@ class ShapeShift { if (currency === 'eth') { return this._wallet.eth.defaultAccount.address } + if (currency === 'bch') { + return this._wallet.bch.defaultAccount.receiveAddress + } throw new Error(`Currency '${currency}' is not supported`) } + saveBtcWithdrawalLabel (quote) { + let label = `ShapeShift order #${quote.orderId}` + let account = this._wallet.hdwallet.defaultAccount + account.setLabel(account.receiveIndex, label) + } + setUSAState (state) { this._USAState = state this.sync() diff --git a/src/shift/shift-payment.js b/src/shift/shift-payment.js index d3393d5a1..561420483 100644 --- a/src/shift/shift-payment.js +++ b/src/shift/shift-payment.js @@ -8,11 +8,8 @@ class ShiftPayment { this._quote = quote } - saveWithdrawalLabel () { - } - - static fromWallet (wallet) { - return new this(wallet) + static fromWallet (wallet, account) { + return new this(wallet, account) } } diff --git a/src/wallet-transaction.js b/src/wallet-transaction.js index b2d6dc004..e36f57627 100644 --- a/src/wallet-transaction.js +++ b/src/wallet-transaction.js @@ -2,6 +2,7 @@ module.exports = Tx; +var { assoc } = require('ramda'); var MyWallet = require('./wallet'); function Tx (object) { @@ -26,7 +27,8 @@ function Tx (object) { this.rbf = obj.rbf; this.publicNote = obj.note; this.note = MyWallet.wallet.getNote(this.hash); - this.confirmations = Tx.setConfirmations(this.block_height); + this.confirmations = Tx.setConfirmations(this.block_height, MyWallet.wallet.latestBlock); + this.coinCode = obj.coinCode || 'btc'; // computed properties var initialIn = { @@ -284,9 +286,10 @@ function isCoinBase (input) { return (input == null || input.prev_out == null || input.prev_out.addr == null); } -Tx.factory = function (o) { +Tx.factory = function (o, coinCode) { if (o instanceof Object && !(o instanceof Tx)) { - return new Tx(o); + let setCoinCode = assoc('coinCode', coinCode === 'bch' ? 'bch' : 'btc'); + return new Tx(setCoinCode(o)); } else { return o; } }; @@ -310,8 +313,7 @@ Tx.IOSfactory = function (tx) { }; }; -Tx.setConfirmations = function (txBlockHeight) { - var lastBlock = MyWallet.wallet.latestBlock; +Tx.setConfirmations = function (txBlockHeight, lastBlock) { var conf = 0; if (lastBlock && txBlockHeight != null && txBlockHeight > 0) { conf = lastBlock.height - txBlockHeight + 1; diff --git a/tests/__mocks__/blockchain-wallet.mock.js b/tests/__mocks__/blockchain-wallet.mock.js new file mode 100644 index 000000000..bd9d4659d --- /dev/null +++ b/tests/__mocks__/blockchain-wallet.mock.js @@ -0,0 +1,68 @@ +const BIP39 = require('bip39'); +const Bitcoin = require('bitcoinjs-lib'); +const MetadataMock = require('./metadata.mock'); +const seedHex = '17eb336a2a3bc73dd4d8bd304830fe32'; +const mnemonic = BIP39.entropyToMnemonic(seedHex); +const masterhex = BIP39.mnemonicToSeed(mnemonic); +const masterHdNode = Bitcoin.HDNode.fromSeedBuffer(masterhex); + +class BlockchainWalletMock { + constructor () { + let addrs = { + '1asdf': { address: '1asdf' }, + '1watch': { address: '1watch', isWatchOnly: true }, + '1arch': { address: '1arch', archived: true } + }; + + this.addresses = Object.keys(addrs); + this.keys = this.addresses.map(a => addrs[a]); + this.activeKeys = this.keys.filter(k => !k.archived); + this.spendableActiveAddresses = this.activeKeys.filter(k => !k.isWatchOnly).map(k => k.address); + + this.hdwallet = { + // mnemonic: 'blood flower surround federal round page fat bless core dose display govern', + // masterSeedHex: '265c86692394fab95d0efc4385b89679d8daef5c9975e1f2b1f1eb4300bc10ad81d4d117c323591d543f6e54aa9d4560cad424bc66bb2bb61dc14285a508dad7', + seedHex, + defaultAccountIndex: 0, + xpubs: [ + 'xpub1', + 'xpub2' + ], + accounts: [ + { + index: 0, + label: 'My Wallet', + extendedPublicKey: 'xpub1', + receiveAddressAtIndex () {}, + changeAddressAtIndex () {} + }, + { + index: 1, + extendedPublicKey: 'xpub2', + receiveAddressAtIndex () {}, + changeAddressAtIndex () {} + } + ], + getMasterHex (seedHex, cipher = x => x) { + return cipher(masterhex); + }, + getMasterHDNode (cipher = x => x) { + return cipher(masterHdNode); + } + }; + this.isDoubleEncrypted = false; + } + metadata (type) { + return new MetadataMock(); + } + createCipher (secPass) { + return (x) => { + if (secPass !== 'correct') { + throw new Error('Second password incorrect'); + } + return x; + }; + } +} + +module.exports = BlockchainWalletMock; diff --git a/tests/__mocks__/metadata.mock.js b/tests/__mocks__/metadata.mock.js new file mode 100644 index 000000000..759383e10 --- /dev/null +++ b/tests/__mocks__/metadata.mock.js @@ -0,0 +1,10 @@ +class MetadataMock { + update () { + return Promise.resolve(null); + } + fetch () { + return Promise.resolve(null); + } +} + +module.exports = MetadataMock; diff --git a/tests/bch/bch-account.spec.js b/tests/bch/bch-account.spec.js new file mode 100644 index 000000000..2d90e7c72 --- /dev/null +++ b/tests/bch/bch-account.spec.js @@ -0,0 +1,72 @@ +/* eslint-disable semi */ +const BitcoinCashWallet = require('../../src/bch') +const BchSpendable = require('../../src/bch/bch-spendable') +const BchAccount = require('../../src/bch/bch-account') +const BlockchainWalletMock = require('../__mocks__/blockchain-wallet.mock') + +describe('BchAccount', () => { + let bch + let wallet + let btcAcc + let account + + beforeEach(() => { + wallet = new BlockchainWalletMock() + bch = BitcoinCashWallet.fromBlockchainWallet(wallet) + btcAcc = wallet.hdwallet.accounts[0] + account = new BchAccount(bch, wallet, btcAcc) + }) + + it('should have: index', () => { + expect(account.index).toEqual(0) + }) + + it('should have: xpub', () => { + expect(account.xpub).toEqual('xpub1') + }) + + it('should have: label', () => { + expect(account.label).toEqual('Bitcoin Cash - My Wallet') + }) + + it('should have: coinCode=bch', () => { + expect(account.coinCode).toEqual('bch') + }) + + it('should have: balance', () => { + spyOn(BchSpendable.prototype, 'getAddressBalance').and.callThrough() + expect(account.balance).toEqual(null) + expect(BchSpendable.prototype.getAddressBalance).toHaveBeenCalledWith(account.xpub) + }) + + it('should have: receiveAddress', () => { + spyOn(bch, 'getAccountIndexes').and.returnValue({ receive: 10 }) + spyOn(btcAcc, 'receiveAddressAtIndex').and.returnValue('1asdf') + expect(account.receiveAddress).toEqual('1asdf') + expect(bch.getAccountIndexes).toHaveBeenCalledWith('xpub1') + expect(btcAcc.receiveAddressAtIndex).toHaveBeenCalledWith(10) + }) + + it('should have: changeAddress', () => { + spyOn(bch, 'getAccountIndexes').and.returnValue({ change: 10 }) + spyOn(btcAcc, 'changeAddressAtIndex').and.returnValue('1asdf') + expect(account.changeAddress).toEqual('1asdf') + expect(bch.getAccountIndexes).toHaveBeenCalledWith('xpub1') + expect(btcAcc.changeAddressAtIndex).toHaveBeenCalledWith(10) + }) + + it('should be able to get the available balance', () => { + spyOn(BchSpendable.prototype, 'getAvailableBalance') + account.getAvailableBalance(10) + expect(BchSpendable.prototype.getAvailableBalance).toHaveBeenCalledWith(0, 10) + }) + + it('should be able to call createPayment()', () => { + let from = jasmine.createSpy('from') + spyOn(btcAcc, 'changeAddressAtIndex').and.returnValue('1asdf') + spyOn(BchSpendable.prototype, 'createPayment').and.returnValue({ from }) + account.createPayment() + expect(from).toHaveBeenCalledWith(0, '1asdf') + expect(BchSpendable.prototype.createPayment).toHaveBeenCalledWith() + }) +}) diff --git a/tests/bch/bch-imported.spec.js b/tests/bch/bch-imported.spec.js new file mode 100644 index 000000000..a97af697a --- /dev/null +++ b/tests/bch/bch-imported.spec.js @@ -0,0 +1,55 @@ +/* eslint-disable semi */ +const BitcoinCashWallet = require('../../src/bch') +const BchSpendable = require('../../src/bch/bch-spendable') +const BchImported = require('../../src/bch/bch-imported') +const BlockchainWalletMock = require('../__mocks__/blockchain-wallet.mock') + +describe('BchImported', () => { + let bch + let wallet + let imported + + beforeEach(() => { + wallet = new BlockchainWalletMock() + bch = BitcoinCashWallet.fromBlockchainWallet(wallet) + imported = new BchImported(bch, wallet) + }) + + it('should have: addresses', () => { + expect(imported.addresses).toEqual(['1asdf']) + }) + + it('should have: label', () => { + expect(imported.label).toEqual('Imported Addresses') + }) + + it('should have: coinCode=bch', () => { + expect(imported.coinCode).toEqual('bch') + }) + + it('should have: balance (null)', () => { + spyOn(BchSpendable.prototype, 'getAddressBalance').and.returnValue(null) + expect(imported.balance).toEqual(null) + expect(BchSpendable.prototype.getAddressBalance).toHaveBeenCalledWith('1asdf') + }) + + it('should have: balance (with value)', () => { + spyOn(BchSpendable.prototype, 'getAddressBalance').and.returnValue(100) + expect(imported.balance).toEqual(100) + expect(BchSpendable.prototype.getAddressBalance).toHaveBeenCalledWith('1asdf') + }) + + it('should be able to get the available balance', () => { + spyOn(BchSpendable.prototype, 'getAvailableBalance') + imported.getAvailableBalance(10) + expect(BchSpendable.prototype.getAvailableBalance).toHaveBeenCalledWith(['1asdf'], 10) + }) + + it('should be able to call createPayment()', () => { + let from = jasmine.createSpy('from') + spyOn(BchSpendable.prototype, 'createPayment').and.returnValue({ from }) + imported.createPayment() + expect(from).toHaveBeenCalledWith(['1asdf'], '1asdf') + expect(BchSpendable.prototype.createPayment).toHaveBeenCalledWith() + }) +}) diff --git a/tests/bch/bch-payment.spec.js b/tests/bch/bch-payment.spec.js new file mode 100644 index 000000000..655b92984 --- /dev/null +++ b/tests/bch/bch-payment.spec.js @@ -0,0 +1,156 @@ +/* eslint-disable semi */ +const { add, reduce, map, compose } = require('ramda') +const BchApi = require('../../src/bch/bch-api') +const BchPayment = require('../../src/bch/bch-payment') +const Coin = require('../../src/bch/coin') +const BlockchainWalletMock = require('../__mocks__/blockchain-wallet.mock') + +const addr = '19kqHHBoYbyY2bAr1SN2GuGcdSZ6fM2Qqz' + +const sumCoins = compose(reduce(add, 0), map(c => c.value)) + +describe('BchPayment', () => { + let wallet + let payment + + beforeEach(() => { + wallet = new BlockchainWalletMock() + payment = new BchPayment(wallet) + + let mocked = [Coin.of(10000), Coin.of(15000), Coin.of(20000)] + spyOn(BchApi, 'getUnspents').and.returnValue(Promise.resolve(mocked)) + }) + + describe('handleError()', () => { + it('should allow handling errors with automatic recovery', (done) => { + let handle = jasmine.createSpy('handleError') + payment + .from(0, addr).to(addr).amount(10000) + .build() // forgot feePerByte() + .handleError(handle) + .feePerByte(10) + .build() // succeeds + .handleError(handle) + .sideEffect(p => { + expect(p.selection).not.toEqual(null) + expect(handle).toHaveBeenCalledTimes(1) + done() + }) + }) + }) + + describe('from()', () => { + it('should throw for an invalid destination', () => { + let f = () => payment.from(addr, addr) + expect(f).toThrow() + }) + + it('should throw for an invalid change address', () => { + let f = () => payment.from([addr], '1asdf') + expect(f).toThrow() + }) + + it('should fetch unspents after setting from', (done) => { + payment + .from(0, addr) + .sideEffect(p => { + expect(p.coins.length).toEqual(3) + done() + }) + }) + }) + + describe('to()', () => { + it('should throw for an invalid source', () => { + let f = () => payment.to('1asdf') + expect(f).toThrow() + }) + + it('should set the to property', (done) => { + payment + .to(addr) + .sideEffect(p => { + expect(p.to).toEqual(addr) + done() + }) + }) + }) + + describe('amount()', () => { + it('should throw for an invalid amount', () => { + let f = () => payment.amount('asdf') + expect(f).toThrow() + }) + + it('should set the amount property', (done) => { + payment + .amount(10000) + .sideEffect(p => { + expect(p.amount).toEqual(10000) + done() + }) + }) + }) + + describe('feePerByte()', () => { + it('should throw for an invalid feePerByte', () => { + let f = () => payment.feePerByte('asdf') + expect(f).toThrow() + }) + + it('should set the feePerByte property', (done) => { + payment + .feePerByte(10) + .sideEffect(p => { + expect(p.feePerByte).toEqual(10) + done() + }) + }) + }) + + describe('clean()', () => { + it('should clean the proper fields', () => { + payment + .map(p => { + p.rawTx = 'rawTx' + p.hash = 'hash' + p.selection = 'selection' + return p + }) + .clean() + .sideEffect(p => { + expect(p.rawTx).toEqual(null) + expect(p.hash).toEqual(null) + expect(p.selection).toEqual(null) + }) + }) + }) + + describe('build()', () => { + it('should build a transaction', (done) => { + payment + .from(0, addr).to(addr).feePerByte(10).amount(10000) + .build() + .sideEffect(p => { + expect(p.selection.fee).toEqual(1910) + expect(sumCoins(p.selection.inputs)).toEqual(20000) + expect(sumCoins(p.selection.outputs)).toEqual(20000 - 1910) + done() + }) + }) + }) + + describe('buildSweep()', () => { + it('should build a sweep transaction', (done) => { + payment + .from(0, addr).to(addr).feePerByte(10) + .buildSweep() + .sideEffect(p => { + expect(p.selection.fee).toEqual(4850) + expect(sumCoins(p.selection.inputs)).toEqual(45000) + expect(sumCoins(p.selection.outputs)).toEqual(45000 - 4850) + done() + }) + }) + }) +}) diff --git a/tests/bch/bch-spendable.spec.js b/tests/bch/bch-spendable.spec.js new file mode 100644 index 000000000..b500aa624 --- /dev/null +++ b/tests/bch/bch-spendable.spec.js @@ -0,0 +1,51 @@ +/* eslint-disable semi */ +const BitcoinCashWallet = require('../../src/bch') +const BchSpendable = require('../../src/bch/bch-spendable') +const BchApi = require('../../src/bch/bch-api') +const Coin = require('../../src/bch/coin') +const BlockchainWalletMock = require('../__mocks__/blockchain-wallet.mock') + +describe('BchSpendable', () => { + let bch + let wallet + let spendable + + beforeEach(() => { + wallet = new BlockchainWalletMock() + bch = BitcoinCashWallet.fromBlockchainWallet(wallet) + spendable = new BchSpendable(bch, wallet) + }) + + it('should be able to call getAddressBalance()', () => { + spyOn(bch, 'getAddressBalance') + spendable.getAddressBalance('1asdf') + expect(bch.getAddressBalance).toHaveBeenCalledWith('1asdf') + }) + + it('should be able to call createPayment()', () => { + spyOn(bch, 'createPayment') + spendable.createPayment() + expect(bch.createPayment).toHaveBeenCalledWith() + }) + + describe('getAvailableBalance()', () => { + beforeEach(() => { + let mocked = [Coin.of(5000), Coin.of(15000)] + spyOn(BchApi, 'getUnspents').and.returnValue(Promise.resolve(mocked)) + }) + + it('should fetch coins for the source', () => { + spendable.getAvailableBalance(['1asdf'], 5) + expect(BchApi.getUnspents).toHaveBeenCalledWith(wallet, ['1asdf']) + }) + + it('should compute the correct values', (done) => { + spendable.getAvailableBalance(['1asdf'], 5).then(values => { + expect(values.fee).toEqual(5) + expect(values.sweepFee).toEqual(1690) + expect(values.amount).toEqual(20000 - 1690) + done() + }) + }) + }) +}) diff --git a/tests/bch/coin-selection.spec.js b/tests/bch/coin-selection.spec.js new file mode 100644 index 000000000..378c9a66e --- /dev/null +++ b/tests/bch/coin-selection.spec.js @@ -0,0 +1,98 @@ +/* eslint-disable semi */ +let { map } = require('ramda') +let cs = require('../../src/bch/coin-selection') +let Coin = require('../../src/bch/coin') + +describe('Coin Selection', () => { + describe('byte sizes', () => { + it('should return the right transaction size (empty tx)', () => { + expect(cs.transactionBytes([], [])).toEqual(10) + }) + it('should return the right transaction size (1 in 2 out tx)', () => { + expect(cs.transactionBytes([{}], [{}, {}])).toEqual(225) + }) + }) + + describe('effective Balances', () => { + it('should return the right effective max Balance', () => { + let inputs = map(Coin.of, [15000, 10000, 20000]) + let outputs = map(Coin.of, [0, 0]) + expect(cs.effectiveBalance(0, inputs, outputs).value).toEqual(45000) + }) + it('should return the right effective max Balance', () => { + let inputs = map(Coin.of, [15000, 10000, 20000]) + let outputs = map(Coin.of, [0, 0]) + expect(cs.effectiveBalance(55, inputs, outputs).value).toEqual(16455) + }) + it('should return the right effective max Balance', () => { + expect(cs.effectiveBalance(55, [], []).value).toEqual(0) + }) + it('should return the right effective max Balance', () => { + expect(cs.effectiveBalance(0, [], []).value).toEqual(0) + }) + }) + + describe('findTarget', () => { + it('should return the right selection', () => { + let selection = cs.findTarget([], 0, []) + expect(selection.fee).toEqual(0) + expect(selection.inputs).toEqual([]) + expect(selection.outputs).toEqual([]) + }) + it('should return the right selection', () => { + let inputs = map(Coin.of, [1, 2, 3]) + let targets = map(Coin.of, [10000]) + let selection = cs.findTarget(targets, 0, inputs) + expect(selection.fee).toEqual(0) + expect(selection.inputs).toEqual([]) + expect(selection.outputs).toEqual(targets) + }) + it('should return the right selection', () => { + let inputs = map(Coin.of, [1, 20000, 300000]) + let targets = map(Coin.of, [10000]) + let selection = cs.findTarget(targets, 55, inputs) + expect(selection.fee).toEqual(18590) + expect(selection.inputs.map(x => x.value)).toEqual([20000, 300000]) + expect(selection.outputs.map(x => x.value)).toEqual([10000, 291410]) + }) + }) + + describe('selectAll', () => { + it('should return the right selection', () => { + let inputs = map(Coin.of, [1, 20000, 0, 0, 300000]) + let selection = cs.selectAll(55, inputs) + expect(selection.fee).toEqual(18590) + expect(selection.inputs.map(x => x.value)).toEqual([20000, 300000]) + expect(selection.outputs.map(x => x.value)).toEqual([301410]) + }) + it('should return the right selection', () => { + let inputs = map(Coin.of, []) + let selection = cs.selectAll(55, inputs) + expect(selection.fee).toEqual(0) + expect(selection.inputs.map(x => x.value)).toEqual([]) + expect(selection.outputs.map(x => x.value)).toEqual([0]) + }) + }) + + describe('descentDraw', () => { + it('should return the right selection', () => { + let inputs = map(Coin.of, [1, 20000, 0, 0, 300000, 50000, 30000]) + let targets = map(Coin.of, [100000]) + let selection = cs.descentDraw(targets, 55, inputs, 'change-address') + expect(selection.fee).toEqual(10505) + expect(selection.inputs.map(x => x.value)).toEqual([300000]) + expect(selection.outputs.map(x => x.value)).toEqual([100000, 189495]) + }) + }) + + describe('ascentDraw', () => { + it('should return the right selection', () => { + let inputs = map(Coin.of, [1, 20000, 0, 0, 300000, 50000, 30000]) + let targets = map(Coin.of, [100000]) + let selection = cs.ascentDraw(targets, 55, inputs, 'change-address') + expect(selection.fee).toEqual(34760) + expect(selection.inputs.map(x => x.value)).toEqual([20000, 30000, 50000, 300000]) + expect(selection.outputs.map(x => x.value)).toEqual([100000, 265240]) + }) + }) +}) diff --git a/tests/bch/coin.spec.js b/tests/bch/coin.spec.js new file mode 100644 index 000000000..ef734c477 --- /dev/null +++ b/tests/bch/coin.spec.js @@ -0,0 +1,86 @@ +/* eslint-disable semi */ +const { map, reduce, curry } = require('ramda') +const Coin = require('../../src/bch/coin') + +const fold = curry((empty, xs) => reduce((acc, x) => acc.concat(x), empty, xs)) + +describe('Coin Selection', () => { + describe('letants', () => { + it('TX_EMPTY_SIZE', () => { + expect(Coin.TX_EMPTY_SIZE).toEqual(10) + }) + it('TX_INPUT_BASE', () => { + expect(Coin.TX_INPUT_BASE).toEqual(41) + }) + it('TX_EMPTY_SIZE', () => { + expect(Coin.TX_INPUT_PUBKEYHASH).toEqual(106) + }) + it('TX_EMPTY_SIZE', () => { + expect(Coin.TX_OUTPUT_BASE).toEqual(9) + }) + it('TX_EMPTY_SIZE', () => { + expect(Coin.TX_OUTPUT_PUBKEYHASH).toEqual(25) + }) + }) + + describe('Coin Type', () => { + it('coins monoid', () => { + let A = Coin.of(100) + let B = Coin.of(300) + expect(A.concat(B).value).toEqual(400) + }) + it('coins monoid', () => { + let coins = map(Coin.fromJS, [{value: 1}, {value: 2}, {value: 3}, {value: 4}, {value: 5}, {value: 6}, {value: 7}, {value: 8}, {value: 9}, {value: 10}]) + let sum = fold(Coin.empty, coins).value + expect(sum).toEqual(55) + }) + it('coins setoid', () => { + let A = Coin.of(100) + let B = Coin.of(100) + expect(A.equals(B)).toEqual(true) + }) + it('coins setoid', () => { + let A = Coin.of(100) + let B = Coin.of(0) + expect(A.equals(B)).toEqual(false) + }) + it('coins setoid', () => { + let A = Coin.of(100) + let B = Coin.of(0) + expect(A.lte(B)).toEqual(false) + }) + it('coins setoid', () => { + let A = Coin.of(0) + let B = Coin.of(100) + expect(A.lte(B)).toEqual(true) + }) + it('coins map', () => { + let A = Coin.of(100) + let square = x => x * x + expect(A.map(square).value).toEqual(square(A.value)) + }) + it('coin empty', () => { + let A = Coin.empty + expect(A.value).toEqual(0) + }) + }) + describe('coin byte sizes', () => { + it('should return the right input size', () => { + expect(Coin.inputBytes({})).toEqual(147) + }) + it('should return the right output size', () => { + expect(Coin.outputBytes({})).toEqual(34) + }) + }) + describe('effective values', () => { + it('should return the right coin value', () => { + expect(Coin.effectiveValue(55, Coin.of(15000))).toEqual(6915) + }) + it('should return zero coin value', () => { + expect(Coin.effectiveValue(55000, Coin.of(15000))).toEqual(0) + }) + it('should return max coin value', () => { + expect(Coin.effectiveValue(0, Coin.of(15000))).toEqual(15000) + }) + }) +}) diff --git a/tests/bch/index.spec.js b/tests/bch/index.spec.js new file mode 100644 index 000000000..1629a2f49 --- /dev/null +++ b/tests/bch/index.spec.js @@ -0,0 +1,104 @@ +/* eslint-disable semi */ +const BitcoinCashWallet = require('../../src/bch') +const BchApi = require('../../src/bch/bch-api') +const BchPayment = require('../../src/bch/bch-payment') +const BlockchainWalletMock = require('../__mocks__/blockchain-wallet.mock') + +describe('bch', () => { + let bch + let wallet + + beforeEach(() => { + wallet = new BlockchainWalletMock(); + bch = BitcoinCashWallet.fromBlockchainWallet(wallet) + }); + + it('should have balance = null', () => { + expect(bch.balance).toEqual(null) + }) + + it('should have txs = []', () => { + expect(bch.txs).toEqual([]) + }) + + it('should have a defaultAccount matching the hdwallet default', () => { + expect(bch.defaultAccount).toEqual(bch.accounts[0]) + wallet.hdwallet.defaultAccountIndex = 1 + expect(bch.defaultAccount).toEqual(bch.accounts[1]) + }) + + it('should have importedAddresses if there are imported addresses', () => { + expect(bch.importedAddresses).not.toEqual(null) + }) + + it('should not have importedAddresses if there are no spendable active addresses', () => { + wallet.spendableActiveAddresses = [] + bch = BitcoinCashWallet.fromBlockchainWallet(wallet) + expect(bch.importedAddresses).toEqual(null) + }) + + it('should have accounts matching the number of hd accounts', () => { + expect(bch.accounts.length).toEqual(wallet.hdwallet.accounts.length) + }) + + describe('getAddressBalance()', () => { + beforeEach(() => { + bch._addressInfo = { '1asdf': { final_balance: 100 } } + }) + + it('should get the balance for an address', () => { + expect(bch.getAddressBalance('1asdf')).toEqual(100) + }) + + it('should return null if the address has no stored balance', () => { + expect(bch.getAddressBalance('1nope')).toEqual(null) + }) + }) + + describe('getAccountIndexes()', () => { + beforeEach(() => { + bch._addressInfo = { 'xpub1': { account_index: 8, change_index: 6 } } + }) + + it('should get receive and change indexes', () => { + expect(bch.getAccountIndexes('xpub1')).toEqual({ receive: 8, change: 6 }) + }) + + it('should return default values of 0', () => { + expect(bch.getAccountIndexes('xpub2')).toEqual({ receive: 0, change: 0 }) + }) + }) + + describe('getHistory()', () => { + beforeEach(() => { + let wallet = { final_balance: 100 } + let addresses = [{ address: '1asdf', final_balance: 100 }] + let txs = [] + let info = { latest_block: { height: 500000 } } + let mocked = { wallet, addresses, txs, info } + spyOn(BchApi, 'multiaddr').and.returnValue(Promise.resolve(mocked)) + }) + + it('should call multiaddr with all addresses and xpubs', (done) => { + bch.getHistory().then(() => { + expect(BchApi.multiaddr).toHaveBeenCalledWith(['1asdf', 'xpub1', 'xpub2'], 50) + done() + }) + }) + + it('should set the balance and address info', (done) => { + bch.getHistory().then(() => { + expect(bch.balance).toEqual(100) + expect(bch.getAddressBalance('1asdf')).toEqual(100) + done() + }) + }) + }) + + describe('createPayment()', () => { + it('should create a payment', () => { + let payment = bch.createPayment() + expect(payment instanceof BchPayment).toEqual(true) + }) + }) +}) diff --git a/tests/eth/eth-wallet.spec.js b/tests/eth/eth-wallet.spec.js index 3df9ea533..a53dc37b7 100644 --- a/tests/eth/eth-wallet.spec.js +++ b/tests/eth/eth-wallet.spec.js @@ -1,52 +1,9 @@ -const BIP39 = require('bip39'); -const Bitcoin = require('bitcoinjs-lib'); const EthWallet = require('../../src/eth/eth-wallet'); const EthAccount = require('../../src/eth/eth-account'); const EthSocket = require('../../src/eth/eth-socket'); - -class MetadataMock { - update () { - return Promise.resolve(null); - } - fetch () { - return Promise.resolve(null); - } -} +const BlockchainWalletMock = require('../__mocks__/blockchain-wallet.mock'); // cache constants for test performance -const seedHex = '17eb336a2a3bc73dd4d8bd304830fe32'; -const mnemonic = BIP39.entropyToMnemonic(seedHex); -const masterhex = BIP39.mnemonicToSeed(mnemonic); -const masterHdNode = Bitcoin.HDNode.fromSeedBuffer(masterhex); - -class BlockchainWalletMock { - constructor () { - this.hdwallet = { - // mnemonic: 'blood flower surround federal round page fat bless core dose display govern', - // masterSeedHex: '265c86692394fab95d0efc4385b89679d8daef5c9975e1f2b1f1eb4300bc10ad81d4d117c323591d543f6e54aa9d4560cad424bc66bb2bb61dc14285a508dad7', - seedHex, - getMasterHex (seedHex, cipher = x => x) { - return cipher(masterhex); - }, - getMasterHDNode (cipher = x => x) { - return cipher(masterHdNode); - } - }; - this.isDoubleEncrypted = false; - } - metadata (type) { - return new MetadataMock(); - } - createCipher (secPass) { - return (x) => { - if (secPass !== 'correct') { - throw new Error('Second password incorrect'); - } - return x; - }; - } -} - describe('EthWallet', () => { const wsUrl = 'wss://ws.blockchain.com/eth/inv'; diff --git a/yarn.lock b/yarn.lock index a419cd99a..62c1fc2fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -956,6 +956,12 @@ base-x@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac" +base-x@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.2.tgz#bf873861b7514279b7969f340929eab87c11d130" + dependencies: + safe-buffer "^5.0.1" + base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -1042,6 +1048,10 @@ bitcoin-exchange-client@~0.4.1: dependencies: isomorphic-fetch "^2.2.0" +bitcoin-ops@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.3.0.tgz#6b126b585537bc679b02ed499f14450cffc37e13" + bitcoin-sfox-client@^0.1.11: version "0.1.12" resolved "https://registry.yarnpkg.com/bitcoin-sfox-client/-/bitcoin-sfox-client-0.1.12.tgz#8340b4d91585976179c1922b40b8c89a66c7ec8f" @@ -1058,6 +1068,25 @@ bitcoin-unocoin-client@^0.3.4: babelify "7.3.*" bitcoin-exchange-client "~0.4.1" +"bitcoincashjs-lib@https://github.com/bitcoinjs/bitcoinjs-lib#9ac221c80dbc3462d7a2392cec2045ae2b590461": + version "3.1.1" + resolved "https://github.com/bitcoinjs/bitcoinjs-lib#9ac221c80dbc3462d7a2392cec2045ae2b590461" + dependencies: + bigi "^1.4.0" + bip66 "^1.1.0" + bitcoin-ops "^1.3.0" + bs58check "^2.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.3" + ecurve "^1.0.0" + merkle-lib "^2.0.10" + pushdata-bitcoin "^1.0.1" + randombytes "^2.0.1" + safe-buffer "^5.0.1" + typeforce "^1.8.7" + varuint-bitcoin "^1.0.4" + wif "^2.0.1" + bitcoinjs-lib@2.1.*: version "2.1.4" resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-2.1.4.tgz#d4fc235b065aa19c48e11d9136eeab07b38c1534" @@ -1338,6 +1367,19 @@ bs58@^3.1.0: dependencies: base-x "^1.1.0" +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + dependencies: + base-x "^3.0.2" + +bs58check@<3.0.0, bs58check@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.0.2.tgz#06f63b01c2fa6173033c90eb87f1fe3d2e13d89a" + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + bs58check@^1.0.5, bs58check@^1.0.6, bs58check@^1.0.8: version "1.3.4" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-1.3.4.tgz#c52540073749117714fa042c3047eb8f9151cbf8" @@ -3722,6 +3764,10 @@ merge-descriptors@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" +merkle-lib@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/merkle-lib/-/merkle-lib-2.0.10.tgz#82b8dbae75e27a7785388b73f9d7725d0f6f3326" + methods@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/methods/-/methods-0.0.1.tgz#277c90f8bef39709645a8371c51c3b6c648e068c" @@ -4401,6 +4447,12 @@ punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +pushdata-bitcoin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz#15931d3cd967ade52206f523aa7331aef7d43af7" + dependencies: + bitcoin-ops "^1.3.0" + q@^1.2.0: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" @@ -4429,6 +4481,16 @@ querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" +ramda-lens@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ramda-lens/-/ramda-lens-0.1.2.tgz#e8b340def6e0787b395dde318f3b68ebf28dba9d" + dependencies: + ramda "^0.19.1" + +ramda@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.19.1.tgz#89c4ad697265ff6b1face9f286439e2520d6679c" + ramda@^0.22.1: version "0.22.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.22.1.tgz#031da0c3df417c5b33c96234757eb37033f36a0e" @@ -5272,10 +5334,6 @@ sync-exec@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/sync-exec/-/sync-exec-0.5.0.tgz#3f7258e4a5ba17245381909fa6a6f6cf506e1661" -synk@^0.0.2: - version "0.0.2" - resolved "https://registry.npmjs.org/synk/-/synk-0.0.02.tgz#615c923a5877400d3928a944f18dbd2c7c506bdc" - syntax-error@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.3.0.tgz#1ed9266c4d40be75dc55bf9bb1cb77062bb96ca1" @@ -5442,7 +5500,7 @@ typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typeforce@^1.5.5: +typeforce@^1.5.5, typeforce@^1.8.7: version "1.11.1" resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.11.1.tgz#ab66f3b094856d00ed0c8913b0742d3dabfafe62" dependencies: @@ -5556,6 +5614,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" +varuint-bitcoin@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.0.4.tgz#d812c5dae16e32f60544b6adee1d4be1307d0283" + verror@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" @@ -5633,18 +5695,12 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" -which@^1.0.5, which@^1.1.1, which@^1.2.4, which@~1.2.10: +which@^1.0.5, which@^1.1.1, which@^1.2.1, which@^1.2.4, which@~1.2.10: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" dependencies: isexe "^2.0.0" -which@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" @@ -5657,6 +5713,12 @@ wif@^1.1.0: dependencies: bs58check "^1.0.6" +wif@^2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + dependencies: + bs58check "<3.0.0" + window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"