|
|
@ -1150,7 +1150,8 @@ WalletService.prototype.getBalance = function(opts, cb) { |
|
|
|
/** |
|
|
|
* Return info needed to send all funds in the wallet |
|
|
|
* @param {Object} opts |
|
|
|
* @param {string} opts.feePerKb - The fee per KB used to compute the TX. |
|
|
|
* @param {number} opts.feeLevel[='normal'] - Optional. Specify the fee level for this TX ('priority', 'normal', 'economy', 'superEconomy') as defined in Defaults.FEE_LEVELS. |
|
|
|
* @param {number} opts.feePerKb - Optional. Specify the fee per KB for this TX (in satoshi). |
|
|
|
* @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs |
|
|
|
* @param {string} opts.returnInputs[=false] - Optional. Return the list of UTXOs that would be included in the tx. |
|
|
|
* @returns {Object} sendMaxInfo |
|
|
@ -1161,7 +1162,26 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
if (!checkRequired(opts, ['feePerKb'], cb)) return; |
|
|
|
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb); |
|
|
|
if (feeArgs > 1) |
|
|
|
return cb(new ClientError('Only one of feeLevel/feePerKb can be specified')); |
|
|
|
|
|
|
|
if (feeArgs == 0) { |
|
|
|
log.debug('No fee provided, using "normal" fee level'); |
|
|
|
opts.feeLevel = 'normal'; |
|
|
|
} |
|
|
|
|
|
|
|
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 (_.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.getWallet({}, function(err, wallet) { |
|
|
|
if (err) return cb(err); |
|
|
@ -1173,6 +1193,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
size: 0, |
|
|
|
amount: 0, |
|
|
|
fee: 0, |
|
|
|
feePerKb: 0, |
|
|
|
inputs: [], |
|
|
|
utxosBelowFee: 0, |
|
|
|
amountBelowFee: 0, |
|
|
@ -1190,47 +1211,53 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
|
|
|
|
if (_.isEmpty(inputs)) return cb(null, info); |
|
|
|
|
|
|
|
var txp = Model.TxProposal.create({ |
|
|
|
walletId: self.walletId, |
|
|
|
network: wallet.network, |
|
|
|
walletM: wallet.m, |
|
|
|
walletN: wallet.n, |
|
|
|
feePerKb: opts.feePerKb, |
|
|
|
}); |
|
|
|
self._getFeePerKb(wallet, opts, function(err, feePerKb) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var baseTxpSize = txp.getEstimatedSize(); |
|
|
|
var baseTxpFee = baseTxpSize * txp.feePerKb / 1000.; |
|
|
|
var sizePerInput = txp.getEstimatedSizeForSingleInput(); |
|
|
|
var feePerInput = sizePerInput * txp.feePerKb / 1000.; |
|
|
|
info.feePerKb = feePerKb; |
|
|
|
|
|
|
|
var partitionedByAmount = _.partition(inputs, function(input) { |
|
|
|
return input.satoshis > feePerInput; |
|
|
|
}); |
|
|
|
var txp = Model.TxProposal.create({ |
|
|
|
walletId: self.walletId, |
|
|
|
network: wallet.network, |
|
|
|
walletM: wallet.m, |
|
|
|
walletN: wallet.n, |
|
|
|
feePerKb: feePerKb, |
|
|
|
}); |
|
|
|
|
|
|
|
info.utxosBelowFee = partitionedByAmount[1].length; |
|
|
|
info.amountBelowFee = _.sum(partitionedByAmount[1], 'satoshis'); |
|
|
|
inputs = partitionedByAmount[0]; |
|
|
|
var baseTxpSize = txp.getEstimatedSize(); |
|
|
|
var baseTxpFee = baseTxpSize * txp.feePerKb / 1000.; |
|
|
|
var sizePerInput = txp.getEstimatedSizeForSingleInput(); |
|
|
|
var feePerInput = sizePerInput * txp.feePerKb / 1000.; |
|
|
|
|
|
|
|
_.each(inputs, function(input, i) { |
|
|
|
var sizeInKb = (baseTxpSize + (i + 1) * sizePerInput) / 1000.; |
|
|
|
if (sizeInKb > Defaults.MAX_TX_SIZE_IN_KB) { |
|
|
|
info.utxosAboveMaxSize = inputs.length - i; |
|
|
|
info.amountAboveMaxSize = _.sum(_.slice(inputs, i), 'satoshis'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
txp.inputs.push(input); |
|
|
|
}); |
|
|
|
var partitionedByAmount = _.partition(inputs, function(input) { |
|
|
|
return input.satoshis > feePerInput; |
|
|
|
}); |
|
|
|
|
|
|
|
if (_.isEmpty(txp.inputs)) return cb(null, info); |
|
|
|
info.utxosBelowFee = partitionedByAmount[1].length; |
|
|
|
info.amountBelowFee = _.sum(partitionedByAmount[1], 'satoshis'); |
|
|
|
inputs = partitionedByAmount[0]; |
|
|
|
|
|
|
|
info.size = txp.getEstimatedSize(); |
|
|
|
info.fee = txp.getEstimatedFee(); |
|
|
|
info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; |
|
|
|
if (opts.returnInputs) { |
|
|
|
info.inputs = _.shuffle(txp.inputs); |
|
|
|
} |
|
|
|
_.each(inputs, function(input, i) { |
|
|
|
var sizeInKb = (baseTxpSize + (i + 1) * sizePerInput) / 1000.; |
|
|
|
if (sizeInKb > Defaults.MAX_TX_SIZE_IN_KB) { |
|
|
|
info.utxosAboveMaxSize = inputs.length - i; |
|
|
|
info.amountAboveMaxSize = _.sum(_.slice(inputs, i), 'satoshis'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
txp.inputs.push(input); |
|
|
|
}); |
|
|
|
|
|
|
|
if (_.isEmpty(txp.inputs)) return cb(null, info); |
|
|
|
|
|
|
|
info.size = txp.getEstimatedSize(); |
|
|
|
info.fee = txp.getEstimatedFee(); |
|
|
|
info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; |
|
|
|
if (opts.returnInputs) { |
|
|
|
info.inputs = _.shuffle(txp.inputs); |
|
|
|
} |
|
|
|
|
|
|
|
return cb(null, info); |
|
|
|
return cb(null, info); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -1704,7 +1731,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) |
|
|
|
return next(new ClientError('Invalid fee per KB')); |
|
|
|
} |
|
|
|
|
|
|
|
if (_.isNumber(opts.fee) && !opts.inputs) |
|
|
|
if (_.isNumber(opts.fee) && _.isEmpty(opts.inputs)) |
|
|
|
return next(new ClientError('fee can only be set when inputs are specified')); |
|
|
|
|
|
|
|
next(); |
|
|
@ -1745,6 +1772,27 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) |
|
|
|
], cb); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._getFeePerKb = function(wallet, opts, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
if (_.isNumber(opts.feePerKb)) return cb(null, opts.feePerKb); |
|
|
|
self.getFeeLevels({ |
|
|
|
network: wallet.network |
|
|
|
}, function(err, levels) { |
|
|
|
if (err) return cb(err); |
|
|
|
var level = _.find(levels, { |
|
|
|
level: opts.feeLevel |
|
|
|
}); |
|
|
|
if (!level) { |
|
|
|
var msg = 'Could not compute fee for "' + opts.feeLevel + '" level'; |
|
|
|
log.error(msg); |
|
|
|
return cb(new ClientError(msg)); |
|
|
|
} |
|
|
|
return cb(null, level.feePerKb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Creates a new transaction proposal. |
|
|
|
* @param {Object} opts |
|
|
@ -1791,26 +1839,6 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
function getFeePerKb(wallet, cb) { |
|
|
|
if (opts.inputs && _.isNumber(opts.fee)) return cb(); |
|
|
|
if (_.isNumber(opts.feePerKb)) return cb(null, opts.feePerKb); |
|
|
|
self.getFeeLevels({ |
|
|
|
network: wallet.network |
|
|
|
}, function(err, levels) { |
|
|
|
if (err) return cb(err); |
|
|
|
var level = _.find(levels, { |
|
|
|
level: opts.feeLevel |
|
|
|
}); |
|
|
|
if (!level) { |
|
|
|
var msg = 'Could not compute fee for "' + opts.feeLevel + '" level'; |
|
|
|
log.error(msg); |
|
|
|
return cb(new ClientError(msg)); |
|
|
|
} |
|
|
|
return cb(null, level.feePerKb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
function checkTxpAlreadyExists(txProposalId, cb) { |
|
|
|
if (!txProposalId) return cb(); |
|
|
|
self.storage.fetchTx(self.walletId, txProposalId, cb); |
|
|
@ -1847,7 +1875,8 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(next) { |
|
|
|
getFeePerKb(wallet, function(err, fee) { |
|
|
|
if (_.isNumber(opts.fee) && !_.isEmpty(opts.inputs)) return next(); |
|
|
|
self._getFeePerKb(wallet, opts, function(err, fee) { |
|
|
|
feePerKb = fee; |
|
|
|
next(); |
|
|
|
}); |
|
|
|