diff --git a/lib/api.js b/lib/api.js index 1d1bdbdd..ab757686 100644 --- a/lib/api.js +++ b/lib/api.js @@ -9,7 +9,6 @@ var events = require('events'); var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = WalletUtils.Bitcore; var sjcl = require('sjcl'); -var io = require('socket.io-client'); var url = require('url'); var querystring = require('querystring'); @@ -50,7 +49,6 @@ function API(opts) { this.payProHttp = null; // Only for testing this.doNotVerifyPayPro = opts.doNotVerifyPayPro; - this.transports = opts.transports || ['polling', 'websocket']; this.timeout = opts.timeout || 50000; @@ -67,51 +65,94 @@ API.privateKeyEncryptionOpts = { }; API.prototype.initNotifications = function(cb) { + log.warn('DEPRECATED: use initialize() instead.'); + this.initialize(cb); +}; + +API.prototype.initialize = function(cb) { $.checkState(this.credentials); var self = this; - var socket = io.connect(self.baseHost, { - 'force new connection': true, - 'reconnection': true, - 'reconnectionDelay': 5000, - 'secure': true, - 'transports': self.transports, - }); + self._initNotifications(); + return cb(); +}; - socket.on('unauthorized', function() { - return cb(new Error('Could not establish web-sockets connection: Unauthorized')); - }); +API.prototype.dispose = function(cb) { + var self = this; + self._disposeNotifications(); + return cb(); +}; - socket.on('authorized', function() { - return cb(); - }); +API.prototype._fetchLatestNotifications = function(interval, cb) { + var self = this; + + cb = cb || function() {}; + + var opts = { + lastNotificationId: self.lastNotificationId, + }; - socket.on('notification', function(data) { - if (data.creatorId != self.credentials.copayerId) { - self.emit('notification', data); + if (!self.lastNotificationId) { + opts.timeSpan = interval; + } + + self.getNotifications(opts, function(err, notifications) { + if (err) { + log.warn('Error receiving notifications.'); + log.debug(err); + return cb(err); + } + if (notifications.length > 0) { + self.lastNotificationId = _.last(notifications).id; } - }); - socket.on('reconnecting', function() { - self.emit('reconnecting'); + _.each(notifications, function(notification) { + self.emit('notification', notification); + }); + return cb(); }); +}; - socket.on('reconnect', function() { - self.emit('reconnect'); - }); +API.prototype._initNotifications = function(opts) { + var self = this; - socket.on('challenge', function(nonce) { - $.checkArgument(nonce); + opts = opts || {}; - var auth = { - copayerId: self.credentials.copayerId, - message: nonce, - signature: WalletUtils.signMessage(nonce, self.credentials.requestPrivKey), - }; - socket.emit('authorize', auth); - }); + var interval = opts.notificationIntervalSeconds || 5; + var firstTime = true; + self.notificationsIntervalId = setInterval(function() { + if (firstTime) { + firstTime = false; + return; + } + self._fetchLatestNotifications(interval, function() {}); + }, interval * 1000); }; +API.prototype._disposeNotifications = function() { + if (self.notificationsIntervalId) { + clearInterval(self.notificationsIntervalId); + self.notificationsIntervalId = null; + } +}; + + +/** + * Reset notification polling with new interval + * @memberof Client.API + * @param {Numeric} notificationIntervalSeconds - use 0 to pause notifications + */ +API.prototype.setNotificationsInterval = function(notificationIntervalSeconds) { + var self = this; + self._disposeNotifications(); + if (notificationIntervalSeconds > 0) { + self._initNotifications({ + notificationIntervalSeconds: notificationIntervalSeconds + }); + } +}; + + /** * Encrypt a message * @private @@ -1013,6 +1054,36 @@ API.prototype._processCustomData = function(result) { this.credentials.addWalletPrivateKey(customData.walletPrivKey) } +/** + * Get latest notifications + * + * @param {object} opts + * @param {String} lastNotificationId (optional) - The ID of the last received notification + * @param {String} timeSpan (optional) - A time window on which to look for notifications (in seconds) + * @returns {Callback} cb - Returns error or an array of notifications + */ +API.prototype.getNotifications = function(opts, cb) { + $.checkState(this.credentials); + + var self = this; + opts = opts || {}; + + var url = '/v1/notifications/'; + if (opts.lastNotificationId) { + url += '?notificationId=' + opts.lastNotificationId; + } else if (opts.timeSpan) { + url += '?timeSpan=' + opts.timeSpan; + } + + self._doGetRequest(url, function(err, result) { + if (err) return cb(err); + var notifications = _.filter(result, function(notification) { + return (notification.creatorId != self.credentials.copayerId); + }); + return cb(null, notifications); + }); +}; + /** * Get status of the wallet * diff --git a/package.json b/package.json index 8277d66a..0b098dee 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bitcore-wallet-client", "description": "Client for bitcore-wallet-service", "author": "BitPay Inc", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "keywords": [ "bitcoin", @@ -33,11 +33,10 @@ "preconditions": "^1.0.8", "request": "^2.53.0", "sjcl": "^1.0.2", - "socket.io-client": "^1.3.5", "uglify": "^0.1.1" }, "devDependencies": { - "bitcore-wallet-service": "~1.0.0", + "bitcore-wallet-service": "~1.1.0", "chai": "^1.9.1", "coveralls": "^2.11.2", "grunt-jsdoc": "^0.5.8", diff --git a/test/client.js b/test/client.js index 7c8fd89d..2994ece2 100644 --- a/test/client.js +++ b/test/client.js @@ -340,6 +340,53 @@ describe('client API', function() { }); }); + describe('Notification polling', function() { + var clock, interval; + beforeEach(function() { + clock = sinon.useFakeTimers(1234000, 'Date'); + }); + afterEach(function() { + clock.restore(); + }); + it('should fetch notifications at intervals', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function() { + clients[0].on('notification', function(data) { + notifications.push(data); + }); + + var notifications = []; + clients[0]._fetchLatestNotifications(5, function() { + _.pluck(notifications, 'type').should.deep.equal(['NewCopayer', 'WalletComplete']); + clock.tick(2000); + notifications = []; + clients[0]._fetchLatestNotifications(5, function() { + notifications.length.should.equal(0); + clock.tick(2000); + clients[1].createAddress(function(err, x) { + should.not.exist(err); + clients[0]._fetchLatestNotifications(5, function() { + _.pluck(notifications, 'type').should.deep.equal(['NewAddress']); + clock.tick(2000); + notifications = []; + clients[0]._fetchLatestNotifications(5, function() { + notifications.length.should.equal(0); + clients[1].createAddress(function(err, x) { + should.not.exist(err); + clock.tick(60 * 1000); + clients[0]._fetchLatestNotifications(5, function() { + notifications.length.should.equal(0); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + describe('Wallet Creation', function() { it('should check balance in a 1-1 ', function(done) { helpers.createAndJoinWallet(clients, 1, 1, function() { @@ -780,6 +827,50 @@ describe('client API', function() { }); }); + describe('Notifications', function() { + var clock; + beforeEach(function(done) { + this.timeout(5000); + clock = sinon.useFakeTimers(1234000, 'Date'); + helpers.createAndJoinWallet(clients, 2, 2, function() { + clock.tick(25 * 1000); + clients[0].createAddress(function(err, x) { + should.not.exist(err); + clock.tick(25 * 1000); + clients[1].createAddress(function(err, x) { + should.not.exist(err); + done(); + }); + }); + }); + }); + afterEach(function() { + clock.restore(); + }); + it('should receive notifications', function(done) { + clients[0].getNotifications({}, function(err, notifications) { + should.not.exist(err); + notifications.length.should.equal(3); + _.pluck(notifications, 'type').should.deep.equal(['NewCopayer', 'WalletComplete', 'NewAddress']); + clients[0].getNotifications({ + lastNotificationId: _.last(notifications).id + }, function(err, notifications) { + should.not.exist(err); + notifications.length.should.equal(0, 'should only return unread notifications'); + done(); + }); + }); + }); + it('should not receive old notifications', function(done) { + clock.tick(61 * 1000); // more than 60 seconds + clients[0].getNotifications({}, function(err, notifications) { + should.not.exist(err); + notifications.length.should.equal(0); + done(); + }); + }); + }); + describe('Transaction Proposals Creation and Locked funds', function() { it('Should create proposal and get it', function(done) { helpers.createAndJoinWallet(clients, 2, 2, function(w) {