diff --git a/bitcorenode/index.js b/bitcorenode/index.js new file mode 100644 index 0000000..ee7c85d --- /dev/null +++ b/bitcorenode/index.js @@ -0,0 +1,201 @@ +'use strict'; + +var util = require('util'); +var fs = require('fs'); +var io = require('socket.io'); +var https = require('https'); +var http = require('http'); +var async = require('async'); +var path = require('path'); +var bitcore = require('bitcore'); +var Networks = bitcore.Networks; +var Locker = require('locker-server'); +var BlockchainMonitor = require('../lib/blockchainmonitor'); +var EmailService = require('../lib/emailservice'); +var ExpressApp = require('../lib/expressapp'); +var WsApp = require('../lib/wsapp'); +var child_process = require('child_process'); +var spawn = child_process.spawn; +var EventEmitter = require('events').EventEmitter; +var baseConfig = require('../config'); + +/** + * A Bitcore Node Service module + * @param {Object} options + * @param {Node} options.node - A reference to the Bitcore Node instance +-* @param {Boolean} options.https - Enable https for this module, defaults to node settings. + * @param {Number} options.bwsPort - Port for Bitcore Wallet Service API + * @param {Number} options.messageBrokerPort - Port for BWS message broker + * @param {Number} options.lockerPort - Port for BWS locker port + */ +var Service = function(options) { + EventEmitter.call(this); + + this.node = options.node; + this.https = options.https || this.node.https; + this.httpsOptions = options.httpsOptions || this.node.httpsOptions; + this.bwsPort = options.bwsPort || baseConfig.port; + this.messageBrokerPort = options.messageBrokerPort || 3380; + if (baseConfig.lockOpts) { + this.lockerPort = baseConfig.lockOpts.lockerServer.port; + } + this.lockerPort = options.lockerPort || this.lockerPort; +}; + +util.inherits(Service, EventEmitter); + +Service.dependencies = ['insight-api']; + +/** + * This method will read `key` and `cert` files from disk based on `httpsOptions` and + * return `serverOpts` with the read files. + * @returns {Object} + */ +Service.prototype._readHttpsOptions = function() { + if(!this.httpsOptions || !this.httpsOptions.key || !this.httpsOptions.cert) { + throw new Error('Missing https options'); + } + + var serverOpts = {}; + serverOpts.key = fs.readFileSync(this.httpsOptions.key); + serverOpts.cert = fs.readFileSync(this.httpsOptions.cert); + + // This sets the intermediate CA certs only if they have all been designated in the config.js + if (this.httpsOptions.CAinter1 && this.httpsOptions.CAinter2 && this.httpsOptions.CAroot) { + serverOpts.ca = [ + fs.readFileSync(this.httpsOptions.CAinter1), + fs.readFileSync(this.httpsOptions.CAinter2), + fs.readFileSync(this.httpsOptions.CAroot) + ]; + } + return serverOpts; +}; + +/** + * Will get the configuration with settings for the locally + * running Insight API. + * @returns {Object} + */ +Service.prototype._getConfiguration = function() { + var self = this; + + var providerOptions = { + provider: 'insight', + url: 'http://localhost:' + self.node.port, + apiPrefix: '/insight-api' + }; + + // A bitcore-node is either livenet or testnet, so we'll pass + // the configuration options to communicate via the local running + // instance of the insight-api service. + if (self.node.network === Networks.livenet) { + baseConfig.blockchainExplorerOpts = { + livenet: providerOptions + }; + } else if (self.node.network === Networks.testnet) { + baseConfig.blockchainExplorerOpts = { + testnet: providerOptions + }; + } else { + throw new Error('Unknown network'); + } + + return baseConfig; + +}; + +/** + * Will start the HTTP web server and socket.io for the wallet service. + */ +Service.prototype._startWalletService = function(config, next) { + var self = this; + var expressApp = new ExpressApp(); + var wsApp = new WsApp(); + + if (self.https) { + var serverOpts = self._readHttpsOptions(); + self.server = https.createServer(serverOpts, expressApp.app); + } else { + self.server = http.Server(expressApp.app); + } + + async.parallel([ + function(done) { + expressApp.start(config, done); + }, + function(done) { + wsApp.start(self.server, config, done); + }, + ], function(err) { + if (err) { + return next(err); + } + self.server.listen(self.bwsPort, next); + }); +}; + +/** + * Called by the node to start the service + */ +Service.prototype.start = function(done) { + + var self = this; + var config; + try { + config = self._getConfiguration(); + } catch(err) { + return done(err); + } + + // Locker Server + var locker = new Locker(); + locker.listen(self.lockerPort); + + // Message Broker + var messageServer = io(self.messageBrokerPort); + messageServer.on('connection', function(s) { + s.on('msg', function(d) { + messageServer.emit('msg', d); + }); + }); + + async.series([ + function(next) { + // Blockchain Monitor + var blockChainMonitor = new BlockchainMonitor(); + blockChainMonitor.start(config, next); + }, + function(next) { + // Email Service + if (config.emailOpts) { + var emailService = new EmailService(); + emailService.start(config, next); + } else { + setImmediate(next); + } + }, + function(next) { + self._startWalletService(config, next); + } + ], done); + +}; + +/** + * Called by node to stop the service + */ +Service.prototype.stop = function(done) { + setImmediate(function() { + done(); + }); +}; + +Service.prototype.getAPIMethods = function() { + return []; +}; + +Service.prototype.getPublishEvents = function() { + return []; +}; + +module.exports = Service; diff --git a/lib/blockchainexplorer.js b/lib/blockchainexplorer.js index 227dedb..a0f4072 100644 --- a/lib/blockchainexplorer.js +++ b/lib/blockchainexplorer.js @@ -29,7 +29,8 @@ function BlockChainExplorer(opts) { case 'insight': return new Insight({ network: network, - url: url + url: url, + apiPrefix: opts.apiPrefix }); default: throw new Error('Provider ' + provider + ' not supported.'); diff --git a/lib/blockchainexplorers/insight.js b/lib/blockchainexplorers/insight.js index 9711055..3961bc2 100644 --- a/lib/blockchainexplorers/insight.js +++ b/lib/blockchainexplorers/insight.js @@ -12,6 +12,7 @@ function Insight(opts) { $.checkArgument(_.contains(['livenet', 'testnet'], opts.network)); $.checkArgument(opts.url); + this.apiPrefix = opts.apiPrefix || '/api'; this.network = opts.network || 'livenet'; this.url = opts.url; }; @@ -34,7 +35,7 @@ Insight.prototype.getConnectionInfo = function() { * Retrieve a list of unspent outputs associated with an address or set of addresses */ Insight.prototype.getUnspentUtxos = function(addresses, cb) { - var url = this.url + '/api/addrs/utxo'; + var url = this.url + this.apiPrefix + '/addrs/utxo'; var args = { method: 'POST', url: url, @@ -53,7 +54,7 @@ Insight.prototype.getUnspentUtxos = function(addresses, cb) { * Broadcast a transaction to the bitcoin network */ Insight.prototype.broadcast = function(rawTx, cb) { - var url = this.url + '/api/tx/send'; + var url = this.url + this.apiPrefix + '/tx/send'; var args = { method: 'POST', url: url, @@ -69,7 +70,7 @@ Insight.prototype.broadcast = function(rawTx, cb) { }; Insight.prototype.getTransaction = function(txid, cb) { - var url = this.url + '/api/tx/' + txid; + var url = this.url + this.apiPrefix + '/tx/' + txid; var args = { method: 'GET', url: url, @@ -89,7 +90,7 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) { if (_.isNumber(from)) qs.push('from=' + from); if (_.isNumber(to)) qs.push('to=' + to); - var url = this.url + '/api/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : ''); + var url = this.url + this.apiPrefix + '/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : ''); var args = { method: 'POST', url: url, @@ -119,7 +120,7 @@ Insight.prototype.getAddressActivity = function(addresses, cb) { }; Insight.prototype.estimateFee = function(nbBlocks, cb) { - var url = this.url + '/api/utils/estimatefee'; + var url = this.url + this.apiPrefix + '/utils/estimatefee'; if (nbBlocks) { url += '?nbBlocks=' + [].concat(nbBlocks).join(','); } diff --git a/package.json b/package.json index e5e6a7a..e221c28 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "jsdoc": "^3.3.0-beta1", "memdown": "^1.0.0", "mocha": "^1.18.2", + "proxyquire": "^1.7.2", "sinon": "1.10.3", "supertest": "*", "tingodb": "^0.3.4" @@ -62,11 +63,19 @@ "test": "./node_modules/.bin/mocha", "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, - "contributors": [{ - "name": "Ivan Socolsky", - "email": "ivan@bitpay.com" - }, { - "name": "Matias Alejo Garcia", - "email": "ematiu@gmail.com" - }] + "bitcoreNode": "./bitcorenode", + "contributors": [ + { + "name": "Braydon Fuller", + "email": "braydon@bitpay.com" + }, + { + "name": "Ivan Socolsky", + "email": "ivan@bitpay.com" + }, + { + "name": "Matias Alejo Garcia", + "email": "ematiu@gmail.com" + } + ] } diff --git a/test/bitcorenode.js b/test/bitcorenode.js new file mode 100644 index 0000000..987809c --- /dev/null +++ b/test/bitcorenode.js @@ -0,0 +1,380 @@ +'use strict'; + +var should = require('chai').should(); +var proxyquire = require('proxyquire'); +var bitcore = require('bitcore'); +var sinon = require('sinon'); +var Service = require('../bitcorenode'); + +describe('Bitcore Node Service', function() { + describe('#constructor', function() { + it('https settings from node', function() { + var node = { + https: true, + httpsOptions: { + key: 'key', + cert: 'cert' + } + }; + var options = { + node: node + }; + var service = new Service(options); + service.node.should.equal(node); + service.https.should.equal(true); + service.httpsOptions.should.deep.equal({ + key: 'key', + cert: 'cert' + }); + service.bwsPort.should.equal(3232); + service.messageBrokerPort.should.equal(3380); + service.lockerPort.should.equal(3231); + }); + it('direct https options', function() { + var node = {}; + var options = { + node: node, + https: true, + httpsOptions: { + key: 'key', + cert: 'cert' + } + }; + var service = new Service(options); + service.https.should.equal(true); + service.httpsOptions.should.deep.equal({ + key: 'key', + cert: 'cert' + }); + service.bwsPort.should.equal(3232); + service.messageBrokerPort.should.equal(3380); + service.lockerPort.should.equal(3231); + }); + it('can set custom ports', function() { + var node = {}; + var options = { + node: node, + bwsPort: 1000, + messageBrokerPort: 1001, + lockerPort: 1002 + }; + var service = new Service(options); + service.bwsPort.should.equal(1000); + service.messageBrokerPort.should.equal(1001); + service.lockerPort.should.equal(1002); + }); + }); + describe('#readHttpsOptions', function() { + var TestService = proxyquire('../bitcorenode', { + fs: { + readFileSync: function(arg) { + return arg; + } + } + }); + it('will create server options from httpsOptions', function() { + var options = { + node: { + https: true, + httpsOptions: { + key: 'key', + cert: 'cert', + CAinter1: 'CAinter1', + CAinter2: 'CAinter2', + CAroot: 'CAroot' + } + } + }; + var service = new TestService(options); + var serverOptions = service._readHttpsOptions(); + serverOptions.key.should.equal('key'); + serverOptions.cert.should.equal('cert'); + serverOptions.ca[0].should.equal('CAinter1'); + serverOptions.ca[1].should.equal('CAinter2'); + serverOptions.ca[2].should.equal('CAroot'); + }); + }); + describe('#_getConfiguration', function() { + it('will throw with an unknown network', function() { + var options = { + node: { + network: 'unknown' + } + }; + var service = new Service(options); + (function() { + service._getConfiguration(); + }).should.throw('Unknown network'); + }); + it('livenet local insight', function() { + var options = { + node: { + network: bitcore.Networks.livenet, + port: 3001 + } + }; + var service = new Service(options); + var config = service._getConfiguration(); + config.blockchainExplorerOpts.livenet.should.deep.equal({ + 'apiPrefix': '/insight-api', + 'provider': 'insight', + 'url': 'http://localhost:3001' + }); + }); + it('testnet local insight', function() { + var options = { + node: { + network: bitcore.Networks.testnet, + port: 3001 + } + }; + var service = new Service(options); + var config = service._getConfiguration(); + config.blockchainExplorerOpts.testnet.should.deep.equal({ + 'apiPrefix': '/insight-api', + 'provider': 'insight', + 'url': 'http://localhost:3001' + }); + }); + }); + describe('#_startWalletService', function() { + it('will start express and web socket servers', function(done) { + function TestExpressApp() {} + TestExpressApp.prototype.start = sinon.stub().callsArg(1); + function TestWSApp() {} + TestWSApp.prototype.start = sinon.stub().callsArg(2); + var listen = sinon.stub().callsArg(1); + var TestService = proxyquire('../bitcorenode', { + '../lib/expressapp': TestExpressApp, + '../lib/wsapp': TestWSApp, + 'http': { + Server: sinon.stub().returns({ + listen: listen + }) + } + }); + var options = { + node: { + bwsPort: 3232 + } + }; + var service = new TestService(options); + var config = {}; + service._startWalletService(config, function(err) { + if (err) { + throw err; + } + TestExpressApp.prototype.start.callCount.should.equal(1); + TestExpressApp.prototype.start.args[0][0].should.equal(config); + TestExpressApp.prototype.start.args[0][1].should.be.a('function'); + TestWSApp.prototype.start.callCount.should.equal(1); + TestWSApp.prototype.start.args[0][0].should.equal(service.server); + TestWSApp.prototype.start.args[0][1].should.equal(config); + TestWSApp.prototype.start.args[0][2].should.be.a('function'); + listen.callCount.should.equal(1); + listen.args[0][0].should.equal(3232); + listen.args[0][1].should.be.a('function'); + done(); + }); + }); + it('error from express', function(done) { + function TestExpressApp() {} + TestExpressApp.prototype.start = sinon.stub().callsArgWith(1, new Error('test')); + function TestWSApp() {} + TestWSApp.prototype.start = sinon.stub().callsArg(2); + var listen = sinon.stub().callsArg(1); + var TestService = proxyquire('../bitcorenode', { + '../lib/expressapp': TestExpressApp, + '../lib/wsapp': TestWSApp, + 'http': { + Server: sinon.stub().returns({ + listen: listen + }) + } + }); + var options = { + node: { + bwsPort: 3232 + } + }; + var service = new TestService(options); + var config = {}; + service._startWalletService(config, function(err) { + err.message.should.equal('test'); + done(); + }); + }); + it('error from web socket', function(done) { + function TestExpressApp() {} + TestExpressApp.prototype.start = sinon.stub().callsArg(1); + function TestWSApp() {} + TestWSApp.prototype.start = sinon.stub().callsArgWith(2, new Error('test')); + var listen = sinon.stub().callsArg(1); + var TestService = proxyquire('../bitcorenode', { + '../lib/expressapp': TestExpressApp, + '../lib/wsapp': TestWSApp, + 'http': { + Server: sinon.stub().returns({ + listen: listen + }) + } + }); + var options = { + node: { + bwsPort: 3232 + } + }; + var service = new TestService(options); + var config = {}; + service._startWalletService(config, function(err) { + err.message.should.equal('test'); + done(); + }); + }); + it('error from server.listen', function(done) { + var app = {}; + function TestExpressApp() { + this.app = app; + } + TestExpressApp.prototype.start = sinon.stub().callsArg(1); + function TestWSApp() {} + TestWSApp.prototype.start = sinon.stub().callsArg(2); + var listen = sinon.stub().callsArgWith(1, new Error('test')); + var TestService = proxyquire('../bitcorenode', { + '../lib/expressapp': TestExpressApp, + '../lib/wsapp': TestWSApp, + 'http': { + Server: function() { + arguments[0].should.equal(app); + return { + listen: listen + }; + } + } + }); + var options = { + node: { + bwsPort: 3232 + } + }; + var service = new TestService(options); + var config = {}; + service._startWalletService(config, function(err) { + err.message.should.equal('test'); + done(); + }); + }); + it('will enable https', function(done) { + var app = {}; + function TestExpressApp() { + this.app = app; + } + TestExpressApp.prototype.start = sinon.stub().callsArg(1); + function TestWSApp() {} + TestWSApp.prototype.start = sinon.stub().callsArg(2); + var listen = sinon.stub().callsArg(1); + var httpsOptions = {}; + var createServer = function() { + arguments[0].should.equal(httpsOptions); + arguments[1].should.equal(app); + return { + listen: listen + }; + }; + var TestService = proxyquire('../bitcorenode', { + '../lib/expressapp': TestExpressApp, + '../lib/wsapp': TestWSApp, + 'https': { + createServer: createServer + } + }); + var options = { + node: { + https: true, + bwsPort: 3232 + } + }; + var service = new TestService(options); + service._readHttpsOptions = sinon.stub().returns(httpsOptions); + var config = {}; + service._startWalletService(config, function(err) { + service._readHttpsOptions.callCount.should.equal(1); + listen.callCount.should.equal(1); + done(); + }); + }); + }); + describe('#start', function(done) { + it('error from configuration', function(done) { + var options = { + node: {} + }; + var service = new Service(options); + service._getConfiguration = function() { + throw new Error('test'); + }; + service.start(function(err) { + err.message.should.equal('test'); + done(); + }); + }); + it('error from blockchain monitor', function(done) { + var app = {}; + function TestBlockchainMonitor() {} + TestBlockchainMonitor.prototype.start = sinon.stub().callsArgWith(1, new Error('test')); + function TestLocker() {} + TestLocker.prototype.listen = sinon.stub(); + function TestEmailService() {} + TestEmailService.prototype.start = sinon.stub(); + var TestService = proxyquire('../bitcorenode', { + '../lib/blockchainmonitor': TestBlockchainMonitor, + '../lib/emailservice': TestEmailService, + 'socket.io': sinon.stub().returns({ + on: sinon.stub() + }), + 'locker-server': TestLocker, + }); + var options = { + node: {} + }; + var service = new TestService(options); + var config = {}; + service._getConfiguration = sinon.stub().returns(config); + service._startWalletService = sinon.stub().callsArg(1); + service.start(function(err) { + err.message.should.equal('test'); + done(); + }); + }); + it('error from email service', function(done) { + var app = {}; + function TestBlockchainMonitor() {} + TestBlockchainMonitor.prototype.start = sinon.stub().callsArg(1); + function TestLocker() {} + TestLocker.prototype.listen = sinon.stub(); + function TestEmailService() {} + TestEmailService.prototype.start = sinon.stub().callsArgWith(1, new Error('test')); + var TestService = proxyquire('../bitcorenode', { + '../lib/blockchainmonitor': TestBlockchainMonitor, + '../lib/emailservice': TestEmailService, + 'socket.io': sinon.stub().returns({ + on: sinon.stub() + }), + 'locker-server': TestLocker, + }); + var options = { + node: {} + }; + var service = new TestService(options); + service._getConfiguration = sinon.stub().returns({ + emailOpts: {} + }); + var config = {}; + service._startWalletService = sinon.stub().callsArg(1); + service.start(function(err) { + err.message.should.equal('test'); + done(); + }); + }); + }); +});