From 33801e9587dc7059babed23a266af223bba6c9a2 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Thu, 28 Aug 2014 14:23:49 -0300 Subject: [PATCH 1/6] Refactor blockchain backing service --- js/controllers/transactions.js | 4 +- js/models/blockchain/Insight.js | 410 +++++++++++------------- js/models/core/Wallet.js | 19 +- js/services/controllerUtils.js | 54 +--- js/services/socket.js | 80 ----- package.json | 1 + test/mocks/FakeBlockchain.js | 9 +- test/mocks/FakeBlockchainSocket.js | 27 ++ test/test.Wallet.js | 2 +- test/test.WalletFactory.js | 3 +- test/test.blockchain.Insight.js | 490 +++++++++++++++++++---------- test/unit/services/servicesSpec.js | 50 --- 12 files changed, 585 insertions(+), 564 deletions(-) delete mode 100644 js/services/socket.js create mode 100644 test/mocks/FakeBlockchainSocket.js diff --git a/js/controllers/transactions.js b/js/controllers/transactions.js index a35553965..7f15c02df 100644 --- a/js/controllers/transactions.js +++ b/js/controllers/transactions.js @@ -109,7 +109,9 @@ angular.module('copayApp.controllers').controller('TransactionsController', var addresses = w.getAddressesStr(); if (addresses.length > 0) { $scope.blockchain_txs = $scope.wallet.txCache || []; - w.blockchain.getTransactions(addresses, function(txs) { + w.blockchain.getTransactions(addresses, function(err, txs) { + if (err) throw err; + $timeout(function() { $scope.blockchain_txs = []; for (var i = 0; i < txs.length; i++) { diff --git a/js/models/blockchain/Insight.js b/js/models/blockchain/Insight.js index 610be2ebe..9c7fce321 100644 --- a/js/models/blockchain/Insight.js +++ b/js/models/blockchain/Insight.js @@ -1,72 +1,176 @@ 'use strict'; +var util = require('util'); +var async = require('async'); +var request = require('request'); var bitcore = require('bitcore'); -var coinUtil = bitcore.util; +var io = require('socket.io-client'); + +var EventEmitter = require('events').EventEmitter; var preconditions = require('preconditions').singleton(); -var http; -if (process.version) { - http = require('http'); -}; +/* + This class lets interfaces with the blockchain, making general queries and + subscribing to transactions on adressess and blocks. -function Insight(opts) { - opts = opts || {}; - this.host = opts.host || 'localhost'; - this.port = opts.port || '3001'; - this.schema = opts.schema || 'http'; - this.retryDelay = opts.retryDelay || 5000; + Opts: + - host + - port + - schema + - reconnection (optional) + - reconnectionDelay (optional) + + Events: + - tx: activity on subscribed address. + - block: a new block that includes a subscribed address. + - connect: the connection with the blockchain is ready. + - disconnect: the connection with the blochckain is unavailable. +*/ + +var Insight = function (opts) { + this.status = this.STATUS.DISCONNECTED; + this.subscribed = []; + this.listeningBlocks = false; + + preconditions.checkArgument(opts).shouldBeObject(opts) + .checkArgument(opts.host) + .checkArgument(opts.port) + .checkArgument(opts.schema); + + this.url = opts.schema + '://' + opts.host + ':' + opts.port; + this.opts = { + 'reconnection': opts.reconnection || true, + 'reconnectionDelay': opts.reconnectionDelay || 1000, + 'secure': opts.schema === 'https' + }; + + this.socket = this.getSocket(this.url, this.opts); + + // Emmit connection events + var self = this; + this.socket.on('connect', function() { + self.status = self.STATUS.CONNECTED; + self.suscribeToBlocks(); + self.emit('connect', 0); + }); + + this.socket.on('connect_error', function() { + if (self.status != self.STATUS.CONNECTED) return; + self.status = self.STATUS.DISCONNECTED; + self.emit('disconnect'); + }); + + this.socket.on('connect_timeout', function() { + if (self.status != self.STATUS.CONNECTED) return; + self.status = self.STATUS.DISCONNECTED; + self.emit('disconnect'); + }); + + this.socket.on('reconnect', function(attempt) { + if (self.status != self.STATUS.DISCONNECTED) return; + self.status = self.STATUS.CONNECTED; + self.emit('connect', attempt); + }); } -function _asyncForEach(array, fn, callback) { - array = array.slice(0); +util.inherits(Insight, EventEmitter); - function processOne() { - var item = array.pop(); - fn(item, function(result) { - if (array.length > 0) { - setTimeout(processOne, 0); // schedule immediately - } else { - callback(); // Done! - } - }); - } - if (array.length > 0) { - setTimeout(processOne, 0); // schedule immediately - } else { - callback(); // Done! - } +Insight.prototype.STATUS = { + CONNECTED: 'connected', + DISCONNECTED: 'disconnected', + DESTROYED: 'destroyed' +} + +/** @private */ +Insight.prototype.suscribeToBlocks = function() { + if (this.listeningBlocks || !this.socket.connected) return; + + var self = this; + this.socket.emit('subscribe', 'inv'); + this.socket.on('block', function(blockHash) { + self.emit('block', blockHash); + }); + this.listeningBlocks = true; +} + +/** @private */ +Insight.prototype.getSocket = function(url, opts) { + return io(this.url, this.opts); +} + +/** @private */ +Insight.prototype.request = function(path, cb) { + preconditions.checkArgument(url).shouldBeFunction(cb); + request(this.url + path, cb); +} + +/** @private */ +Insight.prototype.requestPost = function(path, data, cb) { + preconditions.checkArgument(url).checkArgument(data).shouldBeFunction(cb); + request.post(this.url, cb).form(data); +} + +Insight.prototype.destroy = function() { + this.socket.destroy(); + this.subscribed = []; + this.status = this.STATUS.DESTROYED; + this.removeAllListeners(); }; -Insight.prototype._getOptions = function(method, path, data) { - return { - host: this.host, - port: this.port, - schema: this.schema, - method: method, - path: path, - data: data, - headers: { - 'Access-Control-Request-Headers': '' +Insight.prototype.subscribe = function(addresses) { + addresses = Array.isArray(addresses) ? addresses : [addresses]; + var self = this; + + function handlerFor(self, address) { + return function (txid) { + // verify the address is still subscribed + if (self.subscribed.indexOf(address) == -1) return; + self.emit('tx', {address: address, txid: txid}); } - }; + } + + addresses.forEach(function(address) { + preconditions.checkArgument(new bitcore.Address(address).isValid()); + + self.subscribed.push(address); + self.socket.emit('subscribe', address); + self.socket.on(address, handlerFor(self, address)); + }); }; +Insight.prototype.unsubscribe = function(addresses) { + addresses = Array.isArray(addresses) ? addresses : [addresses]; + var self = this; -// This is vulneable to txid maneability -// TODO: if ret = false, -// check output address from similar transactions. -// -Insight.prototype.checkSentTx = function(tx, cb) { - var hash = coinUtil.formatHashFull(tx.getHash()); - var options = this._getOptions('GET', '/api/tx/' + hash); + addresses.forEach(function(address) { + preconditions.checkArgument(new bitcore.Address(address).isValid()); + self.socket.removeEventListener(address); + }); - this._request(options, function(err, res) { - if (err) return cb(err); - var ret = false; - if (res && res.txid === hash) { - ret = hash; - } - return cb(err, ret); + this.subscribed = this.subscribed.filter(function(a) { + return addresses.indexOf(a) == -1; + }); +}; + +Insight.prototype.unsubscribeAll = function() { + this.unsubscribe(this.subscribed); +}; + +Insight.prototype.broadcast = function(rawtx, cb) { + preconditions.checkArgument(rawtx); + preconditions.shouldBeFunction(cb); + + this.requestPost('/api/tx/send', {rawtx: rawtx}, function(err, res, body) { + if (err || res.statusCode != 200) cb(err || res); + cb(null, JSON.parse(body).txid); + }); +}; + +Insight.prototype.getTransaction = function(txid, cb) { + preconditions.shouldBeFunction(cb); + this.request('/api/tx/' + txid, function(err, res, body) { + if (err || res.statusCode != 200 || !body) return cb(err || res); + cb(null, JSON.parse(body)); }); }; @@ -75,77 +179,53 @@ Insight.prototype.getTransactions = function(addresses, cb) { preconditions.shouldBeFunction(cb); var self = this; - if (!addresses || !addresses.length) return cb([]); + if (!addresses.length) return cb(null, []); - var txids = []; - var txs = []; - - _asyncForEach(addresses, function(addr, callback) { - var options = self._getOptions('GET', '/api/addr/' + addr); - - self._request(options, function(err, res) { - if (res && res.transactions) { - var txids_tmp = res.transactions; - for (var i = 0; i < txids_tmp.length; i++) { - txids.push(txids_tmp[i]); - } - } - callback(); + // Iterator: get a list of transaction ids for an address + function getTransactionIds(address, next) { + self.request('/api/addr/' + address, function(err, res, body) { + if (err || res.statusCode != 200 || !body) return next(err || res); + next(null, JSON.parse(body).transactions); }); - }, function() { - var uniqueTxids = {}; - for (var k in txids) { - uniqueTxids[txids[k]] = 1; - } - _asyncForEach(Object.keys(uniqueTxids), function(txid, callback2) { - var options = self._getOptions('GET', '/api/tx/' + txid); - self._request(options, function(err, res) { - txs.push(res); - callback2(); - }); - }, function() { - return cb(txs); + } + + async.map(addresses, getTransactionIds, function then(err, txids) { + if (err) return cb(err); + + // txids it's a list of list, let's fix that: + var txidsList = txids.reduce(function(a, r) { + return r.concat(a); + }); + + // Remove duplicated txids + txidsList = txidsList.filter(function(elem, pos, self) { + return self.indexOf(elem) == pos; + }); + + // Now get the transactions for that list of txIds + async.map(txidsList, self.getTransaction.bind(self), function then(err, txs) { + if (err) return cb(err); + cb(null, txs); }); }); }; Insight.prototype.getUnspent = function(addresses, cb) { - if (!addresses || !addresses.length) return cb(null, []); + preconditions.shouldBeArray(addresses); + preconditions.shouldBeFunction(cb); - var all = []; - - var options = this._getOptions('POST', '/api/addrs/utxo', 'addrs=' + addresses.join(',')); - - var self = this; - this._request(options, function(err, res) { - if (err) { - return cb(err); - } - - if (res && res.length > 0) { - all = all.concat(res); - } - - return cb(null, all); + this.requestPost('/api/addrs/utxo', {addrs: addresses.join(',')}, function(err, res, body) { + if (err || res.statusCode != 200) return cb(err || res); + cb(null, JSON.parse(body)); }); }; -Insight.prototype.sendRawTransaction = function(rawtx, cb) { - if (!rawtx) throw new Error('rawtx must be set'); +Insight.prototype.getActivity = function(addresses, cb) { + preconditions.shouldBeArray(addresses); - var options = this._getOptions('POST', '/api/tx/send', 'rawtx=' + rawtx); - this._request(options, function(err, res) { - if (err) return cb(); + this.getTransactions(addresses, function then(err, txs) { + if (err) return cb(err); - return cb(res.txid); - }); -}; - - -Insight.prototype.checkActivity = function(addresses, cb) { - if (!addresses) throw new Error('address must be set'); - - this.getTransactions(addresses, function onResult(txs) { var flatArray = function(xss) { return xss.reduce(function(r, xs) { return r.concat(xs); @@ -177,112 +257,4 @@ Insight.prototype.checkActivity = function(addresses, cb) { }); }; -Insight.prototype._requestNode = function(options, callback) { - if (options.method === 'POST') { - options.headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': options.data.length, - - }; - } - - var req = http.request(options, function(response) { - var ret, errTxt, e; - if (response.statusCode == 200 || response.statusCode === 304) { - response.on('data', function(chunk) { - try { - ret = JSON.parse(chunk); - } catch (e2) { - errTxt = 'CRITICAL: Wrong response from insight' + e2; - } - }); - } else { - errTxt = "INSIGHT ERROR:" + response.statusCode; - console.log(errTxt); - e = new Error(errTxt); - return callback(e); - } - response.on('end', function() { - if (errTxt) { - console.log("INSIGHT ERROR:" + errTxt); - e = new Error(errTxt); - } - return callback(e, ret); - }); - response.on('error', function(e) { - return callback(e, ret); - }); - }); - - if (options.data) { - req.write(options.data); - } - req.end(); -}; - -Insight.prototype._requestBrowser = function(options, callback) { - var self = this; - var request = new XMLHttpRequest(); - var url = (options.schema || 'http') + '://' + options.host; - - if (options.port !== 80) { - url = url + ':' + options.port; - } - - url = url + options.path; - - if (options.data && options.method === 'GET') { - url = url + '?' + options.data; - } - - request.open(options.method, url, true); - request.timeout = 5000; - request.ontimeout = function() { - setTimeout(function() { - return self._request(options, callback); - }, self.retryDelay); - return callback(new Error('Insight request timeout')); - }; - - - request.onreadystatechange = function() { - if (request.readyState !== 4) return; - var ret, errTxt, e; - - if (request.status === 200 || request.status === 304) { - try { - ret = JSON.parse(request.responseText); - } catch (e2) { - errTxt = 'CRITICAL: Wrong response from insight' + e2; - } - } else if (request.status >= 400 && request.status < 499) { - errTxt = 'CRITICAL: Bad request to insight: '+request.status; - } else { - errTxt = 'Error code: ' + request.status + ' - Status: ' + request.statusText + ' - Description: ' + request.responseText; - setTimeout(function() { - return self._request(options, callback); - }, self.retryDelay); - } - if (errTxt) { - console.log("INSIGHT ERROR:", e); - e = new Error(errTxt); - } - return callback(e, ret); - }; - - if (options.method === 'POST') { - request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - } - - request.send(options.data || null); -}; - -Insight.prototype._request = function(options, callback) { - if (typeof process === 'undefined' || !process.version) { - this._requestBrowser(options, callback); - } else { - this._requestNode(options, callback); - } -}; - module.exports = Insight; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 7e1c14c83..1c3de8943 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -193,14 +193,12 @@ Wallet.prototype._getKeyMap = function(txp) { Wallet.prototype._checkSentTx = function(ntxid, cb) { var txp = this.txProposals.get(ntxid); var tx = txp.builder.build(); + var txid = bitcore.util.formatHashFull(tx.getHash()); - this.blockchain.checkSentTx(tx, function(err, txid) { - var ret = false; - if (txid) { - txp.setSent(txid); - ret = txid; - } - return cb(ret); + this.blockchain.getTransaction(txid, function(err, tx) { + if (err) return cb(false); + txp.setSent(tx.txid); + cb(ret); }); }; @@ -806,7 +804,9 @@ Wallet.prototype.sendTx = function(ntxid, cb) { this.log('Raw transaction: ', txHex); var self = this; - this.blockchain.sendRawTransaction(txHex, function(txid) { + this.blockchain.broadcast(txHex, function(err, txid) { + if(err) throw err; + self.log('BITCOIND txid:', txid); if (txid) { self.txProposals.get(ntxid).setSent(txid); @@ -1724,7 +1724,7 @@ Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb) // Optimize window to minimize the derivations. var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; var addresses = self.deriveAddresses(scanIndex, scanWindow, change, copayerIndex); - self.blockchain.checkActivity(addresses, function(err, actives) { + self.blockchain.getActivity(addresses, function(err, actives) { if (err) throw err; // Check for new activities in the newlly scanned addresses @@ -1752,6 +1752,7 @@ Wallet.prototype.close = function() { this.log('## CLOSING'); this.lock.release(); this.network.cleanUp(); + this.blockchain.destroy(); }; Wallet.prototype.getNetwork = function() { diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 16cfa5a6d..6b3b32ca7 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -2,7 +2,7 @@ var bitcore = require('bitcore'); angular.module('copayApp.services') - .factory('controllerUtils', function($rootScope, $sce, $location, notification, $timeout, Socket, video, uriHandler) { + .factory('controllerUtils', function($rootScope, $sce, $location, notification, $timeout, video, uriHandler) { var root = {}; root.getVideoMutedStatus = function(copayer) { if (!$rootScope.videoInfo) return; @@ -24,8 +24,6 @@ angular.module('copayApp.services') if ($rootScope.wallet) $rootScope.wallet.close(); - Socket.removeAllListeners(); - $rootScope.wallet = null; delete $rootScope['wallet']; @@ -68,7 +66,6 @@ angular.module('copayApp.services') uriHandler.register(); $rootScope.unitName = config.unitName; $rootScope.txAlertCount = 0; - $rootScope.insightError = 0; $rootScope.isCollapsed = true; $rootScope.$watch('txAlertCount', function(txAlertCount) { if (txAlertCount && txAlertCount > 0) { @@ -100,8 +97,6 @@ angular.module('copayApp.services') root.startNetwork = function(w, $scope) { - Socket.removeAllListeners(); - root.setupRootVariables(); root.installStartupHandlers(w, $scope); root.setSocketHandlers(); @@ -125,6 +120,8 @@ angular.module('copayApp.services') }); w.on('ready', function(myPeerID) { $rootScope.wallet = w; + + if ($rootScope.pendingPayment) { $location.path('send'); } else { @@ -298,47 +295,20 @@ angular.module('copayApp.services') }); } - var connectionLost = false; - $rootScope.$watch('insightError', function(status) { - if (!status) return; - - // Reconnected - if (status === -1) { - if (!connectionLost) return; // Skip on first reconnect - connectionLost = false; + root.setConnectionListeners = function(wallet) { + wallet.blockchain.on('connect', function(attempts) { + if (attempts == 0) return; notification.success('Networking restored', 'Connection to Insight re-established'); - return; - } + }); - // Retry - if (status == 1) return; // Skip the first try - connectionLost = true; - notification.error('Networking problem', 'Connection to Insight lost, reconnecting (attempt number ' + (status - 1) + ')'); - }); - - root._setCommError = function(e) { - if ($rootScope.insightError < 0) - $rootScope.insightError = 0; - $rootScope.insightError++; - }; - - root._clearCommError = function(e) { - if ($rootScope.insightError > 0) - $rootScope.insightError = -1; - else - $rootScope.insightError = 0; - }; + wallet.blockchain.on('disconnect', function() { + notification.error('Networking problem', 'Connection to Insight lost, trying to reconnect...'); + }); + } root.setSocketHandlers = function() { root.updateAddressList(); - if (!Socket.sysEventsSet) { - Socket.sysOn('error', root._setCommError); - Socket.sysOn('reconnect_error', root._setCommError); - Socket.sysOn('reconnect_failed', root._setCommError); - Socket.sysOn('connect', root._clearCommError); - Socket.sysOn('reconnect', root._clearCommError); - Socket.sysEventsSet = true; - } + if (!$rootScope.wallet) return; var currentAddrs = Socket.getListeners(); diff --git a/js/services/socket.js b/js/services/socket.js deleted file mode 100644 index 8e618643d..000000000 --- a/js/services/socket.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('Socket', - function($rootScope) { - var listeners = []; - var url = (config.socket.schema || 'http') + '://' + config.socket.host + ':' + config.socket.port; - var opts = { - 'reconnection': true, - 'reconnectionDelay': config.socket.reconnectDelay || 500, - 'secure': config.socket.schema === 'https' ? true : false, - }; - - var socket = io(url, opts); - - return { - on: function(event, callback) { - var wrappedCallback = function() { - var args = arguments; - $rootScope.$apply(function() { - callback.apply(socket, args); - }); - }; - socket.on(event, wrappedCallback); - if (event !== 'connect') { - listeners.push({ - event: event, - fn: wrappedCallback - }); - } - }, - sysOn: function(event, callback) { - var wrappedCallback = function() { - var args = arguments; - $rootScope.$apply(function() { - callback.apply(socket, args); - }); - }; - socket.io.on(event, wrappedCallback); - }, - getListeners: function() { - var ret = {}; - - var addrList = listeners - .filter(function(i) { - return i.event != 'block'; - }) - .map(function(i) { - return i.event; - }); - - for (var i in addrList) { - ret[addrList[i]] = 1; - } - return ret; - }, - isListeningBlocks: function() { - return listeners.filter(function(i) { - return i.event == 'block'; - }).length > 0; - }, - emit: function(event, data, callback) { - socket.emit(event, data, function() { - var args = arguments; - $rootScope.$apply(function() { - if (callback) { - callback.apply(socket, args); - } - }); - }); - }, - removeAllListeners: function() { - for (var i = 0; i < listeners.length; i++) { - var details = listeners[i]; - socket.removeAllListeners(details.event); - } - - listeners = []; - } - }; - }); diff --git a/package.json b/package.json index 745f3ac48..c67c404a8 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "mocha-lcov-reporter": "0.0.1", "optimist": "^0.6.1", "preconditions": "^1.0.7", + "request": "^2.40.0", "sinon": "1.9.1" }, "scripts": { diff --git a/test/mocks/FakeBlockchain.js b/test/mocks/FakeBlockchain.js index 8438d8a75..1db31f97b 100644 --- a/test/mocks/FakeBlockchain.js +++ b/test/mocks/FakeBlockchain.js @@ -7,7 +7,7 @@ function FakeBlockchain(opts) { } FakeBlockchain.prototype.getTransactions = function(addresses, cb) { - return cb([]); + return cb(null, []); }; @@ -15,6 +15,9 @@ FakeBlockchain.prototype.fixUnspent = function(u) { this.u = u; }; +FakeBlockchain.prototype.destroy = function() { +}; + FakeBlockchain.prototype.getUnspent = function(addresses, cb) { return cb(null, this.u || [{ 'address': 'mji7zocy8QzYywQakwWf99w9bCT6orY1C1', @@ -41,9 +44,9 @@ FakeBlockchain.prototype.getUnspent2 = function(addresses, cb) { }]); }; -FakeBlockchain.prototype.sendRawTransaction = function(rawtx, cb) { +FakeBlockchain.prototype.broadcast = function(rawtx, cb) { var txid = '0be0fb4579911be829e3077202e1ab47fcc12cf3ab8f8487ccceae768e1f95fa'; - return cb(txid); + return cb(null, txid); }; FakeBlockchain.prototype.checkSentTx = function (tx, cb) { diff --git a/test/mocks/FakeBlockchainSocket.js b/test/mocks/FakeBlockchainSocket.js new file mode 100644 index 000000000..b85d3c31e --- /dev/null +++ b/test/mocks/FakeBlockchainSocket.js @@ -0,0 +1,27 @@ +'use strict'; + +var util = require('util'); +var EventEmitter = require('events').EventEmitter; + +var FakeSocket = function (url, opts) { + var self = this; + + self.connected = false; + setTimeout(function() { + self.connected = true; + self.emit('connect'); + }, 0); +} + +util.inherits(FakeSocket, EventEmitter); + +FakeSocket.prototype.removeEventListener = function() { + return; +} + +FakeSocket.prototype.destroy = function() { + this.connected = false; + this.removeAllListeners(); +}; + +module.exports = FakeSocket; \ No newline at end of file diff --git a/test/test.Wallet.js b/test/test.Wallet.js index e90ba751b..2386d3002 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -823,7 +823,7 @@ describe('Wallet model', function() { }); var mockFakeActivity = function(f) { - w.blockchain.checkActivity = function(addresses, cb) { + w.blockchain.getActivity = function(addresses, cb) { var activity = new Array(addresses.length); for (var i = 0; i < addresses.length; i++) { var a1 = ADDRESSES_CHANGE.indexOf(addresses[i]); diff --git a/test/test.WalletFactory.js b/test/test.WalletFactory.js index 8581720f7..7e69bd2dc 100644 --- a/test/test.WalletFactory.js +++ b/test/test.WalletFactory.js @@ -91,7 +91,8 @@ describe('WalletFactory model', function() { }, blockchain: { host: 'test.insight.is', - port: 80 + port: 80, + schema: 'https' }, networkName: 'testnet', passphrase: 'test', diff --git a/test/test.blockchain.Insight.js b/test/test.blockchain.Insight.js index 9eb9af6f1..eacc99808 100644 --- a/test/test.blockchain.Insight.js +++ b/test/test.blockchain.Insight.js @@ -4,23 +4,17 @@ var chai = chai || require('chai'); var should = chai.should(); var sinon = require('sinon'); var bitcore = bitcore || require('bitcore'); +var FakeSocket = require('./mocks/FakeBlockchainSocket'); + try { - var copay = require('copay'); //browser + var copay = require('./copay'); //browser } catch (e) { var copay = require('../copay'); //node } var Buffer = bitcore.Buffer; -var Insight = copay.Insight || require('../js/models/blockchain/Insight'); +var Insight = copay.Insight; -var ID = '933bf321393459b7'; -var copayers = [ - 'tpubD6NzVbkrYhZ4WeSS3M5axcR1EMYPeerA8GozBmYVLKSjriMXhse1C4kiLJMvaaDKRBaP7iSJJo5wMBh3JSYcMz1vrwXKKnAgtt4V4pfSEcq', - 'tpubD6NzVbkrYhZ4XPjvz7c2544jPBY2WKCJVCETEE68ykBLMcE7J3GVDGvmPEdzvTWWXxQsE25rm7f4J1ZNxzWhuR7iEhX1m4dS9HrYbg1ezUP', - 'tpubD6NzVbkrYhZ4YTRVfKf1tHgydyvoEWdsBRVCG6odCZdpY7nPZWxA26sLPtyHkquzHmgdAH8HpftobnJJUvcbi7MyHVqXmPLJCW9KCS6rkw8', - 'tpubD6NzVbkrYhZ4XDY86vJmcCUuUvbqujhM633a5ih8b6ngm1AsskGz3orGkjvbzcJNQUJSK9jqggRwSohq3LAigwWZ8uzGNrGZqCwaE95foAj', - 'tpubD6NzVbkrYhZ4XGHkbBTx4kU5w7RDb9hWXyK9tuEaYrY9SJUWBCUxrcMFkqBa6qAv11FNdVJ4MFxKdnKnjoBWDY6SwBtmP83gjFHTV5zz4RW' -]; -var addresses = [ +var ADDRESSES = [ '2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM', '2NE9hTCffeugo5gQtfB4owq98gyTeWC56yb', // 41btc '2N9D5bcCQ2bPWUDByQ6Qb5bMgMtgsk1rw3x', // 50btc @@ -31,7 +25,7 @@ var addresses = [ '2N9EdxU3co5XKTyj3yhFBeU3qw3EM1rrgzE' ]; -var unspent = [{ +var UNSPENT = [{ address: "2NE9hTCffeugo5gQtfB4owq98gyTeWC56yb", txid: "d5597c6cf7f72507af63a4d5a2f9f84edb45fb42452cc8c514435b7a93158915", vout: 0, @@ -49,182 +43,362 @@ var unspent = [{ confirmations: 6728 }]; -var rawtx = '01000000010c2a03ed71ee18148e8c99c5ff66d5ffb75e5def46cdea2acc6f30103f33bfb5010000006a47304402207f960aeefdfad270dd77d1acca7af17d3a2e47e2059034ff5d6305cf63635e1d02202f061ee196cc4459cdecae6559beac696a9ecde9a17520849f319fa2a627e64f012103870465f9b4efb90b5d186a7a5eacd7081e601020dacd68d942e5918a56ed0bfcffffffff02a086010000000000ad532102a9495c64323cd8c3354dbf0b3400d830ee680da493acbccc3c2c356d1b20fabf21028233cf8bc6112ae2c36468bd447732c5586b52e1ba3284a2319cadfac6367f99210279fd856e5ed13ab6807e85ed7c0cd6f80613be042240fd731c43f5aba3dcae9821021380858a67a4f99eda52ce2d72c300911f9d3eb9d7a45102a2133f14f7b2dc14210215739b613ce42106a11ce433342c13c610bf68a1bc934f607ad7aeb4178e04cf55ae2044d200000000001976a9146917322f0010aaf7ec136a34b476dfc5eb7a331288ac00000000'; - +var FAKE_OPTS = { + host: 'something.com', + port: 123, + schema: 'http' +} describe('Insight model', function() { + before(function() { + sinon.stub(Insight.prototype, "getSocket", function() { + return new FakeSocket(); + }); + }); + + after(function() { + Insight.prototype.getSocket.restore(); + }); it('should create an instance', function() { - var i = new Insight(); - should.exist(i); + var blockchain = new Insight(FAKE_OPTS); + should.exist(blockchain); + blockchain.url.should.be.equal('http://something.com:123'); }); - // Tests for Node - if (typeof process !== 'undefined' && process.version) { - it('should return array of unspent output', function(done) { - var i = new Insight(); + it('should subscribe to inventory', function(done) { + var blockchain = new Insight(FAKE_OPTS); + var emitSpy = sinon.spy(blockchain.socket, 'emit'); + blockchain.on('connect', function() { + emitSpy.calledWith('subscribe', 'inv'); + done(); + }); + }); + it('should be able to destroy the instance', function(done) { + var blockchain = new Insight(FAKE_OPTS); + blockchain.status.should.be.equal('disconnected'); + blockchain.on('connect', function() { + blockchain.subscribe('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribed.length.should.equal(1); + blockchain.destroy(); + blockchain.subscribed.length.should.equal(0); + blockchain.status.should.be.equal('destroyed'); + done(); + }); + }); - var http = require('http'); - var request = { - statusCode: 200 - }; + it('should subscribe to an address', function() { + var blockchain = new Insight(FAKE_OPTS); + var emitSpy = sinon.spy(blockchain.socket, 'emit'); - request.on = function(event, cb) { - if (event === 'error') return; - if (event === 'data') return cb(JSON.stringify(unspent)); - return cb(); - }; + blockchain.subscribe('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribed.length.should.equal(1); + emitSpy.calledWith('subscribe', 'mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + }); - var req = {}; - req.write = function() {}; - req.end = function() {}; + it('should subscribe to a list of addresses', function() { + var blockchain = new Insight(FAKE_OPTS); + var emitSpy = sinon.spy(blockchain.socket, 'emit'); + blockchain.subscribe([ + 'mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM', + '2NBBHBjB5sd7HFqKtout1L7d6dPhwJgP2j8' + ]); + blockchain.subscribed.length.should.equal(2); + emitSpy.calledWith('subscribe', 'mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + emitSpy.calledWith('subscribe', '2NBBHBjB5sd7HFqKtout1L7d6dPhwJgP2j8'); + }); - sinon - .stub(http, 'request') - .returns(req) - .yields(request); + it('should unsubscribe to an address', function() { + var blockchain = new Insight(FAKE_OPTS); + blockchain.subscribe('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribed.length.should.equal(1); + blockchain.unsubscribe('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribed.length.should.equal(0); + }); - i.getUnspent(['2MuD5LnZSViZZYwZbpVsagwrH8WWvCztdmV', '2NBSLoMvsHsf2Uv3LA17zV4beH6Gze6RovA'], function(e, ret) { - should.not.exist(e); - ret.should.deep.equal(unspent); - http.request.restore(); + it('should unsubscribe to all addresses', function() { + var blockchain = new Insight(FAKE_OPTS); + blockchain.subscribe('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribe('2NBBHBjB5sd7HFqKtout1L7d6dPhwJgP2j8'); + blockchain.subscribed.length.should.equal(2); + + blockchain.unsubscribeAll('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribed.length.should.equal(0); + }); + + it('should broadcast a raw transaction', function(done) { + var blockchain = new Insight(FAKE_OPTS); + var rawtx = '01000000010c2a03ed71ee18148e8c99c5ff66d5ffb75e5def46cdea2acc6f30103f33bfb5010000006a47304402207f960aeefdfad270dd77d1acca7af17d3a2e47e2059034ff5d6305cf63635e1d02202f061ee196cc4459cdecae6559beac696a9ecde9a17520849f319fa2a627e64f012103870465f9b4efb90b5d186a7a5eacd7081e601020dacd68d942e5918a56ed0bfcffffffff02a086010000000000ad532102a9495c64323cd8c3354dbf0b3400d830ee680da493acbccc3c2c356d1b20fabf21028233cf8bc6112ae2c36468bd447732c5586b52e1ba3284a2319cadfac6367f99210279fd856e5ed13ab6807e85ed7c0cd6f80613be042240fd731c43f5aba3dcae9821021380858a67a4f99eda52ce2d72c300911f9d3eb9d7a45102a2133f14f7b2dc14210215739b613ce42106a11ce433342c13c610bf68a1bc934f607ad7aeb4178e04cf55ae2044d200000000001976a9146917322f0010aaf7ec136a34b476dfc5eb7a331288ac00000000'; + + sinon.stub(blockchain, "requestPost", function(url, data, cb) { + url.should.be.equal('/api/tx/send'); + var res = {statusCode: 200}; + var body = JSON.stringify({txid: 1234}); + setTimeout(function() { + cb(null, res, body); + }, 0); + }); + + blockchain.broadcast(rawtx, function(err, id) { + id.should.be.equal(1234); + done(); + }); + }); + + describe('getTransaction', function() { + it('should get a transaction by id', function(done) { + var blockchain = new Insight(FAKE_OPTS); + var txid = '123321'; + var tx = {txid: txid, more: 'something'}; + + sinon.stub(blockchain, "request", function(url, cb) { + url.should.be.equal('/api/tx/' + txid); + var res = {statusCode: 200}; + var body = JSON.stringify(tx); + setTimeout(function() { + cb(null, res, body); + }, 0); + }); + + blockchain.getTransaction(txid, function(err, t) { + chai.expect(err).to.be.null; + t.should.be.an('object'); + t.txid.should.be.equal(tx.txid); + t.more.should.be.equal(tx.more); done(); }); }); - it('should return txid', function(done) { - var i = new Insight(); + it('should handle a 404 error code', function(done) { + var blockchain = new Insight(FAKE_OPTS); + var txid = '123321'; - var http = require('http'); - var request = { - statusCode: 200 - }; + sinon.stub(blockchain, "request", function(url, cb) { + url.should.be.equal('/api/tx/' + txid); + var res = {statusCode: 404}; + var body = ''; + setTimeout(function() { + cb(null, res, body); + }, 0); + }); - request.on = function(event, cb) { - if (event === 'error') return; - if (event === 'data') return cb('{ "txid": "1234" }'); - return cb(); - }; - - var req = {}; - req.write = function() {}; - req.end = function() {}; - - sinon - .stub(http, 'request') - .returns(req) - .yields(request); - - i.sendRawTransaction(rawtx, function(a) { - should.exist(a); - a.should.equal('1234'); - http.request.restore(); + blockchain.getTransaction(txid, function(err, t) { + chai.expect(t).to.be.undefined; + chai.expect(err).not.be.null; done(); }); }); - } + it('should handle a null response', function(done) { + var blockchain = new Insight(FAKE_OPTS); + var txid = '123321'; - - it('#checkActivity for innactive addreses', function(done) { - var w = new Insight(); - w.getTransactions = function(addresses, cb) { - cb([]); - }; - - w.checkActivity(addresses, function(err, actives) { - actives.length.should.equal(addresses.length); - actives.filter(function(i) { - return i - }).length.should.equal(0); - done(); - }); - }); - it('#checkActivity for active addreses', function(done) { - var w = new Insight(); - w.getTransactions = function(addresses, cb) { - cb([{ - vin: [{ - addr: '2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM' - }], - vout: [] - }, { - vin: [{ - addr: '2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM' - }], - vout: [] - }, { - vin: [{ - addr: '2N9D5bcCQ2bPWUDByQ6Qb5bMgMtgsk1rw3x' - }], - vout: [] - }, { - vin: [], - vout: [{ - scriptPubKey: { - addresses: ['2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY'] - } - }] - }]); - }; - - w.checkActivity(addresses, function(err, actives) { - actives.length.should.equal(addresses.length); - actives.filter(function(i) { - return i - }).length.should.equal(3); - done(); - }); - }); - - - it('should handle getTransaction null response', function(done) { - var w = new Insight(); - w._request = sinon.stub().yields(); - w.getTransactions(['asdasd'], function(ret) { - ret.length.should.equal(0); - done(); - }); - }); - - - - it('should handle getTransaction empty response', function(done) { - var w = new Insight(); - w._request = sinon.stub().yields([]); - w.getTransactions(['asdasd'], function(ret) { - ret.length.should.equal(0); - done(); - }); - }); - - describe("#checkSentTx", function() { - it('should return true if Tx is found', function(done) { - var w = new Insight(); - w._request = sinon.stub().yields(null, { - txid: "414142", + sinon.stub(blockchain, "request", function(url, cb) { + url.should.be.equal('/api/tx/' + txid); + var res = {statusCode: 200}; + var body = null; + setTimeout(function() { + cb(null, res, body); + }, 0); }); - var tx = function() {}; - tx.prototype.getHash = function(){return new Buffer('BAA')}; - w.checkSentTx(new tx(), function(err, ret) { - should.not.exist(err); - ret.should.equal('414142'); + + blockchain.getTransaction(txid, function(err, t) { + chai.expect(t).to.be.undefined; + chai.expect(err).not.be.null; done(); }); }); - it('should return false if Tx is not found', function(done) { - var w = new Insight(); - w._request = sinon.stub().yields(null, { - txid: "414142", + + it('should handle an empty response', function(done) { + var blockchain = new Insight(FAKE_OPTS); + var txid = '123321'; + + sinon.stub(blockchain, "request", function(url, cb) { + url.should.be.equal('/api/tx/' + txid); + var res = {statusCode: 200}; + var body = null; + setTimeout(function() { + cb(null, res, body); + }, 0); }); - var tx = function() {}; - tx.prototype.getHash = function(){return new Buffer('ABC')}; - w.checkSentTx(new tx(), function(err, ret) { - should.not.exist(err); - ret.should.equal(false); + + blockchain.getTransaction(txid, function(err, t) { + chai.expect(t).to.be.undefined; + chai.expect(err).not.be.null; + done(); + }); + }); + }); + + it('should get a set of transaction by addresses', function(done) { + var blockchain = new Insight(FAKE_OPTS); + + sinon.stub(blockchain, "request", function(url, cb) { + var res = {statusCode: 200}; + + if (url == '/api/addr/2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM') { + return setTimeout(function() { + var body = JSON.stringify({transactions: [1, 2]}); + cb(null, res, body); + }, 0); + } + + if (url == '/api/addr/2NE9hTCffeugo5gQtfB4owq98gyTeWC56yb') { + return setTimeout(function() { + var body = JSON.stringify({transactions: [3]}); + cb(null, res, body); + }, 0); + } + + setTimeout(function() { + var body = JSON.stringify({txid: '123123'}); + cb(null, res, body); + }, 0); + }); + + var addresses = ['2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM', '2NE9hTCffeugo5gQtfB4owq98gyTeWC56yb']; + blockchain.getTransactions(addresses, function(err, txs) { + chai.expect(err).to.be.null; + txs.length.should.be.equal(3); + done(); + }); + }); + + it('should get a list of unspent output', function(done) { + var blockchain = new Insight(FAKE_OPTS); + + sinon.stub(blockchain, "requestPost", function(url, data, cb) { + url.should.be.equal('/api/addrs/utxo'); + data.addrs.should.be.equal('2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM,2NE9hTCffeugo5gQtfB4owq98gyTeWC56yb,2N9D5bcCQ2bPWUDByQ6Qb5bMgMtgsk1rw3x'); + setTimeout(function() { + var res = {statusCode: 200}; + var body = JSON.stringify(UNSPENT); + cb(null, res, body); + }, 0); + }); + + blockchain.getUnspent(ADDRESSES.slice(0, 3), function(err, unspent) { + chai.expect(err).to.be.null; + unspent.length.should.be.equal(2); + unspent[0].address.should.be.equal('2NE9hTCffeugo5gQtfB4owq98gyTeWC56yb'); + unspent[1].address.should.be.equal('2N9D5bcCQ2bPWUDByQ6Qb5bMgMtgsk1rw3x'); + done(); + }); + }); + + describe('getActivity', function() { + it('should get activity for an innactive address', function(done) { + var blockchain = new Insight(FAKE_OPTS); + + sinon.stub(blockchain, "getTransactions", function(addresses, cb) { + cb(null, []); + }); + + blockchain.getActivity(ADDRESSES, function(err, actives) { + chai.expect(err).to.be.null; + actives.length.should.equal(ADDRESSES.length); + actives.filter(function(i) { + return i + }).length.should.equal(0); + done(); + }); + }); + + it('should get activity for active addresses', function(done) { + var blockchain = new Insight(FAKE_OPTS); + + sinon.stub(blockchain, "getTransactions", function(addresses, cb) { + cb(null, [{ + vin: [{ + addr: '2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM' + }], + vout: [] + }, { + vin: [{ + addr: '2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM' + }], + vout: [] + }, { + vin: [{ + addr: '2N9D5bcCQ2bPWUDByQ6Qb5bMgMtgsk1rw3x' + }], + vout: [] + }, { + vin: [], + vout: [{ + scriptPubKey: { + addresses: ['2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY'] + } + }] + }]); + }); + + blockchain.getActivity(ADDRESSES, function(err, actives) { + chai.expect(err).to.be.null; + actives.length.should.equal(ADDRESSES.length); + actives.filter(function(i) { + return i + }).length.should.equal(3); + done(); + }); + }); + }); + + describe('Events', function() { + it('should emmit event on a new block', function(done) { + var blockchain = new Insight(FAKE_OPTS); + blockchain.on('connect', function() { + blockchain.socket.emit('block', '12312312'); + }); + + blockchain.on('block', function(blockid) { + blockid.should.be.equal('12312312'); + done(); + }); + }); + + it('should emmit event on a transaction for subscried addresses', function(done) { + var blockchain = new Insight(FAKE_OPTS); + blockchain.subscribe('2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY'); + blockchain.on('connect', function() { + blockchain.socket.emit('2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY', '1123'); + }); + + blockchain.on('tx', function(ev) { + ev.address.should.be.equal('2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY'); + ev.txid.should.be.equal('1123'); + done(); + }); + }); + + it('should\'t emmit event on a transaction for non subscribed addresses', function(done) { + var blockchain = new Insight(FAKE_OPTS); + blockchain.on('connect', function() { + blockchain.socket.emit('2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY', '1123'); + setTimeout(function() { done(); }, 20); + }); + + blockchain.on('tx', function(ev) { + throw Error('should not call this event!'); + }); + }); + + it('should emmit event on connection', function(done) { + var blockchain = new Insight(FAKE_OPTS); + blockchain.on('connect', function() { + done(); + }); + }); + + it('should emmit event on disconnection', function(done) { + var blockchain = new Insight(FAKE_OPTS); + blockchain.on('connect', function() { + blockchain.socket.emit('connect_error'); + }); + blockchain.on('disconnect', function() { done(); }); }); diff --git a/test/unit/services/servicesSpec.js b/test/unit/services/servicesSpec.js index aa7f7e2c8..b7aa7041b 100644 --- a/test/unit/services/servicesSpec.js +++ b/test/unit/services/servicesSpec.js @@ -18,56 +18,6 @@ describe('Check config', function() { }); }); - -describe("Unit: Socket Service", function() { - beforeEach(angular.mock.module('copayApp.services')); - - it('should contain a Socket service', inject(function(Socket) { - expect(Socket).not.to.equal(null); - })); - - - it('Socket should support #on', inject(function(Socket) { - expect(Socket.on).to.be.a('function'); - })); - - - it('Socket should support #sysOn', inject(function(Socket) { - expect(Socket.sysOn).to.be.a('function'); - })); - - - it('Socket should add handlers with #on', inject(function(Socket) { - Socket.on('a', function() {}); - Socket.on('b', function() {}); - Socket.sysOn('c', function() {}); - var ret = Socket.getListeners(); - expect(ret.a).to.be.equal(1); - expect(ret.b).to.be.equal(1); - expect(Object.keys(ret)).to.have.length(2); - })); - - it('Socket should support block event', inject(function(Socket) { - expect(Socket.isListeningBlocks()).to.be.false; - Socket.on('block', function() {}); - expect(Socket.isListeningBlocks()).to.be.true; - Socket.removeAllListeners(); - expect(Socket.isListeningBlocks()).to.be.false; - })); - - it('Socket should support #removeAllListeners', inject(function(Socket) { - Socket.on('a', function() {}); - Socket.on('b', function() {}); - Socket.sysOn('c', function() {}); - var ret = Socket.getListeners(); - expect(Object.keys(ret)).to.have.length(2); - Socket.removeAllListeners(); - ret = Socket.getListeners(); - expect(Object.keys(ret)).to.have.length(0); - })); -}); - - describe("Unit: Walletfactory Service", function() { beforeEach(angular.mock.module('copayApp.services')); it('should contain a walletFactory service', inject(function(walletFactory) { From 16091bd33077bb6b151fa779b6e29a0410d8e479 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Thu, 28 Aug 2014 15:18:05 -0300 Subject: [PATCH 2/6] Refactor controllers --- js/controllers/addresses.js | 3 +- js/controllers/sidebar.js | 3 -- js/models/blockchain/Insight.js | 13 +++++--- js/models/core/Wallet.js | 2 -- js/services/controllerUtils.js | 52 +++++++++++++----------------- test/mocks/FakeBlockchain.js | 4 +++ test/test.Wallet.js | 2 +- test/test.blockchain.Insight.js | 8 +++++ test/unit/services/servicesSpec.js | 10 +++--- 9 files changed, 50 insertions(+), 47 deletions(-) diff --git a/js/controllers/addresses.js b/js/controllers/addresses.js index cef87776f..d53ff8ae1 100644 --- a/js/controllers/addresses.js +++ b/js/controllers/addresses.js @@ -9,8 +9,7 @@ angular.module('copayApp.controllers').controller('AddressesController', $scope.loading = true; w.generateAddress(null, function() { $timeout(function() { - controllerUtils.setSocketHandlers(); - controllerUtils.updateAddressList(); + controllerUtils.updateGlobalAddresses(); $scope.loading = false; }, 1); }); diff --git a/js/controllers/sidebar.js b/js/controllers/sidebar.js index ee356bb94..feebe40da 100644 --- a/js/controllers/sidebar.js +++ b/js/controllers/sidebar.js @@ -57,9 +57,6 @@ angular.module('copayApp.controllers').controller('SidebarController', function( return new Array(num); } - // Init socket handlers (with no wallet yet) - controllerUtils.setSocketHandlers(); - if ($rootScope.wallet) { $scope.$on('$idleWarn', function(a,countdown) { if (!(countdown%5)) diff --git a/js/models/blockchain/Insight.js b/js/models/blockchain/Insight.js index 9c7fce321..16242ddbf 100644 --- a/js/models/blockchain/Insight.js +++ b/js/models/blockchain/Insight.js @@ -50,7 +50,7 @@ var Insight = function (opts) { var self = this; this.socket.on('connect', function() { self.status = self.STATUS.CONNECTED; - self.suscribeToBlocks(); + self.subscribeToBlocks(); self.emit('connect', 0); }); @@ -82,7 +82,7 @@ Insight.prototype.STATUS = { } /** @private */ -Insight.prototype.suscribeToBlocks = function() { +Insight.prototype.subscribeToBlocks = function() { if (this.listeningBlocks || !this.socket.connected) return; var self = this; @@ -132,9 +132,12 @@ Insight.prototype.subscribe = function(addresses) { addresses.forEach(function(address) { preconditions.checkArgument(new bitcore.Address(address).isValid()); - self.subscribed.push(address); - self.socket.emit('subscribe', address); - self.socket.on(address, handlerFor(self, address)); + // skip already subscibed + if (self.subscribed.indexOf(address) == -1) { + self.subscribed.push(address); + self.socket.emit('subscribe', address); + self.socket.on(address, handlerFor(self, address)); + } }); }; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 1c3de8943..43fd21509 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -805,8 +805,6 @@ Wallet.prototype.sendTx = function(ntxid, cb) { var self = this; this.blockchain.broadcast(txHex, function(err, txid) { - if(err) throw err; - self.log('BITCOIND txid:', txid); if (txid) { self.txProposals.get(ntxid).setSent(txid); diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 6b3b32ca7..f7054383d 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -99,7 +99,7 @@ angular.module('copayApp.services') root.startNetwork = function(w, $scope) { root.setupRootVariables(); root.installStartupHandlers(w, $scope); - root.setSocketHandlers(); + root.updateGlobalAddresses(); var handlePeerVideo = function(err, peerID, url) { if (err) { @@ -120,7 +120,7 @@ angular.module('copayApp.services') }); w.on('ready', function(myPeerID) { $rootScope.wallet = w; - + root.setConnectionListeners(); if ($rootScope.pendingPayment) { $location.path('send'); @@ -132,7 +132,7 @@ angular.module('copayApp.services') }); w.on('publicKeyRingUpdated', function(dontDigest) { - root.setSocketHandlers(); + root.updateGlobalAddresses(); if (!dontDigest) { $rootScope.$digest(); } @@ -304,44 +304,38 @@ angular.module('copayApp.services') wallet.blockchain.on('disconnect', function() { notification.error('Networking problem', 'Connection to Insight lost, trying to reconnect...'); }); + + wallet.blockchain.on('tx', function(tx) { + notification.funds('Funds received!', tx.address); + root.updateBalance(function() { + $rootScope.$digest(); + }); + }); + + if (!$rootScope.wallet.spendUnconfirmed) { + wallet.blockchain.on('block', function(block) { + root.updateBalance(function() { + $rootScope.$digest(); + }); + }); + } } - root.setSocketHandlers = function() { - root.updateAddressList(); - + root.updateGlobalAddresses = function() { if (!$rootScope.wallet) return; - var currentAddrs = Socket.getListeners(); + root.updateAddressList(); + var currentAddrs = $rootScope.wallet.blockchain.getListeners(); var allAddrs = $rootScope.addrInfos; var newAddrs = []; for (var i in allAddrs) { var a = allAddrs[i]; - if (!currentAddrs[a.addressStr]) + if (!currentAddrs[a.addressStr] && !a.isChange) newAddrs.push(a); } for (var i = 0; i < newAddrs.length; i++) { - Socket.emit('subscribe', newAddrs[i].addressStr); - } - newAddrs.forEach(function(a) { - Socket.on(a.addressStr, function(txid) { - - if (!a.isChange) - notification.funds('Funds received!', a.addressStr); - - root.updateBalance(function() { - $rootScope.$digest(); - }); - }); - }); - - if (!$rootScope.wallet.spendUnconfirmed && !Socket.isListeningBlocks()) { - Socket.emit('subscribe', 'inv'); - Socket.on('block', function(block) { - root.updateBalance(function() { - $rootScope.$digest(); - }); - }); + $rootScope.wallet.blockchain.subscribe(newAddrs[i].addressStr); } }; return root; diff --git a/test/mocks/FakeBlockchain.js b/test/mocks/FakeBlockchain.js index 1db31f97b..c6dd8ca4f 100644 --- a/test/mocks/FakeBlockchain.js +++ b/test/mocks/FakeBlockchain.js @@ -6,6 +6,10 @@ function FakeBlockchain(opts) { opts = opts || {}; } +FakeBlockchain.prototype.getTransaction = function(txid, cb) { + return cb(null, {txid: txid}); +}; + FakeBlockchain.prototype.getTransactions = function(addresses, cb) { return cb(null, []); }; diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 2386d3002..09dc4afe5 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -752,7 +752,7 @@ describe('Wallet model', function() { var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); w.createTx(toAddress, amountSatStr, null, function(ntxid) { - sinon.stub(w.blockchain, 'sendRawTransaction').yields(undefined); + sinon.stub(w.blockchain, 'broadcast').yields({statusCode: 303}); var spyCheckSentTx = sinon.spy(w, '_checkSentTx'); w.sendTx(ntxid, function () {}); chai.expect(spyCheckSentTx.calledOnce).to.be.true; diff --git a/test/test.blockchain.Insight.js b/test/test.blockchain.Insight.js index eacc99808..a84b138bd 100644 --- a/test/test.blockchain.Insight.js +++ b/test/test.blockchain.Insight.js @@ -98,6 +98,14 @@ describe('Insight model', function() { emitSpy.calledWith('subscribe', 'mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); }); + it('should subscribe to an address once', function() { + var blockchain = new Insight(FAKE_OPTS); + + blockchain.subscribe('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribe('mg7UbtKgMvWAixTNMbC8soyUnwFk1qxEuM'); + blockchain.subscribed.length.should.equal(1); + }); + it('should subscribe to a list of addresses', function() { var blockchain = new Insight(FAKE_OPTS); var emitSpy = sinon.spy(blockchain.socket, 'emit'); diff --git a/test/unit/services/servicesSpec.js b/test/unit/services/servicesSpec.js index b7aa7041b..284719d3d 100644 --- a/test/unit/services/servicesSpec.js +++ b/test/unit/services/servicesSpec.js @@ -62,26 +62,26 @@ describe("Unit: controllerUtils", function() { expect($rootScope.unitName).to.be.equal('bits'); }); })); - describe("Unit: controllerUtils #setSocketHandlers", function() { + describe("Unit: controllerUtils #updateGlobalAddresses", function() { it(' should call updateAddressList ', inject(function(controllerUtils, $rootScope) { var spy = sinon.spy(controllerUtils, 'updateAddressList'); - controllerUtils.setSocketHandlers(); + controllerUtils.updateGlobalAddresses(); sinon.assert.callCount(spy, 1); })); it('should update addresses', inject(function(controllerUtils, $rootScope) { $rootScope.wallet = new FakeWallet(); var Waddr = Object.keys($rootScope.wallet.balanceByAddr)[0]; - controllerUtils.setSocketHandlers(); + controllerUtils.updateGlobalAddresses(); expect($rootScope.addrInfos[0].address).to.be.equal(Waddr);; })); it('should set System Event Handlers', inject(function(controllerUtils, $rootScope, Socket) { var spy = sinon.spy(Socket, 'sysOn'); $rootScope.wallet = new FakeWallet(); - controllerUtils.setSocketHandlers(); + controllerUtils.updateGlobalAddresses(); sinon.assert.callCount(spy, 5); ['error', 'reconnect_error', 'reconnect_failed', 'connect', 'reconnect'].forEach(function(e) { sinon.assert.calledWith(spy, e); @@ -92,7 +92,7 @@ describe("Unit: controllerUtils", function() { var spy = sinon.spy(Socket, 'on'); $rootScope.wallet = new FakeWallet(); var Waddr = Object.keys($rootScope.wallet.balanceByAddr)[0]; - controllerUtils.setSocketHandlers(); + controllerUtils.updateGlobalAddresses(); sinon.assert.calledWith(spy, Waddr); })); }); From 862dfa72c9fd83e8244930cc15628441b0ed3a4f Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Fri, 29 Aug 2014 10:50:52 -0300 Subject: [PATCH 3/6] Integrate the new model --- js/models/blockchain/Insight.js | 10 +++++----- js/services/controllerUtils.js | 12 +++--------- karma.conf.js | 1 + package.json | 3 ++- util/build.js | 6 ++++++ 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/js/models/blockchain/Insight.js b/js/models/blockchain/Insight.js index 16242ddbf..5776efcd1 100644 --- a/js/models/blockchain/Insight.js +++ b/js/models/blockchain/Insight.js @@ -100,14 +100,14 @@ Insight.prototype.getSocket = function(url, opts) { /** @private */ Insight.prototype.request = function(path, cb) { - preconditions.checkArgument(url).shouldBeFunction(cb); + preconditions.checkArgument(path).shouldBeFunction(cb); request(this.url + path, cb); } /** @private */ Insight.prototype.requestPost = function(path, data, cb) { - preconditions.checkArgument(url).checkArgument(data).shouldBeFunction(cb); - request.post(this.url, cb).form(data); + preconditions.checkArgument(path).checkArgument(data).shouldBeFunction(cb); + request({method: "POST", url: this.url + path, json: data}, cb); } Insight.prototype.destroy = function() { @@ -165,7 +165,7 @@ Insight.prototype.broadcast = function(rawtx, cb) { this.requestPost('/api/tx/send', {rawtx: rawtx}, function(err, res, body) { if (err || res.statusCode != 200) cb(err || res); - cb(null, JSON.parse(body).txid); + cb(null, body.txid); }); }; @@ -219,7 +219,7 @@ Insight.prototype.getUnspent = function(addresses, cb) { this.requestPost('/api/addrs/utxo', {addrs: addresses.join(',')}, function(err, res, body) { if (err || res.statusCode != 200) return cb(err || res); - cb(null, JSON.parse(body)); + cb(null, body); }); }; diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index f7054383d..8404d154d 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -120,7 +120,7 @@ angular.module('copayApp.services') }); w.on('ready', function(myPeerID) { $rootScope.wallet = w; - root.setConnectionListeners(); + root.setConnectionListeners($rootScope.wallet); if ($rootScope.pendingPayment) { $location.path('send'); @@ -196,13 +196,7 @@ angular.module('copayApp.services') $rootScope.updatingBalance = true; w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat) { - if (err) { - console.error('Error: ' + err.message); //TODO - root._setCommError(); - return null; - } else { - root._clearCommError(); - } + if (err) throw err; var satToUnit = 1 / config.unitToSatoshi; var COIN = bitcore.util.COIN; @@ -325,7 +319,7 @@ angular.module('copayApp.services') if (!$rootScope.wallet) return; root.updateAddressList(); - var currentAddrs = $rootScope.wallet.blockchain.getListeners(); + var currentAddrs = $rootScope.wallet.blockchain.subscribed; var allAddrs = $rootScope.addrInfos; var newAddrs = []; diff --git a/karma.conf.js b/karma.conf.js index 79314df62..69fb2faf4 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -54,6 +54,7 @@ module.exports = function(config) { 'test/lib/chai-should.js', 'test/lib/chai-expect.js', 'test/mocks/FakeWallet.js', + 'test/mocks/FakeBlockchainSocket.js', 'test/mocks/FakePayProServer.js', 'test/mocha.conf.js', diff --git a/package.json b/package.json index c67c404a8..383f137d1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "version": "0.4.7", "dependencies": { + "browser-request": "^0.3.2", "mocha": "^1.18.2", "mocha-lcov-reporter": "0.0.1", "optimist": "^0.6.1", @@ -71,7 +72,7 @@ "travis-cov": "0.2.5", "uglifyify": "1.2.3", "crypto-js": "3.1.2", - "shelljs":"0.3.0" + "shelljs": "0.3.0" }, "main": "app.js", "homepage": "https://github.com/bitpay/copay", diff --git a/util/build.js b/util/build.js index 2ea62d84c..bcd6f5ec1 100644 --- a/util/build.js +++ b/util/build.js @@ -40,6 +40,9 @@ var createBundle = function(opts) { b.require('bitcore/node_modules/browserify-buffertools/buffertools.js', { expose: 'buffertools' }); + b.require('browser-request', { + expose: 'request' + }); b.require('./copay', { expose: 'copay' @@ -88,6 +91,9 @@ var createBundle = function(opts) { b.require('./test/mocks/FakeBlockchain', { expose: './mocks/FakeBlockchain' }); + b.require('./test/mocks/FakeBlockchainSocket', { + expose: './mocks/FakeBlockchainSocket' + }); b.require('./test/mocks/FakeNetwork', { expose: './mocks/FakeNetwork' }); From 2989fe1da46cf12e15f909840645dc092e402922 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Fri, 29 Aug 2014 11:22:22 -0300 Subject: [PATCH 4/6] Remove handlers before disconnecting the socket --- js/models/network/Async.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/models/network/Async.js b/js/models/network/Async.js index 675c39966..172133fae 100644 --- a/js/models/network/Async.js +++ b/js/models/network/Async.js @@ -35,6 +35,7 @@ Network.prototype.cleanUp = function() { this.criticalErr = ''; this.removeAllListeners(); if (this.socket) { + this.socket.removeAllListeners(); this.socket.disconnect(); this.socket = null; } From 0623c8772775b59db7c6c9906dc1fd70ee776439 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Fri, 29 Aug 2014 11:36:23 -0300 Subject: [PATCH 5/6] Update tests --- test/mocks/FakeBlockchainSocket.js | 15 +++++++++++++-- test/mocks/FakeWallet.js | 1 + test/test.blockchain.Insight.js | 10 +++------- test/test.network.Async.js | 1 + test/unit/services/servicesSpec.js | 19 +------------------ 5 files changed, 19 insertions(+), 27 deletions(-) diff --git a/test/mocks/FakeBlockchainSocket.js b/test/mocks/FakeBlockchainSocket.js index b85d3c31e..85c6c98e4 100644 --- a/test/mocks/FakeBlockchainSocket.js +++ b/test/mocks/FakeBlockchainSocket.js @@ -1,6 +1,5 @@ 'use strict'; -var util = require('util'); var EventEmitter = require('events').EventEmitter; var FakeSocket = function (url, opts) { @@ -13,7 +12,19 @@ var FakeSocket = function (url, opts) { }, 0); } -util.inherits(FakeSocket, EventEmitter); +var inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +inherits(FakeSocket, EventEmitter); FakeSocket.prototype.removeEventListener = function() { return; diff --git a/test/mocks/FakeWallet.js b/test/mocks/FakeWallet.js index a9a90d073..331b395d7 100644 --- a/test/mocks/FakeWallet.js +++ b/test/mocks/FakeWallet.js @@ -36,6 +36,7 @@ var FakeWallet = function() { return true; } }; + this.blockchain = {subscribed: [], subscribe: function(){}}; this.privateKey = new FakePrivateKey(); }; diff --git a/test/test.blockchain.Insight.js b/test/test.blockchain.Insight.js index a84b138bd..631abba79 100644 --- a/test/test.blockchain.Insight.js +++ b/test/test.blockchain.Insight.js @@ -6,11 +6,7 @@ var sinon = require('sinon'); var bitcore = bitcore || require('bitcore'); var FakeSocket = require('./mocks/FakeBlockchainSocket'); -try { - var copay = require('./copay'); //browser -} catch (e) { - var copay = require('../copay'); //node -} +var copay = copay || require('../copay'); var Buffer = bitcore.Buffer; var Insight = copay.Insight; @@ -144,7 +140,7 @@ describe('Insight model', function() { sinon.stub(blockchain, "requestPost", function(url, data, cb) { url.should.be.equal('/api/tx/send'); var res = {statusCode: 200}; - var body = JSON.stringify({txid: 1234}); + var body = {txid: 1234}; setTimeout(function() { cb(null, res, body); }, 0); @@ -283,7 +279,7 @@ describe('Insight model', function() { data.addrs.should.be.equal('2NATQJnaQe2CUKLyhL1zdNkttJM1dUH9HaM,2NE9hTCffeugo5gQtfB4owq98gyTeWC56yb,2N9D5bcCQ2bPWUDByQ6Qb5bMgMtgsk1rw3x'); setTimeout(function() { var res = {statusCode: 200}; - var body = JSON.stringify(UNSPENT); + var body = UNSPENT; cb(null, res, body); }, 0); }); diff --git a/test/test.network.Async.js b/test/test.network.Async.js index 4d46dabb3..c4273af00 100644 --- a/test/test.network.Async.js +++ b/test/test.network.Async.js @@ -18,6 +18,7 @@ describe('Network / Async', function() { fakeSocket.emit = function() {}; fakeSocket.on = function() {}; fakeSocket.disconnect = function() {}; + fakeSocket.removeAllListeners = function() {}; n.createSocket = function() { return fakeSocket; }; diff --git a/test/unit/services/servicesSpec.js b/test/unit/services/servicesSpec.js index 284719d3d..df7eed5bf 100644 --- a/test/unit/services/servicesSpec.js +++ b/test/unit/services/servicesSpec.js @@ -66,6 +66,7 @@ describe("Unit: controllerUtils", function() { it(' should call updateAddressList ', inject(function(controllerUtils, $rootScope) { + $rootScope.wallet = new FakeWallet(); var spy = sinon.spy(controllerUtils, 'updateAddressList'); controllerUtils.updateGlobalAddresses(); sinon.assert.callCount(spy, 1); @@ -77,24 +78,6 @@ describe("Unit: controllerUtils", function() { controllerUtils.updateGlobalAddresses(); expect($rootScope.addrInfos[0].address).to.be.equal(Waddr);; })); - - it('should set System Event Handlers', inject(function(controllerUtils, $rootScope, Socket) { - var spy = sinon.spy(Socket, 'sysOn'); - $rootScope.wallet = new FakeWallet(); - controllerUtils.updateGlobalAddresses(); - sinon.assert.callCount(spy, 5); - ['error', 'reconnect_error', 'reconnect_failed', 'connect', 'reconnect'].forEach(function(e) { - sinon.assert.calledWith(spy, e); - }); - })); - - it('should set Address Event Handlers', inject(function(controllerUtils, $rootScope, Socket) { - var spy = sinon.spy(Socket, 'on'); - $rootScope.wallet = new FakeWallet(); - var Waddr = Object.keys($rootScope.wallet.balanceByAddr)[0]; - controllerUtils.updateGlobalAddresses(); - sinon.assert.calledWith(spy, Waddr); - })); }); From b97a332ae9b03c4939d8ed7a994795c106b07c4b Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Fri, 29 Aug 2014 15:01:07 -0300 Subject: [PATCH 6/6] Add reconnecting notification, change subscribed list to dict, update balance after reconnect --- css/src/main.css | 13 +++++++++++++ index.html | 2 +- js/models/blockchain/Insight.js | 21 +++++++++++---------- js/services/controllerUtils.js | 16 +++++++++++----- test/mocks/FakeWallet.js | 5 ++++- test/test.blockchain.Insight.js | 18 +++++++++--------- 6 files changed, 49 insertions(+), 26 deletions(-) diff --git a/css/src/main.css b/css/src/main.css index f7e34e300..bbda4a577 100644 --- a/css/src/main.css +++ b/css/src/main.css @@ -128,6 +128,19 @@ input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill, inpu -webkit-box-shadow: 0 0 0px 1000px white inset; } +.status { + border: 1px solid #f0c36d; + background-color: #f9edbe; + position: absolute; + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + top: 10px; + width: 215px; + padding: 7px; +} + .join label, .open label, .setup label { diff --git a/index.html b/index.html index f3a30b346..d4f2613f2 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@
- + Attempting to reconnect...