Merge pull request #573 from isocolsky/feat/fee-level

Allow feeLevel in getSendMaxInfo + v1.11.0
This commit is contained in:
Matias Alejo Garcia 2016-08-24 09:23:36 -03:00 committed by GitHub
commit 8d0c93b3d0
4 changed files with 145 additions and 63 deletions

View File

@ -404,10 +404,12 @@ ExpressApp.prototype.start = function(opts, cb) {
router.get('/v1/sendmaxinfo/', function(req, res) {
getServerWithAuth(req, res, function(server) {
var q = req.query;
var opts = {};
opts.feePerKb = +req.query.feePerKb;
if (req.query.excludeUnconfirmedUtxos == '1') opts.excludeUnconfirmedUtxos = true;
if (req.query.returnInputs == '1') opts.returnInputs = true;
if (q.feePerKb) opts.feePerKb = +q.feePerKb;
if (q.feeLevel) opts.feeLevel = q.feeLevel;
if (q.excludeUnconfirmedUtxos == '1') opts.excludeUnconfirmedUtxos = true;
if (q.returnInputs == '1') opts.returnInputs = true;
server.getSendMaxInfo(opts, function(err, info) {
if (err) return returnError(err, res, req);
res.json(info);

View File

@ -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;
var partitionedByAmount = _.partition(inputs, function(input) {
return input.satoshis > feePerInput;
});
info.utxosBelowFee = partitionedByAmount[1].length;
info.amountBelowFee = _.sum(partitionedByAmount[1], 'satoshis');
inputs = partitionedByAmount[0];
_.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);
}
txp.inputs.push(input);
return cb(null, info);
});
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);
});
});
};
@ -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();
});

View File

@ -2,7 +2,7 @@
"name": "bitcore-wallet-service",
"description": "A service for Mutisig HD Bitcoin Wallets",
"author": "BitPay Inc",
"version": "1.10.0",
"version": "1.11.0",
"keywords": [
"bitcoin",
"copay",

View File

@ -4184,6 +4184,57 @@ describe('Wallet service', function() {
});
});
});
describe('Fee level', function() {
it('should correctly get send max info using feeLevel', function(done) {
helpers.stubFeeLevels({
1: 400e2,
2: 200e2,
6: 180e2,
24: 90e2,
});
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({
feeLevel: 'economy',
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.feePerKb.should.equal(180e2);
info.fee.should.equal(info.size * 180e2 / 1000.);
sendTx(info, done);
});
});
});
it('should assume "normal" fee level if not specified', function(done) {
helpers.stubFeeLevels({
1: 400e2,
2: 200e2,
6: 180e2,
24: 90e2,
});
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.feePerKb.should.equal(200e2);
info.fee.should.equal(info.size * 200e2 / 1000.);
done();
});
});
});
it('should fail on invalid fee level', function(done) {
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({
feeLevel: 'madeUpLevel',
}, function(err, info) {
should.exist(err);
should.not.exist(info);
err.toString().should.contain('Invalid fee level');
done();
});
});
});
});
it('should return inputs in random order', function(done) {
// NOTE: this test has a chance of failing of 1 in 1'073'741'824 :P
helpers.stubUtxos(server, wallet, _.range(1, 31), function(utxos) {