Browse Source

Merge pull request #141 from isocolsky/ref/insufficient_funds

Ref/insufficient funds
activeAddress
Matias Pando 10 years ago
parent
commit
ed9d0ebadf
  1. 6
      lib/model/txproposal.js
  2. 77
      lib/server.js
  3. 44
      test/integration/server.js
  4. 2
      test/models/txproposal.js

6
lib/model/txproposal.js

@ -86,7 +86,7 @@ TxProposal.prototype._getCurrentSignatures = function() {
}); });
}; };
TxProposal.prototype._getBitcoreTx = function() { TxProposal.prototype.getBitcoreTx = function() {
var self = this; var self = this;
var t = new Bitcore.Transaction(); var t = new Bitcore.Transaction();
@ -113,7 +113,7 @@ TxProposal.prototype.getNetworkName = function() {
}; };
TxProposal.prototype.getRawTx = function() { TxProposal.prototype.getRawTx = function() {
var t = this._getBitcoreTx(); var t = this.getBitcoreTx();
return t.uncheckedSerialize(); return t.uncheckedSerialize();
}; };
@ -186,7 +186,7 @@ TxProposal.prototype._addSignaturesToBitcoreTx = function(t, signatures, xpub) {
TxProposal.prototype.sign = function(copayerId, signatures, xpub) { TxProposal.prototype.sign = function(copayerId, signatures, xpub) {
try { try {
// Tests signatures are OK // Tests signatures are OK
var t = this._getBitcoreTx(); var t = this.getBitcoreTx();
this._addSignaturesToBitcoreTx(t, signatures, xpub); this._addSignaturesToBitcoreTx(t, signatures, xpub);
this.addAction(copayerId, 'accept', null, signatures, xpub); this.addAction(copayerId, 'accept', null, signatures, xpub);

77
lib/server.js

@ -421,6 +421,20 @@ WalletService.prototype._getUtxos = function(cb) {
}); });
}; };
WalletService.prototype._totalizeUtxos = function(utxos) {
var balance = {};
balance.totalAmount = Utils.strip(_.reduce(utxos, function(sum, utxo) {
return sum + utxo.satoshis;
}, 0));
balance.lockedAmount = Utils.strip(_.reduce(_.filter(utxos, {
locked: true
}), function(sum, utxo) {
return sum + utxo.satoshis;
}, 0));
return balance;
};
/** /**
* Creates a new transaction proposal. * Creates a new transaction proposal.
@ -433,16 +447,7 @@ WalletService.prototype.getBalance = function(opts, cb) {
self._getUtxos(function(err, utxos) { self._getUtxos(function(err, utxos) {
if (err) return cb(err); if (err) return cb(err);
var balance = {}; var balance = self._totalizeUtxos(utxos);
balance.totalAmount = Utils.strip(_.reduce(utxos, function(sum, utxo) {
return sum + utxo.satoshis;
}, 0));
balance.lockedAmount = Utils.strip(_.reduce(_.filter(utxos, {
locked: true
}), function(sum, utxo) {
return sum + utxo.satoshis;
}, 0));
// Compute balance by address // Compute balance by address
var byAddress = {}; var byAddress = {};
@ -464,33 +469,54 @@ WalletService.prototype.getBalance = function(opts, cb) {
}); });
}; };
WalletService.prototype._selectTxInputs = function(txp, cb) {
var self = this;
self._getUtxos(function(err, utxos) {
if (err) return cb(err);
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'));
if ((balance.totalAmount - balance.lockedAmount) < txMinAmount)
return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals'));
utxos = _.reject(utxos, {
locked: true
});
WalletService.prototype._selectUtxos = function(txp, utxos) {
var i = 0; var i = 0;
var total = 0; var total = 0;
var selected = []; var selected = [];
var inputs = _.sortBy(utxos, 'amount'); var inputs = _.sortBy(utxos, 'amount');
var bitcoreTx;
while (i < inputs.length) { while (i < inputs.length) {
selected.push(inputs[i]); selected.push(inputs[i]);
total += inputs[i].satoshis; total += inputs[i].satoshis;
if (total >= txp.amount + Bitcore.Transaction.FEE_PER_KB) { if (total >= txMinAmount) {
try { try {
// Check if there are enough fees // Check if there are enough fees
txp.inputs = selected; txp.inputs = selected;
var raw = txp.getRawTx(); bitcoreTx = txp.getBitcoreTx();
return; txp.inputPaths = _.pluck(txp.inputs, 'path');
return cb();
} catch (ex) { } catch (ex) {
if (ex.name != 'bitcore.ErrorTransactionFeeError') { if (ex.name != 'bitcore.ErrorTransactionFeeError') {
throw ex.message; return cb(ex);
} }
} }
} }
i++; i++;
}; };
txp.inputs = null;
return; return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds for fee'));
});
}; };
@ -534,15 +560,9 @@ WalletService.prototype.createTx = function(opts, cb) {
if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) if (opts.amount < Bitcore.Transaction.DUST_AMOUNT)
return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold'));
self._getUtxos(function(err, utxos) {
if (err) return cb(err);
var changeAddress = wallet.createAddress(true); var changeAddress = wallet.createAddress(true);
utxos = _.reject(utxos, {
locked: true
});
var txp = TxProposal.create({ var txp = TxProposal.create({
creatorId: self.copayerId, creatorId: self.copayerId,
toAddress: opts.toAddress, toAddress: opts.toAddress,
@ -554,16 +574,11 @@ WalletService.prototype.createTx = function(opts, cb) {
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1),
}); });
try {
self._selectUtxos(txp, utxos);
} catch (ex) {
return cb(new ClientError(ex.toString()));
}
if (!txp.inputs) self._selectTxInputs(txp, function(err) {
return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); if (err) return cb(err);
txp.inputPaths = _.pluck(txp.inputs, 'path'); $.checkState(txp.inputs);
self.storage.storeAddressAndWallet(wallet, changeAddress, function(err) { self.storage.storeAddressAndWallet(wallet, changeAddress, function(err) {
if (err) return cb(err); if (err) return cb(err);

44
test/integration/server.js

@ -18,6 +18,7 @@ var WalletUtils = require('bitcore-wallet-utils');
var Storage = require('../../lib/storage'); var Storage = require('../../lib/storage');
var Wallet = require('../../lib/model/wallet'); var Wallet = require('../../lib/model/wallet');
var TxProposal = require('../../lib/model/txproposal');
var Address = require('../../lib/model/address'); var Address = require('../../lib/model/address');
var Copayer = require('../../lib/model/copayer'); var Copayer = require('../../lib/model/copayer');
var WalletService = require('../../lib/server'); var WalletService = require('../../lib/server');
@ -984,6 +985,46 @@ describe('Copay server', function() {
}); });
}); });
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);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(helpers.toSatoshi(20));
balance.lockedAmount.should.equal(helpers.toSatoshi(20));
txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 8, null, TestData.copayers[0].privKey_1H_0);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('LOCKEDFUNDS');
err.message.should.equal('Funds are locked by pending transaction proposals');
done();
});
});
});
});
});
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'
});
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENTFUNDS');
err.message.should.equal('Insufficient funds for fee');
txpStub.restore();
done();
});
});
});
it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) { it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) {
helpers.stubUtxos(server, wallet, [10], function() { helpers.stubUtxos(server, wallet, [10], function() {
var bitcoreStub = sinon.stub(Bitcore, 'Transaction'); var bitcoreStub = sinon.stub(Bitcore, 'Transaction');
@ -1034,8 +1075,7 @@ describe('Copay server', function() {
should.exist(tx); should.exist(tx);
var txOpts2 = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 24, null, TestData.copayers[0].privKey_1H_0); var txOpts2 = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 24, null, TestData.copayers[0].privKey_1H_0);
server.createTx(txOpts2, function(err, tx) { server.createTx(txOpts2, function(err, tx) {
err.code.should.equal('INSUFFICIENTFUNDS'); err.code.should.equal('LOCKEDFUNDS');
err.message.should.equal('Insufficient funds');
should.not.exist(tx); should.not.exist(tx);
server.getPendingTxs({}, function(err, txs) { server.getPendingTxs({}, function(err, txs) {
should.not.exist(err); should.not.exist(err);

2
test/models/txproposal.js

@ -19,7 +19,7 @@ describe('TXProposal', function() {
describe('#_getBitcoreTx', function() { describe('#_getBitcoreTx', function() {
it('should create a valid bitcore TX', function() { it('should create a valid bitcore TX', function() {
var txp = TXP.fromObj(aTXP()); var txp = TXP.fromObj(aTXP());
var t = txp._getBitcoreTx(); var t = txp.getBitcoreTx();
should.exist(t); should.exist(t);
}); });
}); });

Loading…
Cancel
Save