diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index a05db2c..c3d1879 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -36,7 +36,10 @@ TxProposal.create = function(opts) { x.outputs = _.map(opts.outputs, function(output) { return _.pick(output, ['amount', 'toAddress', 'message', 'script']); }); - x.outputOrder = _.shuffle(_.range(x.outputs.length + 1)); + x.outputOrder = _.range(x.outputs.length + 1); + if (!opts.noShuffleOutputs) { + x.outputOrder = _.shuffle(x.outputOrder); + } x.walletM = opts.walletM; x.walletN = opts.walletN; x.requiredSignatures = x.walletM; diff --git a/lib/server.js b/lib/server.js index d172b05..1726578 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1878,6 +1878,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) * @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) + * @param {Boolean} opts.noShuffleOutputs - Optional. If set, TX outputs won't be shuffled. Defaults to false * @returns {TxProposal} Transaction proposal. */ WalletService.prototype.createTx = function(opts, cb) { @@ -1926,6 +1927,7 @@ WalletService.prototype.createTx = function(opts, cb) { customData: opts.customData, inputs: opts.inputs, fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee : null, + noShuffleOutputs: opts.noShuffleOutputs }; txp = Model.TxProposal.create(txOpts); diff --git a/test/integration/server.js b/test/integration/server.js index f8ac9bb..ffaef48 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -2812,7 +2812,6 @@ describe('Wallet service', function() { }); }); }); - it('should be able to publish a temporary tx proposal', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { @@ -2840,7 +2839,6 @@ 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 = { @@ -2896,7 +2894,6 @@ describe('Wallet service', function() { }); }); }); - it('should fail to publish non-existent tx proposal', function(done) { server.publishTx({ txProposalId: 'wrong-id', @@ -2910,7 +2907,6 @@ describe('Wallet service', function() { }); }); }); - it('should fail to publish tx proposal with wrong signature', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { @@ -2935,7 +2931,6 @@ describe('Wallet service', function() { }); }); }); - it('should fail to publish tx proposal not signed by the creator', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { @@ -2963,7 +2958,6 @@ describe('Wallet service', function() { }); }); }); - it('should accept a tx proposal signed with a custom key', function(done) { var reqPrivKey = new Bitcore.PrivateKey(); var reqPubKey = reqPrivKey.toPublicKey().toString(); @@ -3013,7 +3007,6 @@ describe('Wallet service', function() { }); }); }); - it('should fail to publish a temporary tx proposal if utxos are unavailable', function(done) { var txp1, txp2; var txOpts = { @@ -3082,7 +3075,6 @@ describe('Wallet service', function() { done(); }); }); - it('should fail to list pending proposals from legacy client', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { @@ -3115,7 +3107,6 @@ describe('Wallet service', function() { }); }); }); - it('should be able to specify inputs & absolute fee', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { var txOpts = { @@ -3139,7 +3130,6 @@ describe('Wallet service', function() { }); }); }); - it('should be able to send max funds', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { @@ -3165,7 +3155,40 @@ describe('Wallet service', function() { }); }); }); + it('should shuffle outputs unless specified', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: _.times(30, function(i) { + return { + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: (i + 1) * 100e2, + }; + }), + feePerKb: 123e2, + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + var t = txp.getBitcoreTx(); + var changeOutput = t.getChangeOutput().satoshis; + var outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput); + outputs.should.not.deep.equal(_.pluck(txOpts.outputs, 'amount')); + txOpts.noShuffleOutputs = true; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + + t = txp.getBitcoreTx(); + changeOutput = t.getChangeOutput().satoshis; + outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput); + + outputs.should.deep.equal(_.pluck(txOpts.outputs, 'amount')); + done(); + }); + }); + }); + }); }); describe('Backoff time', function(done) {