From 8d25f238496444ccc26ceb7ea5fa1b732219076b Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 19 Feb 2016 11:11:43 -0300 Subject: [PATCH 01/12] get send max info --- lib/model/txproposal.js | 8 ++++-- lib/server.js | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 6f5d782..dfa98cd 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -252,10 +252,14 @@ TxProposal.prototype.getEstimatedSize = function() { return parseInt((size * (1 + safetyMargin)).toFixed(0)); }; -TxProposal.prototype.estimateFee = function() { +TxProposal.prototype.getEstimatedFee = function() { $.checkState(_.isNumber(this.feePerKb)); var fee = this.feePerKb * this.getEstimatedSize() / 1000; - this.fee = parseInt(fee.toFixed(0)); + return parseInt(fee.toFixed(0)); +}; + +TxProposal.prototype.estimateFee = function() { + this.fee = this.getEstimatedFee(); }; /** diff --git a/lib/server.js b/lib/server.js index 1eefa57..f828faa 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1148,6 +1148,68 @@ 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. + * @returns {Object} sendMaxInfo + */ +WalletService.prototype.getSendMaxInfo = function(opts, cb) { + var self = this; + + + opts = opts || {}; + + if (!Utils.checkRequired(opts, ['feePerKb', 'excludeUnconfirmedUtxos'])) + return cb(new ClientError('Required argument missing')); + + self.getWallet({}, function(err, wallet) { + if (err) return cb(err); + + self._getUtxosForCurrentWallet(null, function(err, utxos) { + if (err) return cb(err); + + var info = { + size: 0, + amount: 0, + fee: 0, + nbInputs: 0, + }; + + var inputs = _.reject(utxos, 'locked'); + if (opts.excludeUnconfirmedUtxos) { + inputs = _.filter(inptus, 'confirmations'); + } + inputs = _.sortBy(inputs, 'satoshis'); + + 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, + }); + + var lastFee = txp.getEstimatedFee(); + _.eachRight(inputs, function(input) { + txp.inputs.push(input); + var fee = txp.getEstimatedFee(); + if (fee - lastFee > input.satoshis) return false; + lastFee = fee; + }); + + info.size = txp.getEstimatedSize(); + info.fee = txp.getEstimatedFee(); + info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; + info.nbInputs = txp.inputs.length; + + return cb(null, info); + }); + }); +}; WalletService.prototype._sampleFeeLevels = function(network, points, cb) { var self = this; From 42ae722db9d32082126963c0b542f405d81cbf0f Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 23 Feb 2016 17:28:59 -0300 Subject: [PATCH 02/12] tests --- lib/server.js | 7 ++- test/integration/server.js | 125 ++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index f828faa..525bd4c 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1179,7 +1179,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { var inputs = _.reject(utxos, 'locked'); if (opts.excludeUnconfirmedUtxos) { - inputs = _.filter(inptus, 'confirmations'); + inputs = _.filter(inputs, 'confirmations'); } inputs = _.sortBy(inputs, 'satoshis'); @@ -1197,7 +1197,10 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { _.eachRight(inputs, function(input) { txp.inputs.push(input); var fee = txp.getEstimatedFee(); - if (fee - lastFee > input.satoshis) return false; + if (fee - lastFee > input.satoshis) { + txp.inputs.pop(); + return false; + } lastFee = fee; }); diff --git a/test/integration/server.js b/test/integration/server.js index c9f1832..bc9d778 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3267,7 +3267,6 @@ describe('Wallet service', function() { }); }); }); - it('should select smaller utxos if within fee constraints', function(done) { helpers.stubUtxos(server, wallet, [1, '800bit', '800bit', '800bit'], function() { var txOpts = { @@ -3546,6 +3545,20 @@ describe('Wallet service', function() { }); }); }); + }); + }); + + describe('#createTx backoff time', function() { + var server, wallet, txid; + + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 2, function(s, w) { + server = s; + wallet = w; + helpers.stubUtxos(server, wallet, _.range(2, 6), function() { + done(); + }); + }); it('should ignore small utxos if fee is higher', function(done) { helpers.stubUtxos(server, wallet, [].concat(_.times(10, function() { return '30bit'; @@ -3583,6 +3596,116 @@ describe('Wallet service', function() { }); }); + describe('#getSendMaxInfo', function() { + var server, wallet; + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 3, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + + 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); + info.size.should.equal(0); + info.amount.should.equal(0); + info.fee.should.equal(0); + info.nbInputs.should.equal(0); + done(); + }); + }); + it('should correctly get send max info', function(done) { + 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); + info.nbInputs.should.equal(4); + info.size.should.equal(1342); + info.fee.should.equal(info.size * 10000 / 1000.); + info.amount.should.equal(1e8 - info.fee); + done(); + }); + }); + }); + it('should exclude unconfirmed inputs', function(done) { + helpers.stubUtxos(server, wallet, ['u0.1', 0.2, 0.3, 0.4], function() { + server.getSendMaxInfo({ + feePerKb: 10000, + excludeUnconfirmedUtxos: true, + }, function(err, info) { + should.not.exist(err); + should.exist(info); + info.nbInputs.should.equal(3); + info.size.should.equal(1031); + info.fee.should.equal(info.size * 10000 / 1000.); + info.amount.should.equal(0.9e8 - info.fee); + done(); + }); + }); + }); + it('should exlude locked inputs', function(done) { + helpers.stubUtxos(server, wallet, ['u0.1', 0.1, 0.1, 0.1], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.09e8, + }], + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + should.exist(tx); + server.getSendMaxInfo({ + feePerKb: 10000, + excludeUnconfirmedUtxos: true, + }, function(err, info) { + should.not.exist(err); + should.exist(info); + info.nbInputs.should.equal(2); + info.size.should.equal(720); + info.fee.should.equal(info.size * 10000 / 1000.); + info.amount.should.equal(0.2e8 - info.fee); + done(); + }); + }); + }); + }); + it('should ignore utxos not contributing to total amount (below their cost in fee)', function(done) { + 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); + info.nbInputs.should.equal(4); + info.size.should.equal(1342); + info.fee.should.equal(info.size * 0.001e8 / 1000.); + info.amount.should.equal(1e8 - info.fee); + server.getSendMaxInfo({ + feePerKb: 0.0001e8, + excludeUnconfirmedUtxos: false, + }, function(err, info) { + should.not.exist(err); + should.exist(info); + info.nbInputs.should.equal(6); + info.size.should.equal(1964); + info.fee.should.equal(info.size * 0.0001e8 / 1000.); + info.amount.should.equal(1.0005e8 - info.fee); + done(); + }); + }); + }); + }); + }) + describe('#rejectTx', function() { var server, wallet, txid; From 0aa0f345a37ac8e86335d1a21a4762ed8ad192a3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 24 Feb 2016 11:10:29 -0300 Subject: [PATCH 03/12] add max size check --- lib/server.js | 4 +++- test/integration/server.js | 43 ++++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/server.js b/lib/server.js index 525bd4c..44acc0f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1197,10 +1197,12 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { _.eachRight(inputs, function(input) { txp.inputs.push(input); var fee = txp.getEstimatedFee(); - if (fee - lastFee > input.satoshis) { + if (fee - lastFee > input.satoshis || + txp.getEstimatedSize() / 1000. > Defaults.MAX_TX_SIZE_IN_KB) { txp.inputs.pop(); return false; } + lastFee = fee; }); diff --git a/test/integration/server.js b/test/integration/server.js index bc9d778..588f2b5 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3606,6 +3606,24 @@ describe('Wallet service', function() { }); }); + function sendTx(info, cb) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: info.amount, + fee: info.fee, + }], + }; + server.createTx(txOpts, function(err, tx) { + console.log('*** [server.js ln3150] err:', err); // TODO + + should.not.exist(err); + should.exist(tx); + tx.inputs.length.should.equal(info.nbInputs); + return cb(); + }); + }; + it('should be able to get send max info on empty wallet', function(done) { server.getSendMaxInfo({ feePerKb: 10000, @@ -3632,7 +3650,7 @@ describe('Wallet service', function() { info.size.should.equal(1342); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(1e8 - info.fee); - done(); + sendTx(info, done); }); }); }); @@ -3648,7 +3666,7 @@ describe('Wallet service', function() { info.size.should.equal(1031); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(0.9e8 - info.fee); - done(); + sendTx(info, done); }); }); }); @@ -3672,7 +3690,7 @@ describe('Wallet service', function() { info.size.should.equal(720); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(0.2e8 - info.fee); - done(); + sendTx(info, done); }); }); }); @@ -3699,11 +3717,28 @@ describe('Wallet service', function() { info.size.should.equal(1964); info.fee.should.equal(info.size * 0.0001e8 / 1000.); info.amount.should.equal(1.0005e8 - info.fee); - done(); + sendTx(info, done); }); }); }); }); + it.only('should not go beyond max tx size', function(done) { + var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB; + Defaults.MAX_TX_SIZE_IN_KB = 2; + 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); + info.size.should.be.below(2000); + info.nbInputs.should.be.below(9); + Defaults.MAX_TX_SIZE_IN_KB = _oldDefault; + sendTx(info, done); + }); + }); + }); }) describe('#rejectTx', function() { From 755449e32d1691ce59fb1355fc9863c0f4caa8f3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 24 Feb 2016 16:42:45 -0300 Subject: [PATCH 04/12] return utxo list --- lib/server.js | 6 ++++-- test/integration/server.js | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/server.js b/lib/server.js index 44acc0f..227b844 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1175,6 +1175,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { amount: 0, fee: 0, nbInputs: 0, + inputs: [], }; var inputs = _.reject(utxos, 'locked'); @@ -1197,8 +1198,8 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { _.eachRight(inputs, function(input) { txp.inputs.push(input); var fee = txp.getEstimatedFee(); - if (fee - lastFee > input.satoshis || - txp.getEstimatedSize() / 1000. > Defaults.MAX_TX_SIZE_IN_KB) { + var sizeInKb = txp.getEstimatedSize() / 1000.; + if (fee - lastFee > input.satoshis || sizeInKb > Defaults.MAX_TX_SIZE_IN_KB) { txp.inputs.pop(); return false; } @@ -1210,6 +1211,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { info.fee = txp.getEstimatedFee(); info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; info.nbInputs = txp.inputs.length; + info.inputs = txp.inputs; return cb(null, info); }); diff --git a/test/integration/server.js b/test/integration/server.js index 588f2b5..52621f8 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3613,10 +3613,9 @@ describe('Wallet service', function() { amount: info.amount, fee: info.fee, }], + inputs: info.inputs, }; server.createTx(txOpts, function(err, tx) { - console.log('*** [server.js ln3150] err:', err); // TODO - should.not.exist(err); should.exist(tx); tx.inputs.length.should.equal(info.nbInputs); @@ -3722,7 +3721,7 @@ describe('Wallet service', function() { }); }); }); - it.only('should not go beyond max tx size', function(done) { + it('should not go beyond max tx size', function(done) { var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB; Defaults.MAX_TX_SIZE_IN_KB = 2; helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { From bdff2cbc355b3ece0fd1bad6394825c375a3de2d Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 25 Feb 2016 11:27:30 -0300 Subject: [PATCH 05/12] 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); From b2fc191f54194daa0e1cba99d07e63f52e809cdd Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 25 Feb 2016 11:45:46 -0300 Subject: [PATCH 06/12] remove generation of change address when sending max --- lib/server.js | 77 ++++++++++++++++++++++++-------------- test/integration/server.js | 18 ++++----- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/lib/server.js b/lib/server.js index 4b7becc..862c72f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1174,7 +1174,6 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { size: 0, amount: 0, fee: 0, - nbInputs: 0, inputs: [], }; @@ -1210,7 +1209,6 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { info.size = txp.getEstimatedSize(); info.fee = txp.getEstimatedFee(); info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; - info.nbInputs = txp.inputs.length; info.inputs = txp.inputs; return cb(null, info); @@ -1803,27 +1801,42 @@ WalletService.prototype._doCreateTx = function(opts, cb) { } self._runLocked(cb, function(cb) { - self.getWallet({}, function(err, wallet) { - if (err) return cb(err); - if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE); - self._canCreateTx(function(err, canCreate) { - if (err) return cb(err); - if (!canCreate) return cb(Errors.TX_CANNOT_CREATE); + var wallet, txp, changeAddress; + async.series([ - if (opts.validateOutputs !== false) { - var validationError = self._validateOutputs(opts, wallet); - if (validationError) { - return cb(validationError); + function(next) { + self.getWallet({}, function(err, w) { + if (err) return next(err); + if (!w.isComplete()) return next(Errors.WALLET_NOT_COMPLETE); + wallet = w; + next(); + }); + }, + function(next) { + self._canCreateTx(function(err, canCreate) { + if (err) return next(err); + if (!canCreate) return next(Errors.TX_CANNOT_CREATE); + + if (opts.validateOutputs !== false) { + var validationError = self._validateOutputs(opts, wallet); + if (validationError) { + return next(validationError); + } } + next(); + }); + }, + function(next) { + if (!opts.sendMax) { + changeAddress = wallet.createAddress(true); } - var txOpts = { walletId: self.walletId, creatorId: self.copayerId, outputs: opts.outputs, message: opts.message, - changeAddress: wallet.createAddress(true), + changeAddress: changeAddress, feePerKb: opts.feePerKb, payProUrl: opts.payProUrl, walletM: wallet.m, @@ -1836,21 +1849,22 @@ WalletService.prototype._doCreateTx = function(opts, cb) { fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee : null, }; - var txp = Model.TxProposal.create(txOpts); - - self._selectTxInputs(txp, opts.utxosToExclude, function(err) { - if (err) return cb(err); - - self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) { - if (err) return cb(err); - - self.storage.storeTx(wallet.id, txp, function(err) { - if (err) return cb(err); - return cb(null, txp); - }); - }); - }); - }); + txp = Model.TxProposal.create(txOpts); + next(); + }, + function(next) { + self._selectTxInputs(txp, opts.utxosToExclude, next); + }, + function(next) { + if (!changeAddress) return next(); + self.storage.storeAddressAndWallet(wallet, txp.changeAddress, next); + }, + function(next) { + self.storage.storeTx(wallet.id, txp, next); + }, + ], function(err) { + if (err) return cb(err); + return cb(null, txp); }); }); }; @@ -1883,6 +1897,11 @@ WalletService.prototype.createTx = function(opts, cb) { if (!_.isArray(opts.outputs) || opts.outputs.length > 1) { return next(new ClientError('Only one output allowed when sendMax is specified')); } + if (_.isNumber(opts.outputs[0].amount)) + return next(new ClientError('Amount is not allowed when sendMax is specified')); + if (_.isNumber(opts.fee)) + return next(new ClientError('Fee is not allowed when sendMax is specified (use feePerKb instead)')); + self.getSendMaxInfo(opts, function(err, info) { if (err) return next(err); opts.outputs[0].amount = info.amount; diff --git a/test/integration/server.js b/test/integration/server.js index 54e4810..dfa68ac 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3127,7 +3127,7 @@ describe('Wallet service', function() { server.createTx(txOpts, function(err, tx) { should.not.exist(err); should.exist(tx); - // should.not.exist(tx.changeAddress); + should.not.exist(tx.changeAddress); tx.amount.should.equal(3e8 - tx.fee); var t = tx.getBitcoreTx(); @@ -3645,7 +3645,7 @@ describe('Wallet service', function() { server.createTx(txOpts, function(err, tx) { should.not.exist(err); should.exist(tx); - tx.inputs.length.should.equal(info.nbInputs); + tx.inputs.length.should.equal(info.inputs.length); return cb(); }); }; @@ -3659,7 +3659,7 @@ describe('Wallet service', function() { info.size.should.equal(0); info.amount.should.equal(0); info.fee.should.equal(0); - info.nbInputs.should.equal(0); + info.inputs.should.be.empty; done(); }); }); @@ -3670,7 +3670,7 @@ describe('Wallet service', function() { }, function(err, info) { should.not.exist(err); should.exist(info); - info.nbInputs.should.equal(4); + info.inputs.length.should.equal(4); info.size.should.equal(1342); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(1e8 - info.fee); @@ -3686,7 +3686,7 @@ describe('Wallet service', function() { }, function(err, info) { should.not.exist(err); should.exist(info); - info.nbInputs.should.equal(3); + info.inputs.length.should.equal(3); info.size.should.equal(1031); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(0.9e8 - info.fee); @@ -3710,7 +3710,7 @@ describe('Wallet service', function() { }, function(err, info) { should.not.exist(err); should.exist(info); - info.nbInputs.should.equal(2); + info.inputs.length.should.equal(2); info.size.should.equal(720); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(0.2e8 - info.fee); @@ -3726,7 +3726,7 @@ describe('Wallet service', function() { }, function(err, info) { should.not.exist(err); should.exist(info); - info.nbInputs.should.equal(4); + info.inputs.length.should.equal(4); info.size.should.equal(1342); info.fee.should.equal(info.size * 0.001e8 / 1000.); info.amount.should.equal(1e8 - info.fee); @@ -3735,7 +3735,7 @@ describe('Wallet service', function() { }, function(err, info) { should.not.exist(err); should.exist(info); - info.nbInputs.should.equal(6); + info.inputs.length.should.equal(6); info.size.should.equal(1964); info.fee.should.equal(info.size * 0.0001e8 / 1000.); info.amount.should.equal(1.0005e8 - info.fee); @@ -3754,7 +3754,7 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(info); info.size.should.be.below(2000); - info.nbInputs.should.be.below(9); + info.inputs.length.should.be.below(9); Defaults.MAX_TX_SIZE_IN_KB = _oldDefault; sendTx(info, done); }); From d23788100e0b1669ec40e2baad1ca9e99081093e Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 25 Feb 2016 14:23:01 -0300 Subject: [PATCH 07/12] allow for external use of getSendMaxInfo + refactor createTx --- lib/server.js | 138 ++++++++++++++++++++----------------- test/integration/server.js | 7 ++ 2 files changed, 80 insertions(+), 65 deletions(-) diff --git a/lib/server.js b/lib/server.js index 862c72f..e01e039 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1153,6 +1153,7 @@ WalletService.prototype.getBalance = function(opts, cb) { * @param {Object} opts * @param {string} opts.feePerKb - The fee per KB used to compute the TX. * @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 */ WalletService.prototype.getSendMaxInfo = function(opts, cb) { @@ -1209,7 +1210,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { info.size = txp.getEstimatedSize(); info.fee = txp.getEstimatedFee(); info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; - info.inputs = txp.inputs; + if (opts.returnInputs) info.inputs = txp.inputs; return cb(null, info); }); @@ -1785,20 +1786,78 @@ WalletService.prototype.createTxLegacy = function(opts, cb) { }); }; -WalletService.prototype._doCreateTx = function(opts, cb) { +WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) { var self = this; - if (!Utils.checkRequired(opts, ['outputs'])) - return cb(new ClientError('Required argument missing')); + async.series([ - // feePerKb is required unless inputs & fee are specified - if (!_.isNumber(opts.feePerKb) && !(opts.inputs && _.isNumber(opts.fee))) - return cb(new ClientError('Required argument missing')); + function(next) { + if (!Utils.checkRequired(opts, ['outputs'])) + return next(new ClientError('Required argument missing')); + next(); + }, + function(next) { + // feePerKb is required unless inputs & fee are specified + if (!_.isNumber(opts.feePerKb) && !(opts.inputs && _.isNumber(opts.fee))) + return next(new ClientError('Required argument missing')); - 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')); - } + if (_.isNumber(opts.feePerKb)) { + if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB) + return next(new ClientError('Invalid fee per KB')); + } + next(); + }, + 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')); + } + if (_.isNumber(opts.outputs[0].amount)) + return next(new ClientError('Amount is not allowed when sendMax is specified')); + if (_.isNumber(opts.fee)) + return next(new ClientError('Fee is not allowed when sendMax is specified (use feePerKb instead)')); + + self.getSendMaxInfo({ + feePerKb: opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB, + excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, + returnInputs: true, + }, function(err, info) { + if (err) return next(err); + opts.outputs[0].amount = info.amount; + opts.inputs = info.inputs; + return next(); + }); + }, + function(next) { + if (opts.validateOutputs === false) return next(); + var validationError = self._validateOutputs(opts, wallet); + if (validationError) { + return next(validationError); + } + next(); + }, + ], 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; self._runLocked(cb, function(cb) { @@ -1813,17 +1872,13 @@ WalletService.prototype._doCreateTx = function(opts, cb) { next(); }); }, + function(next) { + self._validateAndSanitizeTxOpts(wallet, opts, next); + }, function(next) { self._canCreateTx(function(err, canCreate) { if (err) return next(err); if (!canCreate) return next(Errors.TX_CANNOT_CREATE); - - if (opts.validateOutputs !== false) { - var validationError = self._validateOutputs(opts, wallet); - if (validationError) { - return next(validationError); - } - } next(); }); }, @@ -1869,53 +1924,6 @@ WalletService.prototype._doCreateTx = 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')); - } - if (_.isNumber(opts.outputs[0].amount)) - return next(new ClientError('Amount is not allowed when sendMax is specified')); - if (_.isNumber(opts.fee)) - return next(new ClientError('Fee is not allowed when sendMax is specified (use feePerKb instead)')); - - 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 dfa68ac..f6f1497 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3653,6 +3653,7 @@ describe('Wallet service', function() { it('should be able to get send max info on empty wallet', function(done) { server.getSendMaxInfo({ feePerKb: 10000, + returnInputs: true, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3667,6 +3668,7 @@ describe('Wallet service', function() { helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() { server.getSendMaxInfo({ feePerKb: 10000, + returnInputs: true, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3683,6 +3685,7 @@ describe('Wallet service', function() { server.getSendMaxInfo({ feePerKb: 10000, excludeUnconfirmedUtxos: true, + returnInputs: true, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3707,6 +3710,7 @@ describe('Wallet service', function() { server.getSendMaxInfo({ feePerKb: 10000, excludeUnconfirmedUtxos: true, + returnInputs: true, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3723,6 +3727,7 @@ 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, + returnInputs: true, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3732,6 +3737,7 @@ describe('Wallet service', function() { info.amount.should.equal(1e8 - info.fee); server.getSendMaxInfo({ feePerKb: 0.0001e8, + returnInputs: true, }, function(err, info) { should.not.exist(err); should.exist(info); @@ -3750,6 +3756,7 @@ describe('Wallet service', function() { helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { server.getSendMaxInfo({ feePerKb: 10000, + returnInputs: true, }, function(err, info) { should.not.exist(err); should.exist(info); From 0766499cb7b51545b881ff1c7395cb9e829d76d0 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 25 Feb 2016 14:47:03 -0300 Subject: [PATCH 08/12] createTx dry run --- lib/server.js | 5 ++++- test/integration/server.js | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index e01e039..706b48b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -606,6 +606,7 @@ WalletService._getCopayerHash = function(name, xPubKey, requestPubKey) { * @param {string} opts.requestPubKey - Public Key used to check requests from this copayer. * @param {string} opts.copayerSignature - S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret. * @param {string} opts.customData - (optional) Custom data for this copayer. + * @param {string} opts.dryRun[=false] - (optional) Simulate the action but do not change server state. * @param {string} [opts.supportBIP44AndP2PKH = true] - Client supports BIP44 & P2PKH for joining wallets. */ WalletService.prototype.joinWallet = function(opts, cb) { @@ -1854,6 +1855,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) * @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 {string} opts.dryRun[=false] - Optional. Simulate the action but do not change server state. * @returns {TxProposal} Transaction proposal. */ WalletService.prototype.createTx = function(opts, cb) { @@ -1911,10 +1913,11 @@ WalletService.prototype.createTx = function(opts, cb) { self._selectTxInputs(txp, opts.utxosToExclude, next); }, function(next) { - if (!changeAddress) return next(); + if (!changeAddress || opts.dryRun) return next(); self.storage.storeAddressAndWallet(wallet, txp.changeAddress, next); }, function(next) { + if (opts.dryRun) return next(); self.storage.storeTx(wallet.id, txp, next); }, ], function(err) { diff --git a/test/integration/server.js b/test/integration/server.js index f6f1497..371c124 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -2841,6 +2841,33 @@ describe('Wallet service', function() { }); }); + it('should not be able to publish a temporary tx proposal created in a dry run', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8 * 1e8, + }], + message: 'some message', + customData: 'some custom data', + dryRun: true, + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, function(err) { + should.exist(err); + err.code.should.equal('TX_NOT_FOUND'); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(0); + done(); + }); + }); + }); + }); + }); it('should delay NewTxProposal notification until published', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { From bd87a1af743efbda4aa0cfd9f3aae98c0c5b04eb Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 11 Mar 2016 14:03:40 -0300 Subject: [PATCH 09/12] fix tests --- test/integration/server.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test/integration/server.js b/test/integration/server.js index 371c124..5fd3017 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -2848,8 +2848,7 @@ describe('Wallet service', function() { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 0.8 * 1e8, }], - message: 'some message', - customData: 'some custom data', + feePerKb: 100e2, dryRun: true, }; server.createTx(txOpts, function(err, txp) { @@ -3141,7 +3140,7 @@ describe('Wallet service', function() { }); }); - it.only('should be able to send max funds', function(done) { + it('should be able to send max funds', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { outputs: [{ @@ -3665,14 +3664,17 @@ describe('Wallet service', function() { outputs: [{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: info.amount, - fee: info.fee, }], inputs: info.inputs, + fee: info.fee, }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); should.exist(tx); - tx.inputs.length.should.equal(info.inputs.length); + var t = tx.getBitcoreTx(); + t.toObject().inputs.length.should.equal(info.inputs.length); + t.getFee().should.equal(info.fee); + should.not.exist(t.getChangeOutput()); return cb(); }); }; @@ -3700,7 +3702,7 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(info); info.inputs.length.should.equal(4); - info.size.should.equal(1342); + info.size.should.equal(1304); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(1e8 - info.fee); sendTx(info, done); @@ -3717,20 +3719,21 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(info); info.inputs.length.should.equal(3); - info.size.should.equal(1031); + info.size.should.equal(1002); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(0.9e8 - info.fee); sendTx(info, done); }); }); }); - it('should exlude locked inputs', function(done) { + it('should exclude locked inputs', function(done) { helpers.stubUtxos(server, wallet, ['u0.1', 0.1, 0.1, 0.1], function() { var txOpts = { outputs: [{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 0.09e8, }], + feePerKb: 100e2, }; helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); @@ -3742,7 +3745,7 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(info); info.inputs.length.should.equal(2); - info.size.should.equal(720); + info.size.should.equal(700); info.fee.should.equal(info.size * 10000 / 1000.); info.amount.should.equal(0.2e8 - info.fee); sendTx(info, done); @@ -3759,7 +3762,7 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(info); info.inputs.length.should.equal(4); - info.size.should.equal(1342); + info.size.should.equal(1304); info.fee.should.equal(info.size * 0.001e8 / 1000.); info.amount.should.equal(1e8 - info.fee); server.getSendMaxInfo({ @@ -3769,7 +3772,7 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(info); info.inputs.length.should.equal(6); - info.size.should.equal(1964); + info.size.should.equal(1907); info.fee.should.equal(info.size * 0.0001e8 / 1000.); info.amount.should.equal(1.0005e8 - info.fee); sendTx(info, done); From dffdebfb47aa29539f777adb3b4387f5168b3458 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 11 Mar 2016 14:22:54 -0300 Subject: [PATCH 10/12] fix docs for createTx --- lib/server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index 706b48b..d05cb34 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1848,14 +1848,14 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) * @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.feePerKb - Use an alternative fee per KB for this TX. * @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. * @param {string} opts.dryRun[=false] - Optional. Simulate the action but do not change server state. + * @param {Array} opts.inputs - Optional. Inputs for this TX + * @param {number} opts.fee - Optional. Use an fixed fee for this TX (only when opts.inputs is specified) * @returns {TxProposal} Transaction proposal. */ WalletService.prototype.createTx = function(opts, cb) { From f418009ebf4d3b6d82857c62ffd01fe5e13c930c Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 16 Mar 2016 16:42:39 -0300 Subject: [PATCH 11/12] add express endpoint --- lib/expressapp.js | 13 +++++++++++++ lib/server.js | 6 ++++-- test/expressapp.js | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/expressapp.js b/lib/expressapp.js index 74f085b..b4f9a08 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -350,6 +350,19 @@ ExpressApp.prototype.start = function(opts, cb) { }); }); + router.get('/v1/sendmaxinfo/', function(req, res) { + getServerWithAuth(req, res, function(server) { + var opts = {}; + opts.feePerKb = +req.query.feePerKb; + if (req.query.excludeUnconfirmedUtxos == '1') opts.excludeUnconfirmedUtxos = true; + if (req.query.returnInputs == '1') opts.returnInputs = true; + server.getSendMaxInfo(opts, function(err, info) { + if (err) return returnError(err, res, req); + res.json(info); + }); + }); + }); + router.get('/v1/utxos/', function(req, res) { var opts = {}; var addresses = req.query.addresses; diff --git a/lib/server.js b/lib/server.js index d05cb34..e7ac147 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1207,11 +1207,13 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { lastFee = fee; }); - info.size = txp.getEstimatedSize(); info.fee = txp.getEstimatedFee(); info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; - if (opts.returnInputs) info.inputs = txp.inputs; + if (opts.returnInputs) { + // TODO: Shuffle inputs + info.inputs = txp.inputs; + } return cb(null, info); }); diff --git a/test/expressapp.js b/test/expressapp.js index 3f20506..e70b60b 100644 --- a/test/expressapp.js +++ b/test/expressapp.js @@ -113,6 +113,38 @@ describe('ExpressApp', function() { }); }); + it('/v1/sendmaxinfo', function(done) { + var server = { + getSendMaxInfo: sinon.stub().callsArgWith(1, null, { + amount: 123 + }), + }; + var TestExpressApp = proxyquire('../lib/expressapp', { + './server': { + initialize: sinon.stub().callsArg(1), + getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), + } + }); + start(TestExpressApp, function() { + var requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/sendmaxinfo?feePerKb=10000&returnInputs=1', + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + var args = server.getSendMaxInfo.getCalls()[0].args[0]; + args.feePerKb.should.equal(10000); + args.returnInputs.should.be.true; + JSON.parse(body).amount.should.equal(123); + done(); + }); + }); + }); + describe('Balance', function() { it('should handle cache argument', function(done) { var server = { From 30469f921dc8f8f0840f3501f296e6d74275d513 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 16 Mar 2016 16:46:11 -0300 Subject: [PATCH 12/12] shuffle inputs --- lib/server.js | 3 +-- test/integration/server.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/server.js b/lib/server.js index e7ac147..3748a18 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1211,8 +1211,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { info.fee = txp.getEstimatedFee(); info.amount = _.sum(txp.inputs, 'satoshis') - info.fee; if (opts.returnInputs) { - // TODO: Shuffle inputs - info.inputs = txp.inputs; + info.inputs = _.shuffle(txp.inputs); } return cb(null, info); diff --git a/test/integration/server.js b/test/integration/server.js index 5fd3017..277abc0 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3709,6 +3709,26 @@ describe('Wallet service', function() { }); }); }); + 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) { + server.getSendMaxInfo({ + feePerKb: 100e2, + returnInputs: true + }, function(err, info) { + should.not.exist(err); + should.exist(info); + var amounts = _.pluck(info.inputs, 'satoshis'); + amounts.length.should.equal(30); + _.all(amounts, function(amount, i) { + if (i == 0) return true; + return amount < amounts[i - 1]; + }).should.be.false; + done(); + }); + }); + }); + it('should exclude unconfirmed inputs', function(done) { helpers.stubUtxos(server, wallet, ['u0.1', 0.2, 0.3, 0.4], function() { server.getSendMaxInfo({