|
|
@ -12,6 +12,10 @@ var EmailValidator = require('email-validator'); |
|
|
|
var Stringify = require('json-stable-stringify'); |
|
|
|
|
|
|
|
var Bitcore = require('bitcore-lib'); |
|
|
|
var Bitcore_ = { |
|
|
|
btc: Bitcore, |
|
|
|
bch: require('bitcore-lib-cash') |
|
|
|
}; |
|
|
|
|
|
|
|
var Common = require('./common'); |
|
|
|
var Utils = Common.Utils; |
|
|
@ -158,7 +162,7 @@ WalletService.handleIncomingNotification = function(notification, cb) { |
|
|
|
|
|
|
|
if (!notification || notification.type != 'NewBlock') return cb(); |
|
|
|
|
|
|
|
WalletService._clearBlockchainHeightCache(notification.data.network); |
|
|
|
WalletService._clearBlockchainHeightCache(notification.data.coin, notification.data.network); |
|
|
|
return cb(); |
|
|
|
}; |
|
|
|
|
|
|
@ -318,6 +322,7 @@ WalletService.prototype.logout = function(opts, cb) { |
|
|
|
* @param {number} opts.n - Total copayers. |
|
|
|
* @param {string} opts.pubKey - Public key to verify copayers joining have access to the wallet secret. |
|
|
|
* @param {string} opts.singleAddress[=false] - The wallet will only ever have one address. |
|
|
|
* @param {string} opts.coin[='btc'] - The coin for this wallet (btc, bch). |
|
|
|
* @param {string} opts.network[='livenet'] - The Bitcoin network for this wallet. |
|
|
|
* @param {string} opts.supportBIP44AndP2PKH[=true] - Client supports BIP44 & P2PKH for new wallets. |
|
|
|
*/ |
|
|
@ -331,8 +336,12 @@ WalletService.prototype.createWallet = function(opts, cb) { |
|
|
|
if (!Wallet.verifyCopayerLimits(opts.m, opts.n)) |
|
|
|
return cb(new ClientError('Invalid combination of required copayers / total copayers')); |
|
|
|
|
|
|
|
opts.coin = opts.coin || Defaults.COIN; |
|
|
|
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS)) |
|
|
|
return cb(new ClientError('Invalid coin')); |
|
|
|
|
|
|
|
opts.network = opts.network || 'livenet'; |
|
|
|
if (!_.contains(['livenet', 'testnet'], opts.network)) |
|
|
|
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS)) |
|
|
|
return cb(new ClientError('Invalid network')); |
|
|
|
|
|
|
|
opts.supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true; |
|
|
@ -364,6 +373,7 @@ WalletService.prototype.createWallet = function(opts, cb) { |
|
|
|
name: opts.name, |
|
|
|
m: opts.m, |
|
|
|
n: opts.n, |
|
|
|
coin: opts.coin, |
|
|
|
network: opts.network, |
|
|
|
pubKey: pubKey.toString(), |
|
|
|
singleAddress: !!opts.singleAddress, |
|
|
@ -417,7 +427,7 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(done) { |
|
|
|
self.storage.fetchAddress(opts.identifier, function(err, address) { |
|
|
|
self.storage.fetchAddressByCoin(Defaults.COIN, opts.identifier, function(err, address) { |
|
|
|
if (address) walletId = address.walletId; |
|
|
|
return done(err); |
|
|
|
}); |
|
|
@ -435,20 +445,29 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { |
|
|
|
} |
|
|
|
|
|
|
|
// Is identifier a txid form an incomming tx?
|
|
|
|
async.detectSeries(_.values(Constants.NETWORKS), function(network, nextNetwork) { |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
var coinNetworkPairs = []; |
|
|
|
_.each(_.values(Constants.COINS), function(coin) { |
|
|
|
_.each(_.values(Constants.NETWORKS), function(network) { |
|
|
|
coinNetworkPairs.push({ |
|
|
|
coin: coin, |
|
|
|
network: network |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
async.detectSeries(coinNetworkPairs, function(coinNetwork, nextCoinNetwork) { |
|
|
|
var bc = self._getBlockchainExplorer(coinNetwork.coin, coinNetwork.network); |
|
|
|
bc.getTransaction(opts.identifier, function(err, tx) { |
|
|
|
if (err || !tx) return nextNetwork(err, false); |
|
|
|
if (err || !tx) return nextCoinNetwork(err, false); |
|
|
|
var outputs = _.first(self._normalizeTxHistory(tx)).outputs; |
|
|
|
var toAddresses = _.pluck(outputs, 'address'); |
|
|
|
async.detect(toAddresses, function(addressStr, nextAddress) { |
|
|
|
self.storage.fetchAddress(addressStr, function(err, address) { |
|
|
|
self.storage.fetchAddressByCoin(coinNetwork.coin, addressStr, function(err, address) { |
|
|
|
if (err || !address) return nextAddress(err, false); |
|
|
|
walletId = address.walletId; |
|
|
|
nextAddress(null, true); |
|
|
|
}); |
|
|
|
}, function(err) { |
|
|
|
nextNetwork(err, !!walletId); |
|
|
|
nextCoinNetwork(err, !!walletId); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, function(err) { |
|
|
@ -622,6 +641,7 @@ WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var copayer = Model.Copayer.create({ |
|
|
|
coin: wallet.coin, |
|
|
|
name: opts.name, |
|
|
|
copayerIndex: wallet.copayers.length, |
|
|
|
xPubKey: opts.xPubKey, |
|
|
@ -630,6 +650,7 @@ WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) { |
|
|
|
customData: opts.customData, |
|
|
|
derivationStrategy: wallet.derivationStrategy, |
|
|
|
}); |
|
|
|
|
|
|
|
self.storage.fetchCopayerLookup(copayer.id, function(err, res) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (res) return cb(Errors.COPAYER_REGISTERED); |
|
|
@ -755,6 +776,7 @@ WalletService._getCopayerHash = function(name, xPubKey, requestPubKey) { |
|
|
|
* Joins a wallet in creation. |
|
|
|
* @param {Object} opts |
|
|
|
* @param {string} opts.walletId - The wallet id. |
|
|
|
* @param {string} opts.coin[='btc'] - The expected coin for this wallet (btc, bch). |
|
|
|
* @param {string} opts.name - The copayer name. |
|
|
|
* @param {string} opts.xPubKey - Extended Public Key for this copayer. |
|
|
|
* @param {string} opts.requestPubKey - Public Key used to check requests from this copayer. |
|
|
@ -771,6 +793,10 @@ WalletService.prototype.joinWallet = function(opts, cb) { |
|
|
|
if (_.isEmpty(opts.name)) |
|
|
|
return cb(new ClientError('Invalid copayer name')); |
|
|
|
|
|
|
|
opts.coin = opts.coin || Defaults.COIN; |
|
|
|
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS)) |
|
|
|
return cb(new ClientError('Invalid coin')); |
|
|
|
|
|
|
|
try { |
|
|
|
Bitcore.HDPublicKey(opts.xPubKey); |
|
|
|
} catch (ex) { |
|
|
@ -785,6 +811,10 @@ WalletService.prototype.joinWallet = function(opts, cb) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!wallet) return cb(Errors.WALLET_NOT_FOUND); |
|
|
|
|
|
|
|
if (opts.coin != wallet.coin) { |
|
|
|
return cb(new ClientError('The wallet you are trying to join was created for a different coin')); |
|
|
|
} |
|
|
|
|
|
|
|
if (opts.supportBIP44AndP2PKH) { |
|
|
|
// New client trying to join legacy wallet
|
|
|
|
if (wallet.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45) { |
|
|
@ -900,7 +930,7 @@ WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) { |
|
|
|
hasActivity: true |
|
|
|
})) return cb(null, true); |
|
|
|
|
|
|
|
var bc = self._getBlockchainExplorer(latestAddresses[0].network); |
|
|
|
var bc = self._getBlockchainExplorer(latestAddresses[0].coin, latestAddresses[0].network); |
|
|
|
var activityFound = false; |
|
|
|
var i = latestAddresses.length; |
|
|
|
async.whilst(function() { |
|
|
@ -1020,27 +1050,28 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
WalletService.prototype._getBlockchainExplorer = function(network) { |
|
|
|
WalletService.prototype._getBlockchainExplorer = function(coin, network) { |
|
|
|
var opts = {}; |
|
|
|
|
|
|
|
if (this.blockchainExplorer) return this.blockchainExplorer; |
|
|
|
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) { |
|
|
|
opts = this.blockchainExplorerOpts[network]; |
|
|
|
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[coin] && this.blockchainExplorerOpts[coin][network]) { |
|
|
|
opts = this.blockchainExplorerOpts[coin][network]; |
|
|
|
} |
|
|
|
// TODO: provider should be configurable
|
|
|
|
opts.provider = 'insight'; |
|
|
|
opts.coin = coin; |
|
|
|
opts.network = network; |
|
|
|
opts.userAgent = WalletService.getServiceVersion(); |
|
|
|
return new BlockchainExplorer(opts); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._getUtxos = function(addresses, cb) { |
|
|
|
WalletService.prototype._getUtxos = function(coin, addresses, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
if (addresses.length == 0) return cb(null, []); |
|
|
|
var networkName = Bitcore.Address(addresses[0]).toObject().network; |
|
|
|
|
|
|
|
var bc = self._getBlockchainExplorer(networkName); |
|
|
|
var bc = self._getBlockchainExplorer(coin, networkName); |
|
|
|
bc.getUtxos(addresses, function(err, utxos) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
@ -1064,10 +1095,16 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) { |
|
|
|
return utxo.txid + '|' + utxo.vout |
|
|
|
}; |
|
|
|
|
|
|
|
var allAddresses, allUtxos, utxoIndex; |
|
|
|
var coin, allAddresses, allUtxos, utxoIndex; |
|
|
|
|
|
|
|
async.series([ |
|
|
|
|
|
|
|
function(next) { |
|
|
|
self.getWallet({}, function(err, wallet) { |
|
|
|
coin = wallet.coin; |
|
|
|
return next(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(next) { |
|
|
|
if (_.isArray(addresses)) { |
|
|
|
allAddresses = addresses; |
|
|
@ -1082,7 +1119,7 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) { |
|
|
|
if (allAddresses.length == 0) return cb(null, []); |
|
|
|
|
|
|
|
var addressStrs = _.pluck(allAddresses, 'address'); |
|
|
|
self._getUtxos(addressStrs, function(err, utxos) { |
|
|
|
self._getUtxos(coin, addressStrs, function(err, utxos) { |
|
|
|
if (err) return next(err); |
|
|
|
|
|
|
|
if (utxos.length == 0) return cb(null, []); |
|
|
@ -1144,6 +1181,7 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) { |
|
|
|
/** |
|
|
|
* Returns list of UTXOs |
|
|
|
* @param {Object} opts |
|
|
|
* @param {String} [opts.coin='btc'] (optional) |
|
|
|
* @param {Array} opts.addresses (optional) - List of addresses from where to fetch UTXOs. |
|
|
|
* @returns {Array} utxos - List of UTXOs. |
|
|
|
*/ |
|
|
@ -1152,10 +1190,14 @@ WalletService.prototype.getUtxos = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
opts.coin = opts.coin || Defaults.COIN; |
|
|
|
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS)) |
|
|
|
return cb(new ClientError('Invalid coin')); |
|
|
|
|
|
|
|
if (_.isUndefined(opts.addresses)) { |
|
|
|
self._getUtxosForCurrentWallet(null, cb); |
|
|
|
} else { |
|
|
|
self._getUtxos(opts.addresses, cb); |
|
|
|
self._getUtxos(opts.coin, opts.addresses, cb); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
@ -1321,29 +1363,30 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb); |
|
|
|
if (feeArgs > 1) |
|
|
|
return cb(new ClientError('Only one of feeLevel/feePerKb can be specified')); |
|
|
|
self.getWallet({}, function(err, wallet) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (feeArgs == 0) { |
|
|
|
log.debug('No fee provided, using "normal" fee level'); |
|
|
|
opts.feeLevel = 'normal'; |
|
|
|
} |
|
|
|
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb); |
|
|
|
if (feeArgs > 1) |
|
|
|
return cb(new ClientError('Only one of feeLevel/feePerKb can be specified')); |
|
|
|
|
|
|
|
if (opts.feeLevel) { |
|
|
|
if (!_.any(Defaults.FEE_LEVELS, { |
|
|
|
name: opts.feeLevel |
|
|
|
})) |
|
|
|
return cb(new ClientError('Invalid fee level. Valid values are ' + _.pluck(Defaults.FEE_LEVELS, 'name').join(', '))); |
|
|
|
} |
|
|
|
if (feeArgs == 0) { |
|
|
|
log.debug('No fee provided, using "normal" fee level'); |
|
|
|
opts.feeLevel = 'normal'; |
|
|
|
} |
|
|
|
|
|
|
|
if (_.isNumber(opts.feePerKb)) { |
|
|
|
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB) |
|
|
|
return cb(new ClientError('Invalid fee per KB')); |
|
|
|
} |
|
|
|
var feeLevels = Defaults.FEE_LEVELS[wallet.coin]; |
|
|
|
if (opts.feeLevel) { |
|
|
|
if (!_.any(feeLevels, { |
|
|
|
name: opts.feeLevel |
|
|
|
})) |
|
|
|
return cb(new ClientError('Invalid fee level. Valid values are ' + _.pluck(feeLevels, 'name').join(', '))); |
|
|
|
} |
|
|
|
|
|
|
|
self.getWallet({}, function(err, wallet) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (_.isNumber(opts.feePerKb)) { |
|
|
|
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB) |
|
|
|
return cb(new ClientError('Invalid fee per KB')); |
|
|
|
} |
|
|
|
|
|
|
|
self._getUtxosForCurrentWallet(null, function(err, utxos) { |
|
|
|
if (err) return cb(err); |
|
|
@ -1377,6 +1420,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
|
|
|
|
var txp = Model.TxProposal.create({ |
|
|
|
walletId: self.walletId, |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network, |
|
|
|
walletM: wallet.m, |
|
|
|
walletN: wallet.n, |
|
|
@ -1427,10 +1471,10 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._sampleFeeLevels = function(network, points, cb) { |
|
|
|
WalletService.prototype._sampleFeeLevels = function(coin, network, points, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
var bc = self._getBlockchainExplorer(coin, network); |
|
|
|
bc.estimateFee(points, function(err, result) { |
|
|
|
if (err) { |
|
|
|
log.error('Error estimating fee', err); |
|
|
@ -1458,6 +1502,7 @@ WalletService.prototype._sampleFeeLevels = function(network, points, cb) { |
|
|
|
/** |
|
|
|
* Returns fee levels for the current state of the network. |
|
|
|
* @param {Object} opts |
|
|
|
* @param {string} [opts.coin = 'btc'] - The coin to estimate fee levels from. |
|
|
|
* @param {string} [opts.network = 'livenet'] - The Bitcoin network to estimate fee levels from. |
|
|
|
* @returns {Object} feeLevels - A list of fee levels & associated amount per kB in satoshi. |
|
|
|
*/ |
|
|
@ -1466,8 +1511,18 @@ WalletService.prototype.getFeeLevels = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
opts.coin = opts.coin || Defaults.COIN; |
|
|
|
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS)) |
|
|
|
return cb(new ClientError('Invalid coin')); |
|
|
|
|
|
|
|
opts.network = opts.network || 'livenet'; |
|
|
|
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS)) |
|
|
|
return cb(new ClientError('Invalid network')); |
|
|
|
|
|
|
|
var feeLevels = Defaults.FEE_LEVELS[opts.coin]; |
|
|
|
|
|
|
|
function samplePoints() { |
|
|
|
var definedPoints = _.uniq(_.pluck(Defaults.FEE_LEVELS, 'nbBlocks')); |
|
|
|
var definedPoints = _.uniq(_.pluck(feeLevels, 'nbBlocks')); |
|
|
|
return _.uniq(_.flatten(_.map(definedPoints, function(p) { |
|
|
|
return _.range(p, p + Defaults.FEE_LEVELS_FALLBACK + 1); |
|
|
|
}))); |
|
|
@ -1494,12 +1549,8 @@ WalletService.prototype.getFeeLevels = function(opts, cb) { |
|
|
|
return result; |
|
|
|
}; |
|
|
|
|
|
|
|
var network = opts.network || 'livenet'; |
|
|
|
if (network != 'livenet' && network != 'testnet') |
|
|
|
return cb(new ClientError('Invalid network')); |
|
|
|
|
|
|
|
self._sampleFeeLevels(network, samplePoints(), function(err, feeSamples) { |
|
|
|
var values = _.map(Defaults.FEE_LEVELS, function(level) { |
|
|
|
self._sampleFeeLevels(opts.coin, opts.network, samplePoints(), function(err, feeSamples) { |
|
|
|
var values = _.map(feeLevels, function(level) { |
|
|
|
var result = { |
|
|
|
level: level.name, |
|
|
|
}; |
|
|
@ -1550,10 +1601,10 @@ WalletService.prototype._checkTx = function(txp) { |
|
|
|
return ex; |
|
|
|
} |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) |
|
|
|
if (bitcoreError instanceof Bitcore_[txp.coin].errors.Transaction.FeeError) |
|
|
|
return Errors.INSUFFICIENT_FUNDS_FOR_FEE; |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs) |
|
|
|
if (bitcoreError instanceof Bitcore_[txp.coin].errors.Transaction.DustOutputs) |
|
|
|
return Errors.DUST_AMOUNT; |
|
|
|
return bitcoreError; |
|
|
|
}; |
|
|
@ -1682,7 +1733,7 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) { |
|
|
|
var changeAmount = Math.round(total - txpAmount - fee); |
|
|
|
log.debug('Tx change: ', Utils.formatAmountInBtc(changeAmount)); |
|
|
|
|
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT); |
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[txp.coin].Transaction.DUST_AMOUNT); |
|
|
|
if (changeAmount > 0 && changeAmount <= dustThreshold) { |
|
|
|
log.debug('Change below dust threshold (' + Utils.formatAmountInBtc(dustThreshold) + '). Incrementing fee to remove change.'); |
|
|
|
// Remove dust change by incrementing fee
|
|
|
@ -1835,7 +1886,7 @@ WalletService.prototype._canCreateTx = function(cb) { |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._validateOutputs = function(opts, wallet, cb) { |
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT); |
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[wallet.coin].Transaction.DUST_AMOUNT); |
|
|
|
|
|
|
|
if (_.isEmpty(opts.outputs)) return new ClientError('No outputs were specified'); |
|
|
|
|
|
|
@ -1853,7 +1904,7 @@ WalletService.prototype._validateOutputs = function(opts, wallet, cb) { |
|
|
|
} catch (ex) { |
|
|
|
return Errors.INVALID_ADDRESS; |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) { |
|
|
|
if (toAddress.network != wallet.network) { |
|
|
|
return Errors.INCORRECT_ADDRESS_NETWORK; |
|
|
|
} |
|
|
|
|
|
|
@ -1884,11 +1935,12 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) |
|
|
|
opts.feeLevel = 'normal'; |
|
|
|
} |
|
|
|
|
|
|
|
var feeLevels = Defaults.FEE_LEVELS[wallet.coin]; |
|
|
|
if (opts.feeLevel) { |
|
|
|
if (!_.any(Defaults.FEE_LEVELS, { |
|
|
|
if (!_.any(feeLevels, { |
|
|
|
name: opts.feeLevel |
|
|
|
})) |
|
|
|
return next(new ClientError('Invalid fee level. Valid values are ' + _.pluck(Defaults.FEE_LEVELS, 'name').join(', '))); |
|
|
|
return next(new ClientError('Invalid fee level. Valid values are ' + _.pluck(feeLevels, 'name').join(', '))); |
|
|
|
} |
|
|
|
|
|
|
|
if (_.isNumber(opts.feePerKb)) { |
|
|
@ -1916,7 +1968,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) |
|
|
|
return next(new ClientError('Fee is not allowed when sendMax is specified (use feeLevel/feePerKb instead)')); |
|
|
|
|
|
|
|
self.getSendMaxInfo({ |
|
|
|
feePerKb: opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB, |
|
|
|
feePerKb: opts.feePerKb, |
|
|
|
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, |
|
|
|
returnInputs: true, |
|
|
|
}, function(err, info) { |
|
|
@ -1942,6 +1994,7 @@ WalletService.prototype._getFeePerKb = function(wallet, opts, cb) { |
|
|
|
|
|
|
|
if (_.isNumber(opts.feePerKb)) return cb(null, opts.feePerKb); |
|
|
|
self.getFeeLevels({ |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network |
|
|
|
}, function(err, levels) { |
|
|
|
if (err) return cb(err); |
|
|
@ -1994,7 +2047,7 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
}); |
|
|
|
} else { |
|
|
|
if (opts.changeAddress) { |
|
|
|
self.storage.fetchAddress(opts.changeAddress, function(err, address) { |
|
|
|
self.storage.fetchAddressByWalletId(wallet.id, opts.changeAddress, function(err, address) { |
|
|
|
if (err) return cb(Errors.INVALID_CHANGE_ADDRESS); |
|
|
|
return cb(null, address); |
|
|
|
}); |
|
|
@ -2051,6 +2104,8 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
id: opts.txProposalId, |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network, |
|
|
|
outputs: opts.outputs, |
|
|
|
message: opts.message, |
|
|
|
changeAddress: changeAddress, |
|
|
@ -2315,8 +2370,8 @@ WalletService.prototype.removePendingTx = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._broadcastRawTx = function(network, raw, cb) { |
|
|
|
var bc = this._getBlockchainExplorer(network); |
|
|
|
WalletService.prototype._broadcastRawTx = function(coin, network, raw, cb) { |
|
|
|
var bc = this._getBlockchainExplorer(coin, network); |
|
|
|
bc.broadcast(raw, function(err, txid) { |
|
|
|
if (err) return cb(err); |
|
|
|
return cb(null, txid); |
|
|
@ -2326,6 +2381,7 @@ WalletService.prototype._broadcastRawTx = function(network, raw, cb) { |
|
|
|
/** |
|
|
|
* Broadcast a raw transaction. |
|
|
|
* @param {Object} opts |
|
|
|
* @param {string} [opts.coin = 'btc'] - The coin for this transaction. |
|
|
|
* @param {string} [opts.network = 'livenet'] - The Bitcoin network for this transaction. |
|
|
|
* @param {string} opts.rawTx - Raw tx data. |
|
|
|
*/ |
|
|
@ -2334,17 +2390,21 @@ WalletService.prototype.broadcastRawTx = function(opts, cb) { |
|
|
|
|
|
|
|
if (!checkRequired(opts, ['network', 'rawTx'], cb)) return; |
|
|
|
|
|
|
|
var network = opts.network || 'livenet'; |
|
|
|
if (network != 'livenet' && network != 'testnet') |
|
|
|
opts.coin = opts.coin || Defaults.COIN; |
|
|
|
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS)) |
|
|
|
return cb(new ClientError('Invalid coin')); |
|
|
|
|
|
|
|
opts.network = opts.network || 'livenet'; |
|
|
|
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS)) |
|
|
|
return cb(new ClientError('Invalid network')); |
|
|
|
|
|
|
|
self._broadcastRawTx(network, opts.rawTx, cb); |
|
|
|
self._broadcastRawTx(opts.coin, opts.network, opts.rawTx, cb); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
WalletService.prototype._checkTxInBlockchain = function(txp, cb) { |
|
|
|
if (!txp.txid) return cb(); |
|
|
|
var bc = this._getBlockchainExplorer(txp.getNetworkName()); |
|
|
|
var bc = this._getBlockchainExplorer(txp.coin, txp.network); |
|
|
|
bc.getTransaction(txp.txid, function(err, tx) { |
|
|
|
if (err) return cb(err); |
|
|
|
return cb(null, !!tx); |
|
|
@ -2472,7 +2532,7 @@ WalletService.prototype.broadcastTx = function(opts, cb) { |
|
|
|
} catch (ex) { |
|
|
|
return cb(ex); |
|
|
|
} |
|
|
|
self._broadcastRawTx(txp.getNetworkName(), raw, function(err, txid) { |
|
|
|
self._broadcastRawTx(wallet.coin, wallet.network, raw, function(err, txid) { |
|
|
|
if (err) { |
|
|
|
var broadcastErr = err; |
|
|
|
// Check if tx already in blockchain
|
|
|
@ -2678,29 +2738,35 @@ WalletService._cachedBlockheight; |
|
|
|
WalletService._initBlockchainHeightCache = function() { |
|
|
|
if (WalletService._cachedBlockheight) return; |
|
|
|
WalletService._cachedBlockheight = { |
|
|
|
livenet: {}, |
|
|
|
testnet: {} |
|
|
|
btc: { |
|
|
|
livenet: {}, |
|
|
|
testnet: {} |
|
|
|
}, |
|
|
|
bch: { |
|
|
|
livenet: {}, |
|
|
|
testnet: {} |
|
|
|
}, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService._clearBlockchainHeightCache = function(network) { |
|
|
|
WalletService._clearBlockchainHeightCache = function(coin, network) { |
|
|
|
WalletService._initBlockchainHeightCache(); |
|
|
|
if (!_.contains(['livenet', 'testnet'], network)) { |
|
|
|
log.error('Incorrect network in new block: ' + network); |
|
|
|
if (!Utils.checkValueInCollection(network, Constants.NETWORKS)) { |
|
|
|
log.error('Incorrect network in new block: ' + coin + '/' + network); |
|
|
|
return; |
|
|
|
} |
|
|
|
WalletService._cachedBlockheight[network].current = null; |
|
|
|
WalletService._cachedBlockheight[coin][network].current = null; |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._getBlockchainHeight = function(network, cb) { |
|
|
|
WalletService.prototype._getBlockchainHeight = function(coin, network, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var now = Date.now(); |
|
|
|
WalletService._initBlockchainHeightCache(); |
|
|
|
var cache = WalletService._cachedBlockheight[network]; |
|
|
|
var cache = WalletService._cachedBlockheight[coin][network]; |
|
|
|
|
|
|
|
function fetchFromBlockchain(cb) { |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
var bc = self._getBlockchainExplorer(coin, network); |
|
|
|
bc.getBlockchainHeight(function(err, height) { |
|
|
|
if (!err && height > 0) { |
|
|
|
cache.current = height; |
|
|
@ -2868,10 +2934,9 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
function getNormalizedTxs(addresses, from, to, cb) { |
|
|
|
function getNormalizedTxs(wallet, addresses, from, to, cb) { |
|
|
|
var txs, fromCache, totalItems; |
|
|
|
var useCache = addresses.length >= Defaults.HISTORY_CACHE_ADDRESS_THRESOLD; |
|
|
|
var network = Bitcore.Address(addresses[0].address).toObject().network; |
|
|
|
|
|
|
|
async.series([ |
|
|
|
|
|
|
@ -2892,7 +2957,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
if (txs) return next(); |
|
|
|
|
|
|
|
var addressStrs = _.pluck(addresses, 'address'); |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network); |
|
|
|
bc.getTransactions(addressStrs, from, to, function(err, rawTxs, total) { |
|
|
|
if (err) return next(err); |
|
|
|
|
|
|
@ -2919,7 +2984,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
if (!txs) return next(); |
|
|
|
|
|
|
|
// Fix tx confirmations for cached txs
|
|
|
|
self._getBlockchainHeight(network, function(err, height) { |
|
|
|
self._getBlockchainHeight(wallet.coin, wallet.network, function(err, height) { |
|
|
|
if (err || !height) return next(err); |
|
|
|
_.each(txs, function(tx) { |
|
|
|
if (tx.blockheight >= 0) { |
|
|
@ -2946,6 +3011,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
if (_.isEmpty(unconfirmed)) return cb(); |
|
|
|
|
|
|
|
self.getFeeLevels({ |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network |
|
|
|
}, function(err, levels) { |
|
|
|
if (err) { |
|
|
@ -2982,7 +3048,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
async.waterfall([ |
|
|
|
|
|
|
|
function(next) { |
|
|
|
getNormalizedTxs(addresses, from, to, next); |
|
|
|
getNormalizedTxs(wallet, addresses, from, to, next); |
|
|
|
}, |
|
|
|
function(txs, next) { |
|
|
|
// Fetch all proposals in [t - 7 days, t + 1 day]
|
|
|
@ -3041,12 +3107,12 @@ WalletService.prototype.scan = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
function checkActivity(address, network, cb) { |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
function checkActivity(wallet, address, cb) { |
|
|
|
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network); |
|
|
|
bc.getAddressActivity(address, cb); |
|
|
|
}; |
|
|
|
|
|
|
|
function scanBranch(derivator, cb) { |
|
|
|
function scanBranch(wallet, derivator, cb) { |
|
|
|
var inactiveCounter = 0; |
|
|
|
var allAddresses = []; |
|
|
|
var gap = Defaults.SCAN_ADDRESS_GAP; |
|
|
@ -3055,7 +3121,7 @@ WalletService.prototype.scan = function(opts, cb) { |
|
|
|
return inactiveCounter < gap; |
|
|
|
}, function(next) { |
|
|
|
var address = derivator.derive(); |
|
|
|
checkActivity(address.address, address.network, function(err, activity) { |
|
|
|
checkActivity(wallet, address.address, function(err, activity) { |
|
|
|
if (err) return next(err); |
|
|
|
|
|
|
|
allAddresses.push(address); |
|
|
@ -3099,7 +3165,7 @@ WalletService.prototype.scan = function(opts, cb) { |
|
|
|
}); |
|
|
|
|
|
|
|
async.eachSeries(derivators, function(derivator, next) { |
|
|
|
scanBranch(derivator, function(err, addresses) { |
|
|
|
scanBranch(wallet, derivator, function(err, addresses) { |
|
|
|
if (err) return next(err); |
|
|
|
self.storage.storeAddressAndWallet(wallet, addresses, next); |
|
|
|
}); |
|
|
|