diff --git a/lib/blockchainmonitor.js b/lib/blockchainmonitor.js index 8e8a551..c0d6336 100644 --- a/lib/blockchainmonitor.js +++ b/lib/blockchainmonitor.js @@ -83,7 +83,7 @@ BlockchainMonitor.prototype._initExplorer = function(explorer) { log.error('Error connecting to ' + explorer.getConnectionInfo()); }); socket.on('tx', _.bind(self._handleIncommingTx, self)); - socket.on('block', _.bind(self._handleNewBlock, self)); + socket.on('block', _.bind(self._handleNewBlock, self, explorer.network)); }; BlockchainMonitor.prototype._handleTxId = function(data, processIt) { @@ -177,12 +177,13 @@ BlockchainMonitor.prototype._handleIncommingTx = function(data) { this._handleTxOuts(data); }; -BlockchainMonitor.prototype._handleNewBlock = function(hash) { +BlockchainMonitor.prototype._handleNewBlock = function(network, hash) { var self = this; - log.info('New block: ', hash); + log.info('New ' + network + ' block: ', hash); var notification = Notification.create({ type: 'NewBlock', + walletId: network, // use network name as wallet id for global notifications data: { hash: hash, }, diff --git a/lib/expressapp.js b/lib/expressapp.js index df8268d..aafceda 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -439,8 +439,9 @@ ExpressApp.prototype.start = function(opts, cb) { router.get('/v1/notifications/', function(req, res) { getServerWithAuth(req, res, function(server) { + var timeSpan = req.query.timeSpan ? Math.min(+req.query.timeSpan || 0, 60) : 60; var opts = { - minTs: +Date.now() - (60 * 1000), + minTs: +Date.now() - (timeSpan * 1000), notificationId: req.query.notificationId, }; server.getNotifications(opts, function(err, notifications) { diff --git a/lib/server.js b/lib/server.js index 8a88323..e5007e3 100644 --- a/lib/server.js +++ b/lib/server.js @@ -52,12 +52,6 @@ function WalletService() { this.notifyTicker = 0; }; -WalletService.getServiceVersion = function() { - if (!serviceVersion) - serviceVersion = 'bws-' + require('../package').version; - return serviceVersion; -}; - // Time after which a Tx proposal can be erased by any copayer. in seconds WalletService.DELETE_LOCKTIME = 24 * 3600; @@ -89,6 +83,15 @@ WalletService.FEE_LEVELS = [{ }]; +/** + * Gets the current version of BWS + */ +WalletService.getServiceVersion = function() { + if (!serviceVersion) + serviceVersion = 'bws-' + require('../package').version; + return serviceVersion; +}; + /** * Initializes global settings for all instances. * @param {Object} opts @@ -1657,9 +1660,21 @@ WalletService.prototype.getNotifications = function(opts, cb) { var self = this; opts = opts || {}; - self.storage.fetchNotifications(self.walletId, opts.notificationId, opts.minTs || 0, function(err, notifications) { + self.getWallet({}, function(err, wallet) { if (err) return cb(err); - return cb(null, notifications); + + async.map([wallet.network, self.walletId], function(walletId, next) { + self.storage.fetchNotifications(walletId, opts.notificationId, opts.minTs || 0, next); + }, function(err, res) { + if (err) return cb(err); + + var notifications = _.sortBy(_.map(_.flatten(res), function(n) { + n.walletId = self.walletId; + return n; + }), 'id'); + + return cb(null, notifications); + }); }); }; diff --git a/lib/storage.js b/lib/storage.js index 73e4941..983737f 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -277,44 +277,6 @@ Storage.prototype.fetchTxs = function(walletId, opts, cb) { }; -/** - * fetchNotifications - * - * @param walletId - * @param opts.minTs - * @param opts.maxTs - * @param opts.limit - * @param opts.reverse - */ -Storage.prototype.fetchNotifications = function(walletId, opts, cb) { - var self = this; - - opts = opts || {}; - - var tsFilter = {}; - if (_.isNumber(opts.minTs)) tsFilter.$gte = opts.minTs; - if (_.isNumber(opts.maxTs)) tsFilter.$lte = opts.maxTs; - - var filter = { - walletId: walletId - }; - if (!_.isEmpty(tsFilter)) filter.createdOn = tsFilter; - - var mods = {}; - if (_.isNumber(opts.limit)) mods.limit = opts.limit; - - this.db.collection(collections.NOTIFICATIONS).find(filter, mods).sort({ - id: opts.reverse ? -1 : 1, - }).toArray(function(err, result) { - if (err) return cb(err); - if (!result) return cb(); - var notifications = _.map(result, function(notification) { - return Model.Notification.fromObj(notification); - }); - return cb(null, notifications); - }); -}; - /** * Retrieves notifications after a specific id or from a given ts (whichever is more recent). * diff --git a/package.json b/package.json index 31b963b..d20dea0 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": "1.0.0", + "version": "1.1.0", "keywords": [ "bitcoin", "copay", diff --git a/test/expressapp.js b/test/expressapp.js index cbf8671..7b73d2d 100644 --- a/test/expressapp.js +++ b/test/expressapp.js @@ -84,37 +84,85 @@ describe('ExpressApp', function() { }); }); - it('/v1/notifications', function(done) { - var clock = sinon.useFakeTimers(1234000, 'Date'); + describe('/v1/notifications', function(done) { + var server, TestExpressApp, clock; + beforeEach(function() { + clock = sinon.useFakeTimers(1234000, 'Date'); - var server = { - getNotifications: sinon.stub().callsArgWith(1, null, {}) - }; - var TestExpressApp = proxyquire('../lib/expressapp', { - './server': { - initialize: sinon.stub().callsArg(1), - getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), - } - }); - start(TestExpressApp, function() { - var requestOptions = { - url: testHost + ':' + testPort + config.basePath + '/v1/notifications' + '?notificationId=123&minTs=0', - headers: { - 'x-identity': 'identity', - 'x-signature': 'signature' - } + server = { + getNotifications: sinon.stub().callsArgWith(1, null, {}) }; - request(requestOptions, function(err, res, body) { - should.not.exist(err); - res.statusCode.should.equal(200); - body.should.equal('{}'); - server.getNotifications.calledWith({ - notificationId: '123', - minTs: 1234000 - 60000, // override minTs argument with a hardcoded 60 seconds span - }).should.be.true; + TestExpressApp = proxyquire('../lib/expressapp', { + './server': { + initialize: sinon.stub().callsArg(1), + getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), + } + }); + }); + afterEach(function() { + clock.restore(); + }); - clock.restore(); - done(); + it('should fetch notifications from a specified id', function(done) { + start(TestExpressApp, function() { + var requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/notifications' + '?notificationId=123', + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body.should.equal('{}'); + server.getNotifications.calledWith({ + notificationId: '123', + minTs: +Date.now() - 60000, + }).should.be.true; + done(); + }); + }); + }); + it('should allow custom minTs within limits', function(done) { + start(TestExpressApp, function() { + var requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/notifications' + '?timeSpan=30', + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + server.getNotifications.calledWith({ + notificationId: undefined, + minTs: +Date.now() - 30000, + }).should.be.true; + done(); + }); + }); + }); + it('should limit minTs to 60 seconds', function(done) { + start(TestExpressApp, function() { + var requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/notifications' + '?timeSpan=90', + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body.should.equal('{}'); + server.getNotifications.calledWith({ + notificationId: undefined, + minTs: Date.now() - 60000, // override minTs argument with a hardcoded 60 seconds span + }).should.be.true; + done(); + }); }); }); }); diff --git a/test/integration/server.js b/test/integration/server.js index de82df0..2d48500 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3923,6 +3923,38 @@ describe('Wallet service', function() { }); }); + it('should pull new block notifications along with wallet notifications in the last 60 seconds', function(done) { + // Simulate new block notification + server.walletId = 'livenet'; + server._notify('NewBlock', { + hash: 'dummy hash', + }, { + isGlobal: true + }, function(err) { + should.not.exist(err); + server.walletId = 'testnet'; + server._notify('NewBlock', { + hash: 'dummy hash', + }, { + isGlobal: true + }, function(err) { + should.not.exist(err); + server.walletId = wallet.id; + server.getNotifications({ + minTs: +Date.now() - (60 * 1000), + }, function(err, notifications) { + should.not.exist(err); + var types = _.pluck(notifications, 'type'); + types.should.deep.equal(['NewTxProposal', 'NewTxProposal', 'NewBlock']); + var walletIds = _.uniq(_.pluck(notifications, 'walletId')); + walletIds.length.should.equal(1); + walletIds[0].should.equal(wallet.id); + done(); + }); + }); + }); + }); + it('should pull notifications in the last 60 seconds', function(done) { server.getNotifications({ minTs: +Date.now() - (60 * 1000), @@ -4134,7 +4166,7 @@ describe('Wallet service', function() { }); }, function(next) { - server.getNotifications({}, function(err, items) { + server.storage.fetchNotifications(wallet.id, null, 0, function(err, items) { items.length.should.equal(0); next(); });