diff --git a/lib/expressapp.js b/lib/expressapp.js index 7642079..5944b5e 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -277,8 +277,11 @@ ExpressApp.prototype.start = function(opts, cb) { }); router.get('/v1/utxos/', function(req, res) { + var opts = {}; + var addresses = req.query.addresses; + if (addresses && _.isString(addresses)) opts.addresses = req.query.addresses.split(','); getServerWithAuth(req, res, function(server) { - server.getUtxos(function(err, utxos) { + server.getUtxos(opts, function(err, utxos) { if (err) return returnError(err, res, req); res.json(utxos); }); diff --git a/lib/server.js b/lib/server.js index 7c19bf9..d12a04b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -154,7 +154,6 @@ WalletService.getInstance = function(opts) { * @param {string} opts.clientVersion - A string that identifies the client issuing the request */ WalletService.getInstanceWithAuth = function(opts, cb) { - if (!Utils.checkRequired(opts, ['copayerId', 'message', 'signature'])) return cb(new ClientError('Required argument missing')); @@ -618,10 +617,30 @@ WalletService.prototype._getBlockchainExplorer = function(network) { return this.blockchainExplorer; }; -/** - * Returns list of UTXOs - */ -WalletService.prototype.getUtxos = function(cb) { +WalletService.prototype._getUtxosForAddresses = function(addresses, cb) { + var self = this; + + if (addresses.length == 0) return cb(null, []); + var networkName = Bitcore.Address(addresses[0]).toObject().network; + + var bc = self._getBlockchainExplorer(networkName); + bc.getUnspentUtxos(addresses, function(err, utxos) { + if (err) return cb(err); + + var utxos = _.map(utxos, function(utxo) { + var u = _.pick(utxo, ['txid', 'vout', 'address', 'scriptPubKey', 'amount', 'satoshis', 'confirmations']); + u.confirmations = u.confirmations || 0; + u.locked = false; + u.satoshis = u.satoshis ? +u.satoshis : Utils.strip(u.amount * 1e8); + delete u.amount; + return u; + }); + + return cb(null, utxos); + }); +}; + +WalletService.prototype._getUtxosForCurrentWallet = function(cb) { var self = this; function utxoKey(utxo) { @@ -631,29 +650,17 @@ WalletService.prototype.getUtxos = function(cb) { // Get addresses for this wallet self.storage.fetchAddresses(self.walletId, function(err, addresses) { if (err) return cb(err); - if (addresses.length == 0) return cb(null, []); var addressStrs = _.pluck(addresses, 'address'); - var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance - var networkName = Bitcore.Address(addressStrs[0]).toObject().network; - - var bc = self._getBlockchainExplorer(networkName); - bc.getUnspentUtxos(addressStrs, function(err, inutxos) { + self._getUtxosForAddresses(addressStrs, function(err, utxos) { if (err) return cb(err); + if (utxos.length == 0) return cb(null, []); - var utxos = _.map(inutxos, function(utxo) { - var u = _.pick(utxo, ['txid', 'vout', 'address', 'scriptPubKey', 'amount', 'satoshis', 'confirmations']); - u.confirmations = u.confirmations || 0; - u.locked = false; - return u; - }); self.getPendingTxs({}, function(err, txps) { if (err) return cb(err); var lockedInputs = _.map(_.flatten(_.pluck(txps, 'inputs')), utxoKey); - var utxoIndex = _.indexBy(utxos, utxoKey); - _.each(lockedInputs, function(input) { if (utxoIndex[input]) { utxoIndex[input].locked = true; @@ -661,9 +668,8 @@ WalletService.prototype.getUtxos = function(cb) { }); // Needed for the clients to sign UTXOs + var addressToPath = _.indexBy(addresses, 'address'); _.each(utxos, function(utxo) { - utxo.satoshis = utxo.satoshis ? +utxo.satoshis : Utils.strip(utxo.amount * 1e8); - delete utxo.amount; utxo.path = addressToPath[utxo.address].path; utxo.publicKeys = addressToPath[utxo.address].publicKeys; }); @@ -674,6 +680,24 @@ WalletService.prototype.getUtxos = function(cb) { }); }; +/** + * Returns list of UTXOs + * @param {Object} opts + * @param {Array} opts.addresses (optional) - List of addresses from where to fetch UTXOs. + * @returns {Array} utxos - List of UTXOs. + */ +WalletService.prototype.getUtxos = function(opts, cb) { + var self = this; + + opts = opts || {}; + + if (_.isUndefined(opts.addresses)) { + self._getUtxosForCurrentWallet(cb); + } else { + self._getUtxosForAddresses(opts.addresses, cb); + } +}; + WalletService.prototype._totalizeUtxos = function(utxos) { var balance = { totalAmount: _.sum(utxos, 'satoshis'), @@ -718,7 +742,7 @@ WalletService.prototype._computeBytesToSendMax = function(utxos, cb) { WalletService.prototype.getBalance = function(opts, cb) { var self = this; - self.getUtxos(function(err, utxos) { + self.getUtxos({}, function(err, utxos) { if (err) return cb(err); var balance = self._totalizeUtxos(utxos); @@ -828,7 +852,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { return _.pluck(_.sortBy(list, 'order'), 'utxo'); }; - self.getUtxos(function(err, utxos) { + self.getUtxos({}, function(err, utxos) { if (err) return cb(err); var totalAmount; diff --git a/package.json b/package.json index c7a9747..fa1209c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bitcore-wallet-service", "description": "A service for Mutisig HD Bitcoin Wallets", "author": "BitPay Inc", - "version": "0.1.3", + "version": "0.1.4", "keywords": [ "bitcoin", "copay", diff --git a/test/integration/server.js b/test/integration/server.js index da43d8b..0dceb24 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -159,7 +159,12 @@ helpers.stubUtxos = function(server, wallet, amounts, cb) { confirmations: confirmations, }; }); - blockchainExplorer.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); + blockchainExplorer.getUnspentUtxos = function(addresses, cb) { + var selected = _.filter(utxos, function(utxo) { + return _.contains(addresses, utxo.address); + }); + return cb(null, selected); + }; return cb(utxos); }); @@ -1350,6 +1355,55 @@ describe('Wallet service', function() { }); }); + describe('#getUtxos', function() { + var server, wallet; + beforeEach(function(done) { + helpers.createAndJoinWallet(1, 1, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + + it('should get UTXOs for wallet addresses', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + server.getUtxos({}, function(err, utxos) { + should.not.exist(err); + should.exist(utxos); + utxos.length.should.equal(2); + _.sum(utxos, 'satoshis').should.equal(3 * 1e8); + server.getMainAddresses({}, function(err, addresses) { + var utxo = utxos[0]; + var address = _.find(addresses, { + address: utxo.address + }); + should.exist(address); + utxo.path.should.equal(address.path); + utxo.publicKeys.should.deep.equal(address.publicKeys); + done(); + }); + }); + }); + }); + it('should get UTXOs for specific addresses', function(done) { + helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) { + _.uniq(utxos, 'address').length.should.be.above(1); + var address = utxos[0].address; + var amount = _.sum(_.filter(utxos, { + address: address + }), 'satoshis'); + server.getUtxos({ + addresses: [address] + }, function(err, utxos) { + should.not.exist(err); + should.exist(utxos); + _.sum(utxos, 'satoshis').should.equal(amount); + done(); + }); + }); + }); + }); + describe('#getBalance', function() { var server, wallet; beforeEach(function(done) {