Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
Merge pull request #156 from isocolsky/ref/sockets
Browse files Browse the repository at this point in the history
Refactor notifications
  • Loading branch information
matiu committed Oct 20, 2015
2 parents 377d033 + 63a1d0a commit 257d706
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 36 deletions.
137 changes: 104 additions & 33 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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;


Expand All @@ -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
Expand Down Expand Up @@ -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
*
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
91 changes: 91 additions & 0 deletions test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 257d706

Please sign in to comment.