diff --git a/lib/server.js b/lib/server.js index eb081e3..ea00954 100644 --- a/lib/server.js +++ b/lib/server.js @@ -477,14 +477,11 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { var balance = self._totalizeUtxos(utxos); - var txMinAmount = txp.amount + Bitcore.Transaction.FEE_PER_KB; - if (balance.totalAmount < txMinAmount) - return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds' + (balance.totalAmount >= txp.amount ? ' for fee' : ''))); - - if ((balance.totalAmount - balance.lockedAmount) < txMinAmount) + if (balance.totalAmount < txp.amount) + return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); + if ((balance.totalAmount - balance.lockedAmount) < txp.amount) return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals')); - utxos = _.reject(utxos, { locked: true }); @@ -493,30 +490,39 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { var total = 0; var selected = []; var inputs = _.sortBy(utxos, 'amount'); - var bitcoreTx; + var bitcoreTx, bitcoreError; while (i < inputs.length) { selected.push(inputs[i]); total += inputs[i].satoshis; + i++; - if (total >= txMinAmount) { + if (total >= txp.amount) { try { - // Check if there are enough fees txp.inputs = selected; bitcoreTx = txp.getBitcoreTx(); - txp.inputPaths = _.pluck(txp.inputs, 'path'); - txp.fee = bitcoreTx.getFee(); - return cb(); - } catch (ex) { - if (ex.name != 'bitcore.ErrorTransactionFeeError') { - return cb(ex); + bitcoreError = bitcoreTx.getSerializationError({ + disableIsFullySigned: true, + }); + if (!bitcoreError) { + txp.inputPaths = _.pluck(txp.inputs, 'path'); + txp.fee = bitcoreTx.getFee(); + return cb(); } + } catch (ex) { + return cb(ex); } } - i++; }; - return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds for fee')); + if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) { + return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds for fee')); + } + if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs) { + return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); + } + + return cb(error); }); }; diff --git a/test/integration/server.js b/test/integration/server.js index ef7369d..f726385 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -956,6 +956,22 @@ describe('Copay server', function() { }); }); + it('should fail to create tx that would return change for dust amount', function(done) { + helpers.stubUtxos(server, wallet, [1], function() { + var fee = Bitcore.Transaction.FEE_PER_KB / 1e8; + var change = 0.00000001; + var amount = 1 - fee - change; + + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('DUSTAMOUNT'); + err.message.should.equal('Amount below dust threshold'); + done(); + }); + }); + }); + it('should fail with different error for insufficient funds and locked funds', function(done) { helpers.stubUtxos(server, wallet, [10, 10], function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 11, null, TestData.copayers[0].privKey_1H_0); @@ -977,20 +993,18 @@ describe('Copay server', function() { }); }); - it('should fail with insufficient funds if fee is too large', function(done) { - helpers.stubUtxos(server, wallet, 10, function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, null, TestData.copayers[0].privKey_1H_0); - - var txpStub = sinon.stub(TxProposal.prototype, 'getBitcoreTx').throws({ - name: 'bitcore.ErrorTransactionFeeError' - }); + it('should create tx with 0 change output', function(done) { + helpers.stubUtxos(server, wallet, [1], function() { + var fee = Bitcore.Transaction.FEE_PER_KB / 1e8; + var amount = 1 - fee; + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount, null, TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENTFUNDS'); - err.message.should.equal('Insufficient funds for fee'); - - txpStub.restore(); + should.not.exist(err); + should.exist(tx); + var bitcoreTx = tx.getBitcoreTx(); + bitcoreTx.outputs.length.should.equal(1); + bitcoreTx.outputs[0].satoshis.should.equal(tx.amount); done(); }); });