From 41c82e9e764940dd3afb722cf5b6cb0ca2eb5760 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 19 Jun 2017 11:38:38 -0300 Subject: [PATCH 1/4] add support staff flag to copayer lookup collection --- lib/expressapp.js | 3 ++- lib/server.js | 13 +++++++++---- test/integration/server.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/expressapp.js b/lib/expressapp.js index 626847a..8240e8b 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -39,7 +39,7 @@ ExpressApp.prototype.start = function(opts, cb) { this.app.use(function(req, res, next) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE'); - res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-session,x-client-version,X-Requested-With,Content-Type,Authorization'); + res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-session,x-client-version,x-wallet-id,X-Requested-With,Content-Type,Authorization'); res.setHeader('x-service-version', WalletService.getServiceVersion()); next(); }); @@ -165,6 +165,7 @@ ExpressApp.prototype.start = function(opts, cb) { message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body), signature: credentials.signature, clientVersion: req.header('x-client-version'), + walletId: req.header('x-wallet-id'), }; if (opts.allowSession) { auth.session = credentials.session; diff --git a/lib/server.js b/lib/server.js index 5a0fdf8..470d4b0 100644 --- a/lib/server.js +++ b/lib/server.js @@ -200,6 +200,7 @@ WalletService.getInstance = function(opts) { * @param {string} opts.signature - (Optional) Signature of message to be verified using one of the copayer's requestPubKeys. Only needed if no session token is provided. * @param {string} opts.session - (Optional) A valid session token previously obtained using the #login method * @param {string} opts.clientVersion - A string that identifies the client issuing the request + * @param {string} [opts.walletId] - The wallet id to use as current wallet for this request (only when copayer is support staff). */ WalletService.getInstanceWithAuth = function(opts, cb) { function withSignature(cb) { @@ -216,12 +217,16 @@ WalletService.getInstanceWithAuth = function(opts, cb) { if (err) return cb(err); if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found')); - var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys); - if (!isValid) - return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature')); + if (!copayer.isSupportStaff) { + var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys); + if (!isValid) + return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature')); + server.walletId = copayer.walletId; + } else { + server.walletId = opts.walletId || copayer.walletId; + } server.copayerId = opts.copayerId; - server.walletId = copayer.walletId; return cb(null, server); }); }; diff --git a/test/integration/server.js b/test/integration/server.js index 1e0d09c..a8bb6cc 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -103,6 +103,7 @@ describe('Wallet service', function() { message: 'hello world', signature: sig, clientVersion: 'bwc-2.0.0', + walletId: '123', }, function(err, server) { should.not.exist(err); server.walletId.should.equal(wallet.id); @@ -140,6 +141,36 @@ describe('Wallet service', function() { }); }); }); + + it('should get server instance for support staff', function(done) { + helpers.createAndJoinWallet(1, 1, function(s, wallet) { + var collections = require('../../lib/storage').collections; + s.storage.db.collection(collections.COPAYERS_LOOKUP).update({ + copayerId: wallet.copayers[0].id + }, { + $set: { + isSupportStaff: true + } + }); + + var xpriv = TestData.copayers[0].xPrivKey; + var priv = TestData.copayers[0].privKey_1H_0; + + var sig = helpers.signMessage('hello world', priv); + + WalletService.getInstanceWithAuth({ + copayerId: wallet.copayers[0].id, + message: 'hello world', + signature: sig, + walletId: '123', + }, function(err, server) { + should.not.exist(err); + server.walletId.should.equal('123'); + server.copayerId.should.equal(wallet.copayers[0].id); + done(); + }); + }); + }); }); describe('Session management (#login, #logout, #authenticate)', function() { From 2ab93cea1d3199c6973c727cfe77769ff274bbb3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 22 Jun 2017 13:45:16 -0300 Subject: [PATCH 2/4] get wallet from identifier + REST endpoint --- lib/expressapp.js | 29 ++++++++++++++++++ lib/server.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/lib/expressapp.js b/lib/expressapp.js index 8240e8b..ae23f83 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -173,6 +173,12 @@ ExpressApp.prototype.start = function(opts, cb) { WalletService.getInstanceWithAuth(auth, function(err, server) { if (err) return returnError(err, res, req); + if (opts.onlySupportStaff && !server.copayerIsSupportStaff) { + return returnError(new WalletService.ClientError({ + code: 'NOT_AUTHORIZED' + }), res, req); + } + // For logging req.walletId = server.walletId; req.copayerId = server.copayerId; @@ -301,6 +307,29 @@ ExpressApp.prototype.start = function(opts, cb) { }); }); + router.get('/v1/wallets/:identifier/', function(req, res) { + getServerWithAuth(req, res, { + onlySupportStaff: true + }, function(server) { + var opts = { + identifier: req.query.identifier, + }; + server.getWalletFromIdentifier(opts, function(err, wallet) { + if (err) return returnError(err, res, req); + if (!wallet) return res.end(); + + server.walletId = wallet.id; + var opts = {}; + if (req.query.includeExtendedInfo == '1') opts.includeExtendedInfo = true; + if (req.query.twoStep == '1') opts.twoStep = true; + server.getStatus(opts, function(err, status) { + if (err) return returnError(err, res, req); + res.json(status); + }); + }); + }); + }); + router.get('/v1/preferences/', function(req, res) { getServerWithAuth(req, res, function(server) { server.getPreferences({}, function(err, preferences) { diff --git a/lib/server.js b/lib/server.js index 470d4b0..9b8923e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -224,6 +224,7 @@ WalletService.getInstanceWithAuth = function(opts, cb) { server.walletId = copayer.walletId; } else { server.walletId = opts.walletId || copayer.walletId; + server.copayerIsSupportStaff = true; } server.copayerId = opts.copayerId; @@ -395,6 +396,81 @@ WalletService.prototype.getWallet = function(opts, cb) { }); }; +/** + * Retrieves a wallet from storage. + * @param {Object} opts + * @param {string} opts.identifier - The identifier associated with the wallet (one of: walletId, address, txid). + * @returns {Object} wallet + */ +WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { + var self = this; + + var walletId; + async.parallel([ + + function(next) { + self.storage.fetchWallet(identifier, function(err, wallet) { + if (wallet) walletId = wallet.id; + return next(err); + }); + }, + function(next) { + self.storage.fetchAddress(identifier, function(err, address) { + if (address) walletId = address.walletId; + return next(err); + }); + }, + function(next) { + self.storage.fetchTxByHash(identifier, function(err, tx) { + if (tx) walletId = tx.walletId; + return next(err); + }); + }, + ], function(err) { + if (err) return cb(err); + if (walletId) { + return self.storage.fetchWallet(walletId, cb); + } + + // Is identifier a txid form an incomming tx? + async.eachSeries(['livenet', 'testnet'], function(network, nextNetwork) { + var bc = self._getBlockchainExplorer('livenet'); + bc.getTransaction(identifier, function(err, tx) { + if (err || !tx) return cb(err); + + var outputs = _.first(self._normalizeTxHistory(tx)).outputs; + var toAddresses = _.pluck(outputs, 'address'); + async.detect(toAddresses, function(addressStr, nextAddress) { + self.storage.fetchAddress(addressStr, function(err, address) { + if (err || !address) return nextAddress(false); + walletId = address.walletId; + return nextAddress(true); + }); + }, function() { + if (walletId) { + return self.storage.fetchWallet(walletId, cb); + } + return cb(); + }); + }); + return nextNetwork(); + }, function(err) { + if (err) return cb(err); + if (walletId) { + return self.storage.fetchWallet(walletId, cb); + } + return cb(); + }); + }); + + + self.storage.fetchWallet(self.walletId, function(err, wallet) { + if (err) return cb(err); + if (!wallet) return cb(Errors.WALLET_NOT_FOUND); + return cb(null, wallet); + }); +}; + /** * Retrieves wallet status. * @param {Object} opts @@ -2571,7 +2647,7 @@ WalletService.prototype.getNotifications = function(opts, cb) { WalletService.prototype._normalizeTxHistory = function(txs) { var now = Math.floor(Date.now() / 1000); - return _.map(txs, function(tx) { + return _.map([].concat(txs), function(tx) { var inputs = _.map(tx.vin, function(item) { return { address: item.addr, From 565bc013396ef0d7f929e8147a962a4e2e2343b3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 27 Jun 2017 15:56:09 -0300 Subject: [PATCH 3/4] bug fixes --- lib/expressapp.js | 4 +-- lib/server.js | 69 +++++++++++++++++++---------------------------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/lib/expressapp.js b/lib/expressapp.js index ae23f83..14c1c34 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -100,7 +100,7 @@ ExpressApp.prototype.start = function(opts, cb) { var status = (err.code == 'NOT_AUTHORIZED') ? 401 : 400; if (!opts.disableLogs) - log.info('Client Err: ' + status + ' ' + req.url + ' ' + err); + log.info('Client Err: ' + status + ' ' + req.url + ' ' + JSON.stringify(err)); res.status(status).json({ code: err.code, @@ -312,7 +312,7 @@ ExpressApp.prototype.start = function(opts, cb) { onlySupportStaff: true }, function(server) { var opts = { - identifier: req.query.identifier, + identifier: req.params['identifier'], }; server.getWalletFromIdentifier(opts, function(err, wallet) { if (err) return returnError(err, res, req); diff --git a/lib/server.js b/lib/server.js index 9b8923e..a816c55 100644 --- a/lib/server.js +++ b/lib/server.js @@ -408,22 +408,22 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { var walletId; async.parallel([ - function(next) { - self.storage.fetchWallet(identifier, function(err, wallet) { + function(done) { + self.storage.fetchWallet(opts.identifier, function(err, wallet) { if (wallet) walletId = wallet.id; - return next(err); + return done(err); }); }, - function(next) { - self.storage.fetchAddress(identifier, function(err, address) { + function(done) { + self.storage.fetchAddress(opts.identifier, function(err, address) { if (address) walletId = address.walletId; - return next(err); + return done(err); }); }, - function(next) { - self.storage.fetchTxByHash(identifier, function(err, tx) { + function(done) { + self.storage.fetchTxByHash(opts.identifier, function(err, tx) { if (tx) walletId = tx.walletId; - return next(err); + return done(err); }); }, ], function(err) { @@ -433,42 +433,31 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { } // Is identifier a txid form an incomming tx? - async.eachSeries(['livenet', 'testnet'], function(network, nextNetwork) { - var bc = self._getBlockchainExplorer('livenet'); - bc.getTransaction(identifier, function(err, tx) { - if (err || !tx) return cb(err); + async.detectSeries(_.values(Constants.NETWORKS), function(network, nextNetwork) { + var bc = self._getBlockchainExplorer(network); + bc.getTransaction(opts.identifier, function(err, tx) { + if (err || !tx) return nextNetwork(err, false); var outputs = _.first(self._normalizeTxHistory(tx)).outputs; var toAddresses = _.pluck(outputs, 'address'); async.detect(toAddresses, function(addressStr, nextAddress) { self.storage.fetchAddress(addressStr, function(err, address) { - if (err || !address) return nextAddress(false); + if (err || !address) return nextAddress(err, false); walletId = address.walletId; - return nextAddress(true); + nextAddress(null, true); }); - }, function() { - if (walletId) { - return self.storage.fetchWallet(walletId, cb); - } - return cb(); + }, function(err) { + nextNetwork(err, !!walletId); }); }); - return nextNetwork(); }, function(err) { if (err) return cb(err); if (walletId) { return self.storage.fetchWallet(walletId, cb); } - return cb(); + cb(); }); }); - - - self.storage.fetchWallet(self.walletId, function(err, wallet) { - if (err) return cb(err); - if (!wallet) return cb(Errors.WALLET_NOT_FOUND); - return cb(null, wallet); - }); }; /** @@ -1031,19 +1020,17 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { WalletService.prototype._getBlockchainExplorer = function(network) { - if (!this.blockchainExplorer) { - var opts = {}; - if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) { - opts = this.blockchainExplorerOpts[network]; - } - // TODO: provider should be configurable - opts.provider = 'insight'; - opts.network = network; - opts.userAgent = WalletService.getServiceVersion(); - this.blockchainExplorer = new BlockchainExplorer(opts); - } + var opts = {}; - return this.blockchainExplorer; + if (this.blockchainExplorer) return this.blockchainExplorer; + if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) { + opts = this.blockchainExplorerOpts[network]; + } + // TODO: provider should be configurable + opts.provider = 'insight'; + opts.network = network; + opts.userAgent = WalletService.getServiceVersion(); + return new BlockchainExplorer(opts); }; WalletService.prototype._getUtxos = function(addresses, cb) { From 279d2ecc6848c4c62fe7c6f7c1ecbd58bfb70f98 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 29 Jun 2017 10:56:42 -0300 Subject: [PATCH 4/4] test #getWalletFromIdentifier --- lib/server.js | 3 +- test/integration/server.js | 105 ++++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index a816c55..ef07487 100644 --- a/lib/server.js +++ b/lib/server.js @@ -405,6 +405,8 @@ WalletService.prototype.getWallet = function(opts, cb) { WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { var self = this; + if (!opts.identifier) return cb(); + var walletId; async.parallel([ @@ -437,7 +439,6 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { var bc = self._getBlockchainExplorer(network); bc.getTransaction(opts.identifier, function(err, tx) { if (err || !tx) return nextNetwork(err, false); - var outputs = _.first(self._normalizeTxHistory(tx)).outputs; var toAddresses = _.pluck(outputs, 'address'); async.detect(toAddresses, function(addressStr, nextAddress) { diff --git a/test/integration/server.js b/test/integration/server.js index a8bb6cc..98f9eff 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -4395,7 +4395,6 @@ describe('Wallet service', function() { }); }); - describe('#getSendMaxInfo', function() { var server, wallet; beforeEach(function(done) { @@ -5240,7 +5239,6 @@ describe('Wallet service', function() { }); }); }); - }); describe('Tx proposal workflow', function() { @@ -7477,4 +7475,107 @@ describe('Wallet service', function() { }); }); }); + + describe('#getWalletFromIdentifier', function() { + var server, wallet; + beforeEach(function(done) { + helpers.createAndJoinWallet(1, 1, {}, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + + it('should get wallet from id', function(done) { + server.getWalletFromIdentifier({ + identifier: wallet.id + }, function(err, w) { + should.not.exist(err); + should.exist(w); + w.id.should.equal(wallet.id); + done(); + }); + }); + it('should get wallet from address', function(done) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + should.exist(address); + server.getWalletFromIdentifier({ + identifier: address.address + }, function(err, w) { + should.not.exist(err); + should.exist(w); + w.id.should.equal(wallet.id); + done(); + }); + }); + }); + it('should get wallet from tx proposal', function(done) { + helpers.stubUtxos(server, wallet, '1 btc', function() { + helpers.stubBroadcast(); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 1000e2 + }], + feePerKb: 100e2, + message: 'some message', + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { + should.exist(txp); + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); + server.signTx({ + txProposalId: txp.id, + signatures: signatures, + }, function(err) { + should.not.exist(err); + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txp = txps[0]; + server.getWalletFromIdentifier({ + identifier: txp.txid + }, function(err, w) { + should.not.exist(err); + should.exist(w); + w.id.should.equal(wallet.id); + done(); + }); + }); + }); + }); + }); + }); + it('should get wallet from incoming txid', function(done) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + should.exist(address); + blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, { + txid: '999', + vout: [{ + scriptPubKey: { + addresses: [address.address] + } + }], + }); + server.getWalletFromIdentifier({ + identifier: '999' + }, function(err, w) { + should.not.exist(err); + should.exist(w); + w.id.should.equal(wallet.id); + done(); + }); + }); + }); + it('should return nothing if identifier not associated with a wallet', function(done) { + blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null); + server.getWalletFromIdentifier({ + identifier: 'dummy' + }, function(err, w) { + should.not.exist(err); + should.not.exist(w); + done(); + }); + }); + }); });