From a68cf4ecf8b8014856a3babf7f23c7525414df05 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 28 Aug 2015 12:57:01 -0400 Subject: [PATCH 1/5] Web service and other changes in preparation for insight-api --- lib/node.js | 1 + lib/scaffold/start.js | 104 ++-------------------------- lib/service.js | 18 ++++- lib/services/address.js | 1 + lib/services/db.js | 2 + lib/web.js | 145 +++++++++++++++++++++++++++++++++++++++ package.json | 2 + src/libbitcoind.cc | 48 ++++++++----- test/services/db.unit.js | 2 + 9 files changed, 207 insertions(+), 116 deletions(-) create mode 100644 lib/web.js diff --git a/lib/node.js b/lib/node.js index be6b8955..31953996 100644 --- a/lib/node.js +++ b/lib/node.js @@ -10,6 +10,7 @@ var index = require('./'); var log = index.log; var Bus = require('./bus'); var BaseService = require('./service'); +var WebService = require('./web'); function Node(config) { if(!(this instanceof Node)) { diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index 38fca447..093d987b 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -82,102 +82,7 @@ function start(options) { }); node.on('ready', function() { - - var io = socketio(fullConfig.port); - - io.on('connection', function(socket) { - - var bus = node.openBus(); - - var methods = node.getAllAPIMethods(); - var methodsMap = {}; - - methods.forEach(function(data) { - var name = data[0]; - var instance = data[1]; - var method = data[2]; - var args = data[3]; - methodsMap[name] = { - fn: function() { - return method.apply(instance, arguments); - }, - args: args - }; - }); - - socket.on('message', function(message, socketCallback) { - if (methodsMap[message.method]) { - var params = message.params; - - if(!params || !params.length) { - params = []; - } - - if(params.length !== methodsMap[message.method].args) { - return socketCallback({ - error: { - message: 'Expected ' + methodsMap[message.method].args + ' parameters' - } - }); - } - - var callback = function(err, result) { - var response = {}; - if(err) { - response.error = { - message: err.toString() - }; - } - - if(result) { - response.result = result; - } - - socketCallback(response); - }; - - params = params.concat(callback); - methodsMap[message.method].fn.apply(this, params); - } else { - socketCallback({ - error: { - message: 'Method Not Found' - } - }); - } - }); - - socket.on('subscribe', function(name, params) { - bus.subscribe(name, params); - }); - - socket.on('unsubscribe', function(name, params) { - bus.unsubscribe(name, params); - }); - - var events = node.getAllPublishEvents(); - - events.forEach(function(event) { - bus.on(event.name, function() { - if(socket.connected) { - var results = []; - - for(var i = 0; i < arguments.length; i++) { - results.push(arguments[i]); - } - - var params = [event.name].concat(results); - socket.emit.apply(socket, params); - } - }); - }); - - socket.on('disconnect', function() { - bus.close(); - }); - - }); - + log.info('Bitcore Node ready'); }); node.on('error', function(err) { @@ -207,7 +112,12 @@ function start(options) { if(err.stack) { console.log(err.stack); } - process.exit(-1); + node.stop(function(err) { + if(err) { + log.error('Failed to stop services: ' + err); + } + process.exit(-1); + }); } if (options.sigint) { node.stop(function(err) { diff --git a/lib/service.js b/lib/service.js index 4407ff4a..97fed380 100644 --- a/lib/service.js +++ b/lib/service.js @@ -24,7 +24,9 @@ Service.dependencies = []; */ Service.prototype.blockHandler = function(block, add, callback) { // implement in the child class - setImmediate(callback); + setImmediate(function() { + callback(null, []); + }); }; /** @@ -57,12 +59,26 @@ Service.prototype.getAPIMethods = function() { // // }; +/** + * Function which is called when module is first initialized + */ Service.prototype.start = function(done) { setImmediate(done); }; +/** + * Function to be called when bitcore-node is stopped + */ Service.prototype.stop = function(done) { setImmediate(done); }; +/** + * Setup express routes + * @param {Express} app + */ +Service.prototype.setupRoutes = function(app) { + // Setup express routes here +}; + module.exports = Service; diff --git a/lib/services/address.js b/lib/services/address.js index a254016b..f168a1d3 100644 --- a/lib/services/address.js +++ b/lib/services/address.js @@ -505,6 +505,7 @@ AddressService.prototype.getAddressHistoryForAddress = function(address, queryMe var confirmations = 0; if(transaction.__height >= 0) { confirmations = self.node.services.db.tip.__height - transaction.__height; + confirmations = self.node.services.db.tip.__height - transaction.__height + 1; } txinfos[transaction.hash] = { diff --git a/lib/services/db.js b/lib/services/db.js index 582b6088..ca864d13 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -217,6 +217,7 @@ DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback } var tx = Transaction().fromBuffer(obj.buffer); + tx.__blockHash = obj.blockHash; tx.__height = obj.height; tx.__timestamp = obj.timestamp; @@ -657,6 +658,7 @@ DB.prototype.sync = function() { self.bitcoindSyncing = false; self.lastSavedMetadataThreshold = 0; + self.chain.saveMetadata(); // If bitcoind is completely synced if (self.node.services.bitcoind.isSynced()) { diff --git a/lib/web.js b/lib/web.js new file mode 100644 index 00000000..fd4730a8 --- /dev/null +++ b/lib/web.js @@ -0,0 +1,145 @@ +'use strict'; + +var http = require('http'); +var express = require('express'); +var bodyParser = require('body-parser'); +var socketio = require('socket.io'); + +var WebService = function(options) { + var self = this; + this.node = options.node; + this.port = options.port || 3456; + + this.node.on('ready', function() { + self.setupRoutes(); + self.server.listen(self.port); + }); +}; + +WebService.prototype.start = function(callback) { + var self = this; + this.app = express(); + this.app.use(bodyParser.json()); + + this.server = http.createServer(this.app); + + this.io = socketio.listen(this.server); + this.io.on('connection', this.socketHandler.bind(this)); + + var methods = this.node.getAllAPIMethods(); + this.methodsMap = {}; + + methods.forEach(function(data) { + var name = data[0]; + var instance = data[1]; + var method = data[2]; + var args = data[3]; + self.methodsMap[name] = { + fn: function() { + return method.apply(instance, arguments); + }, + args: args + }; + }); + + setImmediate(callback); +}; + +WebService.prototype.stop = function(callback) { + var self = this; + + setImmediate(function() { + if(self.server) { + self.server.close(); + } + + callback(); + }) +}; + +WebService.prototype.setupRoutes = function() { + for(var key in this.node.modules) { + this.node.modules[key].setupRoutes(this.app); + } +}; + +WebService.prototype.socketHandler = function(socket) { + var self = this; + + var bus = this.node.openBus(); + + socket.on('message', this.socketMessageHandler.bind(this)); + + socket.on('subscribe', function(name, params) { + bus.subscribe(name, params); + }); + + socket.on('unsubscribe', function(name, params) { + bus.unsubscribe(name, params); + }); + + var events = self.node.getAllPublishEvents(); + + events.forEach(function(event) { + bus.on(event.name, function() { + if(socket.connected) { + var results = []; + + for(var i = 0; i < arguments.length; i++) { + results.push(arguments[i]); + } + + var params = [event.name].concat(results); + socket.emit.apply(socket, params); + } + }); + }); + + socket.on('disconnect', function() { + bus.close(); + }); +}; + +WebService.prototype.socketMessageHandler = function(message, socketCallback) { + if (this.methodsMap[message.method]) { + var params = message.params; + + if(!params || !params.length) { + params = []; + } + + if(params.length !== this.methodsMap[message.method].args) { + return socketCallback({ + error: { + message: 'Expected ' + this.methodsMap[message.method].args + ' parameters' + } + }); + } + + var callback = function(err, result) { + var response = {}; + if(err) { + response.error = { + message: err.toString() + }; + } + + if(result) { + response.result = result; + } + + socketCallback(response); + }; + + params = params.concat(callback); + this.methodsMap[message.method].fn.apply(this, params); + } else { + socketCallback({ + error: { + message: 'Method Not Found' + } + }); + } +}; + +module.exports = WebService; \ No newline at end of file diff --git a/package.json b/package.json index 3a0b4253..83c382a9 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,13 @@ "bindings": "^1.2.1", "bitcore": "^0.13.0", "colors": "^1.1.2", + "body-parser": "^1.13.3", "commander": "^2.8.1", "errno": "^0.1.4", "leveldown": "^1.4.1", "levelup": "^1.2.1", "liftoff": "^2.1.0", + "express": "^4.13.3", "memdown": "^1.0.0", "mkdirp": "0.5.0", "nan": "1.3.0", diff --git a/src/libbitcoind.cc b/src/libbitcoind.cc index 045c57e3..1c6fc879 100644 --- a/src/libbitcoind.cc +++ b/src/libbitcoind.cc @@ -165,7 +165,7 @@ struct async_block_data { struct async_tx_data { std::string err_msg; std::string txid; - std::string blockhash; + std::string blockHash; uint32_t nTime; int64_t height; bool queryMempool; @@ -1235,6 +1235,7 @@ async_get_tx_and_info(uv_work_t *req) { // Read header first to get block timestamp and hash file >> blockHeader; blockHash = blockHeader.GetHash(); + data->blockHash = blockHash.GetHex(); data->nTime = blockHeader.nTime; fseek(file.Get(), postx.nTxOffset, SEEK_CUR); file >> ctx; @@ -1284,6 +1285,7 @@ async_get_tx_and_info_after(uv_work_t *req) { std::string stx = ssTx.str(); Local rawNodeBuffer = node::Buffer::New(isolate, stx.c_str(), stx.size()); + obj->Set(NanNew("blockHash"), NanNew(data->blockHash)); obj->Set(NanNew("height"), NanNew(data->height)); obj->Set(NanNew("timestamp"), NanNew(data->nTime)); obj->Set(NanNew("buffer"), rawNodeBuffer); @@ -1349,28 +1351,38 @@ NAN_METHOD(GetBlockIndex) { Isolate* isolate = Isolate::GetCurrent(); HandleScope scope(isolate); - String::Utf8Value hash_(args[0]->ToString()); - std::string hashStr = std::string(*hash_); - uint256 hash = uint256S(hashStr); - CBlockIndex* blockIndex; - if (mapBlockIndex.count(hash) == 0) { - NanReturnValue(Undefined(isolate)); + if (args[0]->IsNumber()) { + int64_t height = args[0]->IntegerValue(); + blockIndex = chainActive[height]; + + if (blockIndex == NULL) { + NanReturnValue(Undefined(isolate)); + } + } else { - blockIndex = mapBlockIndex[hash]; - arith_uint256 cw = blockIndex->nChainWork; - CBlockIndex* prevBlockIndex = blockIndex->pprev; - const uint256* prevHash = prevBlockIndex->phashBlock; - - Local obj = NanNew(); - - obj->Set(NanNew("chainWork"), NanNew(cw.GetHex())); - obj->Set(NanNew("prevHash"), NanNew(prevHash->GetHex())); - - NanReturnValue(obj); + String::Utf8Value hash_(args[0]->ToString()); + std::string hashStr = std::string(*hash_); + uint256 hash = uint256S(hashStr); + if (mapBlockIndex.count(hash) == 0) { + NanReturnValue(Undefined(isolate)); + } else { + blockIndex = mapBlockIndex[hash]; + } } + arith_uint256 cw = blockIndex->nChainWork; + CBlockIndex* prevBlockIndex = blockIndex->pprev; + const uint256* prevHash = prevBlockIndex->phashBlock; + + Local obj = NanNew(); + obj->Set(NanNew("hash"), NanNew(blockIndex->phashBlock->GetHex())); + obj->Set(NanNew("chainWork"), NanNew(cw.GetHex())); + obj->Set(NanNew("prevHash"), NanNew(prevHash->GetHex())); + obj->Set(NanNew("height"), NanNew(blockIndex->nHeight)); + + NanReturnValue(obj); }; /** diff --git a/test/services/db.unit.js b/test/services/db.unit.js index 340a79ca..4bfd2388 100644 --- a/test/services/db.unit.js +++ b/test/services/db.unit.js @@ -398,6 +398,7 @@ describe('DB Service', function() { it('should give a transaction with height and timestamp', function(done) { var txBuffer = new Buffer('01000000016f95980911e01c2c664b3e78299527a47933aac61a515930a8fe0213d1ac9abe01000000da0047304402200e71cda1f71e087c018759ba3427eb968a9ea0b1decd24147f91544629b17b4f0220555ee111ed0fc0f751ffebf097bdf40da0154466eb044e72b6b3dcd5f06807fa01483045022100c86d6c8b417bff6cc3bbf4854c16bba0aaca957e8f73e19f37216e2b06bb7bf802205a37be2f57a83a1b5a8cc511dc61466c11e9ba053c363302e7b99674be6a49fc0147522102632178d046673c9729d828cfee388e121f497707f810c131e0d3fc0fe0bd66d62103a0951ec7d3a9da9de171617026442fcd30f34d66100fab539853b43f508787d452aeffffffff0240420f000000000017a9148a31d53a448c18996e81ce67811e5fb7da21e4468738c9d6f90000000017a9148ce5408cfeaddb7ccb2545ded41ef478109454848700000000', 'hex'); var info = { + blockHash: '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6', height: 530482, timestamp: 1439559434000, buffer: txBuffer @@ -412,6 +413,7 @@ describe('DB Service', function() { db.getTransactionWithBlockInfo('2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f', true, function(err, tx) { should.not.exist(err); + tx.__blockHash.should.equal(info.blockHash); tx.__height.should.equal(info.height); tx.__timestamp.should.equal(info.timestamp); done(); From 1b5f30a9a1e70d65b7afdbd074b5250e2ec90824 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 28 Aug 2015 13:54:29 -0400 Subject: [PATCH 2/5] add tests --- integration/regtest.js | 16 +++ lib/node.js | 1 + lib/web.js | 38 ++++--- test/web.unit.js | 239 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 277 insertions(+), 17 deletions(-) create mode 100644 test/web.unit.js diff --git a/integration/regtest.js b/integration/regtest.js index 59ca049b..0b8a7291 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -227,11 +227,27 @@ describe('Daemon Binding Functionality', function() { work.cmp(expectedWork).should.equal(0); expectedWork = expectedWork.add(new BN(2)); should.exist(blockIndex.prevHash); + blockIndex.hash.should.equal(blockHashes[i]); blockIndex.prevHash.should.equal(blockHashes[i - 1]); + blockIndex.height.should.equal(i + 1); }); }); }); + describe('get block index by height', function() { + it('should get block index by height', function() { + var blockIndex = bitcoind.getBlockIndex(2); + should.exist(blockIndex); + should.exist(blockIndex.chainWork); + var work = new BN(blockIndex.chainWork, 'hex'); + work.cmp(new BN(8)).should.equal(0); + should.exist(blockIndex.prevHash); + blockIndex.hash.should.equal(blockHashes[1]); + blockIndex.prevHash.should.equal(blockHashes[0]); + blockIndex.height.should.equal(2); + }); + }); + describe('send transaction functionality', function() { it('will not error and return the transaction hash', function() { diff --git a/lib/node.js b/lib/node.js index 31953996..16bf376a 100644 --- a/lib/node.js +++ b/lib/node.js @@ -11,6 +11,7 @@ var log = index.log; var Bus = require('./bus'); var BaseService = require('./service'); var WebService = require('./web'); +var errors = require('./errors'); function Node(config) { if(!(this instanceof Node)) { diff --git a/lib/web.js b/lib/web.js index fd4730a8..e5656608 100644 --- a/lib/web.js +++ b/lib/web.js @@ -13,6 +13,7 @@ var WebService = function(options) { this.node.on('ready', function() { self.setupRoutes(); self.server.listen(self.port); + self.createMethodsMap(); }); }; @@ -26,22 +27,6 @@ WebService.prototype.start = function(callback) { this.io = socketio.listen(this.server); this.io.on('connection', this.socketHandler.bind(this)); - var methods = this.node.getAllAPIMethods(); - this.methodsMap = {}; - - methods.forEach(function(data) { - var name = data[0]; - var instance = data[1]; - var method = data[2]; - var args = data[3]; - self.methodsMap[name] = { - fn: function() { - return method.apply(instance, arguments); - }, - args: args - }; - }); - setImmediate(callback); }; @@ -63,6 +48,25 @@ WebService.prototype.setupRoutes = function() { } }; +WebService.prototype.createMethodsMap = function() { + var self = this; + var methods = this.node.getAllAPIMethods(); + this.methodsMap = {}; + + methods.forEach(function(data) { + var name = data[0]; + var instance = data[1]; + var method = data[2]; + var args = data[3]; + self.methodsMap[name] = { + fn: function() { + return method.apply(instance, arguments); + }, + args: args + }; + }); +} + WebService.prototype.socketHandler = function(socket) { var self = this; @@ -111,7 +115,7 @@ WebService.prototype.socketMessageHandler = function(message, socketCallback) { if(params.length !== this.methodsMap[message.method].args) { return socketCallback({ error: { - message: 'Expected ' + this.methodsMap[message.method].args + ' parameters' + message: 'Expected ' + this.methodsMap[message.method].args + ' parameter(s)' } }); } diff --git a/test/web.unit.js b/test/web.unit.js new file mode 100644 index 00000000..1c283efe --- /dev/null +++ b/test/web.unit.js @@ -0,0 +1,239 @@ +'use strict'; + +var should = require('chai').should(); +var sinon = require('sinon'); +var WebService = require('../lib/web'); +var EventEmitter = require('events').EventEmitter; + +describe('WebService', function() { + var defaultNode = new EventEmitter(); + + describe('#start', function() { + it('should call the callback with no error', function(done) { + var web = new WebService({node: defaultNode}); + web.start(function(err) { + should.not.exist(err); + done(); + }); + }); + }); + + describe('#stop', function() { + it('should close the server if it exists', function(done) { + var web = new WebService({node: defaultNode}); + web.server = { + close: sinon.spy() + }; + + web.stop(function(err) { + should.not.exist(err); + web.server.close.callCount.should.equal(1); + done(); + }); + }); + }); + + describe('#setupRoutes', function() { + it('should call setupRoutes on each module', function() { + var node = { + on: sinon.spy(), + modules: { + one: { + setupRoutes: sinon.spy() + }, + two: { + setupRoutes: sinon.spy() + } + } + }; + + var web = new WebService({node: node}); + + web.setupRoutes(); + node.modules.one.setupRoutes.callCount.should.equal(1); + node.modules.two.setupRoutes.callCount.should.equal(1); + }); + }); + + describe('#createMethodsMap', function() { + it('should create the methodsMap correctly', function(done) { + var Module1 = function() {}; + Module1.prototype.getAPIMethods = function() { + return [ + ['one', this, this.one, 1], + ['two', this, this.two, 2] + ]; + }; + Module1.prototype.one = function(param1, callback) { + callback(null, param1); + }; + Module1.prototype.two = function(param1, param2, callback) { + callback(null, param1 + param2); + }; + + var module1 = new Module1(); + + var node = { + on: sinon.spy(), + getAllAPIMethods: sinon.stub().returns(module1.getAPIMethods()) + }; + + var web = new WebService({node: node}); + web.createMethodsMap(); + + Object.keys(web.methodsMap).length.should.equal(2); + web.methodsMap.one.args.should.equal(1); + web.methodsMap.two.args.should.equal(2); + web.methodsMap.one.fn(1, function(err, result) { + should.not.exist(err); + result.should.equal(1); + + web.methodsMap.two.fn(1, 2, function(err, result) { + should.not.exist(err); + result.should.equal(3); + done(); + }); + }); + }); + }); + + describe('#socketHandler', function() { + var bus = new EventEmitter(); + + var Module1 = function() {}; + Module1.prototype.getPublishEvents = function() { + return [ + { + name: 'event1' + } + ]; + }; + + var module1 = new Module1(); + var node = { + on: sinon.spy(), + openBus: sinon.stub().returns(bus), + getAllPublishEvents: sinon.stub().returns(module1.getPublishEvents()) + }; + + var web; + var socket; + + it('on message should call socketMessageHandler', function(done) { + web = new WebService({node: node}); + web.socketMessageHandler = function(param1) { + param1.should.equal('data'); + done(); + }; + socket = new EventEmitter(); + web.socketHandler(socket); + socket.emit('message', 'data'); + }); + + it('on subscribe should call bus.subscribe', function(done) { + bus.subscribe = function(param1) { + param1.should.equal('data'); + done(); + }; + + socket.emit('subscribe', 'data'); + }); + + it('on unsubscribe should call bus.unsubscribe', function(done) { + bus.unsubscribe = function(param1) { + param1.should.equal('data'); + done(); + }; + + socket.emit('unsubscribe', 'data'); + }); + + it('publish events from bus should be emitted from socket', function(done) { + socket.once('event1', function(param1, param2) { + param1.should.equal('param1'); + param2.should.equal('param2'); + done(); + }); + socket.connected = true; + bus.emit('event1', 'param1', 'param2'); + }); + + it('on disconnect should close bus', function(done) { + bus.close = function() { + done(); + }; + + socket.emit('disconnect'); + }); + }); + + describe('#socketMessageHandler', function() { + var node = { + on: sinon.spy() + }; + + var web = new WebService({node: node}); + web.methodsMap = { + one: { + fn: function(param1, param2, callback) { + var result = param1 + param2; + if(result > 0) { + return callback(null, result); + } else { + return callback(new Error('error')); + } + }, + args: 2 + } + }; + + it('should give a Method Not Found error if method does not exist', function(done) { + var message = { + method: 'two', + params: [1, 2] + } + web.socketMessageHandler(message, function(response) { + should.exist(response.error); + response.error.message.should.equal('Method Not Found'); + done(); + }); + }); + + it('should call the method and return the result', function(done) { + var message = { + method: 'one', + params: [1, 2] + }; + web.socketMessageHandler(message, function(response) { + should.not.exist(response.error); + response.result.should.equal(3); + done(); + }); + }); + + it('should give an error if there is a param count mismatch', function(done) { + var message = { + method: 'one', + params: [1] + }; + web.socketMessageHandler(message, function(response) { + should.exist(response.error); + response.error.message.should.equal('Expected 2 parameter(s)'); + done(); + }); + }); + + it('should give an error if the method gave an error', function(done) { + var message = { + method: 'one', + params: [-1, -2] + }; + web.socketMessageHandler(message, function(response) { + should.exist(response.error); + response.error.message.should.equal('Error: error'); + done(); + }); + }); + }); + +}); \ No newline at end of file From f1cac3da1da00dee3b1bb4207867e48f66df849b Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Mon, 31 Aug 2015 13:05:46 -0400 Subject: [PATCH 3/5] make webService into a real service --- lib/node.js | 2 +- lib/scaffold/default-config.js | 2 +- lib/services/db.js | 2 +- lib/{ => services}/web.js | 6 +++--- test/{ => services}/web.unit.js | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) rename lib/{ => services}/web.js (95%) rename test/{ => services}/web.unit.js (97%) diff --git a/lib/node.js b/lib/node.js index 16bf376a..9443ac1c 100644 --- a/lib/node.js +++ b/lib/node.js @@ -10,7 +10,6 @@ var index = require('./'); var log = index.log; var Bus = require('./bus'); var BaseService = require('./service'); -var WebService = require('./web'); var errors = require('./errors'); function Node(config) { @@ -32,6 +31,7 @@ function Node(config) { $.checkState(config.datadir, 'Node config expects "datadir"'); this.datadir = config.datadir; + this.port = config.port; this._setNetwork(config); diff --git a/lib/scaffold/default-config.js b/lib/scaffold/default-config.js index 979d110a..3bf92e58 100644 --- a/lib/scaffold/default-config.js +++ b/lib/scaffold/default-config.js @@ -13,7 +13,7 @@ function getDefaultConfig() { datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'), network: process.env.BITCORENODE_NETWORK || 'livenet', port: process.env.BITCORENODE_PORT || 3001, - services: ['bitcoind', 'db', 'address'] + services: ['bitcoind', 'db', 'address', 'web'] } }; } diff --git a/lib/services/db.js b/lib/services/db.js index ca864d13..5762038e 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -658,7 +658,7 @@ DB.prototype.sync = function() { self.bitcoindSyncing = false; self.lastSavedMetadataThreshold = 0; - self.chain.saveMetadata(); + self.saveMetadata(); // If bitcoind is completely synced if (self.node.services.bitcoind.isSynced()) { diff --git a/lib/web.js b/lib/services/web.js similarity index 95% rename from lib/web.js rename to lib/services/web.js index e5656608..81a7641c 100644 --- a/lib/web.js +++ b/lib/services/web.js @@ -8,10 +8,10 @@ var socketio = require('socket.io'); var WebService = function(options) { var self = this; this.node = options.node; - this.port = options.port || 3456; + this.port = options.port || this.node.port || 3456; this.node.on('ready', function() { - self.setupRoutes(); + self.setupAllRoutes(); self.server.listen(self.port); self.createMethodsMap(); }); @@ -42,7 +42,7 @@ WebService.prototype.stop = function(callback) { }) }; -WebService.prototype.setupRoutes = function() { +WebService.prototype.setupAllRoutes = function() { for(var key in this.node.modules) { this.node.modules[key].setupRoutes(this.app); } diff --git a/test/web.unit.js b/test/services/web.unit.js similarity index 97% rename from test/web.unit.js rename to test/services/web.unit.js index 1c283efe..e0803711 100644 --- a/test/web.unit.js +++ b/test/services/web.unit.js @@ -2,7 +2,7 @@ var should = require('chai').should(); var sinon = require('sinon'); -var WebService = require('../lib/web'); +var WebService = require('../../lib/services/web'); var EventEmitter = require('events').EventEmitter; describe('WebService', function() { @@ -33,7 +33,7 @@ describe('WebService', function() { }); }); - describe('#setupRoutes', function() { + describe('#setupAllRoutes', function() { it('should call setupRoutes on each module', function() { var node = { on: sinon.spy(), @@ -49,7 +49,7 @@ describe('WebService', function() { var web = new WebService({node: node}); - web.setupRoutes(); + web.setupAllRoutes(); node.modules.one.setupRoutes.callCount.should.equal(1); node.modules.two.setupRoutes.callCount.should.equal(1); }); From 895e46dcac15c279940c0f0943441dc099179e7c Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Mon, 31 Aug 2015 13:37:11 -0400 Subject: [PATCH 4/5] pass config to services --- lib/node.js | 7 +++-- lib/scaffold/start.js | 40 +++++++++++++++------------ test/scaffold/start.integration.js | 44 ++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/lib/node.js b/lib/node.js index 9443ac1c..6fbfde13 100644 --- a/lib/node.js +++ b/lib/node.js @@ -136,9 +136,10 @@ Node.prototype.getServiceOrder = function() { Node.prototype._instantiateService = function(service) { var self = this; - var mod = new service.module({ - node: this - }); + + var config = service.config || {}; + config.node = this; + var mod = new service.module(config); $.checkState( mod instanceof BaseService, diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index 093d987b..ce4406f0 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -6,6 +6,7 @@ var BitcoreNode = require('../node'); var index = require('../'); var bitcore = require('bitcore'); var _ = bitcore.deps._; +var $ = bitcore.util.preconditions; var log = index.log; log.debug = function() {}; @@ -22,37 +23,42 @@ function start(options) { if (config.services) { for (var i = 0; i < config.services.length; i++) { - var serviceName = config.services[i]; - var service; + var service = {}; + if(typeof config.services[i] === 'object') { + $.checkState(config.services[i].name, 'Service name must be specified in config'); + service.name = config.services[i].name; + service.config = config.services[i].config || {}; + } else { + service.name = config.services[i]; + service.config = {}; + } + try { // first try in the built-in bitcore-node services directory - service = require(path.resolve(__dirname, '../services/' + serviceName)); + service.module = require(path.resolve(__dirname, '../services/' + service.name)); } catch(e) { // check if the package.json specifies a specific file to use - var servicePackage = require(serviceName + '/package.json'); - var serviceModule = serviceName; + var servicePackage = require(service.name + '/package.json'); + var serviceModule = service.name; if (servicePackage.bitcoreNode) { - serviceModule = serviceName + '/' + servicePackage.bitcoreNode; + serviceModule = service.name + '/' + servicePackage.bitcoreNode; } - service = require(serviceModule); + service.module = require(serviceModule); } // check that the service supports expected methods - if (!service.prototype || - !service.dependencies || - !service.prototype.start || - !service.prototype.stop) { + if (!service.module.prototype || + !service.module.dependencies || + !service.module.prototype.start || + !service.module.prototype.stop) { throw new Error( - 'Could not load service "' + serviceName + '" as it does not support necessary methods.' + 'Could not load service "' + service.name + '" as it does not support necessary methods.' ); } - services.push({ - name: serviceName, - module: service, - dependencies: service.dependencies - }); + service.dependencies = service.module.dependencies; + services.push(service); } } diff --git a/test/scaffold/start.integration.js b/test/scaffold/start.integration.js index 228af308..3f25963b 100644 --- a/test/scaffold/start.integration.js +++ b/test/scaffold/start.integration.js @@ -9,13 +9,14 @@ describe('#start', function() { describe('will dynamically create a node from a configuration', function() { - it('require each bitcore-node service', function(done) { + it('require each bitcore-node service with default config', function(done) { var node; var TestNode = function(options) { options.services[0].should.deep.equal({ name: 'address', module: AddressService, - dependencies: ['bitcoind', 'db'] + dependencies: ['bitcoind', 'db'], + config: {} }); }; TestNode.prototype.on = sinon.stub(); @@ -39,5 +40,44 @@ describe('#start', function() { node.should.be.instanceof(TestNode); done(); }); + + it('require each bitcore-node service with explicit config', function(done) { + var node; + var TestNode = function(options) { + options.services[0].should.deep.equal({ + name: 'address', + module: AddressService, + dependencies: ['bitcoind', 'db'], + config: { + param: 'test' + } + }); + }; + TestNode.prototype.on = sinon.stub(); + TestNode.prototype.chain = { + on: sinon.stub() + }; + + var starttest = proxyquire('../../lib/scaffold/start', { + '../node': TestNode + }); + + node = starttest({ + path: __dirname, + config: { + services: [ + { + name: 'address', + config: { + param: 'test' + } + } + ], + datadir: './data' + } + }); + node.should.be.instanceof(TestNode); + done(); + }); }); }); From 4ae637754873723a7c9a8f79bc1a89212a9d4d17 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Mon, 31 Aug 2015 13:38:21 -0400 Subject: [PATCH 5/5] fixes --- integration/regtest-node.js | 6 +++--- integration/regtest.js | 24 ++++++++++++++---------- lib/node.js | 10 ++++++++-- lib/scaffold/start.js | 11 ++--------- lib/services/web.js | 10 ++++++++-- test/node.unit.js | 29 +++++++++++++++++++++-------- test/scaffold/start.integration.js | 14 ++++++-------- test/services/web.unit.js | 6 +++--- 8 files changed, 65 insertions(+), 45 deletions(-) diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 873801d7..bc4909bb 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -69,17 +69,17 @@ describe('Node Functionality', function() { { name: 'db', module: DBService, - dependencies: DBService.dependencies + config: {} }, { name: 'bitcoind', module: BitcoinService, - dependencies: BitcoinService.dependencies + config: {} }, { name: 'address', module: AddressService, - dependencies: AddressService.dependencies + config: {} } ] }; diff --git a/integration/regtest.js b/integration/regtest.js index 0b8a7291..ff1e47c4 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -235,16 +235,20 @@ describe('Daemon Binding Functionality', function() { }); describe('get block index by height', function() { - it('should get block index by height', function() { - var blockIndex = bitcoind.getBlockIndex(2); - should.exist(blockIndex); - should.exist(blockIndex.chainWork); - var work = new BN(blockIndex.chainWork, 'hex'); - work.cmp(new BN(8)).should.equal(0); - should.exist(blockIndex.prevHash); - blockIndex.hash.should.equal(blockHashes[1]); - blockIndex.prevHash.should.equal(blockHashes[0]); - blockIndex.height.should.equal(2); + var expectedWork = new BN(6); + [2,3,4,5,6,7,8,9].forEach(function(i) { + it('generate block ' + i, function() { + var blockIndex = bitcoind.getBlockIndex(i); + should.exist(blockIndex); + should.exist(blockIndex.chainWork); + var work = new BN(blockIndex.chainWork, 'hex'); + work.cmp(expectedWork).should.equal(0); + expectedWork = expectedWork.add(new BN(2)); + should.exist(blockIndex.prevHash); + blockIndex.hash.should.equal(blockHashes[i - 1]); + blockIndex.prevHash.should.equal(blockHashes[i - 2]); + blockIndex.height.should.equal(i); + }); }); }); diff --git a/lib/node.js b/lib/node.js index 6fbfde13..d697070b 100644 --- a/lib/node.js +++ b/lib/node.js @@ -6,6 +6,7 @@ var async = require('async'); var bitcore = require('bitcore'); var Networks = bitcore.Networks; var $ = bitcore.util.preconditions; +var _ = bitcore.deps._; var index = require('./'); var log = index.log; var Bus = require('./bus'); @@ -19,6 +20,8 @@ function Node(config) { var self = this; + this.errors = errors; // So services can use errors without having to have bitcore-node as a dependency + this.log = log; this.network = null; this.services = {}; this._unloadedServices = []; @@ -118,7 +121,7 @@ Node.prototype.getServiceOrder = function() { $.checkState(service, 'Required dependency "' + name + '" not available.'); // first add the dependencies - addToStack(service.dependencies); + addToStack(service.module.dependencies); // add to the stack if it hasn't been added if(!stackNames[name]) { @@ -137,7 +140,10 @@ Node.prototype.getServiceOrder = function() { Node.prototype._instantiateService = function(service) { var self = this; - var config = service.config || {}; + $.checkState(_.isObject(service.config)); + $.checkState(!service.config.node); + + var config = service.config; config.node = this; var mod = new service.module(config); diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index ce4406f0..704a9ff5 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -24,14 +24,8 @@ function start(options) { if (config.services) { for (var i = 0; i < config.services.length; i++) { var service = {}; - if(typeof config.services[i] === 'object') { - $.checkState(config.services[i].name, 'Service name must be specified in config'); - service.name = config.services[i].name; - service.config = config.services[i].config || {}; - } else { - service.name = config.services[i]; - service.config = {}; - } + service.name = config.services[i]; + service.config = config.servicesConfig && config.servicesConfig[service.name] ? config.servicesConfig[service.name] : {}; try { // first try in the built-in bitcore-node services directory @@ -57,7 +51,6 @@ function start(options) { ); } - service.dependencies = service.module.dependencies; services.push(service); } } diff --git a/lib/services/web.js b/lib/services/web.js index 81a7641c..63fac376 100644 --- a/lib/services/web.js +++ b/lib/services/web.js @@ -4,6 +4,8 @@ var http = require('http'); var express = require('express'); var bodyParser = require('body-parser'); var socketio = require('socket.io'); +var BaseService = require('../service'); +var inherits = require('util').inherits; var WebService = function(options) { var self = this; @@ -17,6 +19,10 @@ var WebService = function(options) { }); }; +inherits(WebService, BaseService); + +WebService.dependencies = []; + WebService.prototype.start = function(callback) { var self = this; this.app = express(); @@ -43,8 +49,8 @@ WebService.prototype.stop = function(callback) { }; WebService.prototype.setupAllRoutes = function() { - for(var key in this.node.modules) { - this.node.modules[key].setupRoutes(this.app); + for(var key in this.node.services) { + this.node.services[key].setupRoutes(this.app); } }; diff --git a/test/node.unit.js b/test/node.unit.js index 60a346cd..19e1f2e6 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -183,19 +183,27 @@ describe('Bitcore Node', function() { node._unloadedServices = [ { name: 'chain', - dependencies: ['db'] + module: { + dependencies: ['db'] + } }, { name: 'db', + module: { dependencies: ['daemon', 'p2p'] + } }, { name:'daemon', - dependencies: [] + module: { + dependencies: [] + } }, { name: 'p2p', - dependencies: [] + module: { + dependencies: [] + } } ]; var order = node.getServiceOrder(); @@ -219,7 +227,8 @@ describe('Bitcore Node', function() { }; var service = { name: 'testservice', - module: TestService + module: TestService, + config: {} }; node._instantiateService(service); should.exist(node.services.testservice); @@ -254,11 +263,13 @@ describe('Bitcore Node', function() { node.getServiceOrder = sinon.stub().returns([ { name: 'test1', - module: TestService + module: TestService, + config: {} }, { name: 'test2', - module: TestService2 + module: TestService2, + config: {} } ]); node.start(function() { @@ -295,11 +306,13 @@ describe('Bitcore Node', function() { node.getServiceOrder = sinon.stub().returns([ { name: 'test', - module: TestService + module: TestService, + config: {} }, { name: 'conflict', - module: ConflictService + module: ConflictService, + config: {} } ]); diff --git a/test/scaffold/start.integration.js b/test/scaffold/start.integration.js index 3f25963b..e6985068 100644 --- a/test/scaffold/start.integration.js +++ b/test/scaffold/start.integration.js @@ -15,7 +15,6 @@ describe('#start', function() { options.services[0].should.deep.equal({ name: 'address', module: AddressService, - dependencies: ['bitcoind', 'db'], config: {} }); }; @@ -47,7 +46,6 @@ describe('#start', function() { options.services[0].should.deep.equal({ name: 'address', module: AddressService, - dependencies: ['bitcoind', 'db'], config: { param: 'test' } @@ -66,13 +64,13 @@ describe('#start', function() { path: __dirname, config: { services: [ - { - name: 'address', - config: { - param: 'test' - } - } + 'address' ], + servicesConfig: { + 'address': { + param: 'test' + } + }, datadir: './data' } }); diff --git a/test/services/web.unit.js b/test/services/web.unit.js index e0803711..2db7aad6 100644 --- a/test/services/web.unit.js +++ b/test/services/web.unit.js @@ -37,7 +37,7 @@ describe('WebService', function() { it('should call setupRoutes on each module', function() { var node = { on: sinon.spy(), - modules: { + services: { one: { setupRoutes: sinon.spy() }, @@ -50,8 +50,8 @@ describe('WebService', function() { var web = new WebService({node: node}); web.setupAllRoutes(); - node.modules.one.setupRoutes.callCount.should.equal(1); - node.modules.two.setupRoutes.callCount.should.equal(1); + node.services.one.setupRoutes.callCount.should.equal(1); + node.services.two.setupRoutes.callCount.should.equal(1); }); });