From 2315ed7c3a73c007fa14d78e1f2353419f66d9d8 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 16 Oct 2015 17:34:12 -0300 Subject: [PATCH 01/12] replace sockets with http rest calls to get notifications --- lib/api.js | 91 ++++++++++++++++++++++++++++++-------------------- test/client.js | 44 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 36 deletions(-) diff --git a/lib/api.js b/lib/api.js index 1d1bdbdd..718c0562 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,49 +65,47 @@ API.privateKeyEncryptionOpts = { }; API.prototype.initNotifications = function(cb) { + log.warn('DEPRECATED: use initialize()'); + 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, - }); - - socket.on('unauthorized', function() { - return cb(new Error('Could not establish web-sockets connection: Unauthorized')); - }); + self._initNotifications(cb); +}; - socket.on('authorized', function() { - return cb(); - }); +API.prototype.dispose = function(cb) { + var self = this; + if (self.notificationsIntervalId) clearInterval(self.notificationsIntervalId); - socket.on('notification', function(data) { - if (data.creatorId != self.credentials.copayerId) { - self.emit('notification', data); - } - }); + return cb(); +}; - socket.on('reconnecting', function() { - self.emit('reconnecting'); - }); +API.prototype._initNotifications = function(cb) { + var self = this; - socket.on('reconnect', function() { - self.emit('reconnect'); - }); + var NOTIFICATION_INTERVAL = 5; // in seconds - socket.on('challenge', function(nonce) { - $.checkArgument(nonce); + var lastNotificationId; + self.notificationsIntervalId = setInterval(function() { + var url = '/v1/notifications/'; + if (lastNotificationId) url += '?notificationId=' + lastNotificationId; - var auth = { - copayerId: self.credentials.copayerId, - message: nonce, - signature: WalletUtils.signMessage(nonce, self.credentials.requestPrivKey), - }; - socket.emit('authorize', auth); - }); + self.getNotifications({ + lastNotificationId: lastNotificationId + }, function(err, notifications) { + if (err) { + log.warn('Error receiving notifications.'); + log.debug(err); + return; + } + _.each(notifications, function(notification) { + self.emit('notification', notification.data); + }); + }); + }, NOTIFICATION_INTERVAL * 1000); }; /** @@ -1013,6 +1009,29 @@ 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 + * @returns {Callback} cb - Returns error or an array of notifications + */ +API.prototype.getNotifications = function(opts, cb) { + $.checkState(this.credentials); + + var self = this; + var url = '/v1/notifications/'; + if (opts.lastNotificationId) url += '?notificationId=' + opts.lastNotificationId; + + 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/test/client.js b/test/client.js index 7c8fd89d..99e7e526 100644 --- a/test/client.js +++ b/test/client.js @@ -780,6 +780,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) { From ce23b7aaae997a503cd6dd54fc470bdc5f1c4961 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 16 Oct 2015 17:40:00 -0300 Subject: [PATCH 02/12] bws v0.4.0 --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 8277d66a..c733e18b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "preconditions": "^1.0.8", "request": "^2.53.0", "sjcl": "^1.0.2", - "socket.io-client": "^1.3.5", "uglify": "^0.1.1" }, "devDependencies": { From dd5e6a1d432616ce3f4a4904e9538b249423808d Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 16 Oct 2015 18:04:00 -0300 Subject: [PATCH 03/12] fix notification loop --- lib/api.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/api.js b/lib/api.js index 718c0562..cde29eaa 100644 --- a/lib/api.js +++ b/lib/api.js @@ -101,8 +101,12 @@ API.prototype._initNotifications = function(cb) { log.debug(err); return; } + if (notifications.length > 0) { + lastNotificationId = _.last(notifications).id; + } + _.each(notifications, function(notification) { - self.emit('notification', notification.data); + self.emit('notification', notification); }); }); }, NOTIFICATION_INTERVAL * 1000); From b7ca5f5d5fdc6919c12efd4a40dd50c7df7ffce9 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sat, 17 Oct 2015 13:44:52 -0300 Subject: [PATCH 04/12] reset variables on dispose --- lib/api.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/api.js b/lib/api.js index cde29eaa..5ca811a1 100644 --- a/lib/api.js +++ b/lib/api.js @@ -65,7 +65,7 @@ API.privateKeyEncryptionOpts = { }; API.prototype.initNotifications = function(cb) { - log.warn('DEPRECATED: use initialize()'); + log.warn('DEPRECATED: use initialize() instead.'); this.initialize(cb); }; @@ -78,7 +78,10 @@ API.prototype.initialize = function(cb) { API.prototype.dispose = function(cb) { var self = this; - if (self.notificationsIntervalId) clearInterval(self.notificationsIntervalId); + if (self.notificationsIntervalId) { + clearInterval(self.notificationsIntervalId); + self.notificationsIntervalId = null; + } return cb(); }; From 512378414bced874308cd69c093a7327982d9707 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 19 Oct 2015 15:30:58 -0300 Subject: [PATCH 05/12] make notification interval configurable and allow modifying at runtime --- lib/api.js | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/api.js b/lib/api.js index 5ca811a1..68e9f8a7 100644 --- a/lib/api.js +++ b/lib/api.js @@ -73,23 +73,22 @@ API.prototype.initialize = function(cb) { $.checkState(this.credentials); var self = this; - self._initNotifications(cb); + self._initNotifications(); + return cb(); }; API.prototype.dispose = function(cb) { var self = this; - if (self.notificationsIntervalId) { - clearInterval(self.notificationsIntervalId); - self.notificationsIntervalId = null; - } - + self._disposeNotifications(); return cb(); }; -API.prototype._initNotifications = function(cb) { +API.prototype._initNotifications = function(opts) { var self = this; - var NOTIFICATION_INTERVAL = 5; // in seconds + opts = opts || {}; + + var interval = opts.notificationIntervalSeconds || 5; var lastNotificationId; self.notificationsIntervalId = setInterval(function() { @@ -112,9 +111,33 @@ API.prototype._initNotifications = function(cb) { self.emit('notification', notification); }); }); - }, NOTIFICATION_INTERVAL * 1000); + }, 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 From 6129c4ae7131bd8ef0b9b80be7aa137dc41586b3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 19 Oct 2015 16:56:14 -0300 Subject: [PATCH 06/12] improve notification polling + test --- lib/api.js | 52 +++++++++++++++++++++++++++++++------------------- test/client.js | 47 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/lib/api.js b/lib/api.js index 68e9f8a7..3c82bbb0 100644 --- a/lib/api.js +++ b/lib/api.js @@ -83,6 +83,37 @@ API.prototype.dispose = function(cb) { return cb(); }; +API.prototype._fetchLatestNotifications = function(interval, cb) { + var self = this; + + cb = cb || function() {}; + + var url = '/v1/notifications/'; + if (self.lastNotificationId) { + url += '?notificationId=' + self.lastNotificationId; + } else { + url += '?timeSpan=' + (interval + 2); // Give a couple of seconds for network latency + } + + self.getNotifications({ + lastNotificationId: self.lastNotificationId + }, 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; + } + + _.each(notifications, function(notification) { + self.emit('notification', notification); + }); + return cb(); + }); +}; + API.prototype._initNotifications = function(opts) { var self = this; @@ -90,27 +121,8 @@ API.prototype._initNotifications = function(opts) { var interval = opts.notificationIntervalSeconds || 5; - var lastNotificationId; self.notificationsIntervalId = setInterval(function() { - var url = '/v1/notifications/'; - if (lastNotificationId) url += '?notificationId=' + lastNotificationId; - - self.getNotifications({ - lastNotificationId: lastNotificationId - }, function(err, notifications) { - if (err) { - log.warn('Error receiving notifications.'); - log.debug(err); - return; - } - if (notifications.length > 0) { - lastNotificationId = _.last(notifications).id; - } - - _.each(notifications, function(notification) { - self.emit('notification', notification); - }); - }); + self._fetchLatestNotifications(interval, function() {}); }, interval * 1000); }; diff --git a/test/client.js b/test/client.js index 99e7e526..81e98fd0 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(8000); + 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() { From 7cb3ae5aa8d9507091a95d7df852e82b0d5cca98 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 19 Oct 2015 17:12:11 -0300 Subject: [PATCH 07/12] skip notifications fetching on first call --- lib/api.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/api.js b/lib/api.js index 3c82bbb0..fd6fe96f 100644 --- a/lib/api.js +++ b/lib/api.js @@ -120,8 +120,12 @@ API.prototype._initNotifications = function(opts) { opts = opts || {}; var interval = opts.notificationIntervalSeconds || 5; - + var firstTime = true; self.notificationsIntervalId = setInterval(function() { + if (firstTime) { + firstTime = false; + return; + } self._fetchLatestNotifications(interval, function() {}); }, interval * 1000); }; From da3e9f73dcce814d88a70d8a2f93a106e30c57e0 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 20 Oct 2015 17:07:17 -0300 Subject: [PATCH 08/12] fix test --- test/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/client.js b/test/client.js index 81e98fd0..2994ece2 100644 --- a/test/client.js +++ b/test/client.js @@ -372,7 +372,7 @@ describe('client API', function() { notifications.length.should.equal(0); clients[1].createAddress(function(err, x) { should.not.exist(err); - clock.tick(8000); + clock.tick(60 * 1000); clients[0]._fetchLatestNotifications(5, function() { notifications.length.should.equal(0); done(); From 92cca7cee22d32460d9e266431dd7e3231f5e584 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 20 Oct 2015 17:07:24 -0300 Subject: [PATCH 09/12] BWS 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c733e18b..275e2eea 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "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", From cbd5589c1246fe213cf9e04d3e299de79d47212a Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 20 Oct 2015 17:07:52 -0300 Subject: [PATCH 10/12] v1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 275e2eea..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", From b1d4636682919e3184f5a94b3415a84a888975d2 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 20 Oct 2015 17:19:58 -0300 Subject: [PATCH 11/12] fix request url when still no notificationId received --- lib/api.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/api.js b/lib/api.js index fd6fe96f..b59f1ee5 100644 --- a/lib/api.js +++ b/lib/api.js @@ -88,16 +88,15 @@ API.prototype._fetchLatestNotifications = function(interval, cb) { cb = cb || function() {}; - var url = '/v1/notifications/'; - if (self.lastNotificationId) { - url += '?notificationId=' + self.lastNotificationId; - } else { - url += '?timeSpan=' + (interval + 2); // Give a couple of seconds for network latency + var opts = { + lastNotificationId: self.lastNotificationId, + }; + + if (!self.lastNotificationId) { + opts.timeSpan = interval + 2; // Give a couple of seconds for network latency } - self.getNotifications({ - lastNotificationId: self.lastNotificationId - }, function(err, notifications) { + self.getNotifications(opts, function(err, notifications) { if (err) { log.warn('Error receiving notifications.'); log.debug(err); @@ -1060,14 +1059,21 @@ API.prototype._processCustomData = function(result) { * * @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; + 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); From 63a1d0a5c50d25e10393f8352451ea556a70c369 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 20 Oct 2015 17:38:54 -0300 Subject: [PATCH 12/12] remove latency compensation --- lib/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api.js b/lib/api.js index b59f1ee5..ab757686 100644 --- a/lib/api.js +++ b/lib/api.js @@ -93,7 +93,7 @@ API.prototype._fetchLatestNotifications = function(interval, cb) { }; if (!self.lastNotificationId) { - opts.timeSpan = interval + 2; // Give a couple of seconds for network latency + opts.timeSpan = interval; } self.getNotifications(opts, function(err, notifications) {