diff --git a/README.md b/README.md index 75fb957d..6b5df53c 100644 --- a/README.md +++ b/README.md @@ -364,9 +364,7 @@ The module can then be used when running a node: ```js var configuration = { datadir: process.env.BITCORENODE_DIR || '~/.bitcoin', - db: { - modules: [MyModule] - } + modules: [MyModule] }; var node = new Node(configuration); diff --git a/lib/db.js b/lib/db.js index acbbe019..a3ac5059 100644 --- a/lib/db.js +++ b/lib/db.js @@ -12,8 +12,6 @@ var index = require('./'); var errors = index.errors; var log = index.log; var Transaction = require('./transaction'); -var BaseModule = require('./module'); -var AddressModule = require('./modules/address'); function DB(options) { /* jshint maxstatements: 30 */ @@ -52,12 +50,6 @@ function DB(options) { this.node = options.node; - // Modules to be loaded when ready - this._modules = options.modules || []; - this._modules.push(AddressModule); - - this.modules = []; - this.subscriptions = { transaction: [], block: [] @@ -79,12 +71,6 @@ DB.prototype.initialize = function() { }; DB.prototype.start = function(callback) { - // Add all db option modules - if(this._modules && this._modules.length) { - for(var i = 0; i < this._modules.length; i++) { - this.addModule(this._modules[i]); - } - } this.node.bitcoind.on('tx', this.transactionHandler.bind(this)); this.emit('ready'); setImmediate(callback); @@ -278,9 +264,9 @@ DB.prototype.blockHandler = function(block, add, callback) { } async.eachSeries( - this.modules, - function(module, next) { - module['blockHandler'].call(module, block, add, function(err, ops) { + this.node.modules, + function(bitcoreNodeModule, next) { + bitcoreNodeModule.blockHandler.call(bitcoreNodeModule, block, add, function(err, ops) { if(err) { return next(err); } @@ -308,11 +294,6 @@ DB.prototype.getAPIMethods = function() { ['sendTransaction', this, this.sendTransaction, 1], ['estimateFee', this, this.estimateFee, 1] ]; - - for(var i = 0; i < this.modules.length; i++) { - methods = methods.concat(this.modules[i]['getAPIMethods'].call(this.modules[i])); - } - return methods; }; @@ -333,14 +314,6 @@ DB.prototype.getPublishEvents = function() { ]; }; -DB.prototype.addModule = function(Module) { - var module = new Module({ - node: this.node - }); - $.checkArgumentType(module, BaseModule); - this.modules.push(module); -}; - DB.prototype.subscribe = function(name, emitter) { this.subscriptions[name].push(emitter); }; diff --git a/lib/module.js b/lib/module.js index 82471327..bf713264 100644 --- a/lib/module.js +++ b/lib/module.js @@ -4,6 +4,11 @@ var Module = function(options) { this.node = options.node; }; +/** + * Describes the dependencies that should be loaded before this module. + */ +Module.dependencies = []; + /** * blockHandler * @param {Block} block - the block being added or removed from the chain @@ -45,12 +50,12 @@ Module.prototype.getAPIMethods = function() { // // }; -Module.prototype.start = function() { - +Module.prototype.start = function(done) { + setImmediate(done); }; -Module.prototype.stop = function() { - +Module.prototype.stop = function(done) { + setImmediate(done); }; module.exports = Module; diff --git a/lib/modules/address.js b/lib/modules/address.js index c333c6eb..b09b5af3 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -27,6 +27,11 @@ var AddressModule = function(options) { inherits(AddressModule, BaseModule); +AddressModule.dependencies = [ + 'bitcoind', + 'db' +]; + AddressModule.PREFIXES = { OUTPUTS: 'outs', SPENTS: 'sp' diff --git a/lib/node.js b/lib/node.js index 7f39ab9b..124a4e71 100644 --- a/lib/node.js +++ b/lib/node.js @@ -17,6 +17,7 @@ var index = require('./'); var log = index.log; var daemon = require('./daemon'); var Bus = require('./bus'); +var BaseModule = require('./module'); function Node(config) { if(!(this instanceof Node)) { @@ -27,10 +28,17 @@ function Node(config) { this.chain = null; this.network = null; + this.modules = {}; + this._unloadedModules = []; + + // TODO type check the arguments of config.modules + if (config.modules) { + $.checkArgument(Array.isArray(config.modules)); + this._unloadedModules = config.modules; + } + this._loadConfiguration(config); this._initialize(); - - this.testnet = config.testnet; } util.inherits(Node, EventEmitter); @@ -39,10 +47,41 @@ Node.prototype.openBus = function() { return new Bus({db: this.db}); }; +Node.prototype.addModule = function(service) { + var self = this; + var mod = new service.module({ + node: this + }); + + $.checkState( + mod instanceof BaseModule, + 'Unexpected module instance type for module:' + service.name + ); + + // include in loaded modules + this.modules[service.name] = mod; + + // add API methods + var methodData = mod.getAPIMethods(); + methodData.forEach(function(data) { + var name = data[0]; + var instance = data[1]; + var method = data[2]; + + if (self[name]) { + throw new Error('Existing API method exists:' + name); + } else { + self[name] = function() { + return method.apply(instance, arguments); + }; + } + }); +}; + Node.prototype.getAllAPIMethods = function() { var methods = this.db.getAPIMethods(); - for (var i = 0; i < this.db.modules.length; i++) { - var mod = this.db.modules[i]; + for(var i in this.modules) { + var mod = this.modules[i]; methods = methods.concat(mod.getAPIMethods()); } return methods; @@ -50,8 +89,8 @@ Node.prototype.getAllAPIMethods = function() { Node.prototype.getAllPublishEvents = function() { var events = this.db.getPublishEvents(); - for (var i = 0; i < this.db.modules.length; i++) { - var mod = this.db.modules[i]; + for (var i in this.modules) { + var mod = this.modules[i]; events = events.concat(mod.getPublishEvents()); } return events; @@ -379,7 +418,6 @@ Node.prototype._loadConsensus = function(config) { Node.prototype._loadAPI = function() { var self = this; - var methodData = self.db.getAPIMethods(); methodData.forEach(function(data) { var name = data[0]; @@ -456,32 +494,62 @@ Node.prototype._initializeChain = function() { }; Node.prototype.getServices = function() { - var defaultServices = { - 'bitcoind': [], - 'db': ['bitcoind'], - 'chain': ['db'] - }; - return defaultServices; + var services = [ + { + name: 'bitcoind', + dependencies: [] + }, + { + name: 'db', + dependencies: ['bitcoind'], + }, + { + name: 'chain', + dependencies: ['db'] + } + ]; + + services = services.concat(this._unloadedModules); + + return services; }; -Node.prototype.getServiceOrder = function(keys, stack) { +Node.prototype.getServiceOrder = function() { var services = this.getServices(); - if(!keys) { - keys = Object.keys(services); + // organize data for sorting + var names = []; + var servicesByName = {}; + for (var i = 0; i < services.length; i++) { + var service = services[i]; + names.push(service.name); + servicesByName[service.name] = service; } - if(!stack) { - stack = []; - } + var stackNames = {}; + var stack = []; + + function addToStack(names) { + for(var i = 0; i < names.length; i++) { + + var name = names[i]; + var service = servicesByName[name]; + + // first add the dependencies + addToStack(service.dependencies); + + // add to the stack if it hasn't been added + if(!stackNames[name]) { + stack.push(service); + stackNames[name] = true; + } - for(var i = 0; i < keys.length; i++) { - this.getServiceOrder(services[keys[i]], stack); - if(stack.indexOf(keys[i]) === -1) { - stack.push(keys[i]); } } + + addToStack(names); + return stack; }; @@ -492,8 +560,15 @@ Node.prototype.start = function(callback) { async.eachSeries( servicesOrder, function(service, next) { - log.info('Starting ' + service); - self[service].start(next); + log.info('Starting ' + service.name); + + if (service.module) { + self.addModule(service); + self.modules[service.name].start(next); + } else { + // TODO: implement bitcoind, chain and db as modules + self[service.name].start(next); + } }, callback ); @@ -510,12 +585,16 @@ Node.prototype.stop = function(callback) { async.eachSeries( services, function(service, next) { - log.info('Stopping ' + service); - self[service].stop(next); + log.info('Stopping ' + service.name); + + if (service.module) { + self.modules[service.name].stop(next); + } else { + self[service.name].stop(next); + } }, callback ); }; - module.exports = Node; diff --git a/lib/scaffold/default-config.js b/lib/scaffold/default-config.js index b35437b1..81a7e2ba 100644 --- a/lib/scaffold/default-config.js +++ b/lib/scaffold/default-config.js @@ -12,7 +12,8 @@ function getDefaultConfig() { config: { datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'), network: process.env.BITCORENODE_NETWORK || 'livenet', - port: process.env.BITCORENODE_PORT || 3001 + port: process.env.BITCORENODE_PORT || 3001, + modules: ['address'] } }; } diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index 3d927043..8ce78847 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -36,18 +36,23 @@ function start(options) { bitcoreNodeModule = moduleName + '/' + modulePackage.bitcoreNode; } bitcoreModule = require(bitcoreNodeModule); - } // check that the module supports expected methods if (!bitcoreModule.prototype || + !bitcoreModule.dependencies || !bitcoreModule.prototype.start || !bitcoreModule.prototype.stop) { throw new Error( 'Could not load module "' + moduleName + '" as it does not support necessary methods.' ); } - bitcoreModules.push(bitcoreModule); + bitcoreModules.push({ + name: moduleName, + module: bitcoreModule, + dependencies: bitcoreModule.dependencies + }); + } } @@ -56,13 +61,8 @@ function start(options) { // expand to the full path fullConfig.datadir = path.resolve(configPath, config.datadir); - // delete until modules move to the node - delete fullConfig.modules; - // load the modules - fullConfig.db = { - modules: bitcoreModules - }; + fullConfig.modules = bitcoreModules; var node = new BitcoreNode(fullConfig); diff --git a/test/chain.unit.js b/test/chain.unit.js index 5864eca3..6e7e30c1 100644 --- a/test/chain.unit.js +++ b/test/chain.unit.js @@ -249,26 +249,16 @@ describe('Bitcoin Chain', function() { chain.tip = block2; - chain.on('ready', function() { + delete chain.cache.hashes[block1.hash]; - // remove one of the cached hashes to force db call - delete chain.cache.hashes[block1.hash]; - - // the test - chain.getHashes(block2.hash, function(err, hashes) { - should.not.exist(err); - should.exist(hashes); - hashes.length.should.equal(3); - done(); - }); - }); - - chain.on('error', function(err) { + // the test + chain.getHashes(block2.hash, function(err, hashes) { should.not.exist(err); + should.exist(hashes); + hashes.length.should.equal(3); done(); }); - chain.initialize(); }); }); diff --git a/test/db.unit.js b/test/db.unit.js index c93f5d3f..389b0b45 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -16,7 +16,6 @@ var bitcore = require('bitcore'); var Transaction = bitcore.Transaction; describe('Bitcoin DB', function() { - var coinbaseAmount = 50 * 1e8; describe('#start', function() { it('should emit ready', function(done) { @@ -336,7 +335,8 @@ describe('Bitcoin DB', function() { Module1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']); var Module2 = function() {}; Module2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']); - db.modules = [ + db.node = {}; + db.node.modules = [ new Module1(), new Module2() ]; @@ -355,7 +355,7 @@ describe('Bitcoin DB', function() { it('should give an error if one of the modules gives an error', function(done) { var Module3 = function() {}; Module3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); - db.modules.push(new Module3()); + db.node.modules.push(new Module3()); db.blockHandler('block', true, function(err) { should.exist(err); @@ -367,62 +367,11 @@ describe('Bitcoin DB', function() { describe('#getAPIMethods', function() { it('should return the correct db methods', function() { var db = new DB({store: memdown}); - db.modules = []; + db.node = {}; + db.node.modules = []; var methods = db.getAPIMethods(); methods.length.should.equal(4); }); - - it('should also return modules API methods', function() { - var module1 = { - getAPIMethods: function() { - return [ - ['module1-one', module1, module1, 2], - ['module1-two', module1, module1, 2] - ]; - } - }; - var module2 = { - getAPIMethods: function() { - return [ - ['moudle2-one', module2, module2, 1] - ]; - } - }; - - var db = new DB({store: memdown}); - db.modules = [module1, module2]; - - var methods = db.getAPIMethods(); - methods.length.should.equal(7); - }); }); - describe('#addModule', function() { - it('instantiate module and add to db.modules', function() { - var Module1 = function(options) { - BaseModule.call(this, options); - }; - inherits(Module1, BaseModule); - - var db = new DB({store: memdown}); - var node = {}; - db.node = node; - db.modules = []; - db.addModule(Module1); - - db.modules.length.should.equal(1); - should.exist(db.modules[0].node); - db.modules[0].node.should.equal(node); - }); - - it('should throw an error if module is not an instance of BaseModule', function() { - var Module2 = function(options) {}; - var db = new DB({store: memdown}); - db.modules = []; - - (function() { - db.addModule(Module2); - }).should.throw('bitcore.ErrorInvalidArgumentType'); - }); - }); }); diff --git a/test/node.unit.js b/test/node.unit.js index 35476fdf..20a6ba9b 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -13,8 +13,10 @@ var index = require('..'); var fs = require('fs'); var bitcoinConfBuffer = fs.readFileSync(__dirname + '/data/bitcoin.conf'); var chainHashes = require('./data/hashes.json'); +var util = require('util'); +var BaseModule = require('../lib/module'); -describe('Bitcoind Node', function() { +describe('Bitcore Node', function() { var Node; var BadNode; @@ -47,6 +49,36 @@ describe('Bitcoind Node', function() { }); + describe('@constructor', function() { + it('will set properties', function() { + function TestModule() {} + util.inherits(TestModule, BaseModule); + TestModule.prototype.getData = function() {}; + TestModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + var config = { + modules: [ + { + name: 'test1', + module: TestModule + } + ], + }; + var TestNode = proxyquire('../lib/node', {}); + TestNode.prototype._loadConfiguration = sinon.spy(); + TestNode.prototype._initialize = sinon.spy(); + var node = new TestNode(config); + TestNode.prototype._loadConfiguration.callCount.should.equal(1); + TestNode.prototype._initialize.callCount.should.equal(1); + node._unloadedModules.length.should.equal(1); + node._unloadedModules[0].name.should.equal('test1'); + node._unloadedModules[0].module.should.equal(TestModule); + }); + }); + describe('#openBus', function() { it('will create a new bus', function() { var node = new Node({}); @@ -56,19 +88,41 @@ describe('Bitcoind Node', function() { bus.db.should.equal(db); }); }); + + describe('#addModule', function() { + it('will instantiate an instance and load api methods', function() { + var node = new Node({}); + function TestModule() {} + util.inherits(TestModule, BaseModule); + TestModule.prototype.getData = function() {}; + TestModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + var service = { + name: 'testmodule', + module: TestModule + }; + node.addModule(service); + should.exist(node.modules.testmodule); + should.exist(node.getData); + }); + }); + describe('#getAllAPIMethods', function() { it('should return db methods and modules methods', function() { var node = new Node({}); + node.modules = [ + { + getAPIMethods: sinon.stub().returns(['mda1', 'mda2']) + }, + { + getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2']) + } + ]; var db = { getAPIMethods: sinon.stub().returns(['db1', 'db2']), - modules: [ - { - getAPIMethods: sinon.stub().returns(['mda1', 'mda2']) - }, - { - getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2']) - } - ] }; node.db = db; @@ -79,16 +133,16 @@ describe('Bitcoind Node', function() { describe('#getAllPublishEvents', function() { it('should return modules publish events', function() { var node = new Node({}); + node.modules = [ + { + getPublishEvents: sinon.stub().returns(['mda1', 'mda2']) + }, + { + getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2']) + } + ]; var db = { getPublishEvents: sinon.stub().returns(['db1', 'db2']), - modules: [ - { - getPublishEvents: sinon.stub().returns(['mda1', 'mda2']) - }, - { - getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2']) - } - ] }; node.db = db; @@ -648,15 +702,96 @@ describe('Bitcoind Node', function() { it('should return the services in the correct order', function() { var node = new Node({}); node.getServices = function() { - return { - 'chain': ['db'], - 'db': ['daemon', 'p2p'], - 'daemon': [], - 'p2p': [] - }; + return [ + { + name: 'chain', + dependencies: ['db'] + }, + { + name: 'db', + dependencies: ['daemon', 'p2p'] + }, + { + name:'daemon', + dependencies: [] + }, + { + name: 'p2p', + dependencies: [] + } + ]; }; var order = node.getServiceOrder(); - order.should.deep.equal(['daemon', 'p2p', 'db', 'chain']); + order[0].name.should.equal('daemon'); + order[1].name.should.equal('p2p'); + order[2].name.should.equal('db'); + order[3].name.should.equal('chain'); }); }); + + describe('#start', function() { + it('will call start for each module', function(done) { + var node = new Node({}); + function TestModule() {} + util.inherits(TestModule, BaseModule); + TestModule.prototype.start = sinon.stub().callsArg(0); + TestModule.prototype.getData = function() {}; + TestModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + node.test2 = {}; + node.test2.start = sinon.stub().callsArg(0); + node.getServiceOrder = sinon.stub().returns([ + { + name: 'test1', + module: TestModule + }, + { + name: 'test2' + } + ]); + node.start(function() { + node.test2.start.callCount.should.equal(1); + TestModule.prototype.start.callCount.should.equal(1); + done(); + }); + }); + }); + + describe('#stop', function() { + it('will call stop for each module', function(done) { + var node = new Node({}); + function TestModule() {} + util.inherits(TestModule, BaseModule); + TestModule.prototype.stop = sinon.stub().callsArg(0); + TestModule.prototype.getData = function() {}; + TestModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + node.modules = { + 'test1': new TestModule({node: node}) + }; + node.test2 = {}; + node.test2.stop = sinon.stub().callsArg(0); + node.getServiceOrder = sinon.stub().returns([ + { + name: 'test2' + }, + { + name: 'test1', + module: TestModule + } + ]); + node.stop(function() { + node.test2.stop.callCount.should.equal(1); + TestModule.prototype.stop.callCount.should.equal(1); + done(); + }); + }); + }); + }); diff --git a/test/scaffold/start.integration.js b/test/scaffold/start.integration.js index 07daa336..b7d074f5 100644 --- a/test/scaffold/start.integration.js +++ b/test/scaffold/start.integration.js @@ -12,7 +12,11 @@ describe('#start', function() { it('require each bitcore-node module', function(done) { var node; var TestNode = function(options) { - options.db.modules.should.deep.equal([AddressModule]); + options.modules[0].should.deep.equal({ + name: 'address', + module: AddressModule, + dependencies: ['bitcoind', 'db'] + }); }; TestNode.prototype.on = sinon.stub(); TestNode.prototype.chain = {