From 05de23c1b00601dd8d414897d3a4b1195d90d75e Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 19 Jul 2015 13:04:34 -0300 Subject: [PATCH 1/7] allow stubbing of unconfirmed utxos --- test/integration/server.js | 57 +++++++++++--------------------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/test/integration/server.js b/test/integration/server.js index 4ae1ea6..687f76c 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -135,29 +135,28 @@ helpers.toSatoshi = function(btc) { }; helpers.stubUtxos = function(server, wallet, amounts, cb) { - var amounts = [].concat(amounts); - - async.mapSeries(_.range(1, Math.ceil(amounts.length / 2) + 1), function(i, next) { - server.createAddress({}, function(err, address) { - next(err, address); - }); + async.mapSeries(_.range(0, amounts.length > 2 ? 2 : 1), function(i, next) { + server.createAddress({}, next); }, function(err, addresses) { - if (err) throw new Error('Could not generate addresses'); - - var utxos = _.map(amounts, function(amount, i) { + should.not.exist(err); + addresses.should.not.be.empty; + var utxos = _.map([].concat(amounts), function(amount, i) { var address = addresses[i % addresses.length]; - var obj = { + var confirmations; + if (_.isString(amount) && _.startsWith(amount, 'u')) { + amount = parseFloat(amount.substring(1)); + confirmations = 0; + } else { + confirmations = Math.floor(Math.random() * 100 + 1); + } + return { txid: helpers.randomTXID(), vout: Math.floor(Math.random() * 10 + 1), satoshis: helpers.toSatoshi(amount).toString(), scriptPubKey: address.getScriptPubKey(wallet.m).toBuffer().toString('hex'), address: address.address, - confirmations: Math.floor(Math.random() * 100 + 1), + confirmations: confirmations, }; - obj.toObject = function() { - return obj; - }; - return obj; }); blockchainExplorer.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); @@ -1636,19 +1635,7 @@ describe('Wallet service', function() { }); it('should create a tx using confirmed utxos first', function(done) { - server.createAddress({}, function(err, address) { - var utxos = _.map([1.3, 0.5, 0.1, 1.2], function(amount, i) { - return { - txid: helpers.randomTXID(), - vout: Math.floor((Math.random() * 10) + 1), - satoshis: helpers.toSatoshi(amount).toString(), - scriptPubKey: address.getScriptPubKey(wallet.m).toBuffer().toString('hex'), - address: address.address, - confirmations: amount < 1 ? 0 : 1, - }; - }); - blockchainExplorer.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); - + helpers.stubUtxos(server, wallet, [1.3, 'u0.5', 'u0.1', 1.2], function(utxos) { var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1.5, 'some message', TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { should.not.exist(err); @@ -1661,19 +1648,7 @@ describe('Wallet service', function() { }); it('should use unconfirmed utxos only when no more confirmed utxos are available', function(done) { - server.createAddress({}, function(err, address) { - var utxos = _.map([1.3, 0.5, 0.1, 1.2], function(amount, i) { - return { - txid: helpers.randomTXID(), - vout: Math.floor((Math.random() * 10) + 1), - satoshis: helpers.toSatoshi(amount).toString(), - scriptPubKey: address.getScriptPubKey(wallet.m).toBuffer().toString('hex'), - address: address.address, - confirmations: amount < 1 ? 0 : 1, - }; - }); - blockchainExplorer.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); - + helpers.stubUtxos(server, wallet, [1.3, 'u0.5', 'u0.1', 1.2], function(utxos) { var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2.55, 'some message', TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { should.not.exist(err); From 9a5daa5bf45d48010adbd9302c5423e473344e26 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 20 Jul 2015 12:45:12 -0300 Subject: [PATCH 2/7] add confirmed amounts to balance --- lib/server.js | 21 ++++++++++----------- test/integration/server.js | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/server.js b/lib/server.js index 0043088..5d216df 100644 --- a/lib/server.js +++ b/lib/server.js @@ -687,16 +687,15 @@ 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)); + var balance = { + totalAmount: _.sum(utxos, 'satoshis'), + lockedAmount: _.sum(_.filter(utxos, 'locked'), 'satoshis'), + totalConfirmedAmount: _.sum(_.filter(utxos, 'confirmations'), 'satoshis'), + lockedConfirmedAmount: _.sum(_.filter(_.filter(utxos, 'locked'), 'confirmed'), 'satoshis'), + }; + balance.availableAmount = balance.totalAmount - balance.lockedAmount; + balance.availableConfirmedAmount = balance.totalConfirmedAmount - balance.lockedConfirmedAmount; - balance.lockedAmount = Utils.strip(_.reduce(_.filter(utxos, { - locked: true - }), function(sum, utxo) { - return sum + utxo.satoshis; - }, 0)); return balance; }; @@ -755,7 +754,7 @@ WalletService.prototype.getBalance = function(opts, cb) { balance.byAddress = _.values(byAddress); - self._computeKbToSendMax(utxos, balance.totalAmount - balance.lockedAmount, function(err, sizeInKb) { + self._computeKbToSendMax(utxos, balance.availableAmount, function(err, sizeInKb) { if (err) { log.error('Could not compute fees needed to transfer max amount', err); } @@ -873,7 +872,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { if (balance.totalAmount < txp.getTotalAmount()) return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); - if ((balance.totalAmount - balance.lockedAmount) < txp.amount) + if (balance.availableAmount < txp.amount) return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals')); utxos = _.reject(utxos, { diff --git a/test/integration/server.js b/test/integration/server.js index 687f76c..64ff730 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1363,13 +1363,19 @@ describe('Wallet service', function() { }); it('should get balance', function(done) { - helpers.stubUtxos(server, wallet, [1, 2, 3], function() { + helpers.stubUtxos(server, wallet, [1, 'u2', 3], function() { server.getBalance({}, function(err, balance) { should.not.exist(err); should.exist(balance); balance.totalAmount.should.equal(helpers.toSatoshi(6)); balance.lockedAmount.should.equal(0); + balance.availableAmount.should.equal(helpers.toSatoshi(6)); balance.totalKbToSendMax.should.equal(1); + + balance.totalConfirmedAmount.should.equal(helpers.toSatoshi(4)); + balance.lockedConfirmedAmount.should.equal(0); + balance.availableConfirmedAmount.should.equal(helpers.toSatoshi(4)); + should.exist(balance.byAddress); balance.byAddress.length.should.equal(2); balance.byAddress[0].amount.should.equal(helpers.toSatoshi(4)); @@ -1389,6 +1395,7 @@ describe('Wallet service', function() { should.exist(balance); balance.totalAmount.should.equal(0); balance.lockedAmount.should.equal(0); + balance.availableAmount.should.equal(0); balance.totalKbToSendMax.should.equal(0); should.exist(balance.byAddress); balance.byAddress.length.should.equal(0); @@ -1404,6 +1411,7 @@ describe('Wallet service', function() { should.exist(balance); balance.totalAmount.should.equal(0); balance.lockedAmount.should.equal(0); + balance.availableAmount.should.equal(0); balance.totalKbToSendMax.should.equal(0); should.exist(balance.byAddress); balance.byAddress.length.should.equal(0); @@ -1609,6 +1617,7 @@ describe('Wallet service', function() { balance.totalAmount.should.equal(helpers.toSatoshi(300)); balance.lockedAmount.should.equal(tx.inputs[0].satoshis); balance.lockedAmount.should.be.below(balance.totalAmount); + balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); server.storage.fetchAddresses(wallet.id, function(err, addresses) { should.not.exist(err); var change = _.filter(addresses, { @@ -1931,11 +1940,10 @@ describe('Wallet service', function() { server.getBalance({}, function(err, balance) { should.not.exist(err); balance.totalAmount.should.equal(helpers.toSatoshi(30.6)); - var amountInputs = _.reduce(_.pluck(txs[0].inputs, 'satoshis'), function(memo, satoshis) { - return memo + satoshis; - }, 0); + var amountInputs = _.sum(txs[0].inputs, 'satoshis'); balance.lockedAmount.should.equal(amountInputs); balance.lockedAmount.should.be.below(balance.totalAmount); + balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); done(); }); }); @@ -2030,6 +2038,7 @@ describe('Wallet service', function() { should.not.exist(err); balance.totalAmount.should.equal(helpers.toSatoshi(9)); balance.lockedAmount.should.equal(0); + balance.availableAmount.should.equal(helpers.toSatoshi(9)); balance.totalKbToSendMax.should.equal(3); var max = (balance.totalAmount - balance.lockedAmount) - (balance.totalKbToSendMax * 10000); var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max / 1e8, null, TestData.copayers[0].privKey_1H_0); @@ -2056,6 +2065,7 @@ describe('Wallet service', function() { should.not.exist(err); balance.totalAmount.should.equal(helpers.toSatoshi(9)); balance.lockedAmount.should.equal(helpers.toSatoshi(4)); + balance.availableAmount.should.equal(helpers.toSatoshi(5)); balance.totalKbToSendMax.should.equal(2); var max = (balance.totalAmount - balance.lockedAmount) - (balance.totalKbToSendMax * 2000); var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max / 1e8, null, TestData.copayers[0].privKey_1H_0, 2000); From 859b1cf04287351143e13a9ef1053c62cbf584dd Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 20 Jul 2015 13:44:39 -0300 Subject: [PATCH 3/7] cleaner code --- lib/server.js | 33 ++++++++++----------------------- test/integration/server.js | 4 ++-- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/lib/server.js b/lib/server.js index 5d216df..2f7c6ce 100644 --- a/lib/server.js +++ b/lib/server.js @@ -626,6 +626,9 @@ WalletService.prototype._getBlockchainExplorer = function(network) { WalletService.prototype.getUtxos = function(cb) { var self = this; + function utxoKey(utxo) { + return utxo.txid + '|' + utxo.vout + }; // Get addresses for this wallet self.storage.fetchAddresses(self.walletId, function(err, addresses) { @@ -651,24 +654,13 @@ WalletService.prototype.getUtxos = function(cb) { self.getPendingTxs({}, function(err, txps) { if (err) return cb(err); - var utxoKey = function(utxo) { - return utxo.txid + '|' + utxo.vout - }; + var lockedInputs = _.map(_.flatten(_.pluck(txps, 'inputs')), utxoKey); - var inputs = _.chain(txps) - .pluck('inputs') - .flatten() - .map(utxoKey) - .value(); + var utxoIndex = _.indexBy(utxos, utxoKey); - var dictionary = _.reduce(utxos, function(memo, utxo) { - memo[utxoKey(utxo)] = utxo; - return memo; - }, {}); - - _.each(inputs, function(input) { - if (dictionary[input]) { - dictionary[input].locked = true; + _.each(lockedInputs, function(input) { + if (utxoIndex[input]) { + utxoIndex[input].locked = true; } }); @@ -703,9 +695,7 @@ WalletService.prototype._totalizeUtxos = function(utxos) { WalletService.prototype._computeKbToSendMax = function(utxos, amount, cb) { var self = this; - var unlockedUtxos = _.filter(utxos, { - locked: false - }); + var unlockedUtxos = _.reject(utxos, 'locked'); if (_.isEmpty(unlockedUtxos)) return cb(null, 0); self.getWallet({}, function(err, wallet) { @@ -1500,10 +1490,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { var filter = {}; if (_.isBoolean(isMine)) filter.isMine = isMine; if (_.isBoolean(isChange)) filter.isChange = isChange; - return _.reduce(_.where(items, filter), - function(memo, item) { - return memo + item.amount; - }, 0); + return _.sum(_.filter(items, filter), 'amount'); }; function classify(items) { diff --git a/test/integration/server.js b/test/integration/server.js index 64ff730..2a072a7 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -288,11 +288,11 @@ helpers.createAddresses = function(server, wallet, main, change, cb) { var storage, blockchainExplorer; -var useMongo = false; +var useMongoDb = !!process.env.USE_MONGO_DB; function initStorage(cb) { function getDb(cb) { - if (useMongo) { + if (useMongoDb) { var mongodb = require('mongodb'); mongodb.MongoClient.connect('mongodb://localhost:27017/bws_test', function(err, db) { if (err) throw err; From 38868319b906d780bbe2097837a4f67acc57eba3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 20 Jul 2015 14:46:10 -0300 Subject: [PATCH 4/7] strip feePerKB --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index 2f7c6ce..54ddbc9 100644 --- a/lib/server.js +++ b/lib/server.js @@ -769,7 +769,7 @@ WalletService.prototype._sampleFeeLevels = function(network, points, cb) { if (feePerKB < 0) { log.warn('Could not compute fee estimation (nbBlocks=' + p + ')'); } - return next(null, [p, feePerKB * 1e8]); + return next(null, [p, Utils.strip(feePerKB * 1e8)]); }); }, function(err, results) { if (err) return cb(err); From 1603c200b6b9d7662a57821b371441dc78f70e08 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 20 Jul 2015 19:40:50 -0300 Subject: [PATCH 5/7] add excludeUnconfirmedUtxos arg to txp creation --- lib/model/txproposal.js | 2 ++ lib/server.js | 26 ++++++++++++++++++++------ test/integration/server.js | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index d822ca1..1e7d776 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -68,6 +68,7 @@ TxProposal.create = function(opts) { x.actions = []; x.fee = null; x.feePerKb = opts.feePerKb; + x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos; if (_.isFunction(TxProposal._create[x.type])) { TxProposal._create[x.type](x, opts); @@ -110,6 +111,7 @@ TxProposal.fromObj = function(obj) { x.fee = obj.fee; x.network = obj.network; x.feePerKb = obj.feePerKb; + x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; return x; }; diff --git a/lib/server.js b/lib/server.js index 54ddbc9..b6c97f9 100644 --- a/lib/server.js +++ b/lib/server.js @@ -858,16 +858,28 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { self.getUtxos(function(err, utxos) { if (err) return cb(err); - var balance = self._totalizeUtxos(utxos); + var totalAmount; + var availableAmount; - if (balance.totalAmount < txp.getTotalAmount()) + var balance = self._totalizeUtxos(utxos); + if (txp.excludeUnconfirmedUtxos) { + totalAmount = balance.totalConfirmedAmount; + availableAmount = balance.totalConfirmedAvailable; + } else { + totalAmount = balance.totalAmount; + availableAmount = balance.availableAmount; + } + + if (totalAmount < txp.getTotalAmount()) return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); - if (balance.availableAmount < txp.amount) + if (availableAmount < txp.amount) return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals')); - utxos = _.reject(utxos, { - locked: true - }); + // Prepare UTXOs list + utxos = _.reject(utxos, 'locked'); + if (txp.excludeUnconfirmedUtxos) { + utxos = _.filter(utxos, 'confirmations'); + } var i = 0; var total = 0; @@ -952,6 +964,7 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { * @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal. * @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX * @param {string} opts.payProUrl - Optional: Paypro URL for peers to verify TX + * @param {string} opts.excludeUnconfirmedUtxos - Optional: Do not use UTXOs of unconfirmed transactions as inputs * @returns {TxProposal} Transaction proposal. */ WalletService.prototype.createTx = function(opts, cb) { @@ -1055,6 +1068,7 @@ WalletService.prototype.createTx = function(opts, cb) { payProUrl: opts.payProUrl, requiredSignatures: wallet.m, requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), + excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, }); self._selectTxInputs(txp, function(err) { diff --git a/test/integration/server.js b/test/integration/server.js index 2a072a7..8c3248a 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1671,6 +1671,26 @@ describe('Wallet service', function() { }); }); + it('should use confirmed utxos only if specified', function(done) { + helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) { + var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 3, 'some message', TestData.copayers[0].privKey_1H_0); + txOpts.excludeUnconfirmedUtxos = true; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('INSUFFICIENTFUNDS'); + err.message.should.equal('Insufficient funds'); + var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2.5, 'some message', TestData.copayers[0].privKey_1H_0); + txOpts.excludeUnconfirmedUtxos = true; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('INSUFFICIENTFUNDS'); + err.message.should.equal('Insufficient funds for fee'); + done(); + }); + }); + }); + }); + it('should fail gracefully if unable to reach the blockchain', function(done) { blockchainExplorer.getUnspentUtxos = sinon.stub().callsArgWith(1, 'dummy error'); server.createAddress({}, function(err, address) { From c12846380fd7b4fccbbd8c646aa98da82490fa83 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 20 Jul 2015 19:50:07 -0300 Subject: [PATCH 6/7] update README --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d090f3..de05a57 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,13 @@ Returns: Returns: * totalAmount: Wallet's total balance - * lockedAmount: Current balance of outstanding transaction proposals, that cannot be used on new transactions. + * lockedAmount: Current balance of outstanding transaction proposals, that cannot be used on new transactions. + * availableAmount: Funds available for new proposals. + * totalConfirmedAmount: Same as totalAmount for confirmed UTXOs only. + * lockedConfirmedAmount: Same as lockedAmount for confirmed UTXOs only. + * availableConfirmedAmount: Same as availableAmount for confirmed UTXOs only. * byAddress array ['address', 'path', 'amount']: A list of addresses holding funds. + * totalKbToSendMax: An estimation of the number of KiB required to include all available UTXOs in a tx (including unconfirmed). ## POST Endpoints `/v1/wallets/`: Create a new Wallet @@ -122,11 +127,14 @@ Returns: `/v1/txproposals/`: Add a new transaction proposal Required Arguments: - * toAddress: RCPT Bitcoin address + * toAddress: RCPT Bitcoin address. * amount: amount (in satoshis) of the mount proposed to be transfered * proposalsSignature: Signature of the proposal by the creator peer, using prososalSigningKey. - * (opt) message: Encrypted private message to peers - + * (opt) message: Encrypted private message to peers. + * (opt) payProUrl: Paypro URL for peers to verify TX + * (opt) feePerKb: Use an alternative fee per KB for this TX. + * (opt) excludeUnconfirmedUtxos: Do not use UTXOs of unconfirmed transactions as inputs for this TX. + Returns: * TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.id` is probably needed in this case. From 626e2a1b0649f131f13b98974478042b6467369a Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 20 Jul 2015 21:11:44 -0300 Subject: [PATCH 7/7] fix sum of locked confirmed utxos + tests --- lib/server.js | 6 +++--- test/integration/server.js | 26 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/server.js b/lib/server.js index b6c97f9..a5070ae 100644 --- a/lib/server.js +++ b/lib/server.js @@ -683,7 +683,7 @@ WalletService.prototype._totalizeUtxos = function(utxos) { totalAmount: _.sum(utxos, 'satoshis'), lockedAmount: _.sum(_.filter(utxos, 'locked'), 'satoshis'), totalConfirmedAmount: _.sum(_.filter(utxos, 'confirmations'), 'satoshis'), - lockedConfirmedAmount: _.sum(_.filter(_.filter(utxos, 'locked'), 'confirmed'), 'satoshis'), + lockedConfirmedAmount: _.sum(_.filter(_.filter(utxos, 'locked'), 'confirmations'), 'satoshis'), }; balance.availableAmount = balance.totalAmount - balance.lockedAmount; balance.availableConfirmedAmount = balance.totalConfirmedAmount - balance.lockedConfirmedAmount; @@ -864,7 +864,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { var balance = self._totalizeUtxos(utxos); if (txp.excludeUnconfirmedUtxos) { totalAmount = balance.totalConfirmedAmount; - availableAmount = balance.totalConfirmedAvailable; + availableAmount = balance.availableConfirmedAmount; } else { totalAmount = balance.totalAmount; availableAmount = balance.availableAmount; @@ -909,7 +909,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { return cb(ex); } } - }; + } if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) { return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds for fee')); diff --git a/test/integration/server.js b/test/integration/server.js index 8c3248a..c0f1b9e 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1671,7 +1671,7 @@ describe('Wallet service', function() { }); }); - it('should use confirmed utxos only if specified', function(done) { + it('should exclude unconfirmed utxos if specified', function(done) { helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) { var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 3, 'some message', TestData.copayers[0].privKey_1H_0); txOpts.excludeUnconfirmedUtxos = true; @@ -1691,6 +1691,30 @@ describe('Wallet service', function() { }); }); + it('should use non-locked confirmed utxos when specified', function(done) { + helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) { + var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1.4, 'some message', TestData.copayers[0].privKey_1H_0); + txOpts.excludeUnconfirmedUtxos = true; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.inputs.length.should.equal(2); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.lockedConfirmedAmount.should.equal(helpers.toSatoshi(2.5)); + balance.availableConfirmedAmount.should.equal(0); + var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.01, 'some message', TestData.copayers[0].privKey_1H_0); + txOpts.excludeUnconfirmedUtxos = true; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('LOCKEDFUNDS'); + done(); + }); + }); + }); + }); + }); + it('should fail gracefully if unable to reach the blockchain', function(done) { blockchainExplorer.getUnspentUtxos = sinon.stub().callsArgWith(1, 'dummy error'); server.createAddress({}, function(err, address) {