From bdff2cbc355b3ece0fd1bad6394825c375a3de2d Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 25 Feb 2016 11:27:30 -0300 Subject: [PATCH] sendMax option on createTx --- lib/model/txproposal.js | 2 +- lib/server.js | 68 +++++++++++++++++++++++++++----------- test/integration/server.js | 32 +++++++++++++++--- 3 files changed, 76 insertions(+), 26 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index dfa98cd..7b9d546 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -164,7 +164,7 @@ TxProposal.prototype._buildTx = function() { var totalInputs = _.sum(self.inputs, 'satoshis'); var totalOutputs = _.sum(self.outputs, 'satoshis'); - if (totalInputs - totalOutputs - self.fee > 0) { + if (totalInputs - totalOutputs - self.fee > 0 && self.changeAddress) { t.change(self.changeAddress.address); } diff --git a/lib/server.js b/lib/server.js index 227b844..4b7becc 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1152,7 +1152,7 @@ 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 {string} opts.excludeUnconfirmedUtxos - Exclude unconfirmed UTXOs from calculation. + * @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs * @returns {Object} sendMaxInfo */ WalletService.prototype.getSendMaxInfo = function(opts, cb) { @@ -1161,7 +1161,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { opts = opts || {}; - if (!Utils.checkRequired(opts, ['feePerKb', 'excludeUnconfirmedUtxos'])) + if (!Utils.checkRequired(opts, ['feePerKb'])) return cb(new ClientError('Required argument missing')); self.getWallet({}, function(err, wallet) { @@ -1179,7 +1179,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { }; var inputs = _.reject(utxos, 'locked'); - if (opts.excludeUnconfirmedUtxos) { + if (!!opts.excludeUnconfirmedUtxos) { inputs = _.filter(inputs, 'confirmations'); } inputs = _.sortBy(inputs, 'satoshis'); @@ -1628,12 +1628,14 @@ WalletService.prototype._validateOutputs = function(opts, wallet) { if (toAddress.network != wallet.getNetworkName()) { return Errors.INCORRECT_ADDRESS_NETWORK; } + if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount <= 0) { return new ClientError('Invalid amount'); } if (output.amount < Bitcore.Transaction.DUST_AMOUNT) { return Errors.DUST_AMOUNT; } + output.valid = true; } return null; @@ -1785,23 +1787,7 @@ WalletService.prototype.createTxLegacy = function(opts, cb) { }); }; -/** - * Creates a new transaction proposal. - * @param {Object} opts - * @param {Array} opts.outputs - List of outputs. - * @param {string} opts.outputs[].toAddress - Destination address. - * @param {number} opts.outputs[].amount - Amount to transfer in satoshi. - * @param {string} opts.outputs[].message - A message to attach to this output. - * @param {string} opts.message - A message to attach to this transaction. - * @param {number} opts.feePerKb - The fee per kB to use for this TX. - * @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify TX - * @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs - * @param {string} opts.validateOutputs[=true] - Optional. Perform validation on outputs. - * @param {Array} opts.inputs - Optional. Inputs for this TX - * @param {number} opts.fee - Optional. The fee to use for this TX (used only when opts.inputs is specified). - * @returns {TxProposal} Transaction proposal. - */ -WalletService.prototype.createTx = function(opts, cb) { +WalletService.prototype._doCreateTx = function(opts, cb) { var self = this; if (!Utils.checkRequired(opts, ['outputs'])) @@ -1869,6 +1855,48 @@ WalletService.prototype.createTx = function(opts, cb) { }); }; +/** + * Creates a new transaction proposal. + * @param {Object} opts + * @param {Array} opts.outputs - List of outputs. + * @param {string} opts.outputs[].toAddress - Destination address. + * @param {number} opts.outputs[].amount - Amount to transfer in satoshi. + * @param {string} opts.outputs[].message - A message to attach to this output. + * @param {string} opts.message - A message to attach to this transaction. + * @param {Array} opts.inputs - Optional. Inputs for this TX + * @param {string} opts.fee - Optional. Use an alternative fee for this TX (mutually exclusive with feePerKb) + * @param {string} opts.feePerKb - Optional. Use an alternative fee per KB for this TX (mutually exclusive with fee) + * @param {string} opts.sendMax - Optional. Send maximum amount of funds that make sense under the specified fee/feePerKb conditions. (defaults to false). + * @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify TX + * @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs + * @param {string} opts.validateOutputs[=true] - Optional. Perform validation on outputs. + * @returns {TxProposal} Transaction proposal. + */ +WalletService.prototype.createTx = function(opts, cb) { + var self = this; + + opts = opts || []; + async.series([ + + function(next) { + if (!opts.sendMax) return next(); + if (!_.isArray(opts.outputs) || opts.outputs.length > 1) { + return next(new ClientError('Only one output allowed when sendMax is specified')); + } + self.getSendMaxInfo(opts, function(err, info) { + if (err) return next(err); + opts.outputs[0].amount = info.amount; + opts.inputs = info.inputs; + return next(); + }); + }, + ], function(err) { + if (err) return cb(err); + self._doCreateTx(opts, cb); + }); + +}; + WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) { var pub = (new Bitcore.HDPublicKey(xPubKey)).derive(Constants.PATHS.REQUEST_KEY_AUTH).publicKey; return Utils.verifyMessage(requestPubKey, signature, pub.toString()); diff --git a/test/integration/server.js b/test/integration/server.js index 52621f8..54e4810 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3113,6 +3113,33 @@ describe('Wallet service', function() { }); }); }); + + it.only('should be able to send max funds', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: null, + }], + feePerKb: 10000, + sendMax: true, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + // should.not.exist(tx.changeAddress); + tx.amount.should.equal(3e8 - tx.fee); + + var t = tx.getBitcoreTx(); + t.getFee().should.equal(tx.fee); + should.not.exist(t.getChangeOutput()); + t.toObject().inputs.length.should.equal(tx.inputs.length); + t.toObject().outputs[0].satoshis.should.equal(tx.amount); + done(); + }); + }); + }); + }); describe('Backoff time', function(done) { @@ -3626,7 +3653,6 @@ describe('Wallet service', function() { it('should be able to get send max info on empty wallet', function(done) { server.getSendMaxInfo({ feePerKb: 10000, - excludeUnconfirmedUtxos: false, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3641,7 +3667,6 @@ describe('Wallet service', function() { helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() { server.getSendMaxInfo({ feePerKb: 10000, - excludeUnconfirmedUtxos: false, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3698,7 +3723,6 @@ describe('Wallet service', function() { helpers.stubUtxos(server, wallet, ['u0.1', 0.2, 0.3, 0.4, 0.000001, 0.0002, 0.0003], function() { server.getSendMaxInfo({ feePerKb: 0.001e8, - excludeUnconfirmedUtxos: false, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3708,7 +3732,6 @@ describe('Wallet service', function() { info.amount.should.equal(1e8 - info.fee); server.getSendMaxInfo({ feePerKb: 0.0001e8, - excludeUnconfirmedUtxos: false, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3727,7 +3750,6 @@ describe('Wallet service', function() { helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { server.getSendMaxInfo({ feePerKb: 10000, - excludeUnconfirmedUtxos: false, }, function(err, info) { should.not.exist(err); should.exist(info);