From 96f660422266c1eb8769ee91bb5743814c551430 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Mon, 31 Aug 2015 09:00:00 -0400 Subject: [PATCH 1/5] Convert daemon into bitcoin module. --- bin/start-libbitcoind.js | 16 ++- index.js | 2 +- integration/regtest-node.js | 24 +++- integration/regtest.js | 10 +- lib/chain.js | 4 +- lib/daemon.js | 165 ------------------------- lib/db.js | 19 +-- lib/module.js | 7 ++ lib/modules/address.js | 6 +- lib/modules/bitcoind.js | 220 +++++++++++++++++++++++++++++++++ lib/node.js | 104 +++------------- lib/scaffold/default-config.js | 2 +- test/chain.unit.js | 10 +- test/db.unit.js | 45 ++++--- test/modules/address.unit.js | 103 +++++++-------- test/modules/bitcoind.unit.js | 50 ++++++++ test/node.unit.js | 201 ++++++++++-------------------- 17 files changed, 496 insertions(+), 492 deletions(-) delete mode 100644 lib/daemon.js create mode 100644 lib/modules/bitcoind.js create mode 100644 test/modules/bitcoind.unit.js diff --git a/bin/start-libbitcoind.js b/bin/start-libbitcoind.js index a7f7299d..a2860ce2 100644 --- a/bin/start-libbitcoind.js +++ b/bin/start-libbitcoind.js @@ -2,17 +2,21 @@ 'use strict'; -var chainlib = require('chainlib'); -var log = chainlib.log; +var index = require('..'); +var log = index.log; process.title = 'libbitcoind'; /** * daemon */ -var daemon = require('../').daemon({ - datadir: process.env.BITCORENODE_DIR || '~/.bitcoin', - network: process.env.BITCORENODE_NETWORK || 'livenet' +var daemon = require('../').modules.BitcoinModule({ + node: { + datadir: process.env.BITCORENODE_DIR || process.env.HOME + '/.bitcoin', + network: { + name: process.env.BITCORENODE_NETWORK || 'livenet' + } + } }); daemon.start(function() { @@ -54,4 +58,4 @@ function exitHandler(options, err) { process.on('uncaughtException', exitHandler.bind(null, {exit:true})); //catches ctrl+c event -process.on('SIGINT', exitHandler.bind(null, {sigint:true})); \ No newline at end of file +process.on('SIGINT', exitHandler.bind(null, {sigint:true})); diff --git a/index.js b/index.js index 7225237e..7233605e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ 'use strict'; module.exports = require('./lib'); -module.exports.daemon = require('./lib/daemon'); module.exports.Node = require('./lib/node'); module.exports.Chain = require('./lib/chain'); module.exports.DB = require('./lib/db'); @@ -11,6 +10,7 @@ module.exports.errors = require('./lib/errors'); module.exports.modules = {}; module.exports.modules.AddressModule = require('./lib/modules/address'); +module.exports.modules.BitcoinModule = require('./lib/modules/bitcoind'); module.exports.scaffold = {}; module.exports.scaffold.create = require('./lib/scaffold/create'); diff --git a/integration/regtest-node.js b/integration/regtest-node.js index ebd94108..ef2bcaa1 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -23,7 +23,10 @@ var node; var should = chai.should(); var BitcoinRPC = require('bitcoind-rpc'); -var BitcoreNode = require('..').Node; +var index = require('..'); +var BitcoreNode = index.Node; +var AddressModule = index.modules.AddressModule; +var BitcoinModule = index.modules.BitcoinModule; var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG'; var testKey; var client; @@ -60,7 +63,19 @@ describe('Node Functionality', function() { var configuration = { datadir: datadir, - network: 'regtest' + network: 'regtest', + modules: [ + { + name: 'bitcoind', + module: BitcoinModule, + dependencies: BitcoinModule.dependencies + }, + { + name: 'address', + module: AddressModule, + dependencies: AddressModule.dependencies + } + ] }; node = new BitcoreNode(configuration); @@ -102,7 +117,10 @@ describe('Node Functionality', function() { after(function(done) { this.timeout(20000); - node.bitcoind.stop(function(err, result) { + node.stop(function(err, result) { + if(err) { + throw err; + } done(); }); }); diff --git a/integration/regtest.js b/integration/regtest.js index 6e16ad0d..ae804d8a 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -61,9 +61,13 @@ describe('Daemon Binding Functionality', function() { throw err; } - bitcoind = require('../').daemon({ - datadir: datadir, - network: 'regtest' + bitcoind = require('../').modules.BitcoinModule({ + node: { + datadir: datadir, + network: { + name: 'regtest' + } + } }); bitcoind.on('error', function(err) { diff --git a/lib/chain.js b/lib/chain.js index 66a47777..ceb54e66 100644 --- a/lib/chain.js +++ b/lib/chain.js @@ -90,7 +90,7 @@ Chain.prototype._onInitialized = function() { }; Chain.prototype.start = function(callback) { - this.genesis = Block.fromBuffer(this.node.bitcoind.genesisBuffer); + this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer); this.once('initialized', callback); this.initialize(); }; @@ -152,7 +152,7 @@ Chain.prototype.startBuilder = function() { Chain.prototype.getWeight = function getWeight(blockHash, callback) { var self = this; - var blockIndex = self.node.bitcoind.getBlockIndex(blockHash); + var blockIndex = self.node.modules.bitcoind.getBlockIndex(blockHash); setImmediate(function() { if (blockIndex) { diff --git a/lib/daemon.js b/lib/daemon.js deleted file mode 100644 index 297d02ac..00000000 --- a/lib/daemon.js +++ /dev/null @@ -1,165 +0,0 @@ -'use strict'; - -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var bitcoind = require('bindings')('bitcoind.node'); -var index = require('./'); -var log = index.log; -var bitcore = require('bitcore'); -var $ = bitcore.util.preconditions; - -function Daemon(options) { - var self = this; - - if (!(this instanceof Daemon)) { - return new Daemon(options); - } - - if (Object.keys(this.instances).length) { - throw new Error('Daemon cannot be instantiated more than once.'); - } - - EventEmitter.call(this); - - $.checkArgument(options.datadir, 'Please specify a datadir'); - - this.options = options || {}; - this.options.datadir = this.options.datadir.replace(/^~/, process.env.HOME); - this.datadir = this.options.datadir; - - this.node = options.node; - - this.config = this.datadir + '/bitcoin.conf'; - - Object.keys(exports).forEach(function(key) { - self[key] = exports[key]; - }); - -} - -util.inherits(Daemon, EventEmitter); - -Daemon.instances = {}; -Daemon.prototype.instances = Daemon.instances; - -Daemon.__defineGetter__('global', function() { - return Daemon.instances[Object.keys(Daemon.instances)[0]]; -}); - -Daemon.prototype.__defineGetter__('global', function() { - return Daemon.global; -}); - -Daemon.prototype.start = function(callback) { - var self = this; - - if (this.instances[this.datadir]) { - return callback(new Error('Daemon already started')); - } - this.instances[this.datadir] = true; - - bitcoind.start(this.options, function(err) { - if(err) { - return callback(err); - } - - self._started = true; - - bitcoind.onBlocksReady(function(err, result) { - - function onTipUpdateListener(result) { - if (result) { - // Emit and event that the tip was updated - self.height = result; - self.emit('tip', result); - // Recursively wait until the next update - bitcoind.onTipUpdate(onTipUpdateListener); - } - } - - bitcoind.onTipUpdate(onTipUpdateListener); - - bitcoind.startTxMon(function(txs) { - for(var i = 0; i < txs.length; i++) { - self.emit('tx', txs[i]); - } - }); - - // Set the current chain height - var info = self.getInfo(); - self.height = info.blocks; - - // Get the genesis block - self.getBlock(0, function(err, block) { - self.genesisBuffer = block; - self.emit('ready', result); - setImmediate(callback); - }); - - }); - }); -}; - -Daemon.prototype.isSynced = function() { - return bitcoind.isSynced(); -}; - -Daemon.prototype.syncPercentage = function() { - return bitcoind.syncPercentage(); -}; - -Daemon.prototype.getBlock = function(blockhash, callback) { - return bitcoind.getBlock(blockhash, callback); -}; - -Daemon.prototype.isSpent = function(txid, outputIndex) { - return bitcoind.isSpent(txid, outputIndex); -}; - -Daemon.prototype.getBlockIndex = function(blockHash) { - return bitcoind.getBlockIndex(blockHash); -}; - -Daemon.prototype.estimateFee = function(blocks) { - return bitcoind.estimateFee(blocks); -}; - -Daemon.prototype.sendTransaction = function(transaction, allowAbsurdFees) { - return bitcoind.sendTransaction(transaction, allowAbsurdFees); -}; - -Daemon.prototype.getTransaction = function(txid, queryMempool, callback) { - return bitcoind.getTransaction(txid, queryMempool, callback); -}; - -Daemon.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { - return bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback); -}; - -Daemon.prototype.getMempoolOutputs = function(address) { - return bitcoind.getMempoolOutputs(address); -}; - -Daemon.prototype.addMempoolUncheckedTransaction = function(txBuffer) { - return bitcoind.addMempoolUncheckedTransaction(txBuffer); -}; - -Daemon.prototype.getInfo = function() { - return bitcoind.getInfo(); -}; - -Daemon.prototype.stop = function(callback) { - var self = this; - return bitcoind.stop(function(err, status) { - setImmediate(function() { - if (err) { - return callback(err); - } else { - log.info(status); - return callback(); - } - }); - }); -}; - -module.exports = Daemon; diff --git a/lib/db.js b/lib/db.js index 99bdb43e..b6f3d3a2 100644 --- a/lib/db.js +++ b/lib/db.js @@ -71,7 +71,7 @@ DB.prototype.initialize = function() { }; DB.prototype.start = function(callback) { - this.node.bitcoind.on('tx', this.transactionHandler.bind(this)); + this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this)); this.emit('ready'); setImmediate(callback); }; @@ -93,7 +93,7 @@ DB.prototype.getBlock = function(hash, callback) { var self = this; // get block from bitcoind - this.node.bitcoind.getBlock(hash, function(err, blockData) { + this.node.modules.bitcoind.getBlock(hash, function(err, blockData) { if(err) { return callback(err); } @@ -102,7 +102,7 @@ DB.prototype.getBlock = function(hash, callback) { }; DB.prototype.getPrevHash = function(blockHash, callback) { - var blockIndex = this.node.bitcoind.getBlockIndex(blockHash); + var blockIndex = this.node.modules.bitcoind.getBlockIndex(blockHash); setImmediate(function() { if (blockIndex) { callback(null, blockIndex.prevHash); @@ -118,7 +118,7 @@ DB.prototype.putBlock = function(block, callback) { }; DB.prototype.getTransaction = function(txid, queryMempool, callback) { - this.node.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { + this.node.modules.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { if(err) { return callback(err); } @@ -131,7 +131,7 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) { }; DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { - this.node.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { + this.node.modules.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { if(err) { return callback(err); } @@ -151,7 +151,7 @@ DB.prototype.sendTransaction = function(tx, callback) { $.checkArgument(typeof tx === 'string', 'Argument must be a hex string or Transaction'); try { - var txid = this.node.bitcoind.sendTransaction(tx); + var txid = this.node.modules.bitcoind.sendTransaction(tx); return callback(null, txid); } catch(err) { return callback(err); @@ -162,7 +162,7 @@ DB.prototype.estimateFee = function(blocks, callback) { var self = this; setImmediate(function() { - callback(null, self.node.bitcoind.estimateFee(blocks)); + callback(null, self.node.modules.bitcoind.estimateFee(blocks)); }); }; @@ -278,8 +278,9 @@ DB.prototype.blockHandler = function(block, add, callback) { if(err) { return next(err); } - - operations = operations.concat(ops); + if (ops) { + operations = operations.concat(ops); + } next(); }); }, diff --git a/lib/module.js b/lib/module.js index bf713264..7b7fd19c 100644 --- a/lib/module.js +++ b/lib/module.js @@ -1,9 +1,16 @@ 'use strict'; +var util = require('util'); +var EventEmitter = require('events').EventEmitter; + var Module = function(options) { + EventEmitter.call(this); + this.node = options.node; }; +util.inherits(Module, EventEmitter); + /** * Describes the dependencies that should be loaded before this module. */ diff --git a/lib/modules/address.js b/lib/modules/address.js index b09b5af3..36e0f738 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -21,7 +21,7 @@ var AddressModule = function(options) { this.subscriptions['address/transaction'] = {}; this.subscriptions['address/balance'] = {}; - this.node.bitcoind.on('tx', this.transactionHandler.bind(this)); + this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this)); }; @@ -368,7 +368,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback } if(queryMempool) { - outputs = outputs.concat(self.node.bitcoind.getMempoolOutputs(addressStr)); + outputs = outputs.concat(self.node.modules.bitcoind.getMempoolOutputs(addressStr)); } callback(null, outputs); @@ -435,7 +435,7 @@ AddressModule.prototype.isSpent = function(output, queryMempool, callback) { var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; setImmediate(function() { - callback(self.node.bitcoind.isSpent(txid, output.outputIndex)); + callback(self.node.modules.bitcoind.isSpent(txid, output.outputIndex)); }); }; diff --git a/lib/modules/bitcoind.js b/lib/modules/bitcoind.js new file mode 100644 index 00000000..433c8910 --- /dev/null +++ b/lib/modules/bitcoind.js @@ -0,0 +1,220 @@ +'use strict'; + +var util = require('util'); +var Module = require('../module'); +var bindings = require('bindings')('bitcoind.node'); +var mkdirp = require('mkdirp'); +var fs = require('fs'); +var index = require('../'); +var log = index.log; +var bitcore = require('bitcore'); +var $ = bitcore.util.preconditions; + +/** + * Provides an interface to native bindings to Bitcoin Core + * @param {Object} options + * @param {String} options.datadir - The bitcoin data directory + * @param {Node} options.node - A reference to the node + */ +function Bitcoin(options) { + if (!(this instanceof Bitcoin)) { + return new Bitcoin(options); + } + + var self = this; + + Module.call(this, options); + + if (Object.keys(this.instances).length) { + throw new Error('Bitcoin cannot be instantiated more than once.'); + } + + $.checkState(this.node.datadir, 'Node is missing datadir property'); + + Object.keys(exports).forEach(function(key) { + self[key] = exports[key]; + }); + +} + +util.inherits(Bitcoin, Module); + +Bitcoin.dependencies = []; + +Bitcoin.instances = {}; +Bitcoin.prototype.instances = Bitcoin.instances; + +Bitcoin.__defineGetter__('global', function() { + return Bitcoin.instances[Object.keys(Bitcoin.instances)[0]]; +}); + +Bitcoin.prototype.__defineGetter__('global', function() { + return Bitcoin.global; +}); + +Bitcoin.DEFAULT_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n'; + +Bitcoin.prototype._loadConfiguration = function() { + /* jshint maxstatements: 25 */ + + $.checkArgument(this.node.datadir, 'Please specify "datadir" in configuration options'); + var configPath = this.node.datadir + '/bitcoin.conf'; + this.configuration = {}; + + if (!fs.existsSync(this.node.datadir)) { + mkdirp.sync(this.node.datadir); + } + + if (!fs.existsSync(configPath)) { + fs.writeFileSync(configPath, Bitcoin.DEFAULT_CONFIG); + } + + var file = fs.readFileSync(configPath); + var unparsed = file.toString().split('\n'); + for(var i = 0; i < unparsed.length; i++) { + var line = unparsed[i]; + if (!line.match(/^\#/) && line.match(/\=/)) { + var option = line.split('='); + var value; + if (!Number.isNaN(Number(option[1]))) { + value = Number(option[1]); + } else { + value = option[1]; + } + this.configuration[option[0]] = value; + } + } + + $.checkState( + this.configuration.txindex && this.configuration.txindex === 1, + 'Txindex option is required in order to use most of the features of bitcore-node. ' + + 'Please add "txindex=1" to your configuration and reindex an existing database if ' + + 'necessary with reindex=1' + ); +}; + +Bitcoin.prototype.start = function(callback) { + var self = this; + + this._loadConfiguration(); + + if (this.instances[this.datadir]) { + return callback(new Error('Bitcoin already started')); + } + this.instances[this.datadir] = true; + + bindings.start({ + datadir: this.node.datadir, + network: this.node.network.name + }, function(err) { + if(err) { + return callback(err); + } + + self._started = true; + + bindings.onBlocksReady(function(err, result) { + + function onTipUpdateListener(result) { + if (result) { + // Emit and event that the tip was updated + self.height = result; + self.emit('tip', result); + + // TODO stopping status + if(!self.stopping) { + var percentage = self.syncPercentage(); + log.info('Bitcoin Core Daemon New Height:', self.height, 'Percentage:', percentage); + } + + // Recursively wait until the next update + bindings.onTipUpdate(onTipUpdateListener); + } + } + + bindings.onTipUpdate(onTipUpdateListener); + + bindings.startTxMon(function(txs) { + for(var i = 0; i < txs.length; i++) { + self.emit('tx', txs[i]); + } + }); + + // Set the current chain height + var info = self.getInfo(); + self.height = info.blocks; + + // Get the genesis block + self.getBlock(0, function(err, block) { + self.genesisBuffer = block; + self.emit('ready', result); + log.info('Bitcoin Daemon Ready'); + setImmediate(callback); + }); + + }); + }); +}; + +Bitcoin.prototype.isSynced = function() { + return bindings.isSynced(); +}; + +Bitcoin.prototype.syncPercentage = function() { + return bindings.syncPercentage(); +}; + +Bitcoin.prototype.getBlock = function(blockhash, callback) { + return bindings.getBlock(blockhash, callback); +}; + +Bitcoin.prototype.isSpent = function(txid, outputIndex) { + return bindings.isSpent(txid, outputIndex); +}; + +Bitcoin.prototype.getBlockIndex = function(blockHash) { + return bindings.getBlockIndex(blockHash); +}; + +Bitcoin.prototype.estimateFee = function(blocks) { + return bindings.estimateFee(blocks); +}; + +Bitcoin.prototype.sendTransaction = function(transaction, allowAbsurdFees) { + return bindings.sendTransaction(transaction, allowAbsurdFees); +}; + +Bitcoin.prototype.getTransaction = function(txid, queryMempool, callback) { + return bindings.getTransaction(txid, queryMempool, callback); +}; + +Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { + return bindings.getTransactionWithBlockInfo(txid, queryMempool, callback); +}; + +Bitcoin.prototype.getMempoolOutputs = function(address) { + return bindings.getMempoolOutputs(address); +}; + +Bitcoin.prototype.addMempoolUncheckedTransaction = function(txBuffer) { + return bindings.addMempoolUncheckedTransaction(txBuffer); +}; + +Bitcoin.prototype.getInfo = function() { + return bindings.getInfo(); +}; + +Bitcoin.prototype.stop = function(callback) { + return bindings.stop(function(err, status) { + setImmediate(function() { + if (err) { + return callback(err); + } else { + log.info(status); + return callback(); + } + }); + }); +}; + +module.exports = Bitcoin; diff --git a/lib/node.js b/lib/node.js index cc6fa3d5..1bba402d 100644 --- a/lib/node.js +++ b/lib/node.js @@ -15,7 +15,6 @@ var Chain = require('./chain'); var DB = require('./db'); var index = require('./'); var log = index.log; -var daemon = require('./daemon'); var Bus = require('./bus'); var BaseModule = require('./module'); @@ -37,6 +36,9 @@ function Node(config) { this._unloadedModules = config.modules; } + $.checkState(config.datadir, 'Node config expects "datadir"'); + this.datadir = config.datadir; + this._loadConfiguration(config); this._initialize(); } @@ -97,61 +99,12 @@ Node.prototype.getAllPublishEvents = function() { }; Node.prototype._loadConfiguration = function(config) { - this._loadBitcoinConf(config); - this._loadBitcoind(config); this._loadNetwork(config); this._loadDB(config); this._loadAPI(); this._loadConsensus(config); }; -Node.DEFAULT_DAEMON_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n'; - -Node.prototype._loadBitcoinConf = function(config) { - $.checkArgument(config.datadir, 'Please specify "datadir" in configuration options'); - var configPath = config.datadir + '/bitcoin.conf'; - this.bitcoinConfiguration = {}; - - if (!fs.existsSync(config.datadir)) { - mkdirp.sync(config.datadir); - } - - if (!fs.existsSync(configPath)) { - fs.writeFileSync(configPath, Node.DEFAULT_DAEMON_CONFIG); - } - - var file = fs.readFileSync(configPath); - var unparsed = file.toString().split('\n'); - for(var i = 0; i < unparsed.length; i++) { - var line = unparsed[i]; - if (!line.match(/^\#/) && line.match(/\=/)) { - var option = line.split('='); - var value; - if (!Number.isNaN(Number(option[1]))) { - value = Number(option[1]); - } else { - value = option[1]; - } - this.bitcoinConfiguration[option[0]] = value; - } - } - - $.checkState((this.bitcoinConfiguration.txindex && this.bitcoinConfiguration.txindex == 1), - 'Txindex option is required in order to use most of the features of bitcore-node. \ -Please add "txindex=1" to your configuration and reindex an existing database if necessary with reindex=1'); -}; - -Node.prototype._loadBitcoind = function(config) { - var bitcoindConfig = {}; - bitcoindConfig.datadir = config.datadir; - bitcoindConfig.network = config.network; - bitcoindConfig.node = this; - - // start the bitcoind daemon - this.bitcoind = daemon(bitcoindConfig); - -}; - /** * This function will find the common ancestor between the current chain and a forked block, * by moving backwards from the forked block until it meets the current chain. @@ -182,7 +135,7 @@ Node.prototype._syncBitcoindAncestor = function(block, done) { // and thus don't need to find the entire chain of hashes. while(ancestorHash && !currentHashesMap[ancestorHash]) { - var blockIndex = self.bitcoind.getBlockIndex(ancestorHash); + var blockIndex = self.modules.bitcoind.getBlockIndex(ancestorHash); ancestorHash = blockIndex ? blockIndex.prevHash : null; } @@ -276,9 +229,9 @@ Node.prototype._syncBitcoind = function() { async.whilst(function() { height = self.chain.tip.__height; - return height < self.bitcoind.height && !self.stopping; + return height < self.modules.bitcoind.height && !self.stopping; }, function(done) { - self.bitcoind.getBlock(height + 1, function(err, blockBuffer) { + self.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) { if (err) { return done(err); } @@ -340,7 +293,7 @@ Node.prototype._syncBitcoind = function() { self.chain.lastSavedMetadataThreshold = 0; // If bitcoind is completely synced - if (self.bitcoind.isSynced()) { + if (self.modules.bitcoind.isSynced()) { self.emit('synced'); } @@ -433,7 +386,6 @@ Node.prototype._loadAPI = function() { Node.prototype._initialize = function() { var self = this; - this._initializeBitcoind(); this._initializeDatabase(); this._initializeChain(); @@ -445,30 +397,6 @@ Node.prototype._initialize = function() { }); }; -Node.prototype._initializeBitcoind = function() { - var self = this; - - // Notify that there is a new tip - this.bitcoind.on('ready', function() { - log.info('Bitcoin Daemon Ready'); - }); - - // Notify that there is a new tip - this.bitcoind.on('tip', function(height) { - if(!self.stopping) { - var percentage = self.bitcoind.syncPercentage(); - log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); - self._syncBitcoind(); - } - }); - - this.bitcoind.on('error', function(err) { - Error.captureStackTrace(err); - self.emit('error', err); - }); - -}; - Node.prototype._initializeDatabase = function() { var self = this; this.db.on('ready', function() { @@ -482,10 +410,20 @@ Node.prototype._initializeDatabase = function() { }; Node.prototype._initializeChain = function() { + var self = this; this.chain.on('ready', function() { log.info('Bitcoin Chain Ready'); - self._syncBitcoind(); + + // Notify that there is a new tip + self.modules.bitcoind.on('tip', function(height) { + if(!self.stopping) { + var percentage = self.modules.bitcoind.syncPercentage(); + log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); + self._syncBitcoind(); + } + }); + }); this.chain.on('error', function(err) { Error.captureStackTrace(err); @@ -495,10 +433,6 @@ Node.prototype._initializeChain = function() { Node.prototype.getServices = function() { var services = [ - { - name: 'bitcoind', - dependencies: [] - }, { name: 'db', dependencies: ['bitcoind'], @@ -535,6 +469,7 @@ Node.prototype.getServiceOrder = function() { var name = names[i]; var service = servicesByName[name]; + $.checkState(service, 'Required dependency "' + name + '" not available.'); // first add the dependencies addToStack(service.dependencies); @@ -586,7 +521,6 @@ Node.prototype.stop = function(callback) { services, function(service, next) { log.info('Stopping ' + service.name); - if (service.module) { self.modules[service.name].stop(next); } else { diff --git a/lib/scaffold/default-config.js b/lib/scaffold/default-config.js index 81a7e2ba..59a5e48e 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, - modules: ['address'] + modules: ['bitcoind', 'address'] } }; } diff --git a/test/chain.unit.js b/test/chain.unit.js index 6e7e30c1..185aaa66 100644 --- a/test/chain.unit.js +++ b/test/chain.unit.js @@ -30,8 +30,9 @@ describe('Bitcoin Chain', function() { it('should call the callback when base chain is initialized', function(done) { var chain = new Chain(); chain.node = {}; - chain.node.bitcoind = {}; - chain.node.bitcoind.genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); + chain.node.modules = {}; + chain.node.modules.bitcoind = {}; + chain.node.modules.bitcoind.genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); chain.initialize = function() { chain.emit('initialized'); }; @@ -196,7 +197,8 @@ describe('Bitcoin Chain', function() { var chain = new Chain(); chain.node = {}; chain.node.db = {}; - chain.node.bitcoind = { + chain.node.modules = {}; + chain.node.modules.bitcoind = { getBlockIndex: sinon.stub().returns({ chainWork: work }) @@ -211,7 +213,7 @@ describe('Bitcoin Chain', function() { }); it('should give an error if the weight is undefined', function(done) { - chain.node.bitcoind.getBlockIndex = sinon.stub().returns(undefined); + chain.node.modules.bitcoind.getBlockIndex = sinon.stub().returns(undefined); chain.getWeight('hash2', function(err, weight) { should.exist(err); done(); diff --git a/test/db.unit.js b/test/db.unit.js index a7a7c4c8..7d2a5b5b 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -10,8 +10,6 @@ var Block = bitcore.Block; var transactionData = require('./data/bitcoin-transactions.json'); var errors = index.errors; var memdown = require('memdown'); -var inherits = require('util').inherits; -var BaseModule = require('../lib/module'); var bitcore = require('bitcore'); var Transaction = bitcore.Transaction; @@ -21,7 +19,8 @@ describe('Bitcoin DB', function() { it('should emit ready', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { on: sinon.spy() }; db.addModule = sinon.spy(); @@ -51,7 +50,8 @@ describe('Bitcoin DB', function() { it('will return a NotFound error', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getTransaction: sinon.stub().callsArgWith(2, null, null) }; var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; @@ -63,7 +63,8 @@ describe('Bitcoin DB', function() { it('will return an error from bitcoind', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getTransaction: sinon.stub().callsArgWith(2, new Error('test error')) }; var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; @@ -75,7 +76,8 @@ describe('Bitcoin DB', function() { it('will return an error from bitcoind', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getTransaction: sinon.stub().callsArgWith(2, null, new Buffer(transactionData[0].hex, 'hex')) }; var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; @@ -94,7 +96,8 @@ describe('Bitcoin DB', function() { var blockBuffer = new Buffer(blockData, 'hex'); var expectedBlock = Block.fromBuffer(blockBuffer); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer) }; @@ -107,8 +110,9 @@ describe('Bitcoin DB', function() { }); it('should give an error when bitcoind.js gives an error', function(done) { db.node = {}; - db.node.bitcoind = {}; - db.node.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error')); + db.node.modules = {}; + db.node.modules.bitcoind = {}; + db.node.modules.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error')); db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { should.exist(err); err.message.should.equal('error'); @@ -131,7 +135,8 @@ describe('Bitcoin DB', function() { it('should return prevHash from bitcoind', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getBlockIndex: sinon.stub().returns({ prevHash: 'prevhash' }) @@ -147,7 +152,8 @@ describe('Bitcoin DB', function() { it('should give an error if bitcoind could not find it', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getBlockIndex: sinon.stub().returns(null) }; @@ -169,7 +175,8 @@ describe('Bitcoin DB', function() { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, info) }; @@ -183,7 +190,8 @@ describe('Bitcoin DB', function() { it('should give an error if one occurred', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('error')) }; @@ -198,7 +206,8 @@ describe('Bitcoin DB', function() { it('should give the txid on success', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { sendTransaction: sinon.stub().returns('txid') }; @@ -212,7 +221,8 @@ describe('Bitcoin DB', function() { it('should give an error if bitcoind threw an error', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { sendTransaction: sinon.stub().throws(new Error('error')) }; @@ -228,14 +238,15 @@ describe('Bitcoin DB', function() { it('should pass along the fee from bitcoind', function(done) { var db = new DB({store: memdown}); db.node = {}; - db.node.bitcoind = { + db.node.modules = {}; + db.node.modules.bitcoind = { estimateFee: sinon.stub().returns(1000) }; db.estimateFee(5, function(err, fee) { should.not.exist(err); fee.should.equal(1000); - db.node.bitcoind.estimateFee.args[0][0].should.equal(5); + db.node.modules.bitcoind.estimateFee.args[0][0].should.equal(5); done(); }); }); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index 6031de9b..b8ae8d33 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -11,15 +11,14 @@ var errors = bitcorenode.errors; var levelup = require('levelup'); var mockdb = { - bitcoind: { - on: sinon.stub() - } }; var mocknode = { db: mockdb, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; @@ -103,12 +102,6 @@ describe('AddressModule', function() { describe('#blockHandler', function() { var am; - var db = { - bitcoind: { - on: sinon.stub() - } - }; - var testBlock = bitcore.Block.fromString(blockData); var data = [ @@ -212,12 +205,6 @@ describe('AddressModule', function() { }); }); it('should continue if output script is null', function(done) { - var db = { - bitcoind: { - on: sinon.stub() - } - }; - var am = new AddressModule({node: mocknode, network: 'livenet'}); var block = { @@ -247,15 +234,13 @@ describe('AddressModule', function() { }); it('will call event handlers', function() { var testBlock = bitcore.Block.fromString(blockData); - var db = { - bitcoind: { - on: sinon.stub() - } - }; + var db = {}; var testnode = { db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; var am = new AddressModule({node: testnode, network: 'livenet'}); @@ -444,8 +429,10 @@ describe('AddressModule', function() { } }, db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; @@ -467,7 +454,7 @@ describe('AddressModule', function() { blockHeight: 352532 } ]; - am.node.bitcoind = { + am.node.modules.bitcoind = { getMempoolOutputs: sinon.stub().returns(mempoolOutputs) }; @@ -535,15 +522,13 @@ describe('AddressModule', function() { 'addr3': ['utxo3'] }; - var db = { - bitcoind: { - on: sinon.spy() - } - }; + var db = {}; var testnode = { db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; var am = new AddressModule({node: testnode}); @@ -570,14 +555,18 @@ describe('AddressModule', function() { }; var db = { - bitcoind: { - on: sinon.spy() + modules: { + bitcoind: { + on: sinon.spy() + } } }; var testnode = { db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; var am = new AddressModule({node: testnode}); @@ -604,15 +593,13 @@ describe('AddressModule', function() { 'addr3': ['utxo3'] }; - var db = { - bitcoind: { - on: sinon.spy() - } - }; + var db = {}; var testnode = { db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; var am = new AddressModule({node: testnode}); @@ -721,20 +708,18 @@ describe('AddressModule', function() { describe('#isSpent', function() { var am; - var db = { - bitcoind: { - on: sinon.stub() - } - }; + var db = {}; var testnode = { db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; before(function() { am = new AddressModule({node: testnode}); - am.node.bitcoind = { + am.node.modules.bitcoind = { isSpent: sinon.stub().returns(true), on: sinon.stub() }; @@ -757,8 +742,10 @@ describe('AddressModule', function() { }; var testnode = { db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; var am = new AddressModule({node: testnode}); @@ -875,8 +862,10 @@ describe('AddressModule', function() { } }, db: db, - bitcoind: { - on: sinon.stub() + modules: { + bitcoind: { + on: sinon.stub() + } } }; var am = new AddressModule({node: testnode}); diff --git a/test/modules/bitcoind.unit.js b/test/modules/bitcoind.unit.js new file mode 100644 index 00000000..8dc099b2 --- /dev/null +++ b/test/modules/bitcoind.unit.js @@ -0,0 +1,50 @@ +'use strict'; + +var should = require('chai').should(); +var proxyquire = require('proxyquire'); +var fs = require('fs'); +var sinon = require('sinon'); +var BitcoinModule = proxyquire('../../lib/modules/bitcoind', { + fs: { + readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/bitcoin.conf')) + } +}); +var BadBitcoin = proxyquire('../../lib/modules/bitcoind', { + fs: { + readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/badbitcoin.conf')) + } +}); + +describe('Bitcoin Module', function() { + var baseConfig = { + node: { + datadir: 'testdir', + network: { + name: 'regtest' + } + } + }; + describe('#_loadConfiguration', function() { + it('will parse a bitcoin.conf file', function() { + var bitcoind = new BitcoinModule(baseConfig); + bitcoind._loadConfiguration({datadir: process.env.HOME + '/.bitcoin'}); + should.exist(bitcoind.configuration); + bitcoind.configuration.should.deep.equal({ + server: 1, + whitelist: '127.0.0.1', + txindex: 1, + port: 20000, + rpcallowip: '127.0.0.1', + rpcuser: 'bitcoin', + rpcpassword: 'local321' + }); + }); + it('should throw an exception if txindex isn\'t enabled in the configuration', function() { + var bitcoind = new BadBitcoin(baseConfig); + (function() { + bitcoind._loadConfiguration({datadir: './test'}); + }).should.throw('Txindex option'); + }); + }); +}); + diff --git a/test/node.unit.js b/test/node.unit.js index 3f07475e..78672ae6 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -11,15 +11,17 @@ var blockData = require('./data/livenet-345003.json'); var proxyquire = require('proxyquire'); 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('Bitcore Node', function() { + var baseConfig = { + datadir: 'testdir' + }; + var Node; - var BadNode; function hexlebuf(hexString){ return BufferUtil.reverse(new Buffer(hexString, 'hex')); @@ -30,23 +32,9 @@ describe('Bitcore Node', function() { } before(function() { - - BadNode = proxyquire('../lib/node', { - fs: { - readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/data/badbitcoin.conf')) - } - }); - BadNode.prototype._loadConfiguration = sinon.spy(); - BadNode.prototype._initialize = sinon.spy(); - - Node = proxyquire('../lib/node', { - fs: { - readFileSync: sinon.stub().returns(bitcoinConfBuffer) - } - }); + Node = proxyquire('../lib/node', {}); Node.prototype._loadConfiguration = sinon.spy(); Node.prototype._initialize = sinon.spy(); - }); describe('@constructor', function() { @@ -60,6 +48,7 @@ describe('Bitcore Node', function() { ]; }; var config = { + datadir: 'testdir', modules: [ { name: 'test1', @@ -81,7 +70,7 @@ describe('Bitcore Node', function() { describe('#openBus', function() { it('will create a new bus', function() { - var node = new Node({}); + var node = new Node(baseConfig); var bus = node.openBus(); bus.node.should.equal(node); }); @@ -89,7 +78,7 @@ describe('Bitcore Node', function() { describe('#addModule', function() { it('will instantiate an instance and load api methods', function() { - var node = new Node({}); + var node = new Node(baseConfig); function TestModule() {} util.inherits(TestModule, BaseModule); TestModule.prototype.getData = function() {}; @@ -110,7 +99,7 @@ describe('Bitcore Node', function() { describe('#getAllAPIMethods', function() { it('should return db methods and modules methods', function() { - var node = new Node({}); + var node = new Node(baseConfig); node.modules = { module1: { getAPIMethods: sinon.stub().returns(['mda1', 'mda2']) @@ -130,7 +119,7 @@ describe('Bitcore Node', function() { }); describe('#getAllPublishEvents', function() { it('should return modules publish events', function() { - var node = new Node({}); + var node = new Node(baseConfig); node.modules = { module1: { getPublishEvents: sinon.stub().returns(['mda1', 'mda2']) @@ -150,62 +139,20 @@ describe('Bitcore Node', function() { }); describe('#_loadConfiguration', function() { it('should call the necessary methods', function() { - var TestNode = proxyquire('../lib/node', { - fs: { - readFileSync: sinon.stub().returns(bitcoinConfBuffer) - } - }); + var TestNode = proxyquire('../lib/node', {}); TestNode.prototype._initialize = sinon.spy(); - TestNode.prototype._loadBitcoinConf = sinon.spy(); - TestNode.prototype._loadBitcoind = sinon.spy(); TestNode.prototype._loadDB = sinon.spy(); TestNode.prototype._loadAPI = sinon.spy(); TestNode.prototype._loadConsensus = sinon.spy(); - var node = new TestNode({}); - node._loadBitcoind.callCount.should.equal(1); - node._loadBitcoinConf.callCount.should.equal(1); + var node = new TestNode(baseConfig); node._loadDB.callCount.should.equal(1); node._loadAPI.callCount.should.equal(1); node._loadConsensus.callCount.should.equal(1); }); }); - describe('#_loadBitcoinConf', function() { - it('will parse a bitcoin.conf file', function() { - var node = new Node({}); - node._loadBitcoinConf({datadir: process.env.HOME + '/.bitcoin'}); - should.exist(node.bitcoinConfiguration); - node.bitcoinConfiguration.should.deep.equal({ - server: 1, - whitelist: '127.0.0.1', - txindex: 1, - port: 20000, - rpcallowip: '127.0.0.1', - rpcuser: 'bitcoin', - rpcpassword: 'local321' - }); - }); - }); - describe('#_loadBitcoind', function() { - it('should initialize', function() { - var node = new Node({}); - node._loadBitcoind({datadir: './test'}); - should.exist(node.bitcoind); - }); - it('should initialize with testnet', function() { - var node = new Node({}); - node._loadBitcoind({datadir: './test', testnet: true}); - should.exist(node.bitcoind); - }); - it('should throw an exception if txindex isn\'t enabled in the configuration', function() { - var node = new BadNode({}); - (function() { - node._loadBitcoinConf({datadir: './test'}); - }).should.throw('Txindex option'); - }); - }); describe('#_syncBitcoindAncestor', function() { it('will find an ancestor 6 deep', function() { - var node = new Node({}); + var node = new Node(baseConfig); node.chain = { getHashes: function(tipHash, callback) { callback(null, chainHashes); @@ -248,7 +195,8 @@ describe('Bitcore Node', function() { } }, }; - node.bitcoind = { + node.modules = {}; + node.modules.bitcoind = { getBlockIndex: function(hash) { var block = forkedBlocks[hash]; return { @@ -267,7 +215,7 @@ describe('Bitcore Node', function() { }); describe('#_syncBitcoindRewind', function() { it('will undo blocks 6 deep', function() { - var node = new Node({}); + var node = new Node(baseConfig); var ancestorHash = chainHashes[chainHashes.length - 6]; node.chain = { tip: { @@ -317,10 +265,11 @@ describe('Bitcore Node', function() { }); describe('#_syncBitcoind', function() { it('will get and add block up to the tip height', function(done) { - var node = new Node({}); + var node = new Node(baseConfig); var blockBuffer = new Buffer(blockData, 'hex'); var block = Block.fromBuffer(blockBuffer); - node.bitcoind = { + node.modules = {}; + node.modules.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), isSynced: sinon.stub().returns(true), height: 1 @@ -349,8 +298,9 @@ describe('Bitcore Node', function() { node._syncBitcoind(); }); it('will exit and emit error with error from bitcoind.getBlock', function(done) { - var node = new Node({}); - node.bitcoind = { + var node = new Node(baseConfig); + node.modules = {}; + node.modules.bitcoind = { getBlock: sinon.stub().callsArgWith(1, new Error('test error')), height: 1 }; @@ -366,10 +316,11 @@ describe('Bitcore Node', function() { node._syncBitcoind(); }); it('will stop syncing when the node is stopping', function(done) { - var node = new Node({}); + var node = new Node(baseConfig); var blockBuffer = new Buffer(blockData, 'hex'); var block = Block.fromBuffer(blockBuffer); - node.bitcoind = { + node.modules = {}; + node.modules.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), isSynced: sinon.stub().returns(true), height: 1 @@ -411,6 +362,7 @@ describe('Bitcore Node', function() { describe('#_loadNetwork', function() { it('should use the testnet network if testnet is specified', function() { var config = { + datadir: 'testdir', network: 'testnet' }; var node = new Node(config); @@ -419,6 +371,7 @@ describe('Bitcore Node', function() { }); it('should use the regtest network if regtest is specified', function() { var config = { + datadir: 'testdir', network: 'regtest' }; var node = new Node(config); @@ -426,7 +379,9 @@ describe('Bitcore Node', function() { node.network.name.should.equal('regtest'); }); it('should use the livenet network if nothing is specified', function() { - var config = {}; + var config = { + datadir: 'testdir' + }; var node = new Node(config); node._loadNetwork(config); node.network.name.should.equal('livenet'); @@ -526,7 +481,7 @@ describe('Bitcore Node', function() { var node; before(function() { - node = new Node({}); + node = new Node(baseConfig); }); it('will set properties', function() { @@ -541,11 +496,7 @@ describe('Bitcore Node', function() { var node; before(function() { - var TestNode = proxyquire('../lib/node', { - fs: { - readFileSync: sinon.stub().returns(bitcoinConfBuffer) - } - }); + var TestNode = proxyquire('../lib/node', {}); TestNode.prototype._loadConfiguration = sinon.spy(); TestNode.prototype._initializeBitcoind = sinon.spy(); TestNode.prototype._initializeDatabase = sinon.spy(); @@ -555,7 +506,7 @@ describe('Bitcore Node', function() { var _initialize = TestNode.prototype._initialize; TestNode.prototype._initialize = sinon.spy(); - node = new TestNode({}); + node = new TestNode(baseConfig); node.chain = { on: sinon.spy() }; @@ -581,7 +532,6 @@ describe('Bitcore Node', function() { node._initialize(); // event handlers - node._initializeBitcoind.callCount.should.equal(1); node._initializeDatabase.callCount.should.equal(1); node._initializeChain.callCount.should.equal(1); @@ -599,51 +549,9 @@ describe('Bitcore Node', function() { }); - describe('#_initalizeBitcoind', function() { - - it('will call emit an error from libbitcoind', function(done) { - var node = new Node({}); - node.bitcoind = new EventEmitter(); - node.on('error', function(err) { - should.exist(err); - err.message.should.equal('test error'); - done(); - }); - node._initializeBitcoind(); - node.bitcoind.emit('error', new Error('test error')); - }); - it('will call sync when there is a new tip', function(done) { - var node = new Node({}); - node.bitcoind = new EventEmitter(); - node.bitcoind.syncPercentage = sinon.spy(); - node._syncBitcoind = function() { - node.bitcoind.syncPercentage.callCount.should.equal(1); - done(); - }; - node._initializeBitcoind(); - node.bitcoind.emit('tip', 10); - }); - it('will not call sync when there is a new tip and shutting down', function(done) { - var node = new Node({}); - node.bitcoind = new EventEmitter(); - node._syncBitcoind = sinon.spy(); - node.bitcoind.syncPercentage = sinon.spy(); - node.stopping = true; - node.bitcoind.on('tip', function() { - setImmediate(function() { - node.bitcoind.syncPercentage.callCount.should.equal(0); - node._syncBitcoind.callCount.should.equal(0); - done(); - }); - }); - node._initializeBitcoind(); - node.bitcoind.emit('tip', 10); - }); - }); - describe('#_initializeDatabase', function() { it('will log on ready event', function(done) { - var node = new Node({}); + var node = new Node(baseConfig); node.db = new EventEmitter(); sinon.stub(index.log, 'info'); node.db.on('ready', function() { @@ -657,7 +565,7 @@ describe('Bitcore Node', function() { node.db.emit('ready'); }); it('will call emit an error from db', function(done) { - var node = new Node({}); + var node = new Node(baseConfig); node.db = new EventEmitter(); node.on('error', function(err) { should.exist(err); @@ -670,21 +578,42 @@ describe('Bitcore Node', function() { }); describe('#_initializeChain', function() { - it('will call _syncBitcoind on ready', function(done) { - var node = new Node({}); - node._syncBitcoind = sinon.spy(); + + it('will call sync when there is a new tip', function(done) { + var node = new Node(baseConfig); node.chain = new EventEmitter(); - node.chain.on('ready', function(err) { + node.modules = {}; + node.modules.bitcoind = new EventEmitter(); + node.modules.bitcoind.syncPercentage = sinon.spy(); + node._syncBitcoind = function() { + node.modules.bitcoind.syncPercentage.callCount.should.equal(1); + done(); + }; + node._initializeChain(); + node.chain.emit('ready'); + node.modules.bitcoind.emit('tip', 10); + }); + it('will not call sync when there is a new tip and shutting down', function(done) { + var node = new Node(baseConfig); + node.chain = new EventEmitter(); + node.modules = {}; + node.modules.bitcoind = new EventEmitter(); + node._syncBitcoind = sinon.spy(); + node.modules.bitcoind.syncPercentage = sinon.spy(); + node.stopping = true; + node.modules.bitcoind.on('tip', function() { setImmediate(function() { - node._syncBitcoind.callCount.should.equal(1); + node.modules.bitcoind.syncPercentage.callCount.should.equal(0); + node._syncBitcoind.callCount.should.equal(0); done(); }); }); node._initializeChain(); node.chain.emit('ready'); + node.modules.bitcoind.emit('tip', 10); }); it('will emit an error from the chain', function(done) { - var node = new Node({}); + var node = new Node(baseConfig); node.chain = new EventEmitter(); node.on('error', function(err) { should.exist(err); @@ -698,7 +627,7 @@ describe('Bitcore Node', function() { describe('#getServiceOrder', function() { it('should return the services in the correct order', function() { - var node = new Node({}); + var node = new Node(baseConfig); node.getServices = function() { return [ { @@ -729,7 +658,7 @@ describe('Bitcore Node', function() { describe('#start', function() { it('will call start for each module', function(done) { - var node = new Node({}); + var node = new Node(baseConfig); function TestModule() {} util.inherits(TestModule, BaseModule); TestModule.prototype.start = sinon.stub().callsArg(0); @@ -760,7 +689,7 @@ describe('Bitcore Node', function() { describe('#stop', function() { it('will call stop for each module', function(done) { - var node = new Node({}); + var node = new Node(baseConfig); function TestModule() {} util.inherits(TestModule, BaseModule); TestModule.prototype.stop = sinon.stub().callsArg(0); From df9b62accab2209a9649c6a4941652b1cdd41c24 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Mon, 31 Aug 2015 09:00:00 -0400 Subject: [PATCH 2/5] Convert db into a db module. --- index.js | 2 +- integration/regtest-node.js | 6 + lib/chain.js | 21 +- lib/modules/address.js | 16 +- lib/modules/bitcoind.js | 7 +- lib/{ => modules}/db.js | 398 +++++++++++++++------------------ lib/node.js | 78 +------ lib/scaffold/default-config.js | 2 +- test/chain.unit.js | 55 ++--- test/modules/address.unit.js | 25 +-- test/{ => modules}/db.unit.js | 228 ++++++++++--------- test/node.unit.js | 171 ++------------ test/transaction.unit.js | 4 - 13 files changed, 386 insertions(+), 627 deletions(-) rename lib/{ => modules}/db.js (61%) rename test/{ => modules}/db.unit.js (70%) diff --git a/index.js b/index.js index 7233605e..1d07625d 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,6 @@ module.exports = require('./lib'); module.exports.Node = require('./lib/node'); module.exports.Chain = require('./lib/chain'); -module.exports.DB = require('./lib/db'); module.exports.Transaction = require('./lib/transaction'); module.exports.Module = require('./lib/module'); module.exports.errors = require('./lib/errors'); @@ -11,6 +10,7 @@ module.exports.errors = require('./lib/errors'); module.exports.modules = {}; module.exports.modules.AddressModule = require('./lib/modules/address'); module.exports.modules.BitcoinModule = require('./lib/modules/bitcoind'); +module.exports.modules.DBModule = require('./lib/modules/db'); module.exports.scaffold = {}; module.exports.scaffold.create = require('./lib/scaffold/create'); diff --git a/integration/regtest-node.js b/integration/regtest-node.js index ef2bcaa1..398704e9 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -27,6 +27,7 @@ var index = require('..'); var BitcoreNode = index.Node; var AddressModule = index.modules.AddressModule; var BitcoinModule = index.modules.BitcoinModule; +var DBModule = index.modules.DBModule; var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG'; var testKey; var client; @@ -65,6 +66,11 @@ describe('Node Functionality', function() { datadir: datadir, network: 'regtest', modules: [ + { + name: 'db', + module: DBModule, + dependencies: DBModule.dependencies + }, { name: 'bitcoind', module: BitcoinModule, diff --git a/lib/chain.js b/lib/chain.js index ceb54e66..81690a81 100644 --- a/lib/chain.js +++ b/lib/chain.js @@ -99,30 +99,25 @@ Chain.prototype.initialize = function() { var self = this; // Does our database already have a tip? - self.node.db.getMetadata(function getMetadataCallback(err, metadata) { + self.node.modules.db.getMetadata(function getMetadataCallback(err, metadata) { if(err) { return self.emit('error', err); } else if(!metadata || !metadata.tip) { self.tip = self.genesis; self.tip.__height = 0; self.tip.__weight = self.genesisWeight; - self.node.db.putBlock(self.genesis, function putBlockCallback(err) { + self.node.modules.db.connectBlock(self.genesis, function(err) { if(err) { return self.emit('error', err); } - self.node.db._onChainAddBlock(self.genesis, function(err) { - if(err) { - return self.emit('error', err); - } - self.emit('addblock', self.genesis); - self.saveMetadata(); - self.emit('initialized'); - }); + self.emit('addblock', self.genesis); + self.saveMetadata(); + self.emit('initialized'); }); } else { metadata.tip = metadata.tip; - self.node.db.getBlock(metadata.tip, function getBlockCallback(err, tip) { + self.node.modules.db.getBlock(metadata.tip, function getBlockCallback(err, tip) { if(err) { return self.emit('error', err); } @@ -215,7 +210,7 @@ Chain.prototype.getHashes = function getHashes(tipHash, callback) { } } else { // do a db call if we don't have it - self.node.db.getPrevHash(hash, function(err, prevHash) { + self.node.modules.db.getPrevHash(hash, function(err, prevHash) { if(err) { return callback(err); } @@ -246,7 +241,7 @@ Chain.prototype.saveMetadata = function saveMetadata(callback) { self.lastSavedMetadata = new Date(); - self.node.db.putMetadata(metadata, callback); + self.node.modules.db.putMetadata(metadata, callback); }; module.exports = Chain; diff --git a/lib/modules/address.js b/lib/modules/address.js index 36e0f738..ba47848f 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -82,10 +82,10 @@ AddressModule.prototype.transactionOutputHandler = function(messages, tx, output } // Find the address for the output - var address = script.toAddress(this.node.db.network); + var address = script.toAddress(this.node.network); if (!address && script.isPublicKeyOut()) { var pubkey = script.chunks[0].buf; - address = Address.fromPublicKey(new PublicKey(pubkey), this.node.db.network); + address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network); } else if (!address){ return; } @@ -162,10 +162,10 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { continue; } - var address = script.toAddress(this.node.db.network); + var address = script.toAddress(this.node.network); if (!address && script.isPublicKeyOut()) { var pubkey = script.chunks[0].buf; - address = Address.fromPublicKey(new PublicKey(pubkey), this.node.db.network); + address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network); } else if (!address){ continue; } @@ -329,7 +329,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback var outputs = []; var key = [AddressModule.PREFIXES.OUTPUTS, addressStr].join('-'); - var stream = this.node.db.store.createReadStream({ + var stream = this.node.modules.db.store.createReadStream({ start: key, end: key + '~' }); @@ -443,7 +443,7 @@ AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, call var self = this; var key = [AddressModule.PREFIXES.SPENTS, txid, outputIndex].join('-'); - this.node.db.store.get(key, function(err, value) { + this.node.modules.db.store.get(key, function(err, value) { if(err) { return callback(err); } @@ -492,12 +492,12 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem return callback(null, txinfos[txid]); } - self.node.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) { + self.node.modules.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) { if(err) { return callback(err); } - transaction.populateInputs(self.node.db, [], function(err) { + transaction.populateInputs(self.node.modules.db, [], function(err) { if(err) { return callback(err); } diff --git a/lib/modules/bitcoind.js b/lib/modules/bitcoind.js index 433c8910..96c791cd 100644 --- a/lib/modules/bitcoind.js +++ b/lib/modules/bitcoind.js @@ -1,14 +1,15 @@ 'use strict'; var util = require('util'); -var Module = require('../module'); var bindings = require('bindings')('bitcoind.node'); var mkdirp = require('mkdirp'); var fs = require('fs'); -var index = require('../'); -var log = index.log; var bitcore = require('bitcore'); var $ = bitcore.util.preconditions; +var index = require('../'); +var log = index.log; +var Module = require('../module'); + /** * Provides an interface to native bindings to Bitcoin Core diff --git a/lib/db.js b/lib/modules/db.js similarity index 61% rename from lib/db.js rename to lib/modules/db.js index b6f3d3a2..b5b5ff7f 100644 --- a/lib/db.js +++ b/lib/modules/db.js @@ -1,78 +1,81 @@ 'use strict'; -var EventEmitter = require('events').EventEmitter; var util = require('util'); +var fs = require('fs'); var async = require('async'); var levelup = require('levelup'); var leveldown = require('leveldown'); +var mkdirp = require('mkdirp'); var bitcore = require('bitcore'); +var Networks = bitcore.Networks; var Block = bitcore.Block; var $ = bitcore.util.preconditions; -var index = require('./'); +var index = require('../'); var errors = index.errors; var log = index.log; -var Transaction = require('./transaction'); +var Transaction = require('../transaction'); +var Module = require('../module'); +/** + * Represents the current state of the bitcoin blockchain transaction data. Other modules + * can extend the data that is indexed by implementing a `blockHandler` method. + * + * @param {Object} options + * @param {String} options.datadir - The bitcoin data directory + * @param {Node} options.node - A reference to the node + */ function DB(options) { - /* jshint maxstatements: 30 */ - /* jshint maxcomplexity: 20 */ - if (!(this instanceof DB)) { return new DB(options); } - if(!options) { + if (!options) { options = {}; } - this.coinbaseAmount = options.coinbaseAmount || 50 * 1e8; + Module.call(this, options); - var levelupStore = leveldown; + $.checkState(this.node.network, 'Node is expected to have a "network" property'); + this.network = this.node.network; - if(options.store) { - levelupStore = options.store; - } else if(!options.path) { - throw new Error('Please include database path in options'); + this._setDataPath(); + + this.levelupStore = leveldown; + if (options.store) { + this.levelupStore = options.store; } - this.store = levelup(options.path, { db: levelupStore }); - this.txPrefix = options.txPrefix || DB.PREFIXES.TX; - this.prevHashPrefix = options.prevHashPrefix || DB.PREFIXES.PREV_HASH; - this.blockPrefix = options.blockPrefix || DB.PREFIXES.BLOCK; - this.dataPrefix = options.dataPrefix || DB.PREFIXES.DATA; - this.weightPrefix = options.weightPrefix || DB.PREFIXES.WEIGHT; - this.Transaction = Transaction; - - this.coinbaseAddress = options.coinbaseAddress; - this.coinbaseAmount = options.coinbaseAmount || 50 * 1e8; - this.Transaction = Transaction; - - this.network = bitcore.Networks.get(options.network) || bitcore.Networks.testnet; - - this.node = options.node; - this.subscriptions = { transaction: [], block: [] }; } -DB.PREFIXES = { - TX: 'tx', - PREV_HASH: 'ph', - BLOCK: 'blk', - DATA: 'data', - WEIGHT: 'wt' -}; +util.inherits(DB, Module); -util.inherits(DB, EventEmitter); +DB.dependencies = ['bitcoind']; -DB.prototype.initialize = function() { - this.emit('ready'); +DB.prototype._setDataPath = function() { + $.checkState(this.node.datadir, 'Node is expected to have a "datadir" property'); + var regtest = Networks.get('regtest'); + if (this.node.network === Networks.livenet) { + this.dataPath = this.node.datadir + '/bitcore-node.db'; + } else if (this.node.network === Networks.testnet) { + this.dataPath = this.node.datadir + '/testnet3/bitcore-node.db'; + } else if (this.node.network === regtest) { + this.dataPath = this.node.datadir + '/regtest/bitcore-node.db'; + } else { + throw new Error('Unknown network: ' + this.network); + } }; DB.prototype.start = function(callback) { + if (!fs.existsSync(this.dataPath)) { + mkdirp.sync(this.dataPath); + } + this.store = levelup(this.dataPath, { db: this.levelupStore }); this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this)); this.emit('ready'); + log.info('Bitcoin Database Ready'); setImmediate(callback); }; @@ -89,40 +92,50 @@ DB.prototype.getInfo = function(callback) { }); }; -DB.prototype.getBlock = function(hash, callback) { - var self = this; +DB.prototype.transactionHandler = function(txInfo) { + var tx = Transaction().fromBuffer(txInfo.buffer); + for (var i = 0; i < this.subscriptions.transaction.length; i++) { + this.subscriptions.transaction[i].emit('transaction', { + rejected: !txInfo.mempool, + tx: tx + }); + } +}; - // get block from bitcoind +/** + * Closes the underlying store database + * @param {Function} callback - A function that accepts: Error + */ +DB.prototype.close = function(callback) { + this.store.close(callback); +}; + +DB.prototype.getAPIMethods = function() { + var methods = [ + ['getBlock', this, this.getBlock, 1], + ['getTransaction', this, this.getTransaction, 2], + ['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2], + ['sendTransaction', this, this.sendTransaction, 1], + ['estimateFee', this, this.estimateFee, 1] + ]; + return methods; +}; + +DB.prototype.getBlock = function(hash, callback) { this.node.modules.bitcoind.getBlock(hash, function(err, blockData) { - if(err) { + if (err) { return callback(err); } callback(null, Block.fromBuffer(blockData)); }); }; -DB.prototype.getPrevHash = function(blockHash, callback) { - var blockIndex = this.node.modules.bitcoind.getBlockIndex(blockHash); - setImmediate(function() { - if (blockIndex) { - callback(null, blockIndex.prevHash); - } else { - callback(new Error('Could not get prevHash, block not found')); - } - }); -}; - -DB.prototype.putBlock = function(block, callback) { - // block is already stored in bitcoind - setImmediate(callback); -}; - DB.prototype.getTransaction = function(txid, queryMempool, callback) { this.node.modules.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { - if(err) { + if (err) { return callback(err); } - if(!txBuffer) { + if (!txBuffer) { return callback(new errors.Transaction.NotFound()); } @@ -132,7 +145,7 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) { DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { this.node.modules.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { - if(err) { + if (err) { return callback(err); } @@ -145,7 +158,7 @@ DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback }; DB.prototype.sendTransaction = function(tx, callback) { - if(tx instanceof this.Transaction) { + if (tx instanceof Transaction) { tx = tx.toString(); } $.checkArgument(typeof tx === 'string', 'Argument must be a hex string or Transaction'); @@ -160,153 +173,11 @@ DB.prototype.sendTransaction = function(tx, callback) { DB.prototype.estimateFee = function(blocks, callback) { var self = this; - setImmediate(function() { callback(null, self.node.modules.bitcoind.estimateFee(blocks)); }); }; -DB.prototype.validateBlockData = function(block, callback) { - // bitcoind does the validation - setImmediate(callback); -}; - -DB.prototype._updatePrevHashIndex = function(block, callback) { - // bitcoind has the previous hash for each block - setImmediate(callback); -}; - -DB.prototype._updateWeight = function(hash, weight, callback) { - // bitcoind has all work for each block - setImmediate(callback); -}; - -/** - * Saves metadata to the database - * @param {Object} metadata - The metadata - * @param {Function} callback - A function that accepts: Error - */ -DB.prototype.putMetadata = function(metadata, callback) { - this.store.put('metadata', JSON.stringify(metadata), {}, callback); -}; - -/** - * Retrieves metadata from the database - * @param {Function} callback - A function that accepts: Error and Object - */ -DB.prototype.getMetadata = function(callback) { - var self = this; - - self.store.get('metadata', {}, function(err, data) { - if(err instanceof levelup.errors.NotFoundError) { - return callback(null, {}); - } else if(err) { - return callback(err); - } - - var metadata; - try { - metadata = JSON.parse(data); - } catch(e) { - return callback(new Error('Could not parse metadata')); - } - - callback(null, metadata); - }); -}; - -/** - * Closes the underlying store database - * @param {Function} callback - A function that accepts: Error - */ -DB.prototype.close = function(callback) { - this.store.close(callback); -}; - -DB.prototype.getOutputTotal = function(transactions, excludeCoinbase) { - var totals = transactions.map(function(tx) { - if(tx.isCoinbase() && excludeCoinbase) { - return 0; - } else { - return tx._getOutputAmount(); - } - }); - var grandTotal = totals.reduce(function(previousValue, currentValue) { - return previousValue + currentValue; - }); - return grandTotal; -}; - -DB.prototype.getInputTotal = function(transactions) { - var totals = transactions.map(function(tx) { - if(tx.isCoinbase()) { - return 0; - } else { - return tx._getInputAmount(); - } - }); - var grandTotal = totals.reduce(function(previousValue, currentValue) { - return previousValue + currentValue; - }); - return grandTotal; -}; - -DB.prototype._onChainAddBlock = function(block, callback) { - log.debug('DB handling new chain block'); - - this.blockHandler(block, true, callback); -}; - -DB.prototype._onChainRemoveBlock = function(block, callback) { - log.debug('DB removing chain block'); - this.blockHandler(block, false, callback); -}; - -DB.prototype.blockHandler = function(block, add, callback) { - var self = this; - var operations = []; - - // Notify block subscribers - for(var i = 0; i < this.subscriptions.block.length; i++) { - this.subscriptions.block[i].emit('block', block.hash); - } - - async.eachSeries( - this.node.modules, - function(bitcoreNodeModule, next) { - bitcoreNodeModule.blockHandler.call(bitcoreNodeModule, block, add, function(err, ops) { - if(err) { - return next(err); - } - if (ops) { - operations = operations.concat(ops); - } - next(); - }); - }, - function(err) { - if (err) { - return callback(err); - } - - log.debug('Updating the database with operations', operations); - - self.store.batch(operations, callback); - } - ); -}; - -DB.prototype.getAPIMethods = function() { - var methods = [ - ['getInfo', this, this.getInfo, 0], - ['getBlock', this, this.getBlock, 1], - ['getTransaction', this, this.getTransaction, 2], - ['sendTransaction', this, this.sendTransaction, 1], - ['estimateFee', this, this.estimateFee, 1] - ]; - return methods; -}; - DB.prototype.getPublishEvents = function() { return [ { @@ -330,19 +201,120 @@ DB.prototype.subscribe = function(name, emitter) { DB.prototype.unsubscribe = function(name, emitter) { var index = this.subscriptions[name].indexOf(emitter); - if(index > -1) { + if (index > -1) { this.subscriptions[name].splice(index, 1); } }; -DB.prototype.transactionHandler = function(txInfo) { - var tx = bitcore.Transaction().fromBuffer(txInfo.buffer); - for(var i = 0; i < this.subscriptions.transaction.length; i++) { - this.subscriptions.transaction[i].emit('transaction', { - rejected: !txInfo.mempool, - tx: tx - }); +/** + * Will give the previous hash for a block. + * @param {String} blockHash + * @param {Function} callback + */ +DB.prototype.getPrevHash = function(blockHash, callback) { + var blockIndex = this.node.modules.bitcoind.getBlockIndex(blockHash); + setImmediate(function() { + if (blockIndex) { + callback(null, blockIndex.prevHash); + } else { + callback(new Error('Could not get prevHash, block not found')); + } + }); +}; + +/** + * Saves metadata to the database + * @param {Object} metadata - The metadata + * @param {Function} callback - A function that accepts: Error + */ +DB.prototype.putMetadata = function(metadata, callback) { + this.store.put('metadata', JSON.stringify(metadata), {}, callback); +}; + +/** + * Retrieves metadata from the database + * @param {Function} callback - A function that accepts: Error and Object + */ +DB.prototype.getMetadata = function(callback) { + var self = this; + + self.store.get('metadata', {}, function(err, data) { + if (err instanceof levelup.errors.NotFoundError) { + return callback(null, {}); + } else if (err) { + return callback(err); + } + + var metadata; + try { + metadata = JSON.parse(data); + } catch(e) { + return callback(new Error('Could not parse metadata')); + } + + callback(null, metadata); + }); +}; + +/** + * Connects a block to the database and add indexes + * @param {Block} block - The bitcore block + * @param {Function} callback + */ +DB.prototype.connectBlock = function(block, callback) { + log.debug('DB handling new chain block'); + this.runAllBlockHandlers(block, true, callback); +}; + +/** + * Disconnects a block from the database and removes indexes + * @param {Block} block - The bitcore block + * @param {Function} callback + */ +DB.prototype.disconnectBlock = function(block, callback) { + log.debug('DB removing chain block'); + this.runAllBlockHandlers(block, false, callback); +}; + +/** + * Will collect all database operations for a block from other modules + * and save to the database. + * @param {Block} block - The bitcore block + * @param {Boolean} add - If the block is being added/connected or removed/disconnected + * @param {Function} callback + */ +DB.prototype.runAllBlockHandlers = function(block, add, callback) { + var self = this; + var operations = []; + + // Notify block subscribers + for (var i = 0; i < this.subscriptions.block.length; i++) { + this.subscriptions.block[i].emit('block', block.hash); } + + async.eachSeries( + this.node.modules, + function(mod, next) { + mod.blockHandler.call(mod, block, add, function(err, ops) { + if (err) { + return next(err); + } + if (ops) { + operations = operations.concat(ops); + } + next(); + }); + }, + function(err) { + if (err) { + return callback(err); + } + + log.debug('Updating the database with operations', operations); + + self.store.batch(operations, callback); + } + ); }; module.exports = DB; diff --git a/lib/node.js b/lib/node.js index 1bba402d..e71cd39c 100644 --- a/lib/node.js +++ b/lib/node.js @@ -1,10 +1,8 @@ 'use strict'; -var fs = require('fs'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var async = require('async'); -var mkdirp = require('mkdirp'); var bitcore = require('bitcore'); var BufferUtil = bitcore.util.buffer; var Networks = bitcore.Networks; @@ -12,7 +10,6 @@ var _ = bitcore.deps._; var $ = bitcore.util.preconditions; var Block = bitcore.Block; var Chain = require('./chain'); -var DB = require('./db'); var index = require('./'); var log = index.log; var Bus = require('./bus'); @@ -23,7 +20,6 @@ function Node(config) { return new Node(config); } - this.db = null; this.chain = null; this.network = null; @@ -81,7 +77,7 @@ Node.prototype.addModule = function(service) { }; Node.prototype.getAllAPIMethods = function() { - var methods = this.db.getAPIMethods(); + var methods = []; for(var i in this.modules) { var mod = this.modules[i]; methods = methods.concat(mod.getAPIMethods()); @@ -90,7 +86,7 @@ Node.prototype.getAllAPIMethods = function() { }; Node.prototype.getAllPublishEvents = function() { - var events = this.db.getPublishEvents(); + var events = []; for (var i in this.modules) { var mod = this.modules[i]; events = events.concat(mod.getPublishEvents()); @@ -100,8 +96,6 @@ Node.prototype.getAllPublishEvents = function() { Node.prototype._loadConfiguration = function(config) { this._loadNetwork(config); - this._loadDB(config); - this._loadAPI(); this._loadConsensus(config); }; @@ -185,7 +179,7 @@ Node.prototype._syncBitcoindRewind = function(block, done) { } // Undo the related indexes for this block - self.db._onChainRemoveBlock(tip, function(err) { + self.modules.db.disconnectBlock(tip, function(err) { if (err) { return removeDone(err); } @@ -258,7 +252,7 @@ Node.prototype._syncBitcoind = function() { return done(err); } // Create indexes - self.db._onChainAddBlock(block, function(err) { + self.modules.db.connectBlock(block, function(err) { if (err) { return done(err); } @@ -325,39 +319,6 @@ Node.prototype._loadNetwork = function(config) { $.checkState(this.network, 'Unrecognized network'); }; -Node.prototype._loadDB = function(config) { - var options = _.clone(config.db || {}); - - if (config.DB) { - // Other modules can inherit from our DB and replace it with their own - DB = config.DB; - } - - // Store the additional indexes in a new directory - // based on the network configuration and the datadir - $.checkArgument(config.datadir, 'Please specify "datadir" in configuration options'); - $.checkState(this.network, 'Network property not defined'); - var regtest = Networks.get('regtest'); - if (this.network === Networks.livenet) { - options.path = config.datadir + '/bitcore-node.db'; - } else if (this.network === Networks.testnet) { - options.path = config.datadir + '/testnet3/bitcore-node.db'; - } else if (this.network === regtest) { - options.path = config.datadir + '/regtest/bitcore-node.db'; - } else { - throw new Error('Unknown network: ' + this.network); - } - options.network = this.network; - - if (!fs.existsSync(options.path)) { - mkdirp.sync(options.path); - } - - options.node = this; - - this.db = new DB(options); -}; - Node.prototype._loadConsensus = function(config) { var options; if (!config) { @@ -369,24 +330,9 @@ Node.prototype._loadConsensus = function(config) { this.chain = new Chain(options); }; -Node.prototype._loadAPI = function() { - var self = this; - var methodData = self.db.getAPIMethods(); - methodData.forEach(function(data) { - var name = data[0]; - var instance = data[1]; - var method = data[2]; - - self[name] = function() { - return method.apply(instance, arguments); - }; - }); -}; - Node.prototype._initialize = function() { var self = this; - this._initializeDatabase(); this._initializeChain(); this.start(function(err) { @@ -397,18 +343,6 @@ Node.prototype._initialize = function() { }); }; -Node.prototype._initializeDatabase = function() { - var self = this; - this.db.on('ready', function() { - log.info('Bitcoin Database Ready'); - }); - - this.db.on('error', function(err) { - Error.captureStackTrace(err); - self.emit('error', err); - }); -}; - Node.prototype._initializeChain = function() { var self = this; @@ -433,10 +367,6 @@ Node.prototype._initializeChain = function() { Node.prototype.getServices = function() { var services = [ - { - name: 'db', - dependencies: ['bitcoind'], - }, { name: 'chain', dependencies: ['db'] diff --git a/lib/scaffold/default-config.js b/lib/scaffold/default-config.js index 59a5e48e..bb849e77 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, - modules: ['bitcoind', 'address'] + modules: ['bitcoind', 'db', 'address'] } }; } diff --git a/test/chain.unit.js b/test/chain.unit.js index 185aaa66..3413c1c5 100644 --- a/test/chain.unit.js +++ b/test/chain.unit.js @@ -46,21 +46,21 @@ describe('Bitcoin Chain', function() { it('should initialize the chain with the genesis block if no metadata is found in the db', function(done) { var db = {}; db.getMetadata = sinon.stub().callsArgWith(0, null, {}); - db.putBlock = sinon.stub().callsArg(1); db.putMetadata = sinon.stub().callsArg(1); db.getTransactionsFromBlock = sinon.stub(); - db._onChainAddBlock = sinon.stub().callsArg(1); + db.connectBlock = sinon.stub().callsArg(1); db.mempool = { on: sinon.spy() }; var node = { - db: db + modules: { + db: db + } }; var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); chain.on('ready', function() { should.exist(chain.tip); - db.putBlock.callCount.should.equal(1); chain.tip.hash.should.equal('genesis'); Number(chain.tip.__weight.toString(10)).should.equal(0); done(); @@ -76,7 +76,6 @@ describe('Bitcoin Chain', function() { it('should initialize the chain with the metadata from the database if it exists', function(done) { var db = {}; db.getMetadata = sinon.stub().callsArgWith(0, null, {tip: 'block2', tipWeight: 2}); - db.putBlock = sinon.stub().callsArg(1); db.putMetadata = sinon.stub().callsArg(1); db.getBlock = sinon.stub().callsArgWith(1, null, {hash: 'block2', prevHash: 'block1'}); db.getTransactionsFromBlock = sinon.stub(); @@ -84,14 +83,15 @@ describe('Bitcoin Chain', function() { on: sinon.spy() }; var node = { - db: db + modules: { + db: db + } }; var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); chain.getHeightForBlock = sinon.stub().callsArgWith(1, null, 10); chain.getWeight = sinon.stub().callsArgWith(1, null, new BN(50)); chain.on('ready', function() { should.exist(chain.tip); - db.putBlock.callCount.should.equal(0); chain.tip.hash.should.equal('block2'); done(); }); @@ -113,7 +113,9 @@ describe('Bitcoin Chain', function() { on: sinon.spy() }; var node = { - db: db + modules: { + db: db + } }; var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); chain.on('error', function(error) { @@ -124,31 +126,6 @@ describe('Bitcoin Chain', function() { chain.initialize(); }); - it('emit error from putBlock', function(done) { - var db = { - getMetadata: function(cb) { - cb(null, null); - }, - putBlock: function(block, cb) { - cb(new Error('putBlockError')); - } - }; - db.getTransactionsFromBlock = sinon.stub(); - db.mempool = { - on: sinon.spy() - }; - var node = { - db: db - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - chain.on('error', function(error) { - should.exist(error); - error.message.should.equal('putBlockError'); - done(); - }); - chain.initialize(); - }); - it('emit error from getBlock', function(done) { var db = { getMetadata: function(cb) { @@ -163,7 +140,9 @@ describe('Bitcoin Chain', function() { on: sinon.spy() }; var node = { - db: db + modules: { + db: db + } }; var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); chain.on('error', function(error) { @@ -196,8 +175,8 @@ describe('Bitcoin Chain', function() { var work = '000000000000000000000000000000000000000000005a7b3c42ea8b844374e9'; var chain = new Chain(); chain.node = {}; - chain.node.db = {}; chain.node.modules = {}; + chain.node.modules.db = {}; chain.node.modules.bitcoind = { getBlockIndex: sinon.stub().returns({ chainWork: work @@ -233,7 +212,7 @@ describe('Bitcoin Chain', function() { blocks[block1.hash] = block1; blocks[block2.hash] = block2; - var db = new DB({store: memdown}); + var db = {}; db.getPrevHash = function(blockHash, cb) { // TODO: expose prevHash as a string from bitcore var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex'); @@ -241,7 +220,9 @@ describe('Bitcoin Chain', function() { }; var node = { - db: db + modules: { + db: db + } }; var chain = new Chain({ diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index b8ae8d33..5332afd3 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -6,6 +6,7 @@ var bitcorenode = require('../../'); var AddressModule = bitcorenode.modules.AddressModule; var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); +var Networks = bitcore.Networks; var EventEmitter = require('events').EventEmitter; var errors = bitcorenode.errors; var levelup = require('levelup'); @@ -71,6 +72,7 @@ describe('AddressModule', function() { var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); var tx = bitcore.Transaction().fromBuffer(txBuf); var am = new AddressModule({node: mocknode}); + am.node.network = Networks.livenet; var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var messages = {}; am.transactionOutputHandler(messages, tx, 0, true); @@ -151,7 +153,8 @@ describe('AddressModule', function() { var value64 = data[2].value; before(function() { - am = new AddressModule({node: mocknode, network: 'livenet'}); + am = new AddressModule({node: mocknode}); + am.node.network = Networks.livenet; }); it('should create the correct operations when updating/adding outputs', function(done) { @@ -428,8 +431,8 @@ describe('AddressModule', function() { __height: 1 } }, - db: db, modules: { + db: db, bitcoind: { on: sinon.stub() } @@ -442,7 +445,7 @@ describe('AddressModule', function() { it('should get outputs for an address', function(done) { var readStream1 = new EventEmitter(); - am.node.db.store = { + am.node.modules.db.store = { createReadStream: sinon.stub().returns(readStream1) }; var mempoolOutputs = [ @@ -497,7 +500,7 @@ describe('AddressModule', function() { it('should give an error if the readstream has an error', function(done) { var readStream2 = new EventEmitter(); - am.node.db.store = { + am.node.modules.db.store = { createReadStream: sinon.stub().returns(readStream2) }; @@ -524,8 +527,8 @@ describe('AddressModule', function() { var db = {}; var testnode = { - db: db, modules: { + db: db, bitcoind: { on: sinon.stub() } @@ -554,13 +557,7 @@ describe('AddressModule', function() { 'addr3': ['utxo3'] }; - var db = { - modules: { - bitcoind: { - on: sinon.spy() - } - } - }; + var db = {}; var testnode = { db: db, modules: { @@ -741,8 +738,8 @@ describe('AddressModule', function() { } }; var testnode = { - db: db, modules: { + db: db, bitcoind: { on: sinon.stub() } @@ -861,8 +858,8 @@ describe('AddressModule', function() { __height: 1 } }, - db: db, modules: { + db: db, bitcoind: { on: sinon.stub() } diff --git a/test/db.unit.js b/test/modules/db.unit.js similarity index 70% rename from test/db.unit.js rename to test/modules/db.unit.js index 7d2a5b5b..32bafcbc 100644 --- a/test/db.unit.js +++ b/test/modules/db.unit.js @@ -2,22 +2,113 @@ var should = require('chai').should(); var sinon = require('sinon'); -var index = require('../'); -var DB = index.DB; -var blockData = require('./data/livenet-345003.json'); +var index = require('../../'); +var DB = index.modules.DBModule; +var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); +var Networks = bitcore.Networks; var Block = bitcore.Block; -var transactionData = require('./data/bitcoin-transactions.json'); +var transactionData = require('../data/bitcoin-transactions.json'); var errors = index.errors; var memdown = require('memdown'); var bitcore = require('bitcore'); var Transaction = bitcore.Transaction; -describe('Bitcoin DB', function() { +describe('DB Module', function() { + + var baseConfig = { + node: { + network: Networks.testnet, + datadir: 'testdir' + }, + store: memdown + }; + + describe('#_setDataPath', function() { + it('should set the database path', function() { + var config = { + node: { + network: Networks.livenet, + datadir: process.env.HOME + '/.bitcoin' + }, + store: memdown + }; + var db = new DB(config); + db.dataPath.should.equal(process.env.HOME + '/.bitcoin/bitcore-node.db'); + }); + it('should load the db for testnet', function() { + var config = { + node: { + network: Networks.testnet, + datadir: process.env.HOME + '/.bitcoin' + }, + store: memdown + }; + var db = new DB(config); + db.dataPath.should.equal(process.env.HOME + '/.bitcoin/testnet3/bitcore-node.db'); + }); + it('error with unknown network', function() { + var config = { + node: { + network: 'unknown', + datadir: process.env.HOME + '/.bitcoin' + }, + store: memdown + }; + (function() { + var db = new DB(config); + }).should.throw('Unknown network'); + }); + it('should load the db with regtest', function() { + // Switch to use regtest + Networks.remove(Networks.testnet); + Networks.add({ + name: 'regtest', + alias: 'regtest', + pubkeyhash: 0x6f, + privatekey: 0xef, + scripthash: 0xc4, + xpubkey: 0x043587cf, + xprivkey: 0x04358394, + networkMagic: 0xfabfb5da, + port: 18444, + dnsSeeds: [ ] + }); + var regtest = Networks.get('regtest'); + var config = { + node: { + network: regtest, + datadir: process.env.HOME + '/.bitcoin' + }, + store: memdown + }; + var db = new DB(config); + db.dataPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-node.db'); + Networks.remove(regtest); + // Add testnet back + Networks.add({ + name: 'testnet', + alias: 'testnet', + pubkeyhash: 0x6f, + privatekey: 0xef, + scripthash: 0xc4, + xpubkey: 0x043587cf, + xprivkey: 0x04358394, + networkMagic: 0x0b110907, + port: 18333, + dnsSeeds: [ + 'testnet-seed.bitcoin.petertodd.org', + 'testnet-seed.bluematt.me', + 'testnet-seed.alexykot.me', + 'testnet-seed.bitcoin.schildbach.de' + ] + }); + }); + }); describe('#start', function() { it('should emit ready', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -37,7 +128,7 @@ describe('Bitcoin DB', function() { describe('#stop', function() { it('should immediately call the callback', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.stop(function(err) { should.not.exist(err); @@ -48,7 +139,7 @@ describe('Bitcoin DB', function() { describe('#getTransaction', function() { it('will return a NotFound error', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -61,7 +152,7 @@ describe('Bitcoin DB', function() { }); }); it('will return an error from bitcoind', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -74,7 +165,7 @@ describe('Bitcoin DB', function() { }); }); it('will return an error from bitcoind', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -92,7 +183,7 @@ describe('Bitcoin DB', function() { }); describe('#getBlock', function() { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); var blockBuffer = new Buffer(blockData, 'hex'); var expectedBlock = Block.fromBuffer(blockBuffer); db.node = {}; @@ -121,19 +212,9 @@ describe('Bitcoin DB', function() { }); }); - describe('#putBlock', function() { - it('should call callback', function(done) { - var db = new DB({store: memdown}); - db.putBlock('block', function(err) { - should.not.exist(err); - done(); - }); - }); - }); - describe('#getPrevHash', function() { it('should return prevHash from bitcoind', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -150,7 +231,7 @@ describe('Bitcoin DB', function() { }); it('should give an error if bitcoind could not find it', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -173,7 +254,7 @@ describe('Bitcoin DB', function() { buffer: txBuffer }; - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -188,7 +269,7 @@ describe('Bitcoin DB', function() { }); }); it('should give an error if one occurred', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -204,7 +285,7 @@ describe('Bitcoin DB', function() { describe('#sendTransaction', function() { it('should give the txid on success', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -219,7 +300,7 @@ describe('Bitcoin DB', function() { }); }); it('should give an error if bitcoind threw an error', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -236,7 +317,7 @@ describe('Bitcoin DB', function() { describe("#estimateFee", function() { it('should pass along the fee from bitcoind', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { @@ -252,95 +333,35 @@ describe('Bitcoin DB', function() { }); }); - describe('#getOutputTotal', function() { - it('should return the correct value including the coinbase', function() { - var totals = [10, 20, 30]; - var db = new DB({path: 'path', store: memdown}); - var transactions = totals.map(function(total) { - return { - _getOutputAmount: function() { - return total; - }, - isCoinbase: function() { - return total === 10 ? true : false; - } - }; - }); - var grandTotal = db.getOutputTotal(transactions); - grandTotal.should.equal(60); - }); - it('should return the correct value excluding the coinbase', function() { - var totals = [10, 20, 30]; - var db = new DB({path: 'path', store: memdown}); - var transactions = totals.map(function(total) { - return { - _getOutputAmount: function() { - return total; - }, - isCoinbase: function() { - return total === 10 ? true : false; - } - }; - }); - var grandTotal = db.getOutputTotal(transactions, true); - grandTotal.should.equal(50); - }); - }); - - describe('#getInputTotal', function() { - it('should return the correct value', function() { - var totals = [10, 20, 30]; - var db = new DB({path: 'path', store: memdown}); - var transactions = totals.map(function(total) { - return { - _getInputAmount: function() { - return total; - }, - isCoinbase: sinon.stub().returns(false) - }; - }); - var grandTotal = db.getInputTotal(transactions); - grandTotal.should.equal(60); - }); - it('should return 0 if the tx is a coinbase', function() { - var db = new DB({store: memdown}); - var tx = { - isCoinbase: sinon.stub().returns(true) - }; - var total = db.getInputTotal([tx]); - total.should.equal(0); - }); - }); - - describe('#_onChainAddBlock', function() { + describe('#connectBlock', function() { it('should remove block from mempool and call blockHandler with true', function(done) { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.mempool = { removeBlock: sinon.stub() }; - db.blockHandler = sinon.stub().callsArg(2); - db._onChainAddBlock({hash: 'hash'}, function(err) { + db.runAllBlockHandlers = sinon.stub().callsArg(2); + db.connectBlock({hash: 'hash'}, function(err) { should.not.exist(err); - db.blockHandler.args[0][1].should.equal(true); + db.runAllBlockHandlers.args[0][1].should.equal(true); done(); }); }); }); - describe('#_onChainRemoveBlock', function() { + describe('#disconnectBlock', function() { it('should call blockHandler with false', function(done) { - var db = new DB({store: memdown}); - db.blockHandler = sinon.stub().callsArg(2); - db._onChainRemoveBlock({hash: 'hash'}, function(err) { + var db = new DB(baseConfig); + db.runAllBlockHandlers = sinon.stub().callsArg(2); + db.disconnectBlock({hash: 'hash'}, function(err) { should.not.exist(err); - db.blockHandler.args[0][1].should.equal(false); + db.runAllBlockHandlers.args[0][1].should.equal(false); done(); }); }); }); - describe('#blockHandler', function() { - var db = new DB({store: memdown}); + describe('#runAllBlockHandlers', function() { + var db = new DB(baseConfig); var Module1 = function() {}; Module1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']); var Module2 = function() {}; @@ -355,7 +376,7 @@ describe('Bitcoin DB', function() { }; it('should call blockHandler in all modules and perform operations', function(done) { - db.blockHandler('block', true, function(err) { + db.runAllBlockHandlers('block', true, function(err) { should.not.exist(err); db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']); done(); @@ -367,7 +388,7 @@ describe('Bitcoin DB', function() { Module3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); db.node.modules.module3 = new Module3(); - db.blockHandler('block', true, function(err) { + db.runAllBlockHandlers('block', true, function(err) { should.exist(err); done(); }); @@ -376,12 +397,11 @@ describe('Bitcoin DB', function() { describe('#getAPIMethods', function() { it('should return the correct db methods', function() { - var db = new DB({store: memdown}); + var db = new DB(baseConfig); db.node = {}; db.node.modules = {}; var methods = db.getAPIMethods(); methods.length.should.equal(5); }); }); - }); diff --git a/test/node.unit.js b/test/node.unit.js index 78672ae6..59313f80 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -101,6 +101,9 @@ describe('Bitcore Node', function() { it('should return db methods and modules methods', function() { var node = new Node(baseConfig); node.modules = { + db: { + getAPIMethods: sinon.stub().returns(['db1', 'db2']), + }, module1: { getAPIMethods: sinon.stub().returns(['mda1', 'mda2']) }, @@ -108,10 +111,6 @@ describe('Bitcore Node', function() { getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2']) } }; - var db = { - getAPIMethods: sinon.stub().returns(['db1', 'db2']), - }; - node.db = db; var methods = node.getAllAPIMethods(); methods.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); @@ -121,6 +120,9 @@ describe('Bitcore Node', function() { it('should return modules publish events', function() { var node = new Node(baseConfig); node.modules = { + db: { + getPublishEvents: sinon.stub().returns(['db1', 'db2']), + }, module1: { getPublishEvents: sinon.stub().returns(['mda1', 'mda2']) }, @@ -128,11 +130,6 @@ describe('Bitcore Node', function() { getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2']) } }; - var db = { - getPublishEvents: sinon.stub().returns(['db1', 'db2']), - }; - node.db = db; - var events = node.getAllPublishEvents(); events.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); }); @@ -141,12 +138,8 @@ describe('Bitcore Node', function() { it('should call the necessary methods', function() { var TestNode = proxyquire('../lib/node', {}); TestNode.prototype._initialize = sinon.spy(); - TestNode.prototype._loadDB = sinon.spy(); - TestNode.prototype._loadAPI = sinon.spy(); TestNode.prototype._loadConsensus = sinon.spy(); var node = new TestNode(baseConfig); - node._loadDB.callCount.should.equal(1); - node._loadAPI.callCount.should.equal(1); node._loadConsensus.callCount.should.equal(1); }); }); @@ -243,8 +236,9 @@ describe('Bitcore Node', function() { } }); }; - node.db = { - _onChainRemoveBlock: function(block, callback) { + node.modules = {}; + node.modules.db = { + disconnectBlock: function(block, callback) { setImmediate(callback); } }; @@ -286,8 +280,8 @@ describe('Bitcore Node', function() { hashes: {} } }; - node.db = { - _onChainAddBlock: function(block, callback) { + node.modules.db = { + connectBlock: function(block, callback) { node.chain.tip.__height += 1; callback(); } @@ -336,8 +330,8 @@ describe('Bitcore Node', function() { hashes: {} } }; - node.db = { - _onChainAddBlock: function(block, callback) { + node.modules.db = { + connectBlock: function(block, callback) { node.chain.tip.__height += 1; callback(); } @@ -387,119 +381,21 @@ describe('Bitcore Node', function() { node.network.name.should.equal('livenet'); }); }); - describe('#_loadDB', function() { - it('should load the db', function() { - var DB = function(config) { - config.path.should.equal(process.env.HOME + '/.bitcoin/bitcore-node.db'); - }; - var config = { - DB: DB, - datadir: process.env.HOME + '/.bitcoin' - }; - - var node = new Node(config); - node.network = Networks.livenet; - node._loadDB(config); - node.db.should.be.instanceof(DB); - }); - it('should load the db for testnet', function() { - var DB = function(config) { - config.path.should.equal(process.env.HOME + '/.bitcoin/testnet3/bitcore-node.db'); - }; - var config = { - DB: DB, - datadir: process.env.HOME + '/.bitcoin' - }; - - var node = new Node(config); - node.network = Networks.testnet; - node._loadDB(config); - node.db.should.be.instanceof(DB); - }); - it('error with unknown network', function() { - var config = { - datadir: process.env.HOME + '/.bitcoin' - }; - - var node = new Node(config); - node.network = 'not a network'; - (function() { - node._loadDB(config); - }).should.throw('Unknown network'); - }); - it('should load the db with regtest', function() { - var DB = function(config) { - config.path.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-node.db'); - }; - var config = { - DB: DB, - datadir: process.env.HOME + '/.bitcoin' - }; - - var node = new Node(config); - // Switch to use regtest - Networks.remove(Networks.testnet); - Networks.add({ - name: 'regtest', - alias: 'regtest', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0xfabfb5da, - port: 18444, - dnsSeeds: [ ] - }); - var regtest = Networks.get('regtest'); - node.network = regtest; - node._loadDB(config); - node.db.should.be.instanceof(DB); - Networks.remove(regtest); - // Add testnet back - Networks.add({ - name: 'testnet', - alias: 'testnet', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0x0b110907, - port: 18333, - dnsSeeds: [ - 'testnet-seed.bitcoin.petertodd.org', - 'testnet-seed.bluematt.me', - 'testnet-seed.alexykot.me', - 'testnet-seed.bitcoin.schildbach.de' - ] - }); - }); - }); describe('#_loadConsensus', function() { - var node; - before(function() { node = new Node(baseConfig); }); - it('will set properties', function() { node._loadConsensus(); should.exist(node.chain); }); - }); - describe('#_initialize', function() { - var node; - before(function() { var TestNode = proxyquire('../lib/node', {}); TestNode.prototype._loadConfiguration = sinon.spy(); - TestNode.prototype._initializeBitcoind = sinon.spy(); - TestNode.prototype._initializeDatabase = sinon.spy(); TestNode.prototype._initializeChain = sinon.spy(); // mock the _initialize during construction @@ -510,14 +406,13 @@ describe('Bitcore Node', function() { node.chain = { on: sinon.spy() }; - node.Block = 'Block'; - node.bitcoind = { + node.modules = {}; + node.modules.bitcoind = { on: sinon.spy() }; - node.db = { + node.modules.db = { on: sinon.spy() }; - // restore the original method node._initialize = _initialize; }); @@ -526,15 +421,9 @@ describe('Bitcore Node', function() { node.once('ready', function() { done(); }); - node.start = sinon.stub().callsArg(0); - node._initialize(); - - // event handlers - node._initializeDatabase.callCount.should.equal(1); node._initializeChain.callCount.should.equal(1); - }); it('should emit an error if an error occurred starting services', function(done) { @@ -549,34 +438,6 @@ describe('Bitcore Node', function() { }); - describe('#_initializeDatabase', function() { - it('will log on ready event', function(done) { - var node = new Node(baseConfig); - node.db = new EventEmitter(); - sinon.stub(index.log, 'info'); - node.db.on('ready', function() { - setImmediate(function() { - index.log.info.callCount.should.equal(1); - index.log.info.restore(); - done(); - }); - }); - node._initializeDatabase(); - node.db.emit('ready'); - }); - it('will call emit an error from db', function(done) { - var node = new Node(baseConfig); - node.db = new EventEmitter(); - node.on('error', function(err) { - should.exist(err); - err.message.should.equal('test error'); - done(); - }); - node._initializeDatabase(); - node.db.emit('error', new Error('test error')); - }); - }); - describe('#_initializeChain', function() { it('will call sync when there is a new tip', function(done) { diff --git a/test/transaction.unit.js b/test/transaction.unit.js index 62bf9449..eee529a4 100644 --- a/test/transaction.unit.js +++ b/test/transaction.unit.js @@ -4,10 +4,6 @@ var should = require('chai').should(); var sinon = require('sinon'); var bitcoinlib = require('../'); var Transaction = bitcoinlib.Transaction; -var transactionData = require('./data/bitcoin-transactions.json'); -var memdown = require('memdown'); -var DB = bitcoinlib.DB; -var db = new DB({store: memdown}); var levelup = require('levelup'); describe('Bitcoin Transaction', function() { From 16eef1279c3bf452347d7bdb2aecfb1902350b4c Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Mon, 31 Aug 2015 09:00:00 -0400 Subject: [PATCH 3/5] Combine chain with db module. --- index.js | 1 - integration/regtest-node.js | 10 +- lib/chain.js | 247 -------------- lib/modules/address.js | 4 +- lib/modules/db.js | 374 ++++++++++++++++++++- lib/node.js | 411 +++++------------------ lib/scaffold/start.js | 24 +- package.json | 2 +- test/chain.unit.js | 249 -------------- test/modules/address.unit.js | 21 +- test/modules/db.unit.js | 427 ++++++++++++++++++++++-- test/node.unit.js | 613 +++++++++++------------------------ 12 files changed, 1058 insertions(+), 1325 deletions(-) delete mode 100644 lib/chain.js delete mode 100644 test/chain.unit.js diff --git a/index.js b/index.js index 1d07625d..94317300 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,6 @@ module.exports = require('./lib'); module.exports.Node = require('./lib/node'); -module.exports.Chain = require('./lib/chain'); module.exports.Transaction = require('./lib/transaction'); module.exports.Module = require('./lib/module'); module.exports.errors = require('./lib/errors'); diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 398704e9..6edcb693 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -102,7 +102,7 @@ describe('Node Functionality', function() { }); var syncedHandler = function() { - if (node.chain.tip.__height === 150) { + if (node.modules.db.tip.__height === 150) { node.removeListener('synced', syncedHandler); done(); } @@ -178,18 +178,18 @@ describe('Node Functionality', function() { blocksRemoved++; }; - node.chain.on('removeblock', removeBlock); + node.modules.db.on('removeblock', removeBlock); var addBlock = function() { blocksAdded++; if (blocksAdded === 2 && blocksRemoved === 1) { - node.chain.removeListener('addblock', addBlock); - node.chain.removeListener('removeblock', removeBlock); + node.modules.db.removeListener('addblock', addBlock); + node.modules.db.removeListener('removeblock', removeBlock); done(); } }; - node.chain.on('addblock', addBlock); + node.modules.db.on('addblock', addBlock); // We need to add a transaction to the mempool so that the next block will // have a different hash as the hash has been invalidated. diff --git a/lib/chain.js b/lib/chain.js deleted file mode 100644 index 81690a81..00000000 --- a/lib/chain.js +++ /dev/null @@ -1,247 +0,0 @@ -'use strict'; - -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var bitcore = require('bitcore'); -var BN = bitcore.crypto.BN; -var $ = bitcore.util.preconditions; -var Block = bitcore.Block; -var index = require('./index'); -var log = index.log; -var utils = require('./utils'); - -var MAX_STACK_DEPTH = 1000; - -/** - * Will instantiate a new Chain instance - * @param {Object} options - The options for the chain - * @param {Number} options.minBits - The minimum number of bits - * @param {Number} options.maxBits - The maximum number of bits - * @param {BN|Number} options.targetTimespan - The number of milliseconds for difficulty retargeting - * @param {BN|Number} options.targetSpacing - The number of milliseconds between blocks - * @returns {Chain} - * @extends BaseChain - * @constructor - */ -function Chain(opts) { - /* jshint maxstatements: 30 */ - if (!(this instanceof Chain)) { - return new Chain(opts); - } - - var self = this; - if(!opts) { - opts = {}; - } - - this.genesis = opts.genesis; - this.genesisOptions = opts.genesisOptions; - this.genesisWeight = new BN(0); - this.tip = null; - this.overrideTip = opts.overrideTip; - this.cache = { - hashes: {}, // dictionary of hash -> prevHash - chainHashes: {} - }; - this.lastSavedMetadata = null; - this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance - this.blockQueue = []; - this.processingBlockQueue = false; - this.builder = opts.builder || false; - this.ready = false; - - this.on('initialized', function() { - self.initialized = true; - }); - - this.on('initialized', this._onInitialized.bind(this)); - - this.on('ready', function() { - log.debug('Chain is ready'); - self.ready = true; - self.startBuilder(); - }); - - this.minBits = opts.minBits || Chain.DEFAULTS.MIN_BITS; - this.maxBits = opts.maxBits || Chain.DEFAULTS.MAX_BITS; - - this.maxHashes = opts.maxHashes || Chain.DEFAULTS.MAX_HASHES; - - this.targetTimespan = opts.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN; - this.targetSpacing = opts.targetSpacing || Chain.DEFAULTS.TARGET_SPACING; - - this.node = opts.node; - - return this; -} - -util.inherits(Chain, EventEmitter); - -Chain.DEFAULTS = { - MAX_HASHES: new BN('10000000000000000000000000000000000000000000000000000000000000000', 'hex'), - TARGET_TIMESPAN: 14 * 24 * 60 * 60 * 1000, // two weeks - TARGET_SPACING: 10 * 60 * 1000, // ten minutes - MAX_BITS: 0x1d00ffff, - MIN_BITS: 0x03000000 -}; - -Chain.prototype._onInitialized = function() { - this.emit('ready'); -}; - -Chain.prototype.start = function(callback) { - this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer); - this.once('initialized', callback); - this.initialize(); -}; - -Chain.prototype.initialize = function() { - var self = this; - - // Does our database already have a tip? - self.node.modules.db.getMetadata(function getMetadataCallback(err, metadata) { - if(err) { - return self.emit('error', err); - } else if(!metadata || !metadata.tip) { - self.tip = self.genesis; - self.tip.__height = 0; - self.tip.__weight = self.genesisWeight; - self.node.modules.db.connectBlock(self.genesis, function(err) { - if(err) { - return self.emit('error', err); - } - - self.emit('addblock', self.genesis); - self.saveMetadata(); - self.emit('initialized'); - }); - } else { - metadata.tip = metadata.tip; - self.node.modules.db.getBlock(metadata.tip, function getBlockCallback(err, tip) { - if(err) { - return self.emit('error', err); - } - - self.tip = tip; - self.tip.__height = metadata.tipHeight; - self.tip.__weight = new BN(metadata.tipWeight, 'hex'); - self.cache = metadata.cache; - self.emit('initialized'); - }); - } - }); -}; - -Chain.prototype.stop = function(callback) { - setImmediate(callback); -}; - -Chain.prototype._validateBlock = function(block, callback) { - // All validation is done by bitcoind - setImmediate(callback); -}; - -Chain.prototype.startBuilder = function() { - // Unused in bitcoind.js -}; - -Chain.prototype.getWeight = function getWeight(blockHash, callback) { - var self = this; - var blockIndex = self.node.modules.bitcoind.getBlockIndex(blockHash); - - setImmediate(function() { - if (blockIndex) { - callback(null, new BN(blockIndex.chainWork, 'hex')); - } else { - return callback(new Error('Weight not found for ' + blockHash)); - } - }); -}; - -/** - * Will get an array of hashes all the way to the genesis block for - * the chain based on "block hash" as the tip. - * - * @param {String} block hash - a block hash - * @param {Function} callback - A function that accepts: Error and Array of hashes - */ -Chain.prototype.getHashes = function getHashes(tipHash, callback) { - var self = this; - - $.checkArgument(utils.isHash(tipHash)); - - var hashes = []; - var depth = 0; - - getHashAndContinue(null, tipHash); - - function getHashAndContinue(err, hash) { - if (err) { - return callback(err); - } - - depth++; - - hashes.unshift(hash); - - if (hash === self.genesis.hash) { - // Stop at the genesis block - self.cache.chainHashes[tipHash] = hashes; - callback(null, hashes); - } else if(self.cache.chainHashes[hash]) { - hashes.shift(); - hashes = self.cache.chainHashes[hash].concat(hashes); - delete self.cache.chainHashes[hash]; - self.cache.chainHashes[tipHash] = hashes; - callback(null, hashes); - } else { - // Continue with the previous hash - // check cache first - var prevHash = self.cache.hashes[hash]; - if(prevHash) { - // Don't let the stack get too deep. Otherwise we will crash. - if(depth >= MAX_STACK_DEPTH) { - depth = 0; - return setImmediate(function() { - getHashAndContinue(null, prevHash); - }); - } else { - return getHashAndContinue(null, prevHash); - } - } else { - // do a db call if we don't have it - self.node.modules.db.getPrevHash(hash, function(err, prevHash) { - if(err) { - return callback(err); - } - - return getHashAndContinue(null, prevHash); - }); - } - } - } - -}; - -Chain.prototype.saveMetadata = function saveMetadata(callback) { - var self = this; - - callback = callback || function() {}; - - if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) { - return callback(); - } - - var metadata = { - tip: self.tip ? self.tip.hash : null, - tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0, - tipWeight: self.tip && self.tip.__weight ? self.tip.__weight.toString(16) : '0', - cache: self.cache - }; - - self.lastSavedMetadata = new Date(); - - self.node.modules.db.putMetadata(metadata, callback); -}; - -module.exports = Chain; diff --git a/lib/modules/address.js b/lib/modules/address.js index ba47848f..5da5b577 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -347,7 +347,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback satoshis: Number(value[0]), script: value[1], blockHeight: Number(value[2]), - confirmations: self.node.chain.tip.__height - Number(value[2]) + 1 + confirmations: self.node.modules.db.tip.__height - Number(value[2]) + 1 }; outputs.push(output); @@ -504,7 +504,7 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem var confirmations = 0; if(transaction.__height >= 0) { - confirmations = self.node.chain.tip.__height - transaction.__height; + confirmations = self.node.modules.db.tip.__height - transaction.__height; } txinfos[transaction.hash] = { diff --git a/lib/modules/db.js b/lib/modules/db.js index b5b5ff7f..eb22de74 100644 --- a/lib/modules/db.js +++ b/lib/modules/db.js @@ -7,6 +7,7 @@ var levelup = require('levelup'); var leveldown = require('leveldown'); var mkdirp = require('mkdirp'); var bitcore = require('bitcore'); +var BufferUtil = bitcore.util.buffer; var Networks = bitcore.Networks; var Block = bitcore.Block; var $ = bitcore.util.preconditions; @@ -15,9 +16,12 @@ var errors = index.errors; var log = index.log; var Transaction = require('../transaction'); var Module = require('../module'); +var utils = require('../utils'); + +var MAX_STACK_DEPTH = 1000; /** - * Represents the current state of the bitcoin blockchain transaction data. Other modules + * Represents the current state of the bitcoin blockchain. Other modules * can extend the data that is indexed by implementing a `blockHandler` method. * * @param {Object} options @@ -25,6 +29,8 @@ var Module = require('../module'); * @param {Node} options.node - A reference to the node */ function DB(options) { + /* jshint maxstatements: 20 */ + if (!(this instanceof DB)) { return new DB(options); } @@ -34,11 +40,21 @@ function DB(options) { Module.call(this, options); + this.tip = null; + this.genesis = null; + $.checkState(this.node.network, 'Node is expected to have a "network" property'); this.network = this.node.network; this._setDataPath(); + this.cache = { + hashes: {}, // dictionary of hash -> prevHash + chainHashes: {} + }; + this.lastSavedMetadata = null; + this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance + this.levelupStore = leveldown; if (options.store) { this.levelupStore = options.store; @@ -69,14 +85,65 @@ DB.prototype._setDataPath = function() { }; DB.prototype.start = function(callback) { + var self = this; if (!fs.existsSync(this.dataPath)) { mkdirp.sync(this.dataPath); } + + this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer); this.store = levelup(this.dataPath, { db: this.levelupStore }); this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this)); - this.emit('ready'); - log.info('Bitcoin Database Ready'); - setImmediate(callback); + + this.once('ready', function() { + log.info('Bitcoin Database Ready'); + + // Notify that there is a new tip + self.node.modules.bitcoind.on('tip', function(height) { + if(!self.node.stopping) { + var percentage = self.node.modules.bitcoind.syncPercentage(); + log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); + self.sync(); + } + }); + }); + + // Does our database already have a tip? + self.getMetadata(function(err, metadata) { + if(err) { + return callback(err); + } else if(!metadata || !metadata.tip) { + self.tip = self.genesis; + self.tip.__height = 0; + self.connectBlock(self.genesis, function(err) { + if(err) { + return callback(err); + } + + self.emit('addblock', self.genesis); + self.saveMetadata(); + self.sync(); + self.emit('ready'); + setImmediate(callback); + + }); + } else { + metadata.tip = metadata.tip; + self.getBlock(metadata.tip, function(err, tip) { + if(err) { + return callback(err); + } + + self.tip = tip; + self.tip.__height = metadata.tipHeight; + self.cache = metadata.cache; + self.sync(); + self.emit('ready'); + setImmediate(callback); + + }); + } + }); + }; DB.prototype.stop = function(callback) { @@ -92,6 +159,14 @@ DB.prototype.getInfo = function(callback) { }); }; +/** + * Closes the underlying store database + * @param {Function} callback - A function that accepts: Error + */ +DB.prototype.close = function(callback) { + this.store.close(callback); +}; + DB.prototype.transactionHandler = function(txInfo) { var tx = Transaction().fromBuffer(txInfo.buffer); for (var i = 0; i < this.subscriptions.transaction.length; i++) { @@ -102,14 +177,6 @@ DB.prototype.transactionHandler = function(txInfo) { } }; -/** - * Closes the underlying store database - * @param {Function} callback - A function that accepts: Error - */ -DB.prototype.close = function(callback) { - this.store.close(callback); -}; - DB.prototype.getAPIMethods = function() { var methods = [ ['getBlock', this, this.getBlock, 1], @@ -231,6 +298,26 @@ DB.prototype.putMetadata = function(metadata, callback) { this.store.put('metadata', JSON.stringify(metadata), {}, callback); }; +DB.prototype.saveMetadata = function(callback) { + var self = this; + + callback = callback || function() {}; + + if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) { + return callback(); + } + + var metadata = { + tip: self.tip ? self.tip.hash : null, + tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0, + cache: self.cache + }; + + self.lastSavedMetadata = new Date(); + + self.putMetadata(metadata, callback); +}; + /** * Retrieves metadata from the database * @param {Function} callback - A function that accepts: Error and Object @@ -317,4 +404,267 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) { ); }; +/** + * Will get an array of hashes all the way to the genesis block for + * the chain based on "block hash" as the tip. + * + * @param {String} block hash - a block hash + * @param {Function} callback - A function that accepts: Error and Array of hashes + */ +DB.prototype.getHashes = function getHashes(tipHash, callback) { + var self = this; + + $.checkArgument(utils.isHash(tipHash)); + + var hashes = []; + var depth = 0; + + function getHashAndContinue(err, hash) { + /* jshint maxstatements: 20 */ + + if (err) { + return callback(err); + } + + depth++; + + hashes.unshift(hash); + + if (hash === self.genesis.hash) { + // Stop at the genesis block + self.cache.chainHashes[tipHash] = hashes; + callback(null, hashes); + } else if(self.cache.chainHashes[hash]) { + hashes.shift(); + hashes = self.cache.chainHashes[hash].concat(hashes); + delete self.cache.chainHashes[hash]; + self.cache.chainHashes[tipHash] = hashes; + callback(null, hashes); + } else { + // Continue with the previous hash + // check cache first + var prevHash = self.cache.hashes[hash]; + if(prevHash) { + // Don't let the stack get too deep. Otherwise we will crash. + if(depth >= MAX_STACK_DEPTH) { + depth = 0; + return setImmediate(function() { + getHashAndContinue(null, prevHash); + }); + } else { + return getHashAndContinue(null, prevHash); + } + } else { + // do a db call if we don't have it + self.getPrevHash(hash, function(err, prevHash) { + if(err) { + return callback(err); + } + + return getHashAndContinue(null, prevHash); + }); + } + } + } + + getHashAndContinue(null, tipHash); + +}; + +/** + * This function will find the common ancestor between the current chain and a forked block, + * by moving backwards from the forked block until it meets the current chain. + * @param {Block} block - The new tip that forks the current chain. + * @param {Function} done - A callback function that is called when complete. + */ +DB.prototype.findCommonAncestor = function(block, done) { + + var self = this; + + // The current chain of hashes will likely already be available in a cache. + self.getHashes(self.tip.hash, function(err, currentHashes) { + if (err) { + done(err); + } + + // Create a hash map for faster lookups + var currentHashesMap = {}; + var length = currentHashes.length; + for (var i = 0; i < length; i++) { + currentHashesMap[currentHashes[i]] = true; + } + + // TODO: expose prevHash as a string from bitcore + var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); + + // We only need to go back until we meet the main chain for the forked block + // and thus don't need to find the entire chain of hashes. + + while(ancestorHash && !currentHashesMap[ancestorHash]) { + var blockIndex = self.node.modules.bitcoind.getBlockIndex(ancestorHash); + ancestorHash = blockIndex ? blockIndex.prevHash : null; + } + + // Hash map is no-longer needed, quickly let + // scavenging garbage collection know to cleanup + currentHashesMap = null; + + if (!ancestorHash) { + return done(new Error('Unknown common ancestor.')); + } + + done(null, ancestorHash); + + }); +}; + +/** + * This function will attempt to rewind the chain to the common ancestor + * between the current chain and a forked block. + * @param {Block} block - The new tip that forks the current chain. + * @param {Function} done - A callback function that is called when complete. + */ +DB.prototype.syncRewind = function(block, done) { + + var self = this; + + self.findCommonAncestor(block, function(err, ancestorHash) { + if (err) { + return done(err); + } + // Rewind the chain to the common ancestor + async.whilst( + function() { + // Wait until the tip equals the ancestor hash + return self.tip.hash !== ancestorHash; + }, + function(removeDone) { + + var tip = self.tip; + + // TODO: expose prevHash as a string from bitcore + var prevHash = BufferUtil.reverse(tip.header.prevHash).toString('hex'); + + self.getBlock(prevHash, function(err, previousTip) { + if (err) { + removeDone(err); + } + + // Undo the related indexes for this block + self.disconnectBlock(tip, function(err) { + if (err) { + return removeDone(err); + } + + // Set the new tip + previousTip.__height = self.tip.__height - 1; + self.tip = previousTip; + self.saveMetadata(); + self.emit('removeblock', tip); + removeDone(); + }); + + }); + + }, done + ); + }); +}; + +/** + * This function will synchronize additional indexes for the chain based on + * the current active chain in the bitcoin daemon. In the event that there is + * a reorganization in the daemon, the chain will rewind to the last common + * ancestor and then resume syncing. + */ +DB.prototype.sync = function() { + var self = this; + + if (self.bitcoindSyncing) { + return; + } + + if (!self.tip) { + return; + } + + self.bitcoindSyncing = true; + self.lastSavedMetadataThreshold = 30000; + + var height; + + async.whilst(function() { + height = self.tip.__height; + return height < self.node.modules.bitcoind.height && !self.node.stopping; + }, function(done) { + self.node.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) { + if (err) { + return done(err); + } + + var block = Block.fromBuffer(blockBuffer); + + // TODO: expose prevHash as a string from bitcore + var prevHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); + + if (prevHash === self.tip.hash) { + + // This block appends to the current chain tip and we can + // immediately add it to the chain and create indexes. + + // Populate height + block.__height = self.tip.__height + 1; + + // Update cache.hashes + self.cache.hashes[block.hash] = prevHash; + + // Update cache.chainHashes + self.getHashes(block.hash, function(err, hashes) { + if (err) { + return done(err); + } + // Create indexes + self.connectBlock(block, function(err) { + if (err) { + return done(err); + } + self.tip = block; + log.debug('Saving metadata'); + self.saveMetadata(); + log.debug('Chain added block to main chain'); + self.emit('addblock', block); + setImmediate(done); + }); + }); + + } else { + // This block doesn't progress the current tip, so we'll attempt + // to rewind the chain to the common ancestor of the block and + // then we can resume syncing. + self.syncRewind(block, done); + + } + }); + }, function(err) { + if (err) { + Error.captureStackTrace(err); + return self.node.emit('error', err); + } + + if(self.node.stopping) { + return; + } + + self.bitcoindSyncing = false; + self.lastSavedMetadataThreshold = 0; + + // If bitcoind is completely synced + if (self.node.modules.bitcoind.isSynced()) { + self.node.emit('synced'); + } + + }); + +}; + module.exports = DB; diff --git a/lib/node.js b/lib/node.js index e71cd39c..4c147632 100644 --- a/lib/node.js +++ b/lib/node.js @@ -4,12 +4,8 @@ var util = require('util'); var EventEmitter = require('events').EventEmitter; var async = require('async'); var bitcore = require('bitcore'); -var BufferUtil = bitcore.util.buffer; var Networks = bitcore.Networks; -var _ = bitcore.deps._; var $ = bitcore.util.preconditions; -var Block = bitcore.Block; -var Chain = require('./chain'); var index = require('./'); var log = index.log; var Bus = require('./bus'); @@ -20,9 +16,9 @@ function Node(config) { return new Node(config); } - this.chain = null; - this.network = null; + var self = this; + this.network = null; this.modules = {}; this._unloadedModules = []; @@ -35,45 +31,47 @@ function Node(config) { $.checkState(config.datadir, 'Node config expects "datadir"'); this.datadir = config.datadir; - this._loadConfiguration(config); - this._initialize(); + this._setNetwork(config); + + this.start(function(err) { + if(err) { + return self.emit('error', err); + } + self.emit('ready'); + }); + } util.inherits(Node, EventEmitter); -Node.prototype.openBus = function() { - return new Bus({node: this}); +util.inherits(Node, EventEmitter); + +Node.prototype._setNetwork = function(config) { + if (config.network === 'testnet') { + this.network = Networks.get('testnet'); + } else if (config.network === 'regtest') { + Networks.remove(Networks.testnet); + Networks.add({ + name: 'regtest', + alias: 'regtest', + pubkeyhash: 0x6f, + privatekey: 0xef, + scripthash: 0xc4, + xpubkey: 0x043587cf, + xprivkey: 0x04358394, + networkMagic: 0xfabfb5da, + port: 18444, + dnsSeeds: [ ] + }); + this.network = Networks.get('regtest'); + } else { + this.network = Networks.defaultNetwork; + } + $.checkState(this.network, 'Unrecognized network'); }; -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.openBus = function() { + return new Bus({db: this.modules.db}); }; Node.prototype.getAllAPIMethods = function() { @@ -94,293 +92,9 @@ Node.prototype.getAllPublishEvents = function() { return events; }; -Node.prototype._loadConfiguration = function(config) { - this._loadNetwork(config); - this._loadConsensus(config); -}; - -/** - * This function will find the common ancestor between the current chain and a forked block, - * by moving backwards from the forked block until it meets the current chain. - * @param {Block} block - The new tip that forks the current chain. - * @param {Function} done - A callback function that is called when complete. - */ -Node.prototype._syncBitcoindAncestor = function(block, done) { - - var self = this; - - // The current chain of hashes will likely already be available in a cache. - self.chain.getHashes(self.chain.tip.hash, function(err, currentHashes) { - if (err) { - done(err); - } - - // Create a hash map for faster lookups - var currentHashesMap = {}; - var length = currentHashes.length; - for (var i = 0; i < length; i++) { - currentHashesMap[currentHashes[i]] = true; - } - - // TODO: expose prevHash as a string from bitcore - var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); - - // We only need to go back until we meet the main chain for the forked block - // and thus don't need to find the entire chain of hashes. - - while(ancestorHash && !currentHashesMap[ancestorHash]) { - var blockIndex = self.modules.bitcoind.getBlockIndex(ancestorHash); - ancestorHash = blockIndex ? blockIndex.prevHash : null; - } - - // Hash map is no-longer needed, quickly let - // scavenging garbage collection know to cleanup - currentHashesMap = null; - - if (!ancestorHash) { - return done(new Error('Unknown common ancestor.')); - } - - done(null, ancestorHash); - - }); -}; - -/** - * This function will attempt to rewind the chain to the common ancestor - * between the current chain and a forked block. - * @param {Block} block - The new tip that forks the current chain. - * @param {Function} done - A callback function that is called when complete. - */ -Node.prototype._syncBitcoindRewind = function(block, done) { - - var self = this; - - self._syncBitcoindAncestor(block, function(err, ancestorHash) { - if (err) { - return done(err); - } - // Rewind the chain to the common ancestor - async.whilst( - function() { - // Wait until the tip equals the ancestor hash - return self.chain.tip.hash !== ancestorHash; - }, - function(removeDone) { - - var tip = self.chain.tip; - - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(tip.header.prevHash).toString('hex'); - - self.getBlock(prevHash, function(err, previousTip) { - if (err) { - removeDone(err); - } - - // Undo the related indexes for this block - self.modules.db.disconnectBlock(tip, function(err) { - if (err) { - return removeDone(err); - } - - // Set the new tip - previousTip.__height = self.chain.tip.__height - 1; - self.chain.tip = previousTip; - self.chain.saveMetadata(); - self.chain.emit('removeblock', tip); - removeDone(); - }); - - }); - - }, done - ); - }); -}; - -/** - * This function will synchronize additional indexes for the chain based on - * the current active chain in the bitcoin daemon. In the event that there is - * a reorganization in the daemon, the chain will rewind to the last common - * ancestor and then resume syncing. - */ -Node.prototype._syncBitcoind = function() { - var self = this; - - if (self.bitcoindSyncing) { - return; - } - - if (!self.chain.tip) { - return; - } - - self.bitcoindSyncing = true; - self.chain.lastSavedMetadataThreshold = 30000; - - var height; - - async.whilst(function() { - height = self.chain.tip.__height; - return height < self.modules.bitcoind.height && !self.stopping; - }, function(done) { - self.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) { - if (err) { - return done(err); - } - - var block = Block.fromBuffer(blockBuffer); - - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); - - if (prevHash === self.chain.tip.hash) { - - // This block appends to the current chain tip and we can - // immediately add it to the chain and create indexes. - - // Populate height - block.__height = self.chain.tip.__height + 1; - - // Update chain.cache.hashes - self.chain.cache.hashes[block.hash] = prevHash; - - // Update chain.cache.chainHashes - self.chain.getHashes(block.hash, function(err, hashes) { - if (err) { - return done(err); - } - // Create indexes - self.modules.db.connectBlock(block, function(err) { - if (err) { - return done(err); - } - self.chain.tip = block; - log.debug('Saving metadata'); - self.chain.saveMetadata(); - log.debug('Chain added block to main chain'); - self.chain.emit('addblock', block); - setImmediate(done); - }); - }); - - } else { - // This block doesn't progress the current tip, so we'll attempt - // to rewind the chain to the common ancestor of the block and - // then we can resume syncing. - self._syncBitcoindRewind(block, done); - - } - }); - }, function(err) { - if (err) { - Error.captureStackTrace(err); - return self.emit('error', err); - } - - if(self.stopping) { - return; - } - - self.bitcoindSyncing = false; - self.chain.lastSavedMetadataThreshold = 0; - - // If bitcoind is completely synced - if (self.modules.bitcoind.isSynced()) { - self.emit('synced'); - } - - }); - -}; - -Node.prototype._loadNetwork = function(config) { - if (config.network === 'testnet') { - this.network = Networks.get('testnet'); - } else if (config.network === 'regtest') { - Networks.remove(Networks.testnet); - Networks.add({ - name: 'regtest', - alias: 'regtest', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0xfabfb5da, - port: 18444, - dnsSeeds: [ ] - }); - this.network = Networks.get('regtest'); - } else { - this.network = Networks.get('livenet'); - } - $.checkState(this.network, 'Unrecognized network'); -}; - -Node.prototype._loadConsensus = function(config) { - var options; - if (!config) { - options = {}; - } else { - options = _.clone(config.consensus || {}); - } - options.node = this; - this.chain = new Chain(options); -}; - -Node.prototype._initialize = function() { - var self = this; - - this._initializeChain(); - - this.start(function(err) { - if(err) { - return self.emit('error', err); - } - self.emit('ready'); - }); -}; - -Node.prototype._initializeChain = function() { - - var self = this; - this.chain.on('ready', function() { - log.info('Bitcoin Chain Ready'); - - // Notify that there is a new tip - self.modules.bitcoind.on('tip', function(height) { - if(!self.stopping) { - var percentage = self.modules.bitcoind.syncPercentage(); - log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); - self._syncBitcoind(); - } - }); - - }); - this.chain.on('error', function(err) { - Error.captureStackTrace(err); - self.emit('error', err); - }); -}; - -Node.prototype.getServices = function() { - var services = [ - { - name: 'chain', - dependencies: ['db'] - } - ]; - - services = services.concat(this._unloadedModules); - - return services; -}; - Node.prototype.getServiceOrder = function() { - var services = this.getServices(); + var services = this._unloadedModules; // organize data for sorting var names = []; @@ -418,6 +132,37 @@ Node.prototype.getServiceOrder = function() { return stack; }; +Node.prototype._instantiateModule = 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.start = function(callback) { var self = this; var servicesOrder = this.getServiceOrder(); @@ -426,14 +171,12 @@ Node.prototype.start = function(callback) { servicesOrder, function(service, 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); + try { + self._instantiateModule(service); + } catch(err) { + return callback(err); } + self.modules[service.name].start(next); }, callback ); @@ -451,11 +194,7 @@ Node.prototype.stop = function(callback) { services, function(service, next) { log.info('Stopping ' + service.name); - if (service.module) { - self.modules[service.name].stop(next); - } else { - self[service.name].stop(next); - } + self.modules[service.name].stop(next); }, callback ); diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index 8ce78847..27bda159 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -68,8 +68,8 @@ function start(options) { function logSyncStatus() { log.info( - 'Sync Status: Tip:', node.chain.tip.hash, - 'Height:', node.chain.tip.__height, + 'Sync Status: Tip:', node.modules.db.tip.hash, + 'Height:', node.modules.db.tip.__height, 'Rate:', count/10, 'blocks per second' ); } @@ -184,15 +184,17 @@ function start(options) { log.error(err); }); - node.chain.on('addblock', function(block) { - count++; - // Initialize logging if not already instantiated - if (!interval) { - interval = setInterval(function() { - logSyncStatus(); - count = 0; - }, 10000); - } + node.on('ready', function() { + node.modules.db.on('addblock', function(block) { + count++; + // Initialize logging if not already instantiated + if (!interval) { + interval = setInterval(function() { + logSyncStatus(); + count = 0; + }, 10000); + } + }); }); node.on('stopping', function() { diff --git a/package.json b/package.json index 8e30d982..3a0b4253 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "package": "node bin/package.js", "upload": "node bin/upload.js", "start": "node bin/start.js", - "test": "NODE_ENV=test mocha --recursive", + "test": "NODE_ENV=test mocha -R spec --recursive", "coverage": "istanbul cover _mocha -- --recursive", "libbitcoind": "node bin/start-libbitcoind.js" }, diff --git a/test/chain.unit.js b/test/chain.unit.js deleted file mode 100644 index 3413c1c5..00000000 --- a/test/chain.unit.js +++ /dev/null @@ -1,249 +0,0 @@ -'use strict'; - -var chai = require('chai'); -var should = chai.should(); -var sinon = require('sinon'); -var memdown = require('memdown'); - -var index = require('../'); -var DB = index.DB; -var Chain = index.Chain; -var bitcore = require('bitcore'); -var BufferUtil = bitcore.util.buffer; -var Block = bitcore.Block; -var BN = bitcore.crypto.BN; - -var chainData = require('./data/testnet-blocks.json'); - -describe('Bitcoin Chain', function() { - - describe('@constructor', function() { - - it('can create a new instance with and without `new`', function() { - var chain = new Chain(); - chain = Chain(); - }); - - }); - - describe('#start', function() { - it('should call the callback when base chain is initialized', function(done) { - var chain = new Chain(); - chain.node = {}; - chain.node.modules = {}; - chain.node.modules.bitcoind = {}; - chain.node.modules.bitcoind.genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); - chain.initialize = function() { - chain.emit('initialized'); - }; - - chain.start(done); - }); - }); - - describe('#initialize', function() { - - it('should initialize the chain with the genesis block if no metadata is found in the db', function(done) { - var db = {}; - db.getMetadata = sinon.stub().callsArgWith(0, null, {}); - db.putMetadata = sinon.stub().callsArg(1); - db.getTransactionsFromBlock = sinon.stub(); - db.connectBlock = sinon.stub().callsArg(1); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - - chain.on('ready', function() { - should.exist(chain.tip); - chain.tip.hash.should.equal('genesis'); - Number(chain.tip.__weight.toString(10)).should.equal(0); - done(); - }); - chain.on('error', function(err) { - should.not.exist(err); - done(); - }); - - chain.initialize(); - }); - - it('should initialize the chain with the metadata from the database if it exists', function(done) { - var db = {}; - db.getMetadata = sinon.stub().callsArgWith(0, null, {tip: 'block2', tipWeight: 2}); - db.putMetadata = sinon.stub().callsArg(1); - db.getBlock = sinon.stub().callsArgWith(1, null, {hash: 'block2', prevHash: 'block1'}); - db.getTransactionsFromBlock = sinon.stub(); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - chain.getHeightForBlock = sinon.stub().callsArgWith(1, null, 10); - chain.getWeight = sinon.stub().callsArgWith(1, null, new BN(50)); - chain.on('ready', function() { - should.exist(chain.tip); - chain.tip.hash.should.equal('block2'); - done(); - }); - chain.on('error', function(err) { - should.not.exist(err); - done(); - }); - chain.initialize(); - }); - - it('emit error from getMetadata', function(done) { - var db = { - getMetadata: function(cb) { - cb(new Error('getMetadataError')); - } - }; - db.getTransactionsFromBlock = sinon.stub(); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - chain.on('error', function(error) { - should.exist(error); - error.message.should.equal('getMetadataError'); - done(); - }); - chain.initialize(); - }); - - it('emit error from getBlock', function(done) { - var db = { - getMetadata: function(cb) { - cb(null, {tip: 'tip'}); - }, - getBlock: function(tip, cb) { - cb(new Error('getBlockError')); - } - }; - db.getTransactionsFromBlock = sinon.stub(); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - chain.on('error', function(error) { - should.exist(error); - error.message.should.equal('getBlockError'); - done(); - }); - chain.initialize(); - }); - }); - - describe('#stop', function() { - it('should call the callback', function(done) { - var chain = new Chain(); - chain.stop(done); - }); - }); - - describe('#_validateBlock', function() { - it('should call the callback', function(done) { - var chain = new Chain(); - chain._validateBlock('block', function(err) { - should.not.exist(err); - done(); - }); - }); - }); - - describe('#getWeight', function() { - var work = '000000000000000000000000000000000000000000005a7b3c42ea8b844374e9'; - var chain = new Chain(); - chain.node = {}; - chain.node.modules = {}; - chain.node.modules.db = {}; - chain.node.modules.bitcoind = { - getBlockIndex: sinon.stub().returns({ - chainWork: work - }) - }; - - it('should give the weight as a BN', function(done) { - chain.getWeight('hash', function(err, weight) { - should.not.exist(err); - weight.toString(16, 64).should.equal(work); - done(); - }); - }); - - it('should give an error if the weight is undefined', function(done) { - chain.node.modules.bitcoind.getBlockIndex = sinon.stub().returns(undefined); - chain.getWeight('hash2', function(err, weight) { - should.exist(err); - done(); - }); - }); - }); - - describe('#getHashes', function() { - - it('should get an array of chain hashes', function(done) { - - var blocks = {}; - var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex')); - var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex')); - var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex')); - blocks[genesisBlock.hash] = genesisBlock; - blocks[block1.hash] = block1; - blocks[block2.hash] = block2; - - var db = {}; - db.getPrevHash = function(blockHash, cb) { - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex'); - cb(null, prevHash); - }; - - var node = { - modules: { - db: db - } - }; - - var chain = new Chain({ - node: node, - genesis: genesisBlock - }); - - chain.tip = block2; - - 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(); - }); - - }); - }); - - -}); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index 5332afd3..69a1e34d 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -23,7 +23,7 @@ var mocknode = { } }; -describe('AddressModule', function() { +describe('Address Module', function() { describe('#getAPIMethods', function() { it('should return the correct methods', function() { @@ -424,13 +424,12 @@ describe('AddressModule', function() { describe('#getOutputs', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var db = {}; + var db = { + tip: { + __height: 1 + } + }; var testnode = { - chain: { - tip: { - __height: 1 - } - }, modules: { db: db, bitcoind: { @@ -819,6 +818,9 @@ describe('AddressModule', function() { ]; var db = { + tip: { + __height: 1 + }, getTransactionWithBlockInfo: function(txid, queryMempool, callback) { var transaction = { populateInputs: sinon.stub().callsArg(2) @@ -853,11 +855,6 @@ describe('AddressModule', function() { } }; var testnode = { - chain: { - tip: { - __height: 1 - } - }, modules: { db: db, bitcoind: { diff --git a/test/modules/db.unit.js b/test/modules/db.unit.js index 32bafcbc..1be47388 100644 --- a/test/modules/db.unit.js +++ b/test/modules/db.unit.js @@ -2,13 +2,18 @@ var should = require('chai').should(); var sinon = require('sinon'); +var EventEmitter = require('events').EventEmitter; +var proxyquire = require('proxyquire'); var index = require('../../'); var DB = index.modules.DBModule; var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); var Networks = bitcore.Networks; var Block = bitcore.Block; +var BufferUtil = bitcore.util.buffer; var transactionData = require('../data/bitcoin-transactions.json'); +var chainHashes = require('../data/hashes.json'); +var chainData = require('../data/testnet-blocks.json'); var errors = index.errors; var memdown = require('memdown'); var bitcore = require('bitcore'); @@ -16,6 +21,14 @@ var Transaction = bitcore.Transaction; describe('DB Module', function() { + function hexlebuf(hexString){ + return BufferUtil.reverse(new Buffer(hexString, 'hex')); + } + + function lebufhex(buf) { + return BufferUtil.reverse(buf).toString('hex'); + } + var baseConfig = { node: { network: Networks.testnet, @@ -61,7 +74,7 @@ describe('DB Module', function() { }); it('should load the db with regtest', function() { // Switch to use regtest - Networks.remove(Networks.testnet); + // Networks.remove(Networks.testnet); Networks.add({ name: 'regtest', alias: 'regtest', @@ -85,36 +98,36 @@ describe('DB Module', function() { var db = new DB(config); db.dataPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-node.db'); Networks.remove(regtest); - // Add testnet back - Networks.add({ - name: 'testnet', - alias: 'testnet', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0x0b110907, - port: 18333, - dnsSeeds: [ - 'testnet-seed.bitcoin.petertodd.org', - 'testnet-seed.bluematt.me', - 'testnet-seed.alexykot.me', - 'testnet-seed.bitcoin.schildbach.de' - ] - }); }); }); describe('#start', function() { + var TestDB; + var genesisBuffer; + + before(function() { + TestDB = proxyquire('../../lib/modules/db', { + fs: { + existsSync: sinon.stub().returns(true) + }, + levelup: sinon.stub() + }); + genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); + }); + it('should emit ready', function(done) { - var db = new DB(baseConfig); + var db = new TestDB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { - on: sinon.spy() + on: sinon.spy(), + genesisBuffer: genesisBuffer }; - db.addModule = sinon.spy(); + db._addModule = sinon.spy(); + db.getMetadata = sinon.stub().callsArg(0); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); var readyFired = false; db.on('ready', function() { readyFired = true; @@ -124,6 +137,143 @@ describe('DB Module', function() { done(); }); }); + + it('genesis block if no metadata is found in the db', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var db = new TestDB({node: node}); + db.getMetadata = sinon.stub().callsArgWith(0, null, null); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); + db.start(function() { + should.exist(db.tip); + db.tip.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'); + done(); + }); + }); + + it('metadata from the database if it exists', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var tip = Block.fromBuffer(genesisBuffer); + var db = new TestDB({node: node}); + var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; + db.getMetadata = sinon.stub().callsArgWith(0, null, { + tip: tipHash, + tipHeight: 0 + }); + db.getBlock = sinon.stub().callsArgWith(1, null, tip); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); + db.start(function() { + should.exist(db.tip); + db.tip.hash.should.equal(tipHash); + done(); + }); + }); + + it('emit error from getMetadata', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var db = new TestDB({node: node}); + db.getMetadata = sinon.stub().callsArgWith(0, new Error('test')); + db.start(function(err) { + should.exist(err); + err.message.should.equal('test'); + done(); + }); + }); + + it('emit error from getBlock', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var db = new TestDB({node: node}); + var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; + db.getMetadata = sinon.stub().callsArgWith(0, null, { + tip: tipHash, + tipHeigt: 0 + }); + db.getBlock = sinon.stub().callsArgWith(1, new Error('test')); + db.start(function(err) { + should.exist(err); + err.message.should.equal('test'); + done(); + }); + }); + + it('will call sync when there is a new tip', function(done) { + var db = new TestDB(baseConfig); + db.node.modules = {}; + db.node.modules.bitcoind = new EventEmitter(); + db.node.modules.bitcoind.syncPercentage = sinon.spy(); + db.node.modules.bitcoind.genesisBuffer = genesisBuffer; + db.getMetadata = sinon.stub().callsArg(0); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); + db.start(function() { + db.sync = function() { + db.node.modules.bitcoind.syncPercentage.callCount.should.equal(1); + done(); + }; + db.node.modules.bitcoind.emit('tip', 10); + }); + }); + + it('will not call sync when there is a new tip and shutting down', function(done) { + var db = new TestDB(baseConfig); + db.node.modules = {}; + db.node.modules.bitcoind = new EventEmitter(); + db.node.modules.bitcoind.syncPercentage = sinon.spy(); + db.node.modules.bitcoind.genesisBuffer = genesisBuffer; + db.getMetadata = sinon.stub().callsArg(0); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.node.stopping = true; + db.sync = sinon.stub(); + db.start(function() { + db.sync.callCount.should.equal(1); + db.node.modules.bitcoind.once('tip', function() { + db.sync.callCount.should.equal(1); + done(); + }); + db.node.modules.bitcoind.emit('tip', 10); + }); + }); + }); describe('#stop', function() { @@ -404,4 +554,237 @@ describe('DB Module', function() { methods.length.should.equal(5); }); }); + + describe('#getHashes', function() { + + it('should get an array of chain hashes', function(done) { + + var blocks = {}; + var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex')); + var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex')); + var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex')); + blocks[genesisBlock.hash] = genesisBlock; + blocks[block1.hash] = block1; + blocks[block2.hash] = block2; + + var db = new DB(baseConfig); + db.genesis = genesisBlock; + db.getPrevHash = function(blockHash, cb) { + // TODO: expose prevHash as a string from bitcore + var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex'); + cb(null, prevHash); + }; + + db.tip = block2; + + // the test + db.getHashes(block2.hash, function(err, hashes) { + should.not.exist(err); + should.exist(hashes); + hashes.length.should.equal(3); + done(); + }); + + }); + }); + + describe('#findCommonAncestor', function() { + it('will find an ancestor 6 deep', function() { + var db = new DB(baseConfig); + db.getHashes = function(tipHash, callback) { + callback(null, chainHashes); + }; + db.tip = { + hash: chainHashes[chainHashes.length] + }; + var expectedAncestor = chainHashes[chainHashes.length - 6]; + + var forkedBlocks = { + 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': { + header: { + prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a') + } + }, + '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': { + header: { + prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c') + } + }, + 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': { + header: { + prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31') + } + }, + '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': { + header: { + prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453') + } + }, + 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': { + header: { + prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618') + } + }, + '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': { + header: { + prevHash: hexlebuf(expectedAncestor) + } + }, + }; + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlockIndex: function(hash) { + var block = forkedBlocks[hash]; + return { + prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex') + }; + } + }; + var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82']; + db.findCommonAncestor(block, function(err, ancestorHash) { + if (err) { + throw err; + } + ancestorHash.should.equal(expectedAncestor); + }); + }); + }); + + describe('#syncRewind', function() { + it('will undo blocks 6 deep', function() { + var db = new DB(baseConfig); + var ancestorHash = chainHashes[chainHashes.length - 6]; + db.tip = { + __height: 10, + hash: chainHashes[chainHashes.length], + header: { + prevHash: hexlebuf(chainHashes[chainHashes.length - 1]) + } + }; + db.saveMetadata = sinon.stub(); + db.emit = sinon.stub(); + db.getBlock = function(hash, callback) { + setImmediate(function() { + for(var i = chainHashes.length; i > 0; i--) { + var block = { + hash: chainHashes[i], + header: { + prevHash: hexlebuf(chainHashes[i - 1]) + } + }; + if (chainHashes[i] === hash) { + callback(null, block); + } + } + }); + }; + db.node.modules = {}; + db.disconnectBlock = function(block, callback) { + setImmediate(callback); + }; + db.findCommonAncestor = function(block, callback) { + setImmediate(function() { + callback(null, ancestorHash); + }); + }; + var forkedBlock = {}; + db.syncRewind(forkedBlock, function(err) { + if (err) { + throw err; + } + db.tip.__height.should.equal(4); + db.tip.hash.should.equal(ancestorHash); + }); + }); + }); + + describe('#sync', function() { + var node = new EventEmitter(); + var syncConfig = { + node: node, + store: memdown + }; + syncConfig.node.network = Networks.testnet; + syncConfig.node.datadir = 'testdir'; + it('will get and add block up to the tip height', function(done) { + var db = new DB(syncConfig); + var blockBuffer = new Buffer(blockData, 'hex'); + var block = Block.fromBuffer(blockBuffer); + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), + isSynced: sinon.stub().returns(true), + height: 1 + }; + db.tip = { + __height: 0, + hash: lebufhex(block.header.prevHash) + }; + db.getHashes = sinon.stub().callsArgWith(1, null); + db.saveMetadata = sinon.stub(); + db.emit = sinon.stub(); + db.cache = { + hashes: {} + }; + db.connectBlock = function(block, callback) { + db.tip.__height += 1; + callback(); + }; + db.node.once('synced', function() { + done(); + }); + db.sync(); + }); + it('will exit and emit error with error from bitcoind.getBlock', function(done) { + var db = new DB(syncConfig); + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlock: sinon.stub().callsArgWith(1, new Error('test error')), + height: 1 + }; + db.tip = { + __height: 0 + }; + db.node.on('error', function(err) { + err.message.should.equal('test error'); + done(); + }); + db.sync(); + }); + it('will stop syncing when the node is stopping', function(done) { + var db = new DB(syncConfig); + var blockBuffer = new Buffer(blockData, 'hex'); + var block = Block.fromBuffer(blockBuffer); + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), + isSynced: sinon.stub().returns(true), + height: 1 + }; + db.tip = { + __height: 0, + hash: block.prevHash + }; + db.saveMetadata = sinon.stub(); + db.emit = sinon.stub(); + db.cache = { + hashes: {} + }; + db.connectBlock = function(block, callback) { + db.tip.__height += 1; + callback(); + }; + db.node.stopping = true; + var synced = false; + db.node.once('synced', function() { + synced = true; + }); + db.sync(); + setTimeout(function() { + synced.should.equal(false); + done(); + }, 10); + }); + }); + }); diff --git a/test/node.unit.js b/test/node.unit.js index 59313f80..195cb2ce 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -2,16 +2,9 @@ var should = require('chai').should(); var sinon = require('sinon'); -var EventEmitter = require('events').EventEmitter; var bitcore = require('bitcore'); var Networks = bitcore.Networks; -var BufferUtil = bitcore.util.buffer; -var Block = bitcore.Block; -var blockData = require('./data/livenet-345003.json'); var proxyquire = require('proxyquire'); -var index = require('..'); -var fs = require('fs'); -var chainHashes = require('./data/hashes.json'); var util = require('util'); var BaseModule = require('../lib/module'); @@ -23,30 +16,43 @@ describe('Bitcore Node', function() { var Node; - function hexlebuf(hexString){ - return BufferUtil.reverse(new Buffer(hexString, 'hex')); - } - - function lebufhex(buf) { - return BufferUtil.reverse(buf).toString('hex'); - } - before(function() { Node = proxyquire('../lib/node', {}); Node.prototype._loadConfiguration = sinon.spy(); Node.prototype._initialize = sinon.spy(); }); + after(function() { + var regtest = Networks.get('regtest'); + if (regtest) { + Networks.remove(regtest); + } + // restore testnet + Networks.add({ + name: 'testnet', + alias: 'testnet', + pubkeyhash: 0x6f, + privatekey: 0xef, + scripthash: 0xc4, + xpubkey: 0x043587cf, + xprivkey: 0x04358394, + networkMagic: 0x0b110907, + port: 18333, + dnsSeeds: [ + 'testnet-seed.bitcoin.petertodd.org', + 'testnet-seed.bluematt.me', + 'testnet-seed.alexykot.me', + 'testnet-seed.bitcoin.schildbach.de' + ], + }); + }); describe('@constructor', function() { - it('will set properties', function() { - function TestModule() {} + var TestModule; + before(function() { + TestModule = function TestModule() {}; util.inherits(TestModule, BaseModule); - TestModule.prototype.getData = function() {}; - TestModule.prototype.getAPIMethods = function() { - return [ - ['getData', this, this.getData, 1] - ]; - }; + }); + it('will set properties', function() { var config = { datadir: 'testdir', modules: [ @@ -57,14 +63,70 @@ describe('Bitcore Node', function() { ], }; var TestNode = proxyquire('../lib/node', {}); - TestNode.prototype._loadConfiguration = sinon.spy(); - TestNode.prototype._initialize = sinon.spy(); + TestNode.prototype.start = sinon.spy(); var node = new TestNode(config); - TestNode.prototype._loadConfiguration.callCount.should.equal(1); - TestNode.prototype._initialize.callCount.should.equal(1); + TestNode.prototype.start.callCount.should.equal(1); node._unloadedModules.length.should.equal(1); node._unloadedModules[0].name.should.equal('test1'); node._unloadedModules[0].module.should.equal(TestModule); + node.network.should.equal(Networks.defaultNetwork); + }); + it('will set network to testnet', function() { + var config = { + network: 'testnet', + datadir: 'testdir', + modules: [ + { + name: 'test1', + module: TestModule + } + ], + }; + var TestNode = proxyquire('../lib/node', {}); + TestNode.prototype.start = sinon.spy(); + var node = new TestNode(config); + node.network.should.equal(Networks.testnet); + }); + it('will set network to regtest', function() { + var config = { + network: 'regtest', + datadir: 'testdir', + modules: [ + { + name: 'test1', + module: TestModule + } + ], + }; + var TestNode = proxyquire('../lib/node', {}); + TestNode.prototype.start = sinon.spy(); + var node = new TestNode(config); + var regtest = Networks.get('regtest'); + should.exist(regtest); + node.network.should.equal(regtest); + }); + it('should emit error if an error occurred starting services', function(done) { + var config = { + datadir: 'testdir', + modules: [ + { + name: 'test1', + module: TestModule + } + ], + }; + var TestNode = proxyquire('../lib/node', {}); + TestNode.prototype.start = function(callback) { + setImmediate(function() { + callback(new Error('error')); + }); + }; + var node = new TestNode(config); + node.once('error', function(err) { + should.exist(err); + err.message.should.equal('error'); + done(); + }); }); }); @@ -76,27 +138,6 @@ describe('Bitcore Node', function() { }); }); - describe('#addModule', function() { - it('will instantiate an instance and load api methods', function() { - var node = new Node(baseConfig); - 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(baseConfig); @@ -116,6 +157,7 @@ describe('Bitcore Node', function() { methods.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); }); }); + describe('#getAllPublishEvents', function() { it('should return modules publish events', function() { var node = new Node(baseConfig); @@ -134,381 +176,28 @@ describe('Bitcore Node', function() { events.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); }); }); - describe('#_loadConfiguration', function() { - it('should call the necessary methods', function() { - var TestNode = proxyquire('../lib/node', {}); - TestNode.prototype._initialize = sinon.spy(); - TestNode.prototype._loadConsensus = sinon.spy(); - var node = new TestNode(baseConfig); - node._loadConsensus.callCount.should.equal(1); - }); - }); - describe('#_syncBitcoindAncestor', function() { - it('will find an ancestor 6 deep', function() { - var node = new Node(baseConfig); - node.chain = { - getHashes: function(tipHash, callback) { - callback(null, chainHashes); - }, - tip: { - hash: chainHashes[chainHashes.length] - } - }; - var expectedAncestor = chainHashes[chainHashes.length - 6]; - - var forkedBlocks = { - 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': { - header: { - prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a') - } - }, - '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': { - header: { - prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c') - } - }, - 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': { - header: { - prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31') - } - }, - '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': { - header: { - prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453') - } - }, - 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': { - header: { - prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618') - } - }, - '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': { - header: { - prevHash: hexlebuf(expectedAncestor) - } - }, - }; - node.modules = {}; - node.modules.bitcoind = { - getBlockIndex: function(hash) { - var block = forkedBlocks[hash]; - return { - prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex') - }; - } - }; - var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82']; - node._syncBitcoindAncestor(block, function(err, ancestorHash) { - if (err) { - throw err; - } - ancestorHash.should.equal(expectedAncestor); - }); - }); - }); - describe('#_syncBitcoindRewind', function() { - it('will undo blocks 6 deep', function() { - var node = new Node(baseConfig); - var ancestorHash = chainHashes[chainHashes.length - 6]; - node.chain = { - tip: { - __height: 10, - hash: chainHashes[chainHashes.length], - header: { - prevHash: hexlebuf(chainHashes[chainHashes.length - 1]) - } - }, - saveMetadata: sinon.stub(), - emit: sinon.stub() - }; - node.getBlock = function(hash, callback) { - setImmediate(function() { - for(var i = chainHashes.length; i > 0; i--) { - var block = { - hash: chainHashes[i], - header: { - prevHash: hexlebuf(chainHashes[i - 1]) - } - }; - if (chainHashes[i] === hash) { - callback(null, block); - } - } - }); - }; - node.modules = {}; - node.modules.db = { - disconnectBlock: function(block, callback) { - setImmediate(callback); - } - }; - node._syncBitcoindAncestor = function(block, callback) { - setImmediate(function() { - callback(null, ancestorHash); - }); - }; - var forkedBlock = {}; - node._syncBitcoindRewind(forkedBlock, function(err) { - if (err) { - throw err; - } - node.chain.tip.__height.should.equal(4); - node.chain.tip.hash.should.equal(ancestorHash); - }); - }); - }); - describe('#_syncBitcoind', function() { - it('will get and add block up to the tip height', function(done) { - var node = new Node(baseConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - node.modules = {}; - node.modules.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - node.chain = { - tip: { - __height: 0, - hash: lebufhex(block.header.prevHash) - }, - getHashes: sinon.stub().callsArgWith(1, null), - saveMetadata: sinon.stub(), - emit: sinon.stub(), - cache: { - hashes: {} - } - }; - node.modules.db = { - connectBlock: function(block, callback) { - node.chain.tip.__height += 1; - callback(); - } - }; - node.on('synced', function() { - done(); - }); - node._syncBitcoind(); - }); - it('will exit and emit error with error from bitcoind.getBlock', function(done) { - var node = new Node(baseConfig); - node.modules = {}; - node.modules.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, new Error('test error')), - height: 1 - }; - node.chain = { - tip: { - __height: 0 - } - }; - node.on('error', function(err) { - err.message.should.equal('test error'); - done(); - }); - node._syncBitcoind(); - }); - it('will stop syncing when the node is stopping', function(done) { - var node = new Node(baseConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - node.modules = {}; - node.modules.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - node.chain = { - tip: { - __height: 0, - hash: block.prevHash - }, - saveMetadata: sinon.stub(), - emit: sinon.stub(), - cache: { - hashes: {} - } - }; - node.modules.db = { - connectBlock: function(block, callback) { - node.chain.tip.__height += 1; - callback(); - } - }; - node.stopping = true; - - var synced = false; - - node.on('synced', function() { - synced = true; - }); - - node._syncBitcoind(); - - setTimeout(function() { - synced.should.equal(false); - done(); - }, 10); - }); - }); - - describe('#_loadNetwork', function() { - it('should use the testnet network if testnet is specified', function() { - var config = { - datadir: 'testdir', - network: 'testnet' - }; - var node = new Node(config); - node._loadNetwork(config); - node.network.name.should.equal('testnet'); - }); - it('should use the regtest network if regtest is specified', function() { - var config = { - datadir: 'testdir', - network: 'regtest' - }; - var node = new Node(config); - node._loadNetwork(config); - node.network.name.should.equal('regtest'); - }); - it('should use the livenet network if nothing is specified', function() { - var config = { - datadir: 'testdir' - }; - var node = new Node(config); - node._loadNetwork(config); - node.network.name.should.equal('livenet'); - }); - }); - describe('#_loadConsensus', function() { - var node; - before(function() { - node = new Node(baseConfig); - }); - it('will set properties', function() { - node._loadConsensus(); - should.exist(node.chain); - }); - }); - describe('#_initialize', function() { - var node; - before(function() { - var TestNode = proxyquire('../lib/node', {}); - TestNode.prototype._loadConfiguration = sinon.spy(); - TestNode.prototype._initializeChain = sinon.spy(); - - // mock the _initialize during construction - var _initialize = TestNode.prototype._initialize; - TestNode.prototype._initialize = sinon.spy(); - - node = new TestNode(baseConfig); - node.chain = { - on: sinon.spy() - }; - node.modules = {}; - node.modules.bitcoind = { - on: sinon.spy() - }; - node.modules.db = { - on: sinon.spy() - }; - // restore the original method - node._initialize = _initialize; - }); - - it('should initialize', function(done) { - node.once('ready', function() { - done(); - }); - node.start = sinon.stub().callsArg(0); - node._initialize(); - node._initializeChain.callCount.should.equal(1); - }); - - it('should emit an error if an error occurred starting services', function(done) { - node.once('error', function(err) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - node.start = sinon.stub().callsArgWith(0, new Error('error')); - node._initialize(); - }); - - }); - - describe('#_initializeChain', function() { - - it('will call sync when there is a new tip', function(done) { - var node = new Node(baseConfig); - node.chain = new EventEmitter(); - node.modules = {}; - node.modules.bitcoind = new EventEmitter(); - node.modules.bitcoind.syncPercentage = sinon.spy(); - node._syncBitcoind = function() { - node.modules.bitcoind.syncPercentage.callCount.should.equal(1); - done(); - }; - node._initializeChain(); - node.chain.emit('ready'); - node.modules.bitcoind.emit('tip', 10); - }); - it('will not call sync when there is a new tip and shutting down', function(done) { - var node = new Node(baseConfig); - node.chain = new EventEmitter(); - node.modules = {}; - node.modules.bitcoind = new EventEmitter(); - node._syncBitcoind = sinon.spy(); - node.modules.bitcoind.syncPercentage = sinon.spy(); - node.stopping = true; - node.modules.bitcoind.on('tip', function() { - setImmediate(function() { - node.modules.bitcoind.syncPercentage.callCount.should.equal(0); - node._syncBitcoind.callCount.should.equal(0); - done(); - }); - }); - node._initializeChain(); - node.chain.emit('ready'); - node.modules.bitcoind.emit('tip', 10); - }); - it('will emit an error from the chain', function(done) { - var node = new Node(baseConfig); - node.chain = new EventEmitter(); - node.on('error', function(err) { - should.exist(err); - err.message.should.equal('test error'); - done(); - }); - node._initializeChain(); - node.chain.emit('error', new Error('test error')); - }); - }); describe('#getServiceOrder', function() { it('should return the services in the correct order', function() { var node = new Node(baseConfig); - node.getServices = function() { - return [ - { - name: 'chain', - dependencies: ['db'] - }, - { - name: 'db', + node._unloadedModules = [ + { + name: 'chain', + dependencies: ['db'] + }, + { + name: 'db', dependencies: ['daemon', 'p2p'] - }, - { - name:'daemon', - dependencies: [] - }, - { - name: 'p2p', - dependencies: [] - } - ]; - }; + }, + { + name:'daemon', + dependencies: [] + }, + { + name: 'p2p', + dependencies: [] + } + ]; var order = node.getServiceOrder(); order[0].name.should.equal('daemon'); order[1].name.should.equal('p2p'); @@ -517,9 +206,31 @@ describe('Bitcore Node', function() { }); }); + describe('#_instantiateModule', function() { + it('will instantiate an instance and load api methods', function() { + var node = new Node(baseConfig); + 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._instantiateModule(service); + should.exist(node.modules.testmodule); + should.exist(node.getData); + }); + }); + describe('#start', function() { it('will call start for each module', function(done) { var node = new Node(baseConfig); + function TestModule() {} util.inherits(TestModule, BaseModule); TestModule.prototype.start = sinon.stub().callsArg(0); @@ -529,23 +240,76 @@ describe('Bitcore Node', function() { ['getData', this, this.getData, 1] ]; }; - node.test2 = {}; - node.test2.start = sinon.stub().callsArg(0); + + function TestModule2() {} + util.inherits(TestModule2, BaseModule); + TestModule2.prototype.start = sinon.stub().callsArg(0); + TestModule2.prototype.getData2 = function() {}; + TestModule2.prototype.getAPIMethods = function() { + return [ + ['getData2', this, this.getData2, 1] + ]; + }; + node.getServiceOrder = sinon.stub().returns([ { name: 'test1', module: TestModule }, { - name: 'test2' + name: 'test2', + module: TestModule2 } ]); node.start(function() { - node.test2.start.callCount.should.equal(1); + TestModule2.prototype.start.callCount.should.equal(1); TestModule.prototype.start.callCount.should.equal(1); + should.exist(node.getData2); + should.exist(node.getData); done(); }); }); + it('will error if there are conflicting API methods', function(done) { + var node = new Node(baseConfig); + + 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] + ]; + }; + + function ConflictModule() {} + util.inherits(ConflictModule, BaseModule); + ConflictModule.prototype.start = sinon.stub().callsArg(0); + ConflictModule.prototype.getData = function() {}; + ConflictModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + + node.getServiceOrder = sinon.stub().returns([ + { + name: 'test', + module: TestModule + }, + { + name: 'conflict', + module: ConflictModule + } + ]); + + node.start(function(err) { + should.exist(err); + err.message.should.match(/^Existing API method exists/); + done(); + }); + + }); }); describe('#stop', function() { @@ -566,20 +330,15 @@ describe('Bitcore Node', function() { 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(); }); }); }); - }); From 7551f487f80ee1562ac12749485f17b4dae73e58 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Mon, 31 Aug 2015 09:00:00 -0400 Subject: [PATCH 4/5] Rename modules to services. --- bin/start-libbitcoind.js | 2 +- index.js | 10 +- integration/regtest-node.js | 30 ++--- integration/regtest.js | 2 +- lib/bus.js | 24 ++-- lib/node.js | 42 +++--- lib/scaffold/add.js | 40 +++--- lib/scaffold/create.js | 4 +- lib/scaffold/default-config.js | 2 +- lib/scaffold/start.js | 54 ++++---- lib/{module.js => service.js} | 22 +-- lib/{modules => services}/address.js | 78 +++++------ lib/{modules => services}/bitcoind.js | 9 +- lib/{modules => services}/db.js | 40 +++--- test/bus.unit.js | 124 +++++++++-------- test/node.unit.js | 140 ++++++++++---------- test/scaffold/add.integration.js | 14 +- test/scaffold/create.integration.js | 2 +- test/scaffold/start.integration.js | 10 +- test/{modules => services}/address.unit.js | 84 ++++++------ test/{modules => services}/bitcoind.unit.js | 8 +- test/{modules => services}/db.unit.js | 139 ++++++++++--------- 22 files changed, 436 insertions(+), 444 deletions(-) rename lib/{module.js => service.js} (69%) rename lib/{modules => services}/address.js (82%) rename lib/{modules => services}/bitcoind.js (97%) rename lib/{modules => services}/db.js (93%) rename test/{modules => services}/address.unit.js (94%) rename test/{modules => services}/bitcoind.unit.js (85%) rename test/{modules => services}/db.unit.js (88%) diff --git a/bin/start-libbitcoind.js b/bin/start-libbitcoind.js index a2860ce2..e2f1c637 100644 --- a/bin/start-libbitcoind.js +++ b/bin/start-libbitcoind.js @@ -10,7 +10,7 @@ process.title = 'libbitcoind'; /** * daemon */ -var daemon = require('../').modules.BitcoinModule({ +var daemon = require('../').services.Bitcoin({ node: { datadir: process.env.BITCORENODE_DIR || process.env.HOME + '/.bitcoin', network: { diff --git a/index.js b/index.js index 94317300..b5c1bb45 100644 --- a/index.js +++ b/index.js @@ -3,13 +3,13 @@ module.exports = require('./lib'); module.exports.Node = require('./lib/node'); module.exports.Transaction = require('./lib/transaction'); -module.exports.Module = require('./lib/module'); +module.exports.Service = require('./lib/service'); module.exports.errors = require('./lib/errors'); -module.exports.modules = {}; -module.exports.modules.AddressModule = require('./lib/modules/address'); -module.exports.modules.BitcoinModule = require('./lib/modules/bitcoind'); -module.exports.modules.DBModule = require('./lib/modules/db'); +module.exports.services = {}; +module.exports.services.Address = require('./lib/services/address'); +module.exports.services.Bitcoin = require('./lib/services/bitcoind'); +module.exports.services.DB = require('./lib/services/db'); module.exports.scaffold = {}; module.exports.scaffold.create = require('./lib/scaffold/create'); diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 6edcb693..873801d7 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -25,9 +25,9 @@ var should = chai.should(); var BitcoinRPC = require('bitcoind-rpc'); var index = require('..'); var BitcoreNode = index.Node; -var AddressModule = index.modules.AddressModule; -var BitcoinModule = index.modules.BitcoinModule; -var DBModule = index.modules.DBModule; +var AddressService = index.services.Address; +var BitcoinService = index.services.Bitcoin; +var DBService = index.services.DB; var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG'; var testKey; var client; @@ -65,21 +65,21 @@ describe('Node Functionality', function() { var configuration = { datadir: datadir, network: 'regtest', - modules: [ + services: [ { name: 'db', - module: DBModule, - dependencies: DBModule.dependencies + module: DBService, + dependencies: DBService.dependencies }, { name: 'bitcoind', - module: BitcoinModule, - dependencies: BitcoinModule.dependencies + module: BitcoinService, + dependencies: BitcoinService.dependencies }, { name: 'address', - module: AddressModule, - dependencies: AddressModule.dependencies + module: AddressService, + dependencies: AddressService.dependencies } ] }; @@ -102,7 +102,7 @@ describe('Node Functionality', function() { }); var syncedHandler = function() { - if (node.modules.db.tip.__height === 150) { + if (node.services.db.tip.__height === 150) { node.removeListener('synced', syncedHandler); done(); } @@ -178,18 +178,18 @@ describe('Node Functionality', function() { blocksRemoved++; }; - node.modules.db.on('removeblock', removeBlock); + node.services.db.on('removeblock', removeBlock); var addBlock = function() { blocksAdded++; if (blocksAdded === 2 && blocksRemoved === 1) { - node.modules.db.removeListener('addblock', addBlock); - node.modules.db.removeListener('removeblock', removeBlock); + node.services.db.removeListener('addblock', addBlock); + node.services.db.removeListener('removeblock', removeBlock); done(); } }; - node.modules.db.on('addblock', addBlock); + node.services.db.on('addblock', addBlock); // We need to add a transaction to the mempool so that the next block will // have a different hash as the hash has been invalidated. diff --git a/integration/regtest.js b/integration/regtest.js index ae804d8a..59ca049b 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -61,7 +61,7 @@ describe('Daemon Binding Functionality', function() { throw err; } - bitcoind = require('../').modules.BitcoinModule({ + bitcoind = require('../').services.Bitcoin({ node: { datadir: datadir, network: { diff --git a/lib/bus.js b/lib/bus.js index 8c8afc55..73b4383a 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -11,11 +11,11 @@ function Bus(params) { util.inherits(Bus, events.EventEmitter); Bus.prototype.subscribe = function(name) { - var events = this.node.db.getPublishEvents(); + var events = []; - for(var i in this.node.modules) { - var mod = this.node.modules[i]; - events = events.concat(mod.getPublishEvents()); + for(var i in this.node.services) { + var service = this.node.services[i]; + events = events.concat(service.getPublishEvents()); } for (var j = 0; j < events.length; j++) { @@ -29,11 +29,11 @@ Bus.prototype.subscribe = function(name) { }; Bus.prototype.unsubscribe = function(name) { - var events = this.node.db.getPublishEvents(); + var events = []; - for(var i in this.node.modules) { - var mod = this.node.modules[i]; - events = events.concat(mod.getPublishEvents()); + for(var i in this.node.services) { + var service = this.node.services[i]; + events = events.concat(service.getPublishEvents()); } for (var j = 0; j < events.length; j++) { @@ -47,11 +47,11 @@ Bus.prototype.unsubscribe = function(name) { }; Bus.prototype.close = function() { - var events = this.node.db.getPublishEvents(); + var events = []; - for(var i in this.node.modules) { - var mod = this.node.modules[i]; - events = events.concat(mod.getPublishEvents()); + for(var i in this.node.services) { + var service = this.node.services[i]; + events = events.concat(service.getPublishEvents()); } // Unsubscribe from all events diff --git a/lib/node.js b/lib/node.js index 4c147632..be6b8955 100644 --- a/lib/node.js +++ b/lib/node.js @@ -9,7 +9,7 @@ var $ = bitcore.util.preconditions; var index = require('./'); var log = index.log; var Bus = require('./bus'); -var BaseModule = require('./module'); +var BaseService = require('./service'); function Node(config) { if(!(this instanceof Node)) { @@ -19,13 +19,13 @@ function Node(config) { var self = this; this.network = null; - this.modules = {}; - this._unloadedModules = []; + this.services = {}; + this._unloadedServices = []; - // TODO type check the arguments of config.modules - if (config.modules) { - $.checkArgument(Array.isArray(config.modules)); - this._unloadedModules = config.modules; + // TODO type check the arguments of config.services + if (config.services) { + $.checkArgument(Array.isArray(config.services)); + this._unloadedServices = config.services; } $.checkState(config.datadir, 'Node config expects "datadir"'); @@ -71,13 +71,13 @@ Node.prototype._setNetwork = function(config) { }; Node.prototype.openBus = function() { - return new Bus({db: this.modules.db}); + return new Bus({node: this}); }; Node.prototype.getAllAPIMethods = function() { var methods = []; - for(var i in this.modules) { - var mod = this.modules[i]; + for(var i in this.services) { + var mod = this.services[i]; methods = methods.concat(mod.getAPIMethods()); } return methods; @@ -85,8 +85,8 @@ Node.prototype.getAllAPIMethods = function() { Node.prototype.getAllPublishEvents = function() { var events = []; - for (var i in this.modules) { - var mod = this.modules[i]; + for (var i in this.services) { + var mod = this.services[i]; events = events.concat(mod.getPublishEvents()); } return events; @@ -94,7 +94,7 @@ Node.prototype.getAllPublishEvents = function() { Node.prototype.getServiceOrder = function() { - var services = this._unloadedModules; + var services = this._unloadedServices; // organize data for sorting var names = []; @@ -132,19 +132,19 @@ Node.prototype.getServiceOrder = function() { return stack; }; -Node.prototype._instantiateModule = function(service) { +Node.prototype._instantiateService = function(service) { var self = this; var mod = new service.module({ node: this }); $.checkState( - mod instanceof BaseModule, - 'Unexpected module instance type for module:' + service.name + mod instanceof BaseService, + 'Unexpected module instance type for service:' + service.name ); - // include in loaded modules - this.modules[service.name] = mod; + // include in loaded services + this.services[service.name] = mod; // add API methods var methodData = mod.getAPIMethods(); @@ -172,11 +172,11 @@ Node.prototype.start = function(callback) { function(service, next) { log.info('Starting ' + service.name); try { - self._instantiateModule(service); + self._instantiateService(service); } catch(err) { return callback(err); } - self.modules[service.name].start(next); + self.services[service.name].start(next); }, callback ); @@ -194,7 +194,7 @@ Node.prototype.stop = function(callback) { services, function(service, next) { log.info('Stopping ' + service.name); - self.modules[service.name].stop(next); + self.services[service.name].stop(next); }, callback ); diff --git a/lib/scaffold/add.js b/lib/scaffold/add.js index d8d159d9..0d503120 100644 --- a/lib/scaffold/add.js +++ b/lib/scaffold/add.js @@ -10,10 +10,10 @@ var _ = bitcore.deps._; /** * @param {String} configFilePath - The absolute path to the configuration file - * @param {String} module - The name of the module + * @param {String} service - The name of the service * @param {Function} done */ -function addConfig(configFilePath, module, done) { +function addConfig(configFilePath, service, done) { $.checkState(path.isAbsolute(configFilePath), 'An absolute path is expected'); fs.readFile(configFilePath, function(err, data) { if (err) { @@ -21,12 +21,12 @@ function addConfig(configFilePath, module, done) { } var config = JSON.parse(data); $.checkState( - Array.isArray(config.modules), - 'Configuration file is expected to have a modules array.' + Array.isArray(config.services), + 'Configuration file is expected to have a services array.' ); - config.modules.push(module); - config.modules = _.unique(config.modules); - config.modules.sort(function(a, b) { + config.services.push(service); + config.services = _.unique(config.services); + config.services.sort(function(a, b) { return a > b; }); fs.writeFile(configFilePath, JSON.stringify(config, null, 2), done); @@ -35,12 +35,12 @@ function addConfig(configFilePath, module, done) { /** * @param {String} configDir - The absolute configuration directory path - * @param {String} module - The name of the module + * @param {String} service - The name of the service * @param {Function} done */ -function addModule(configDir, module, done) { +function addService(configDir, service, done) { $.checkState(path.isAbsolute(configDir), 'An absolute path is expected'); - var npm = spawn('npm', ['install', module, '--save'], {cwd: configDir}); + var npm = spawn('npm', ['install', service, '--save'], {cwd: configDir}); npm.stdout.on('data', function(data) { process.stdout.write(data); @@ -52,7 +52,7 @@ function addModule(configDir, module, done) { npm.on('close', function(code) { if (code !== 0) { - return done(new Error('There was an error installing module: ' + module)); + return done(new Error('There was an error installing service: ' + service)); } else { return done(); } @@ -62,7 +62,7 @@ function addModule(configDir, module, done) { /** * @param {String} options.cwd - The current working directory * @param {String} options.dirname - The bitcore-node configuration directory - * @param {Array} options.modules - An array of strings of module names + * @param {Array} options.services - An array of strings of service names * @param {Function} done - A callback function called when finished */ function add(options, done) { @@ -72,10 +72,10 @@ function add(options, done) { _.isString(options.path) && path.isAbsolute(options.path), 'An absolute path is expected' ); - $.checkArgument(Array.isArray(options.modules)); + $.checkArgument(Array.isArray(options.services)); var configPath = options.path; - var modules = options.modules; + var services = options.services; var bitcoreConfigPath = path.resolve(configPath, 'bitcore-node.json'); var packagePath = path.resolve(configPath, 'package.json'); @@ -87,15 +87,15 @@ function add(options, done) { } async.eachSeries( - modules, - function(module, next) { - // npm install --save - addModule(configPath, module, function(err) { + services, + function(service, next) { + // npm install --save + addService(configPath, service, function(err) { if (err) { return next(err); } - // add module to bitcore-node.json - addConfig(bitcoreConfigPath, module, next); + // add service to bitcore-node.json + addConfig(bitcoreConfigPath, service, next); }); }, done ); diff --git a/lib/scaffold/create.js b/lib/scaffold/create.js index 22dcd954..951c026b 100644 --- a/lib/scaffold/create.js +++ b/lib/scaffold/create.js @@ -12,7 +12,7 @@ var fs = require('fs'); var BASE_CONFIG = { name: 'My Node', - modules: [ + services: [ 'address' ], datadir: './data', @@ -61,7 +61,7 @@ function createBitcoinDirectory(datadir, done) { * @param {String} configDir - The absolute path * @param {String} name - The name of the node * @param {String} datadir - The bitcoin database directory - * @param {Boolean} isGlobal - If the configuration depends on globally installed node modules. + * @param {Boolean} isGlobal - If the configuration depends on globally installed node services. * @param {Function} done - The callback function called when finished */ function createConfigDirectory(configDir, name, datadir, isGlobal, done) { diff --git a/lib/scaffold/default-config.js b/lib/scaffold/default-config.js index bb849e77..979d110a 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, - modules: ['bitcoind', 'db', 'address'] + services: ['bitcoind', 'db', 'address'] } }; } diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index 27bda159..38fca447 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -15,42 +15,42 @@ var interval = false; function start(options) { /* jshint maxstatements: 100 */ - var bitcoreModules = []; + var services = []; var configPath = options.path; var config = options.config; - if (config.modules) { - for (var i = 0; i < config.modules.length; i++) { - var moduleName = config.modules[i]; - var bitcoreModule; + if (config.services) { + for (var i = 0; i < config.services.length; i++) { + var serviceName = config.services[i]; + var service; try { - // first try in the built-in bitcore-node modules directory - bitcoreModule = require(path.resolve(__dirname, '../modules/' + moduleName)); + // first try in the built-in bitcore-node services directory + service = require(path.resolve(__dirname, '../services/' + serviceName)); } catch(e) { // check if the package.json specifies a specific file to use - var modulePackage = require(moduleName + '/package.json'); - var bitcoreNodeModule = moduleName; - if (modulePackage.bitcoreNode) { - bitcoreNodeModule = moduleName + '/' + modulePackage.bitcoreNode; + var servicePackage = require(serviceName + '/package.json'); + var serviceModule = serviceName; + if (servicePackage.bitcoreNode) { + serviceModule = serviceName + '/' + servicePackage.bitcoreNode; } - bitcoreModule = require(bitcoreNodeModule); + service = require(serviceModule); } - // check that the module supports expected methods - if (!bitcoreModule.prototype || - !bitcoreModule.dependencies || - !bitcoreModule.prototype.start || - !bitcoreModule.prototype.stop) { + // check that the service supports expected methods + if (!service.prototype || + !service.dependencies || + !service.prototype.start || + !service.prototype.stop) { throw new Error( - 'Could not load module "' + moduleName + '" as it does not support necessary methods.' + 'Could not load service "' + serviceName + '" as it does not support necessary methods.' ); } - bitcoreModules.push({ - name: moduleName, - module: bitcoreModule, - dependencies: bitcoreModule.dependencies + services.push({ + name: serviceName, + module: service, + dependencies: service.dependencies }); } @@ -61,15 +61,15 @@ function start(options) { // expand to the full path fullConfig.datadir = path.resolve(configPath, config.datadir); - // load the modules - fullConfig.modules = bitcoreModules; + // load the services + fullConfig.services = services; var node = new BitcoreNode(fullConfig); function logSyncStatus() { log.info( - 'Sync Status: Tip:', node.modules.db.tip.hash, - 'Height:', node.modules.db.tip.__height, + 'Sync Status: Tip:', node.services.db.tip.hash, + 'Height:', node.services.db.tip.__height, 'Rate:', count/10, 'blocks per second' ); } @@ -185,7 +185,7 @@ function start(options) { }); node.on('ready', function() { - node.modules.db.on('addblock', function(block) { + node.services.db.on('addblock', function(block) { count++; // Initialize logging if not already instantiated if (!interval) { diff --git a/lib/module.js b/lib/service.js similarity index 69% rename from lib/module.js rename to lib/service.js index 7b7fd19c..4407ff4a 100644 --- a/lib/module.js +++ b/lib/service.js @@ -3,18 +3,18 @@ var util = require('util'); var EventEmitter = require('events').EventEmitter; -var Module = function(options) { +var Service = function(options) { EventEmitter.call(this); this.node = options.node; }; -util.inherits(Module, EventEmitter); +util.inherits(Service, EventEmitter); /** - * Describes the dependencies that should be loaded before this module. + * Describes the dependencies that should be loaded before this service. */ -Module.dependencies = []; +Service.dependencies = []; /** * blockHandler @@ -22,7 +22,7 @@ Module.dependencies = []; * @param {Boolean} add - whether the block is being added or removed * @param {Function} callback - call with the leveldb database operations to perform */ -Module.prototype.blockHandler = function(block, add, callback) { +Service.prototype.blockHandler = function(block, add, callback) { // implement in the child class setImmediate(callback); }; @@ -31,7 +31,7 @@ Module.prototype.blockHandler = function(block, add, callback) { * the bus events available for subscription * @return {Array} an array of event info */ -Module.prototype.getPublishEvents = function() { +Service.prototype.getPublishEvents = function() { // Example: // return [ // ['eventname', this, this.subscribeEvent, this.unsubscribeEvent], @@ -43,7 +43,7 @@ Module.prototype.getPublishEvents = function() { * the API methods to expose * @return {Array} return array of methods */ -Module.prototype.getAPIMethods = function() { +Service.prototype.getAPIMethods = function() { // Example: // return [ // ['getData', this, this.getData, 1] @@ -53,16 +53,16 @@ Module.prototype.getAPIMethods = function() { }; // Example: -// Module.prototype.getData = function(arg1, callback) { +// Service.prototype.getData = function(arg1, callback) { // // }; -Module.prototype.start = function(done) { +Service.prototype.start = function(done) { setImmediate(done); }; -Module.prototype.stop = function(done) { +Service.prototype.stop = function(done) { setImmediate(done); }; -module.exports = Module; +module.exports = Service; diff --git a/lib/modules/address.js b/lib/services/address.js similarity index 82% rename from lib/modules/address.js rename to lib/services/address.js index 5da5b577..a254016b 100644 --- a/lib/modules/address.js +++ b/lib/services/address.js @@ -1,6 +1,6 @@ 'use strict'; -var BaseModule = require('../module'); +var BaseService = require('../service'); var inherits = require('util').inherits; var async = require('async'); var index = require('../'); @@ -14,30 +14,30 @@ var EventEmitter = require('events').EventEmitter; var PublicKey = bitcore.PublicKey; var Address = bitcore.Address; -var AddressModule = function(options) { - BaseModule.call(this, options); +var AddressService = function(options) { + BaseService.call(this, options); this.subscriptions = {}; this.subscriptions['address/transaction'] = {}; this.subscriptions['address/balance'] = {}; - this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this)); + this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this)); }; -inherits(AddressModule, BaseModule); +inherits(AddressService, BaseService); -AddressModule.dependencies = [ +AddressService.dependencies = [ 'bitcoind', 'db' ]; -AddressModule.PREFIXES = { +AddressService.PREFIXES = { OUTPUTS: 'outs', SPENTS: 'sp' }; -AddressModule.prototype.getAPIMethods = function() { +AddressService.prototype.getAPIMethods = function() { return [ ['getBalance', this, this.getBalance, 2], ['getOutputs', this, this.getOutputs, 2], @@ -47,7 +47,7 @@ AddressModule.prototype.getAPIMethods = function() { ]; }; -AddressModule.prototype.getPublishEvents = function() { +AddressService.prototype.getPublishEvents = function() { return [ { name: 'address/transaction', @@ -73,7 +73,7 @@ AddressModule.prototype.getPublishEvents = function() { * @param {Number} outputIndex - The index of the output in the transaction * @param {Boolean} rejected - If the transaction was rejected by the mempool */ -AddressModule.prototype.transactionOutputHandler = function(messages, tx, outputIndex, rejected) { +AddressService.prototype.transactionOutputHandler = function(messages, tx, outputIndex, rejected) { var script = tx.outputs[outputIndex].script; // If the script is invalid skip @@ -112,7 +112,7 @@ AddressModule.prototype.transactionOutputHandler = function(messages, tx, output * @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool * @param {String} txInfo.hash - The hash of the transaction */ -AddressModule.prototype.transactionHandler = function(txInfo) { +AddressService.prototype.transactionHandler = function(txInfo) { // Basic transaction format is handled by the daemon // and we can safely assume the buffer is properly formatted. @@ -130,7 +130,7 @@ AddressModule.prototype.transactionHandler = function(txInfo) { } }; -AddressModule.prototype.blockHandler = function(block, addOutput, callback) { +AddressService.prototype.blockHandler = function(block, addOutput, callback) { var txs = block.transactions; var action = 'put'; @@ -178,7 +178,7 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { var addressStr = address.toString(); var scriptHex = output._scriptBuffer.toString('hex'); - var key = [AddressModule.PREFIXES.OUTPUTS, addressStr, timestamp, txid, outputIndex].join('-'); + var key = [AddressService.PREFIXES.OUTPUTS, addressStr, timestamp, txid, outputIndex].join('-'); var value = [output.satoshis, scriptHex, height].join(':'); operations.push({ @@ -217,7 +217,7 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { var input = inputs[k].toObject(); operations.push({ type: action, - key: [AddressModule.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'), + key: [AddressService.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'), value: [txid, k].join(':') }); } @@ -238,7 +238,7 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { * @param {Number} [obj.height] - The height of the block the transaction was included * @param {Boolean} [obj.rejected] - If the transaction was not accepted in the mempool */ -AddressModule.prototype.transactionEventHandler = function(obj) { +AddressService.prototype.transactionEventHandler = function(obj) { if(this.subscriptions['address/transaction'][obj.address]) { var emitters = this.subscriptions['address/transaction'][obj.address]; for(var i = 0; i < emitters.length; i++) { @@ -247,7 +247,7 @@ AddressModule.prototype.transactionEventHandler = function(obj) { } }; -AddressModule.prototype.balanceEventHandler = function(block, address) { +AddressService.prototype.balanceEventHandler = function(block, address) { if(this.subscriptions['address/balance'][address]) { var emitters = this.subscriptions['address/balance'][address]; this.getBalance(address, true, function(err, balance) { @@ -262,7 +262,7 @@ AddressModule.prototype.balanceEventHandler = function(block, address) { } }; -AddressModule.prototype.subscribe = function(name, emitter, addresses) { +AddressService.prototype.subscribe = function(name, emitter, addresses) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); $.checkArgument(Array.isArray(addresses), 'Second argument is expected to be an Array of addresses'); @@ -274,7 +274,7 @@ AddressModule.prototype.subscribe = function(name, emitter, addresses) { } }; -AddressModule.prototype.unsubscribe = function(name, emitter, addresses) { +AddressService.prototype.unsubscribe = function(name, emitter, addresses) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); $.checkArgument(Array.isArray(addresses) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined'); @@ -293,7 +293,7 @@ AddressModule.prototype.unsubscribe = function(name, emitter, addresses) { } }; -AddressModule.prototype.unsubscribeAll = function(name, emitter) { +AddressService.prototype.unsubscribeAll = function(name, emitter) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); for(var address in this.subscriptions[name]) { @@ -305,7 +305,7 @@ AddressModule.prototype.unsubscribeAll = function(name, emitter) { } }; -AddressModule.prototype.getBalance = function(address, queryMempool, callback) { +AddressService.prototype.getBalance = function(address, queryMempool, callback) { this.getUnspentOutputs(address, queryMempool, function(err, outputs) { if(err) { return callback(err); @@ -323,13 +323,13 @@ AddressModule.prototype.getBalance = function(address, queryMempool, callback) { }); }; -AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback) { +AddressService.prototype.getOutputs = function(addressStr, queryMempool, callback) { var self = this; var outputs = []; - var key = [AddressModule.PREFIXES.OUTPUTS, addressStr].join('-'); + var key = [AddressService.PREFIXES.OUTPUTS, addressStr].join('-'); - var stream = this.node.modules.db.store.createReadStream({ + var stream = this.node.services.db.store.createReadStream({ start: key, end: key + '~' }); @@ -347,7 +347,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback satoshis: Number(value[0]), script: value[1], blockHeight: Number(value[2]), - confirmations: self.node.modules.db.tip.__height - Number(value[2]) + 1 + confirmations: self.node.services.db.tip.__height - Number(value[2]) + 1 }; outputs.push(output); @@ -368,7 +368,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback } if(queryMempool) { - outputs = outputs.concat(self.node.modules.bitcoind.getMempoolOutputs(addressStr)); + outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr)); } callback(null, outputs); @@ -378,7 +378,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback }; -AddressModule.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) { +AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) { var self = this; if(!Array.isArray(addresses)) { @@ -403,7 +403,7 @@ AddressModule.prototype.getUnspentOutputs = function(addresses, queryMempool, ca }); }; -AddressModule.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) { +AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var self = this; @@ -424,26 +424,26 @@ AddressModule.prototype.getUnspentOutputsForAddress = function(address, queryMem }); }; -AddressModule.prototype.isUnspent = function(output, queryMempool, callback) { +AddressService.prototype.isUnspent = function(output, queryMempool, callback) { this.isSpent(output, queryMempool, function(spent) { callback(!spent); }); }; -AddressModule.prototype.isSpent = function(output, queryMempool, callback) { +AddressService.prototype.isSpent = function(output, queryMempool, callback) { var self = this; var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; setImmediate(function() { - callback(self.node.modules.bitcoind.isSpent(txid, output.outputIndex)); + callback(self.node.services.bitcoind.isSpent(txid, output.outputIndex)); }); }; -AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) { +AddressService.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) { var self = this; - var key = [AddressModule.PREFIXES.SPENTS, txid, outputIndex].join('-'); - this.node.modules.db.store.get(key, function(err, value) { + var key = [AddressService.PREFIXES.SPENTS, txid, outputIndex].join('-'); + this.node.services.db.store.get(key, function(err, value) { if(err) { return callback(err); } @@ -459,7 +459,7 @@ AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, call }); }; -AddressModule.prototype.getAddressHistory = function(addresses, queryMempool, callback) { +AddressService.prototype.getAddressHistory = function(addresses, queryMempool, callback) { var self = this; if(!Array.isArray(addresses)) { @@ -482,7 +482,7 @@ AddressModule.prototype.getAddressHistory = function(addresses, queryMempool, ca }); }; -AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMempool, callback) { +AddressService.prototype.getAddressHistoryForAddress = function(address, queryMempool, callback) { var self = this; var txinfos = {}; @@ -492,19 +492,19 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem return callback(null, txinfos[txid]); } - self.node.modules.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) { + self.node.services.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) { if(err) { return callback(err); } - transaction.populateInputs(self.node.modules.db, [], function(err) { + transaction.populateInputs(self.node.services.db, [], function(err) { if(err) { return callback(err); } var confirmations = 0; if(transaction.__height >= 0) { - confirmations = self.node.modules.db.tip.__height - transaction.__height; + confirmations = self.node.services.db.tip.__height - transaction.__height; } txinfos[transaction.hash] = { @@ -581,4 +581,4 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem }); }; -module.exports = AddressModule; +module.exports = AddressService; diff --git a/lib/modules/bitcoind.js b/lib/services/bitcoind.js similarity index 97% rename from lib/modules/bitcoind.js rename to lib/services/bitcoind.js index 96c791cd..317b1ca4 100644 --- a/lib/modules/bitcoind.js +++ b/lib/services/bitcoind.js @@ -8,8 +8,7 @@ var bitcore = require('bitcore'); var $ = bitcore.util.preconditions; var index = require('../'); var log = index.log; -var Module = require('../module'); - +var Service = require('../service'); /** * Provides an interface to native bindings to Bitcoin Core @@ -24,7 +23,7 @@ function Bitcoin(options) { var self = this; - Module.call(this, options); + Service.call(this, options); if (Object.keys(this.instances).length) { throw new Error('Bitcoin cannot be instantiated more than once.'); @@ -38,7 +37,7 @@ function Bitcoin(options) { } -util.inherits(Bitcoin, Module); +util.inherits(Bitcoin, Service); Bitcoin.dependencies = []; @@ -89,7 +88,7 @@ Bitcoin.prototype._loadConfiguration = function() { $.checkState( this.configuration.txindex && this.configuration.txindex === 1, 'Txindex option is required in order to use most of the features of bitcore-node. ' + - 'Please add "txindex=1" to your configuration and reindex an existing database if ' + + 'Please add "txindex=1" to your configuration and reindex an existing database if ' + 'necessary with reindex=1' ); }; diff --git a/lib/modules/db.js b/lib/services/db.js similarity index 93% rename from lib/modules/db.js rename to lib/services/db.js index eb22de74..582b6088 100644 --- a/lib/modules/db.js +++ b/lib/services/db.js @@ -15,13 +15,13 @@ var index = require('../'); var errors = index.errors; var log = index.log; var Transaction = require('../transaction'); -var Module = require('../module'); +var Service = require('../service'); var utils = require('../utils'); var MAX_STACK_DEPTH = 1000; /** - * Represents the current state of the bitcoin blockchain. Other modules + * Represents the current state of the bitcoin blockchain. Other services * can extend the data that is indexed by implementing a `blockHandler` method. * * @param {Object} options @@ -38,7 +38,7 @@ function DB(options) { options = {}; } - Module.call(this, options); + Service.call(this, options); this.tip = null; this.genesis = null; @@ -66,7 +66,7 @@ function DB(options) { }; } -util.inherits(DB, Module); +util.inherits(DB, Service); DB.dependencies = ['bitcoind']; @@ -90,17 +90,17 @@ DB.prototype.start = function(callback) { mkdirp.sync(this.dataPath); } - this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer); + this.genesis = Block.fromBuffer(this.node.services.bitcoind.genesisBuffer); this.store = levelup(this.dataPath, { db: this.levelupStore }); - this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this)); + this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this)); this.once('ready', function() { log.info('Bitcoin Database Ready'); // Notify that there is a new tip - self.node.modules.bitcoind.on('tip', function(height) { + self.node.services.bitcoind.on('tip', function(height) { if(!self.node.stopping) { - var percentage = self.node.modules.bitcoind.syncPercentage(); + var percentage = self.node.services.bitcoind.syncPercentage(); log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); self.sync(); } @@ -189,7 +189,7 @@ DB.prototype.getAPIMethods = function() { }; DB.prototype.getBlock = function(hash, callback) { - this.node.modules.bitcoind.getBlock(hash, function(err, blockData) { + this.node.services.bitcoind.getBlock(hash, function(err, blockData) { if (err) { return callback(err); } @@ -198,7 +198,7 @@ DB.prototype.getBlock = function(hash, callback) { }; DB.prototype.getTransaction = function(txid, queryMempool, callback) { - this.node.modules.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { + this.node.services.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { if (err) { return callback(err); } @@ -211,7 +211,7 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) { }; DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { - this.node.modules.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { + this.node.services.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { if (err) { return callback(err); } @@ -231,7 +231,7 @@ DB.prototype.sendTransaction = function(tx, callback) { $.checkArgument(typeof tx === 'string', 'Argument must be a hex string or Transaction'); try { - var txid = this.node.modules.bitcoind.sendTransaction(tx); + var txid = this.node.services.bitcoind.sendTransaction(tx); return callback(null, txid); } catch(err) { return callback(err); @@ -241,7 +241,7 @@ DB.prototype.sendTransaction = function(tx, callback) { DB.prototype.estimateFee = function(blocks, callback) { var self = this; setImmediate(function() { - callback(null, self.node.modules.bitcoind.estimateFee(blocks)); + callback(null, self.node.services.bitcoind.estimateFee(blocks)); }); }; @@ -279,7 +279,7 @@ DB.prototype.unsubscribe = function(name, emitter) { * @param {Function} callback */ DB.prototype.getPrevHash = function(blockHash, callback) { - var blockIndex = this.node.modules.bitcoind.getBlockIndex(blockHash); + var blockIndex = this.node.services.bitcoind.getBlockIndex(blockHash); setImmediate(function() { if (blockIndex) { callback(null, blockIndex.prevHash); @@ -364,7 +364,7 @@ DB.prototype.disconnectBlock = function(block, callback) { }; /** - * Will collect all database operations for a block from other modules + * Will collect all database operations for a block from other services * and save to the database. * @param {Block} block - The bitcore block * @param {Boolean} add - If the block is being added/connected or removed/disconnected @@ -380,7 +380,7 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) { } async.eachSeries( - this.node.modules, + this.node.services, function(mod, next) { mod.blockHandler.call(mod, block, add, function(err, ops) { if (err) { @@ -501,7 +501,7 @@ DB.prototype.findCommonAncestor = function(block, done) { // and thus don't need to find the entire chain of hashes. while(ancestorHash && !currentHashesMap[ancestorHash]) { - var blockIndex = self.node.modules.bitcoind.getBlockIndex(ancestorHash); + var blockIndex = self.node.services.bitcoind.getBlockIndex(ancestorHash); ancestorHash = blockIndex ? blockIndex.prevHash : null; } @@ -595,9 +595,9 @@ DB.prototype.sync = function() { async.whilst(function() { height = self.tip.__height; - return height < self.node.modules.bitcoind.height && !self.node.stopping; + return height < self.node.services.bitcoind.height && !self.node.stopping; }, function(done) { - self.node.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) { + self.node.services.bitcoind.getBlock(height + 1, function(err, blockBuffer) { if (err) { return done(err); } @@ -659,7 +659,7 @@ DB.prototype.sync = function() { self.lastSavedMetadataThreshold = 0; // If bitcoind is completely synced - if (self.node.modules.bitcoind.isSynced()) { + if (self.node.services.bitcoind.isSynced()) { self.node.emit('synced'); } diff --git a/test/bus.unit.js b/test/bus.unit.js index 7c40b4e9..0b2bf298 100644 --- a/test/bus.unit.js +++ b/test/bus.unit.js @@ -7,28 +7,26 @@ var Bus = require('../lib/bus'); describe('Bus', function() { describe('#subscribe', function() { - it('will call db and modules subscribe function with the correct arguments', function() { + it('will call db and services subscribe function with the correct arguments', function() { var subscribeDb = sinon.spy(); - var subscribeModule = sinon.spy(); - var db = { - getPublishEvents: sinon.stub().returns([ - { - name: 'dbtest', - scope: this, - subscribe: subscribeDb - } - ] - ) - }; + var subscribeService = sinon.spy(); var node = { - db: db, - modules: { - module1: { + services: { + db: { + getPublishEvents: sinon.stub().returns([ + { + name: 'dbtest', + scope: this, + subscribe: subscribeDb + } + ]) + }, + service1: { getPublishEvents: sinon.stub().returns([ { name: 'test', scope: this, - subscribe: subscribeModule, + subscribe: subscribeService, } ]) } @@ -37,42 +35,40 @@ describe('Bus', function() { var bus = new Bus({node: node}); bus.subscribe('dbtest', 'a', 'b', 'c'); bus.subscribe('test', 'a', 'b', 'c'); - subscribeModule.callCount.should.equal(1); + subscribeService.callCount.should.equal(1); subscribeDb.callCount.should.equal(1); subscribeDb.args[0][0].should.equal(bus); subscribeDb.args[0][1].should.equal('a'); subscribeDb.args[0][2].should.equal('b'); subscribeDb.args[0][3].should.equal('c'); - subscribeModule.args[0][0].should.equal(bus); - subscribeModule.args[0][1].should.equal('a'); - subscribeModule.args[0][2].should.equal('b'); - subscribeModule.args[0][3].should.equal('c'); + subscribeService.args[0][0].should.equal(bus); + subscribeService.args[0][1].should.equal('a'); + subscribeService.args[0][2].should.equal('b'); + subscribeService.args[0][3].should.equal('c'); }); }); describe('#unsubscribe', function() { - it('will call db and modules unsubscribe function with the correct arguments', function() { + it('will call db and services unsubscribe function with the correct arguments', function() { var unsubscribeDb = sinon.spy(); - var unsubscribeModule = sinon.spy(); - var db = { - getPublishEvents: sinon.stub().returns([ - { - name: 'dbtest', - scope: this, - unsubscribe: unsubscribeDb - } - ] - ) - }; + var unsubscribeService = sinon.spy(); var node = { - db: db, - modules: { - module1: { + services: { + db: { + getPublishEvents: sinon.stub().returns([ + { + name: 'dbtest', + scope: this, + unsubscribe: unsubscribeDb + } + ]) + }, + service1: { getPublishEvents: sinon.stub().returns([ { name: 'test', scope: this, - unsubscribe: unsubscribeModule, + unsubscribe: unsubscribeService, } ]) } @@ -81,43 +77,41 @@ describe('Bus', function() { var bus = new Bus({node: node}); bus.unsubscribe('dbtest', 'a', 'b', 'c'); bus.unsubscribe('test', 'a', 'b', 'c'); - unsubscribeModule.callCount.should.equal(1); + unsubscribeService.callCount.should.equal(1); unsubscribeDb.callCount.should.equal(1); unsubscribeDb.args[0][0].should.equal(bus); unsubscribeDb.args[0][1].should.equal('a'); unsubscribeDb.args[0][2].should.equal('b'); unsubscribeDb.args[0][3].should.equal('c'); - unsubscribeModule.args[0][0].should.equal(bus); - unsubscribeModule.args[0][1].should.equal('a'); - unsubscribeModule.args[0][2].should.equal('b'); - unsubscribeModule.args[0][3].should.equal('c'); + unsubscribeService.args[0][0].should.equal(bus); + unsubscribeService.args[0][1].should.equal('a'); + unsubscribeService.args[0][2].should.equal('b'); + unsubscribeService.args[0][3].should.equal('c'); }); }); describe('#close', function() { it('will unsubscribe from all events', function() { var unsubscribeDb = sinon.spy(); - var unsubscribeModule = sinon.spy(); - var db = { - getPublishEvents: sinon.stub().returns([ - { - name: 'dbtest', - scope: this, - unsubscribe: unsubscribeDb - } - ] - ) - }; + var unsubscribeService = sinon.spy(); var node = { - db: db, - modules: { - module1: { + services: { + db: { getPublishEvents: sinon.stub().returns([ - { - name: 'test', - scope: this, - unsubscribe: unsubscribeModule - } + { + name: 'dbtest', + scope: this, + unsubscribe: unsubscribeDb + } + ]) + }, + service1: { + getPublishEvents: sinon.stub().returns([ + { + name: 'test', + scope: this, + unsubscribe: unsubscribeService + } ]) } } @@ -126,11 +120,11 @@ describe('Bus', function() { bus.close(); unsubscribeDb.callCount.should.equal(1); - unsubscribeModule.callCount.should.equal(1); + unsubscribeService.callCount.should.equal(1); unsubscribeDb.args[0].length.should.equal(1); - unsubscribeDb.args[0][0].should.equal(bus); - unsubscribeModule.args[0].length.should.equal(1); - unsubscribeModule.args[0][0].should.equal(bus); + unsubscribeDb.args[0][0].should.equal(bus); + unsubscribeService.args[0].length.should.equal(1); + unsubscribeService.args[0][0].should.equal(bus); }); }); diff --git a/test/node.unit.js b/test/node.unit.js index 195cb2ce..60a346cd 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -6,7 +6,7 @@ var bitcore = require('bitcore'); var Networks = bitcore.Networks; var proxyquire = require('proxyquire'); var util = require('util'); -var BaseModule = require('../lib/module'); +var BaseService = require('../lib/service'); describe('Bitcore Node', function() { @@ -47,18 +47,18 @@ describe('Bitcore Node', function() { }); describe('@constructor', function() { - var TestModule; + var TestService; before(function() { - TestModule = function TestModule() {}; - util.inherits(TestModule, BaseModule); + TestService = function TestService() {}; + util.inherits(TestService, BaseService); }); it('will set properties', function() { var config = { datadir: 'testdir', - modules: [ + services: [ { name: 'test1', - module: TestModule + module: TestService } ], }; @@ -66,19 +66,19 @@ describe('Bitcore Node', function() { TestNode.prototype.start = sinon.spy(); var node = new TestNode(config); TestNode.prototype.start.callCount.should.equal(1); - node._unloadedModules.length.should.equal(1); - node._unloadedModules[0].name.should.equal('test1'); - node._unloadedModules[0].module.should.equal(TestModule); + node._unloadedServices.length.should.equal(1); + node._unloadedServices[0].name.should.equal('test1'); + node._unloadedServices[0].module.should.equal(TestService); node.network.should.equal(Networks.defaultNetwork); }); it('will set network to testnet', function() { var config = { network: 'testnet', datadir: 'testdir', - modules: [ + services: [ { name: 'test1', - module: TestModule + module: TestService } ], }; @@ -91,10 +91,10 @@ describe('Bitcore Node', function() { var config = { network: 'regtest', datadir: 'testdir', - modules: [ + services: [ { name: 'test1', - module: TestModule + module: TestService } ], }; @@ -108,10 +108,10 @@ describe('Bitcore Node', function() { it('should emit error if an error occurred starting services', function(done) { var config = { datadir: 'testdir', - modules: [ + services: [ { name: 'test1', - module: TestModule + module: TestService } ], }; @@ -139,16 +139,16 @@ describe('Bitcore Node', function() { }); describe('#getAllAPIMethods', function() { - it('should return db methods and modules methods', function() { + it('should return db methods and service methods', function() { var node = new Node(baseConfig); - node.modules = { + node.services = { db: { getAPIMethods: sinon.stub().returns(['db1', 'db2']), }, - module1: { + service1: { getAPIMethods: sinon.stub().returns(['mda1', 'mda2']) }, - module2: { + service2: { getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2']) } }; @@ -159,16 +159,16 @@ describe('Bitcore Node', function() { }); describe('#getAllPublishEvents', function() { - it('should return modules publish events', function() { + it('should return services publish events', function() { var node = new Node(baseConfig); - node.modules = { + node.services = { db: { getPublishEvents: sinon.stub().returns(['db1', 'db2']), }, - module1: { + service1: { getPublishEvents: sinon.stub().returns(['mda1', 'mda2']) }, - module2: { + service2: { getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2']) } }; @@ -180,7 +180,7 @@ describe('Bitcore Node', function() { describe('#getServiceOrder', function() { it('should return the services in the correct order', function() { var node = new Node(baseConfig); - node._unloadedModules = [ + node._unloadedServices = [ { name: 'chain', dependencies: ['db'] @@ -206,46 +206,46 @@ describe('Bitcore Node', function() { }); }); - describe('#_instantiateModule', function() { + describe('#_instantiateService', function() { it('will instantiate an instance and load api methods', function() { var node = new Node(baseConfig); - function TestModule() {} - util.inherits(TestModule, BaseModule); - TestModule.prototype.getData = function() {}; - TestModule.prototype.getAPIMethods = function() { + function TestService() {} + util.inherits(TestService, BaseService); + TestService.prototype.getData = function() {}; + TestService.prototype.getAPIMethods = function() { return [ ['getData', this, this.getData, 1] ]; }; var service = { - name: 'testmodule', - module: TestModule + name: 'testservice', + module: TestService }; - node._instantiateModule(service); - should.exist(node.modules.testmodule); + node._instantiateService(service); + should.exist(node.services.testservice); should.exist(node.getData); }); }); describe('#start', function() { - it('will call start for each module', function(done) { + it('will call start for each service', function(done) { var node = new Node(baseConfig); - function TestModule() {} - util.inherits(TestModule, BaseModule); - TestModule.prototype.start = sinon.stub().callsArg(0); - TestModule.prototype.getData = function() {}; - TestModule.prototype.getAPIMethods = function() { + function TestService() {} + util.inherits(TestService, BaseService); + TestService.prototype.start = sinon.stub().callsArg(0); + TestService.prototype.getData = function() {}; + TestService.prototype.getAPIMethods = function() { return [ ['getData', this, this.getData, 1] ]; }; - function TestModule2() {} - util.inherits(TestModule2, BaseModule); - TestModule2.prototype.start = sinon.stub().callsArg(0); - TestModule2.prototype.getData2 = function() {}; - TestModule2.prototype.getAPIMethods = function() { + function TestService2() {} + util.inherits(TestService2, BaseService); + TestService2.prototype.start = sinon.stub().callsArg(0); + TestService2.prototype.getData2 = function() {}; + TestService2.prototype.getAPIMethods = function() { return [ ['getData2', this, this.getData2, 1] ]; @@ -254,16 +254,16 @@ describe('Bitcore Node', function() { node.getServiceOrder = sinon.stub().returns([ { name: 'test1', - module: TestModule + module: TestService }, { name: 'test2', - module: TestModule2 + module: TestService2 } ]); node.start(function() { - TestModule2.prototype.start.callCount.should.equal(1); - TestModule.prototype.start.callCount.should.equal(1); + TestService2.prototype.start.callCount.should.equal(1); + TestService.prototype.start.callCount.should.equal(1); should.exist(node.getData2); should.exist(node.getData); done(); @@ -272,21 +272,21 @@ describe('Bitcore Node', function() { it('will error if there are conflicting API methods', function(done) { var node = new Node(baseConfig); - function TestModule() {} - util.inherits(TestModule, BaseModule); - TestModule.prototype.start = sinon.stub().callsArg(0); - TestModule.prototype.getData = function() {}; - TestModule.prototype.getAPIMethods = function() { + function TestService() {} + util.inherits(TestService, BaseService); + TestService.prototype.start = sinon.stub().callsArg(0); + TestService.prototype.getData = function() {}; + TestService.prototype.getAPIMethods = function() { return [ ['getData', this, this.getData, 1] ]; }; - function ConflictModule() {} - util.inherits(ConflictModule, BaseModule); - ConflictModule.prototype.start = sinon.stub().callsArg(0); - ConflictModule.prototype.getData = function() {}; - ConflictModule.prototype.getAPIMethods = function() { + function ConflictService() {} + util.inherits(ConflictService, BaseService); + ConflictService.prototype.start = sinon.stub().callsArg(0); + ConflictService.prototype.getData = function() {}; + ConflictService.prototype.getAPIMethods = function() { return [ ['getData', this, this.getData, 1] ]; @@ -295,11 +295,11 @@ describe('Bitcore Node', function() { node.getServiceOrder = sinon.stub().returns([ { name: 'test', - module: TestModule + module: TestService }, { name: 'conflict', - module: ConflictModule + module: ConflictService } ]); @@ -313,30 +313,30 @@ describe('Bitcore Node', function() { }); describe('#stop', function() { - it('will call stop for each module', function(done) { + it('will call stop for each service', function(done) { var node = new Node(baseConfig); - function TestModule() {} - util.inherits(TestModule, BaseModule); - TestModule.prototype.stop = sinon.stub().callsArg(0); - TestModule.prototype.getData = function() {}; - TestModule.prototype.getAPIMethods = function() { + function TestService() {} + util.inherits(TestService, BaseService); + TestService.prototype.stop = sinon.stub().callsArg(0); + TestService.prototype.getData = function() {}; + TestService.prototype.getAPIMethods = function() { return [ ['getData', this, this.getData, 1] ]; }; - node.modules = { - 'test1': new TestModule({node: node}) + node.services = { + 'test1': new TestService({node: node}) }; node.test2 = {}; node.test2.stop = sinon.stub().callsArg(0); node.getServiceOrder = sinon.stub().returns([ { name: 'test1', - module: TestModule + module: TestService } ]); node.stop(function() { - TestModule.prototype.stop.callCount.should.equal(1); + TestService.prototype.stop.callCount.should.equal(1); done(); }); }); diff --git a/test/scaffold/add.integration.js b/test/scaffold/add.integration.js index bfea5917..04c52a95 100644 --- a/test/scaffold/add.integration.js +++ b/test/scaffold/add.integration.js @@ -15,7 +15,7 @@ describe('#add', function() { var testDir = path.resolve(basePath, 'temporary-test-data'); var startConfig = { name: 'My Node', - modules: [] + services: [] }; var startPackage = {}; @@ -56,7 +56,7 @@ describe('#add', function() { it('will give an error if expected files do not exist', function(done) { add({ path: path.resolve(testDir, 's0'), - modules: ['a', 'b', 'c'] + services: ['a', 'b', 'c'] }, function(err) { should.exist(err); err.message.match(/^Invalid state/); @@ -82,15 +82,15 @@ describe('#add', function() { addtest({ path: path.resolve(testDir, 's0/s1/'), - modules: ['a', 'b', 'c'] + services: ['a', 'b', 'c'] }, function(err) { should.exist(err); - err.message.should.equal('There was an error installing module: a'); + err.message.should.equal('There was an error installing service: a'); done(); }); }); - it('will update bitcore-node.json modules', function(done) { + it('will update bitcore-node.json services', function(done) { var spawn = sinon.stub().returns({ stdout: { on: sinon.stub() @@ -107,12 +107,12 @@ describe('#add', function() { }); addtest({ path: path.resolve(testDir, 's0/s1/'), - modules: ['a', 'b', 'c'] + services: ['a', 'b', 'c'] }, function(err) { should.not.exist(err); var configPath = path.resolve(testDir, 's0/s1/bitcore-node.json'); var config = JSON.parse(fs.readFileSync(configPath)); - config.modules.should.deep.equal(['a','b','c']); + config.services.should.deep.equal(['a','b','c']); done(); }); }); diff --git a/test/scaffold/create.integration.js b/test/scaffold/create.integration.js index 8930d207..08cc3d26 100644 --- a/test/scaffold/create.integration.js +++ b/test/scaffold/create.integration.js @@ -75,7 +75,7 @@ describe('#create', function() { var config = JSON.parse(fs.readFileSync(configPath)); config.name.should.equal('My Node 1'); - config.modules.should.deep.equal(['address']); + config.services.should.deep.equal(['address']); config.datadir.should.equal('./data'); config.network.should.equal('livenet'); diff --git a/test/scaffold/start.integration.js b/test/scaffold/start.integration.js index b7d074f5..228af308 100644 --- a/test/scaffold/start.integration.js +++ b/test/scaffold/start.integration.js @@ -3,18 +3,18 @@ var should = require('chai').should(); var sinon = require('sinon'); var proxyquire = require('proxyquire'); -var AddressModule = require('../../lib/modules/address'); +var AddressService = require('../../lib/services/address'); describe('#start', function() { describe('will dynamically create a node from a configuration', function() { - it('require each bitcore-node module', function(done) { + it('require each bitcore-node service', function(done) { var node; var TestNode = function(options) { - options.modules[0].should.deep.equal({ + options.services[0].should.deep.equal({ name: 'address', - module: AddressModule, + module: AddressService, dependencies: ['bitcoind', 'db'] }); }; @@ -30,7 +30,7 @@ describe('#start', function() { node = starttest({ path: __dirname, config: { - modules: [ + services: [ 'address' ], datadir: './data' diff --git a/test/modules/address.unit.js b/test/services/address.unit.js similarity index 94% rename from test/modules/address.unit.js rename to test/services/address.unit.js index 69a1e34d..50476f86 100644 --- a/test/modules/address.unit.js +++ b/test/services/address.unit.js @@ -3,7 +3,7 @@ var should = require('chai').should(); var sinon = require('sinon'); var bitcorenode = require('../../'); -var AddressModule = bitcorenode.modules.AddressModule; +var AddressService = bitcorenode.services.Address; var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); var Networks = bitcore.Networks; @@ -16,18 +16,18 @@ var mockdb = { var mocknode = { db: mockdb, - modules: { + services: { bitcoind: { on: sinon.stub() } } }; -describe('Address Module', function() { +describe('Address Service', function() { describe('#getAPIMethods', function() { it('should return the correct methods', function() { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var methods = am.getAPIMethods(); methods.length.should.equal(5); }); @@ -35,7 +35,7 @@ describe('Address Module', function() { describe('#getPublishEvents', function() { it('will return an array of publish event objects', function() { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); am.subscribe = sinon.spy(); am.unsubscribe = sinon.spy(); var events = am.getPublishEvents(); @@ -71,7 +71,7 @@ describe('Address Module', function() { it('create a message for an address', function() { var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); var tx = bitcore.Transaction().fromBuffer(txBuf); - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); am.node.network = Networks.livenet; var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var messages = {}; @@ -88,7 +88,7 @@ describe('Address Module', function() { describe('#transactionHandler', function() { it('will pass outputs to transactionOutputHandler and call transactionEventHandler', function() { var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; var message = {}; am.transactionOutputHandler = function(messages) { @@ -153,7 +153,7 @@ describe('Address Module', function() { var value64 = data[2].value; before(function() { - am = new AddressModule({node: mocknode}); + am = new AddressService({node: mocknode}); am.node.network = Networks.livenet; }); @@ -208,7 +208,7 @@ describe('Address Module', function() { }); }); it('should continue if output script is null', function(done) { - var am = new AddressModule({node: mocknode, network: 'livenet'}); + var am = new AddressService({node: mocknode, network: 'livenet'}); var block = { __height: 345003, @@ -240,13 +240,13 @@ describe('Address Module', function() { var db = {}; var testnode = { db: db, - modules: { + services: { bitcoind: { on: sinon.stub() } } }; - var am = new AddressModule({node: testnode, network: 'livenet'}); + var am = new AddressService({node: testnode, network: 'livenet'}); am.transactionEventHandler = sinon.spy(); am.balanceEventHandler = sinon.spy(); @@ -274,7 +274,7 @@ describe('Address Module', function() { describe('#transactionEventHandler', function() { it('will emit a transaction if there is a subscriber', function(done) { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var emitter = new EventEmitter(); am.subscriptions['address/transaction'] = { '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter] @@ -304,7 +304,7 @@ describe('Address Module', function() { describe('#balanceEventHandler', function() { it('will emit a balance if there is a subscriber', function(done) { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var emitter = new EventEmitter(); am.subscriptions['address/balance'] = { '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter] @@ -324,7 +324,7 @@ describe('Address Module', function() { describe('#subscribe', function() { it('will add emitters to the subscribers array (transaction)', function() { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var emitter = new EventEmitter(); var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; @@ -341,7 +341,7 @@ describe('Address Module', function() { am.subscriptions['address/transaction'][address].should.deep.equal([emitter, emitter2]); }); it('will add an emitter to the subscribers array (balance)', function() { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var emitter = new EventEmitter(); var name = 'address/balance'; var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; @@ -360,7 +360,7 @@ describe('Address Module', function() { describe('#unsubscribe', function() { it('will remove emitter from subscribers array (transaction)', function() { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; @@ -370,7 +370,7 @@ describe('Address Module', function() { am.subscriptions['address/transaction'][address].should.deep.equal([emitter2]); }); it('will remove emitter from subscribers array (balance)', function() { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; @@ -380,7 +380,7 @@ describe('Address Module', function() { am.subscriptions['address/balance'][address].should.deep.equal([emitter2]); }); it('should unsubscribe from all addresses if no addresses are specified', function() { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); am.subscriptions['address/balance'] = { @@ -397,7 +397,7 @@ describe('Address Module', function() { describe('#getBalance', function() { it('should sum up the unspent outputs', function(done) { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); var outputs = [ {satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000} ]; @@ -410,7 +410,7 @@ describe('Address Module', function() { }); it('will handle error from unspent outputs', function(done) { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); am.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error')); am.getBalance('someaddress', false, function(err) { should.exist(err); @@ -430,7 +430,7 @@ describe('Address Module', function() { } }; var testnode = { - modules: { + services: { db: db, bitcoind: { on: sinon.stub() @@ -439,12 +439,12 @@ describe('Address Module', function() { }; before(function() { - am = new AddressModule({node: testnode}); + am = new AddressService({node: testnode}); }); it('should get outputs for an address', function(done) { var readStream1 = new EventEmitter(); - am.node.modules.db.store = { + am.node.services.db.store = { createReadStream: sinon.stub().returns(readStream1) }; var mempoolOutputs = [ @@ -456,7 +456,7 @@ describe('Address Module', function() { blockHeight: 352532 } ]; - am.node.modules.bitcoind = { + am.node.services.bitcoind = { getMempoolOutputs: sinon.stub().returns(mempoolOutputs) }; @@ -499,7 +499,7 @@ describe('Address Module', function() { it('should give an error if the readstream has an error', function(done) { var readStream2 = new EventEmitter(); - am.node.modules.db.store = { + am.node.services.db.store = { createReadStream: sinon.stub().returns(readStream2) }; @@ -526,14 +526,14 @@ describe('Address Module', function() { var db = {}; var testnode = { - modules: { + services: { db: db, bitcoind: { on: sinon.stub() } } }; - var am = new AddressModule({node: testnode}); + var am = new AddressService({node: testnode}); am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var result = addresses[address]; if(result instanceof Error) { @@ -559,13 +559,13 @@ describe('Address Module', function() { var db = {}; var testnode = { db: db, - modules: { + services: { bitcoind: { on: sinon.stub() } } }; - var am = new AddressModule({node: testnode}); + var am = new AddressService({node: testnode}); am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var result = addresses[address]; if(result instanceof Error) { @@ -592,13 +592,13 @@ describe('Address Module', function() { var db = {}; var testnode = { db: db, - modules: { + services: { bitcoind: { on: sinon.stub() } } }; - var am = new AddressModule({node: testnode}); + var am = new AddressService({node: testnode}); am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var result = addresses[address]; if(result instanceof Error) { @@ -634,7 +634,7 @@ describe('Address Module', function() { ]; var i = 0; - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); am.isUnspent = function(output, queryMempool, callback) { callback(!outputs[i].spent); @@ -650,7 +650,7 @@ describe('Address Module', function() { }); }); it('should handle an error from getOutputs', function(done) { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); am.getOutputs = sinon.stub().callsArgWith(2, new Error('error')); am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.exist(err); @@ -659,7 +659,7 @@ describe('Address Module', function() { }); }); it('should handle when there are no outputs', function(done) { - var am = new AddressModule({node: mocknode}); + var am = new AddressService({node: mocknode}); am.getOutputs = sinon.stub().callsArgWith(2, null, []); am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.exist(err); @@ -674,7 +674,7 @@ describe('Address Module', function() { var am; before(function() { - am = new AddressModule({node: mocknode}); + am = new AddressService({node: mocknode}); }); it('should give true when isSpent() gives false', function(done) { @@ -707,15 +707,15 @@ describe('Address Module', function() { var db = {}; var testnode = { db: db, - modules: { + services: { bitcoind: { on: sinon.stub() } } }; before(function() { - am = new AddressModule({node: testnode}); - am.node.modules.bitcoind = { + am = new AddressService({node: testnode}); + am.node.services.bitcoind = { isSpent: sinon.stub().returns(true), on: sinon.stub() }; @@ -737,14 +737,14 @@ describe('Address Module', function() { } }; var testnode = { - modules: { + services: { db: db, bitcoind: { on: sinon.stub() } } }; - var am = new AddressModule({node: testnode}); + var am = new AddressService({node: testnode}); am.getSpendInfoForOutput('txid', 3, function(err, info) { should.not.exist(err); info.txid.should.equal('spendtxid'); @@ -855,14 +855,14 @@ describe('Address Module', function() { } }; var testnode = { - modules: { + services: { db: db, bitcoind: { on: sinon.stub() } } }; - var am = new AddressModule({node: testnode}); + var am = new AddressService({node: testnode}); am.getOutputs = sinon.stub().callsArgWith(2, null, incoming); am.getSpendInfoForOutput = function(txid, outputIndex, callback) { diff --git a/test/modules/bitcoind.unit.js b/test/services/bitcoind.unit.js similarity index 85% rename from test/modules/bitcoind.unit.js rename to test/services/bitcoind.unit.js index 8dc099b2..dc3ad276 100644 --- a/test/modules/bitcoind.unit.js +++ b/test/services/bitcoind.unit.js @@ -4,18 +4,18 @@ var should = require('chai').should(); var proxyquire = require('proxyquire'); var fs = require('fs'); var sinon = require('sinon'); -var BitcoinModule = proxyquire('../../lib/modules/bitcoind', { +var BitcoinService = proxyquire('../../lib/services/bitcoind', { fs: { readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/bitcoin.conf')) } }); -var BadBitcoin = proxyquire('../../lib/modules/bitcoind', { +var BadBitcoin = proxyquire('../../lib/services/bitcoind', { fs: { readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/badbitcoin.conf')) } }); -describe('Bitcoin Module', function() { +describe('Bitcoin Service', function() { var baseConfig = { node: { datadir: 'testdir', @@ -26,7 +26,7 @@ describe('Bitcoin Module', function() { }; describe('#_loadConfiguration', function() { it('will parse a bitcoin.conf file', function() { - var bitcoind = new BitcoinModule(baseConfig); + var bitcoind = new BitcoinService(baseConfig); bitcoind._loadConfiguration({datadir: process.env.HOME + '/.bitcoin'}); should.exist(bitcoind.configuration); bitcoind.configuration.should.deep.equal({ diff --git a/test/modules/db.unit.js b/test/services/db.unit.js similarity index 88% rename from test/modules/db.unit.js rename to test/services/db.unit.js index 1be47388..340a79ca 100644 --- a/test/modules/db.unit.js +++ b/test/services/db.unit.js @@ -5,7 +5,7 @@ var sinon = require('sinon'); var EventEmitter = require('events').EventEmitter; var proxyquire = require('proxyquire'); var index = require('../../'); -var DB = index.modules.DBModule; +var DB = index.services.DB; var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); var Networks = bitcore.Networks; @@ -19,7 +19,7 @@ var memdown = require('memdown'); var bitcore = require('bitcore'); var Transaction = bitcore.Transaction; -describe('DB Module', function() { +describe('DB Service', function() { function hexlebuf(hexString){ return BufferUtil.reverse(new Buffer(hexString, 'hex')); @@ -106,7 +106,7 @@ describe('DB Module', function() { var genesisBuffer; before(function() { - TestDB = proxyquire('../../lib/modules/db', { + TestDB = proxyquire('../../lib/services/db', { fs: { existsSync: sinon.stub().returns(true) }, @@ -118,12 +118,11 @@ describe('DB Module', function() { it('should emit ready', function(done) { var db = new TestDB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { on: sinon.spy(), genesisBuffer: genesisBuffer }; - db._addModule = sinon.spy(); db.getMetadata = sinon.stub().callsArg(0); db.connectBlock = sinon.stub().callsArg(1); db.saveMetadata = sinon.stub(); @@ -142,7 +141,7 @@ describe('DB Module', function() { var node = { network: Networks.testnet, datadir: 'testdir', - modules: { + services: { bitcoind: { genesisBuffer: genesisBuffer, on: sinon.stub() @@ -165,7 +164,7 @@ describe('DB Module', function() { var node = { network: Networks.testnet, datadir: 'testdir', - modules: { + services: { bitcoind: { genesisBuffer: genesisBuffer, on: sinon.stub() @@ -193,7 +192,7 @@ describe('DB Module', function() { var node = { network: Networks.testnet, datadir: 'testdir', - modules: { + services: { bitcoind: { genesisBuffer: genesisBuffer, on: sinon.stub() @@ -213,7 +212,7 @@ describe('DB Module', function() { var node = { network: Networks.testnet, datadir: 'testdir', - modules: { + services: { bitcoind: { genesisBuffer: genesisBuffer, on: sinon.stub() @@ -236,29 +235,29 @@ describe('DB Module', function() { it('will call sync when there is a new tip', function(done) { var db = new TestDB(baseConfig); - db.node.modules = {}; - db.node.modules.bitcoind = new EventEmitter(); - db.node.modules.bitcoind.syncPercentage = sinon.spy(); - db.node.modules.bitcoind.genesisBuffer = genesisBuffer; + db.node.services = {}; + db.node.services.bitcoind = new EventEmitter(); + db.node.services.bitcoind.syncPercentage = sinon.spy(); + db.node.services.bitcoind.genesisBuffer = genesisBuffer; db.getMetadata = sinon.stub().callsArg(0); db.connectBlock = sinon.stub().callsArg(1); db.saveMetadata = sinon.stub(); db.sync = sinon.stub(); db.start(function() { db.sync = function() { - db.node.modules.bitcoind.syncPercentage.callCount.should.equal(1); + db.node.services.bitcoind.syncPercentage.callCount.should.equal(1); done(); }; - db.node.modules.bitcoind.emit('tip', 10); + db.node.services.bitcoind.emit('tip', 10); }); }); it('will not call sync when there is a new tip and shutting down', function(done) { var db = new TestDB(baseConfig); - db.node.modules = {}; - db.node.modules.bitcoind = new EventEmitter(); - db.node.modules.bitcoind.syncPercentage = sinon.spy(); - db.node.modules.bitcoind.genesisBuffer = genesisBuffer; + db.node.services = {}; + db.node.services.bitcoind = new EventEmitter(); + db.node.services.bitcoind.syncPercentage = sinon.spy(); + db.node.services.bitcoind.genesisBuffer = genesisBuffer; db.getMetadata = sinon.stub().callsArg(0); db.connectBlock = sinon.stub().callsArg(1); db.saveMetadata = sinon.stub(); @@ -266,11 +265,11 @@ describe('DB Module', function() { db.sync = sinon.stub(); db.start(function() { db.sync.callCount.should.equal(1); - db.node.modules.bitcoind.once('tip', function() { + db.node.services.bitcoind.once('tip', function() { db.sync.callCount.should.equal(1); done(); }); - db.node.modules.bitcoind.emit('tip', 10); + db.node.services.bitcoind.emit('tip', 10); }); }); @@ -291,8 +290,8 @@ describe('DB Module', function() { it('will return a NotFound error', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getTransaction: sinon.stub().callsArgWith(2, null, null) }; var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; @@ -304,8 +303,8 @@ describe('DB Module', function() { it('will return an error from bitcoind', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getTransaction: sinon.stub().callsArgWith(2, new Error('test error')) }; var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; @@ -317,8 +316,8 @@ describe('DB Module', function() { it('will return an error from bitcoind', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getTransaction: sinon.stub().callsArgWith(2, null, new Buffer(transactionData[0].hex, 'hex')) }; var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; @@ -337,8 +336,8 @@ describe('DB Module', function() { var blockBuffer = new Buffer(blockData, 'hex'); var expectedBlock = Block.fromBuffer(blockBuffer); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer) }; @@ -351,9 +350,9 @@ describe('DB Module', function() { }); it('should give an error when bitcoind.js gives an error', function(done) { db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = {}; - db.node.modules.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error')); + db.node.services = {}; + db.node.services.bitcoind = {}; + db.node.services.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error')); db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { should.exist(err); err.message.should.equal('error'); @@ -366,8 +365,8 @@ describe('DB Module', function() { it('should return prevHash from bitcoind', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getBlockIndex: sinon.stub().returns({ prevHash: 'prevhash' }) @@ -383,8 +382,8 @@ describe('DB Module', function() { it('should give an error if bitcoind could not find it', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getBlockIndex: sinon.stub().returns(null) }; @@ -406,8 +405,8 @@ describe('DB Module', function() { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, info) }; @@ -421,8 +420,8 @@ describe('DB Module', function() { it('should give an error if one occurred', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('error')) }; @@ -437,8 +436,8 @@ describe('DB Module', function() { it('should give the txid on success', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { sendTransaction: sinon.stub().returns('txid') }; @@ -452,8 +451,8 @@ describe('DB Module', function() { it('should give an error if bitcoind threw an error', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { sendTransaction: sinon.stub().throws(new Error('error')) }; @@ -469,15 +468,15 @@ describe('DB Module', function() { it('should pass along the fee from bitcoind', function(done) { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { estimateFee: sinon.stub().returns(1000) }; db.estimateFee(5, function(err, fee) { should.not.exist(err); fee.should.equal(1000); - db.node.modules.bitcoind.estimateFee.args[0][0].should.equal(5); + db.node.services.bitcoind.estimateFee.args[0][0].should.equal(5); done(); }); }); @@ -512,20 +511,20 @@ describe('DB Module', function() { describe('#runAllBlockHandlers', function() { var db = new DB(baseConfig); - var Module1 = 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']); + var Service1 = function() {}; + Service1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']); + var Service2 = function() {}; + Service2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']); db.node = {}; - db.node.modules = { - module1: new Module1(), - module2: new Module2() + db.node.services = { + service1: new Service1(), + service2: new Service2() }; db.store = { batch: sinon.stub().callsArg(1) }; - it('should call blockHandler in all modules and perform operations', function(done) { + it('should call blockHandler in all services and perform operations', function(done) { db.runAllBlockHandlers('block', true, function(err) { should.not.exist(err); db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']); @@ -533,10 +532,10 @@ describe('DB Module', 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.node.modules.module3 = new Module3(); + it('should give an error if one of the services gives an error', function(done) { + var Service3 = function() {}; + Service3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); + db.node.services.service3 = new Service3(); db.runAllBlockHandlers('block', true, function(err) { should.exist(err); @@ -549,7 +548,7 @@ describe('DB Module', function() { it('should return the correct db methods', function() { var db = new DB(baseConfig); db.node = {}; - db.node.modules = {}; + db.node.services = {}; var methods = db.getAPIMethods(); methods.length.should.equal(5); }); @@ -631,8 +630,8 @@ describe('DB Module', function() { } }, }; - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getBlockIndex: function(hash) { var block = forkedBlocks[hash]; return { @@ -678,7 +677,7 @@ describe('DB Module', function() { } }); }; - db.node.modules = {}; + db.node.services = {}; db.disconnectBlock = function(block, callback) { setImmediate(callback); }; @@ -710,8 +709,8 @@ describe('DB Module', function() { var db = new DB(syncConfig); var blockBuffer = new Buffer(blockData, 'hex'); var block = Block.fromBuffer(blockBuffer); - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), isSynced: sinon.stub().returns(true), height: 1 @@ -737,8 +736,8 @@ describe('DB Module', function() { }); it('will exit and emit error with error from bitcoind.getBlock', function(done) { var db = new DB(syncConfig); - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getBlock: sinon.stub().callsArgWith(1, new Error('test error')), height: 1 }; @@ -755,8 +754,8 @@ describe('DB Module', function() { var db = new DB(syncConfig); var blockBuffer = new Buffer(blockData, 'hex'); var block = Block.fromBuffer(blockBuffer); - db.node.modules = {}; - db.node.modules.bitcoind = { + db.node.services = {}; + db.node.services.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), isSynced: sinon.stub().returns(true), height: 1 From 854f98fe439c6dc1901c850788e7fcc967a7e7b8 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Mon, 31 Aug 2015 09:00:00 -0400 Subject: [PATCH 5/5] Update Documentation to reflect Service Architecture --- LICENSE | 29 +-- README.md | 394 ++-------------------------------- docs/build.md | 92 ++++++++ docs/bus.md | 32 +++ docs/errors.md | 17 ++ docs/node.md | 41 ++++ docs/patch.md | 7 + RELEASE.md => docs/release.md | 6 +- docs/scaffold.md | 32 +++ docs/services.md | 56 +++++ docs/services/address.md | 35 +++ docs/services/bitcoind.md | 20 ++ docs/services/db.md | 34 +++ docs/testing.md | 52 +++++ 14 files changed, 445 insertions(+), 402 deletions(-) create mode 100644 docs/build.md create mode 100644 docs/bus.md create mode 100644 docs/errors.md create mode 100644 docs/node.md create mode 100644 docs/patch.md rename RELEASE.md => docs/release.md (97%) create mode 100644 docs/scaffold.md create mode 100644 docs/services.md create mode 100644 docs/services/address.md create mode 100644 docs/services/bitcoind.md create mode 100644 docs/services/db.md create mode 100644 docs/testing.md diff --git a/LICENSE b/LICENSE index d3e4d2e1..3f3b4df3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,30 +1,7 @@ -bitcoind,js --------------------------------------------------------------------------------- +Copyright (c) 2014-2015 BitPay, Inc. -Copyright (c) 2014-2015, BitPay - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -bcoin --------------------------------------------------------------------------------- - -Copyright Fedor Indutny, 2014. +Parts of this software are based on Bitcoin Core +Copyright (c) 2009-2015 The Bitcoin Core developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6b5df53c..3b56ca2c 100644 --- a/README.md +++ b/README.md @@ -1,398 +1,47 @@ Bitcore Node -======= +============ -A Node.js module that adds a native interface to Bitcoin Core for querying information about the Bitcoin blockchain. Bindings are linked to Bitcoin Core compiled as a static library. +A Bitcoin full node for building applications and services with Node.js. A node is extensible and can be configured to run additional services. At the minimum a node has native bindings to Bitcoin Core with the [Bitcoin Service](docs/services/bitcoind.md). Additional services can be enabled to make a node more useful such as exposing new APIs, adding new indexes for addresses with the [Address Service](docs/services/address.md), running a block explorer, wallet service, and other customizations. ## Install -Here is how you can you install and start your node: - ```bash -npm install -g bitcore-node@0.2.0-beta.4 +npm install -g bitcore-node@0.2.0-beta.5 bitcore-node start ``` -Note: For your convenience, we distribute binaries for x86_64 Linux and x86_64 Mac OS X. Upon npm install, the binaries for your platform will be downloaded. If you want to compile the project yourself, then please see the [Build & Install](#build--install) for full detailed instructions to build the project from source. +Note: For your convenience, we distribute binaries for x86_64 Linux and x86_64 Mac OS X. Upon npm install, the binaries for your platform will be downloaded. For more detailed installation instructions, or if you want to compile the project yourself, then please see the [Build & Install](docs/build.md) documentation to build the project from source. ## Configuration -Bitcore Node includes a Command Line Interface (CLI) for managing, configuring and interfacing with your Bitcore Node. At the minimum, your node can function with all of the features from Bitcoin Core running as a full node. However you can enable additional features to make your node more useful such as exposing new APIs, adding new indexes for addresses, running a block explorer and custom modules. +Bitcore Node includes a Command Line Interface (CLI) for managing, configuring and interfacing with your Bitcore Node. ```bash bitcore-node create -d mynode "My Node" cd mynode -bitcore-node add +bitcore-node add bitcore-node add https://github.com/yourname/helloworld ``` -This will create a directory with configuration files for your node and install the necessary dependencies. If you're interested in developing a module, please see the [Module Development Guide](#modules). +This will create a directory with configuration files for your node and install the necessary dependencies. For more information about (and developing) services, please see the [Service Documentation](docs/services.md). -## Build & Install +## Documentation -This includes a detailed instructions for compiling. There are two main parts of the build, compiling Bitcoin Core as a static library and the Node.js bindings. +- [Services](docs/services.md) + - [Bitcoind](docs/services/bitcoind.md) - Native bindings to Bitcoin Core + - [Database](docs/services/db.md) - The foundation API methods for getting information about blocks and transactions. + - [Address](docs/services/address.md) - Adds additional API methods for querying and subscribing to events with bitcoin addresses. +- [Build & Install](docs/build.md) - How to build and install from source +- [Testing & Development](docs/testing.md) - Developer guide for testing +- [Node](docs/node.md) - Details on the node constructor +- [Bus](docs/bus.md) - Overview of the event bus constructor +- [Errors](docs/errors.md) - Reference for error handling and types +- [Patch](docs/patch.md) - Information about the patch applied to Bitcoin Core +- [Release Process](docs/release.md) - Information about verifying a release and the release process. -### Ubuntu 14.04 (Unix/Linux) +## Contributing -If git is not already installed, it can be installed by running: - -```bash -sudo apt-get install git -git config --global user.email "you@example.com" -git config --global user.name "Your Name" -``` - -If Node.js v0.12 isn't installed, it can be installed using "nvm", it can be done by following the installation script at https://github.com/creationix/nvm#install-script and then install version v0.12 - -```bash -nvm install v0.12 -``` - -To build Bitcoin Core and bindings development packages are needed: - -```bash -sudo apt-get install build-essential libtool autotools-dev automake autoconf pkg-config libssl-dev -``` - -Clone the bitcore-node repository locally: - -```bash -git clone https://github.com/bitpay/bitcore-node.git -cd bitcore-node -``` - -And finally run the build which will take several minutes. A script in the "bin" directory will download Bitcoin Core v0.11, apply a patch (see more info below), and compile the static library and Node.js bindings. You can start this by running: - -```bash -npm install -``` -Once everything is built, you can run bitcore-node via: - -```bash -npm start -``` -This will then start the syncing process for Bitcoin Core and the extended capabilities as provided by the built-in Address Module (details below). - -### Fedora - -Later versions of Fedora (>= 22) should also work with this project. The directions for Ubuntu should generally work except the installation of system utilities and libraries is a bit different. Git is already installed and ready for use without installation. - -```bash -yum install libtool automake autoconf pkgconfig openssl make gcc gcc-c++ kernel-devel openssl-devel.x86_64 patch -``` - -### Mac OS X Yosemite - -If Xcode is not already installed, it can be installed via the Mac App Store (will take several minutes). XCode includes "Clang", "git" and other build tools. Once Xcode is installed, you'll then need to install "xcode-select" via running in a terminal and following the prompts: - -```bash -xcode-select --install -``` - -If "Homebrew" is not yet installed, it's needed to install "autoconf" and others. You can install it using the script at http://brew.sh and following the directions at https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Installation.md And then run in a terminal: - -```bash -brew install autoconf automake libtool openssl pkg-config -``` - -If Node.js v0.12 and associated commands "node", "npm" and "nvm" are not already installed, you can use "nvm" by running the script at https://github.com/creationix/nvm#install-script And then run this command to install Node.js v0.12 - -```bash -nvm install v0.12 -``` - -Clone the bitcore-node repository locally: - -```bash -git clone https://github.com/bitpay/bitcore-node.git -cd bitcore-node -``` - -And finally run the build which will take several minutes. A script in the "bin" directory will download Bitcoin Core v0.11, apply a patch (see more info below), and compile the static library and Node.js bindings. You can start this by running: - -```bash -npm install -``` -Once everything is built, you can run bitcore-node via: - -```bash -npm start -``` - -This will then start the syncing process for Bitcoin Core and the extended capabilities as provided by the built-in Address Module (details below). - -## Development & Testing - -To run all of the JavaScript tests: - -```bash -npm run test -``` - -To run tests against the bindings, as defined in `bindings.gyp` the regtest feature of Bitcoin Core is used, and to enable this feature we currently need to build with the wallet enabled *(not a part of the regular build)*. To do this, export an environment variable and recompile: - -```bash -export BITCORENODE_ENV=test -npm run build -``` - -If you do not already have mocha installed: - -```bash -npm install mocha -g -``` - -To run the integration tests: - -```bash -mocha -R spec integration/regtest.js -``` - -If any changes have been made to the bindings in the "src" directory, manually compile the Node.js bindings, as defined in `bindings.gyp`, you can run (-d for debug): - -```bash -$ node-gyp -d rebuild -``` - -Note: `node-gyp` can be installed with `npm install node-gyp -g` - -To be able to debug you'll need to have `gdb` and `node` compiled for debugging with gdb using `--gdb` (sometimes called node_g), and you can then run: - -```bash -$ gdb --args node examples/node.js -``` - -To run mocha from within gdb (notice `_mocha` and not `mocha` so that the tests run in the same process): -```bash -$ gdb --args node /path/to/_mocha -R spec integration/regtest.js -``` - -To run the benchmarks: - -```bash -$ cd benchmarks -$ node index.js -``` - -## Static Library Patch - -To provide native bindings to JavaScript *(or any other language for that matter)*, Bitcoin code, itself, must be linkable. Currently, Bitcoin Core provides a JSON RPC interface to bitcoind as well as a shared library for script validation *(and hopefully more)* called libbitcoinconsensus. There is a node module, [node-libbitcoinconsensus](https://github.com/bitpay/node-libbitcoinconsensus), that exposes these methods. While these interfaces are useful for several use cases, there are additional use cases that are not fulfilled, and being able to implement customized interfaces is necessary. To be able to do this a few simple changes need to be made to Bitcoin Core to compile as a static library. - -The patch is located at `etc/bitcoin.patch` and adds a configure option `--enable-daemonlib` to compile all object files with `-fPIC` (Position Independent Code - needed to create a shared object), exposes leveldb variables and objects, exposes the threadpool to the bindings, and conditionally includes the main function. - -Every effort will be made to ensure that this patch stays up-to-date with the latest release of Bitcoin. At the very least, this project began supporting Bitcoin Core v0.11. - -## Example Usage - -```js - -var BitcoinNode = require('bitcore-node').Node; - -var configuration = { - datadir: '~/.bitcoin', - network: 'testnet' -}; - -var node = new BitcoinNode(configuration); - -node.on('ready', function() { - console.log('Bitcoin Node Ready'); -}); - -node.on('error', function(err) { - console.error(err); -}); - -node.chain.on('addblock', function(block) { - console.log('New Best Tip:', block.hash); -}); - -``` - -## API Documentation - -Get Unspent Outputs - -```js -var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; -var includeMempool = true; -node.getUnspentOutputs(address, includeMempool, function(err, unspentOutputs) { - //... -}); -``` - -View Balances - -```js -var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; -var includeMempool = true; -node.getBalance(address, includeMempool, function(err, balance) { - //... -}); -``` - -Get Outputs - -```js -var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; -var includeMempool = true; -node.getOutputs(address, includeMempool, function(err, outputs) { - //... -}); -``` - -Get Transaction - -```js -var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; -var includeMempool = true; -node.getTransaction(txid, includeMempool, function(err, transaction) { - //... -}); -``` - -Get Block - -```js -var blockHash = '00000000d17332a156a807b25bc5a2e041d2c730628ceb77e75841056082a2c2'; -node.getBlock(blockHash, function(err, block) { - //... -}); -``` - -You can log output from the daemon using: - -``` bash -$ tail -f ~/.bitcoin/debug.log -``` - -^C (SIGINT) will call `StartShutdown()` in bitcoind on the node thread pool. - -## Modules - -Bitcore Node has a module system where additional information can be indexed and queried from -the blockchain. One built-in module is the address module which exposes the API methods for getting balances and outputs. - -### Writing a Module - -A new module can be created by inheriting from `Node.Module`, implementing the methods `blockHandler()`, `getAPIMethods()`, `getPublishEvents()` and any additional methods for querying the data. Here is an example: - -```js -var inherits = require('util').inherits; -var Node = require('bitcore-node').Node; - -var MyModule = function(options) { - Node.Module.call(this, options); -}; - -inherits(MyModule, Node.Module); - -/** - * blockHandler - * @param {Block} block - the block being added or removed from the chain - * @param {Boolean} add - whether the block is being added or removed - * @param {Function} callback - call with the leveldb database operations to perform - */ -MyModule.prototype.blockHandler = function(block, add, callback) { - var transactions = block.transactions; - // loop through transactions and outputs - // call the callback with leveldb database operations - var operations = []; - if(add) { - operations.push({ - type: 'put', - key: 'key', - value: 'value' - }); - } else { - operations.push({ - type: 'del', - key: 'key' - }); - } - - // If your function is not asynchronous, it is important to use setImmediate. - setImmediate(function() { - callback(null, operations); - }); -}; - -/** - * the API methods to expose - * @return {Array} return array of methods - */ -MyModule.prototype.getAPIMethods = function() { - return [ - ['getData', this, this.getData, 1] - ]; -}; - -/** - * the bus events available for subscription - * @return {Array} array of events - */ -MyModule.prototype.getPublishEvents = function() { - return [ - { - name: 'custom', - scope: this, - subscribe: this.subscribeCustom, - unsubscribe: this.unsubscribeCustom - } - ] -}; - -/** - * Will keep track of event listeners to later publish and emit events. - */ -MyModule.prototype.subscribeCustom = function(emitter, param) { - if(!this.subscriptions[param]) { - this.subscriptions[param] = []; - } - this.subscriptions[param].push(emitter); -} - -MyModule.prototype.getData = function(arg1, callback) { - // You can query the data by reading from the leveldb store on db - this.node.db.store.get(arg1, callback); -}; - -module.exports = MyModule; -``` - -The module can then be used when running a node: - -```js -var configuration = { - datadir: process.env.BITCORENODE_DIR || '~/.bitcoin', - modules: [MyModule] -}; - -var node = new Node(configuration); - -node.on('ready', function() { - node.getData('key', function(err, value) { - console.log(err || value); - }); -}); -``` - -Note that if you already have a bitcore-node database, and you want to query data from previous blocks in the blockchain, you will need to reindex. Reindexing right now means deleting your bitcore-node database and resyncing. - -## Daemon Documentation - -- `daemon.start([options], [callback])` - Start the JavaScript Bitcoin node. -- `daemon.getBlock(blockHash|blockHeight, callback)` - Get any block asynchronously by block hash or height as a node buffer. -- `daemon.isSpent(txid, outputIndex)` - Returns a boolean if a txid and outputIndex is already spent. -- `daemon.getBlockIndex(blockHash)` - Will return the block chain work and previous hash. -- `daemon.estimateFee(blocks)` - Estimates the fees required to have a transaction included in the number of blocks specified as the first argument. -- `daemon.sendTransaction(transaction, allowAbsurdFees)` - Will attempt to add a transaction to the mempool and broadcast to peers. -- `daemon.getTransaction(txid, queryMempool, callback)` - Get any tx asynchronously by reading it from disk, with an argument to optionally not include the mempool. -- `daemon.getTransactionWithBlockInfo(txid, queryMempool, callback)` - Similar to getTransaction but will also include the block timestamp and height. -- `daemon.getMempoolOutputs(address)` - Will return an array of outputs that match an address from the mempool. -- `daemon.getInfo()` - Basic information about the chain including total number of blocks. -- `daemon.isSynced()` - Returns a boolean if the daemon is fully synced (not the initial block download) -- `daemon.syncPercentage()` - Returns the current estimate of blockchain download as a percentage. -- `daemon.stop([callback])` - Stop the JavaScript bitcoin node safely, the callback will be called when bitcoind is closed. This will also be done automatically on `process.exit`. It also takes the bitcoind node off the libuv event loop. If the daemon object is the only thing on the event loop. Node will simply close. +Please send pull requests for bug fixes, code optimization, and ideas for improvement. For more information on how to contribute, please refer to our [CONTRIBUTING](https://github.com/bitpay/bitcore/blob/master/CONTRIBUTING.md) file. ## License @@ -401,4 +50,3 @@ Code released under [the MIT license](https://github.com/bitpay/bitcore-node/blo Copyright 2013-2015 BitPay, Inc. - bitcoin: Copyright (c) 2009-2015 Bitcoin Core Developers (MIT License) -- bcoin (some code borrowed temporarily): Copyright Fedor Indutny, 2014. diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 00000000..604d11de --- /dev/null +++ b/docs/build.md @@ -0,0 +1,92 @@ +## Build & Install + +This includes a detailed instructions for compiling. There are two main parts of the build, compiling Bitcoin Core as a static library and the Node.js bindings. + +## Ubuntu 14.04 (Unix/Linux) + +If git is not already installed, it can be installed by running: + +```bash +sudo apt-get install git +git config --global user.email "you@example.com" +git config --global user.name "Your Name" +``` + +If Node.js v0.12 isn't installed, it can be installed using "nvm", it can be done by following the installation script at https://github.com/creationix/nvm#install-script and then install version v0.12 + +```bash +nvm install v0.12 +``` + +To build Bitcoin Core and bindings development packages are needed: + +```bash +sudo apt-get install build-essential libtool autotools-dev automake autoconf pkg-config libssl-dev +``` + +Clone the bitcore-node repository locally: + +```bash +git clone https://github.com/bitpay/bitcore-node.git +cd bitcore-node +``` + +And finally run the build which will take several minutes. A script in the "bin" directory will download Bitcoin Core v0.11, apply a patch (see more info below), and compile the static library and Node.js bindings. You can start this by running: + +```bash +npm install +``` +Once everything is built, you can run bitcore-node via: + +```bash +npm start +``` +This will then start the syncing process for Bitcoin Core and the extended capabilities as provided by the built-in Address Module (details below). + +## Fedora + +Later versions of Fedora (>= 22) should also work with this project. The directions for Ubuntu should generally work except the installation of system utilities and libraries is a bit different. Git is already installed and ready for use without installation. + +```bash +yum install libtool automake autoconf pkgconfig openssl make gcc gcc-c++ kernel-devel openssl-devel.x86_64 patch +``` + +## Mac OS X Yosemite + +If Xcode is not already installed, it can be installed via the Mac App Store (will take several minutes). XCode includes "Clang", "git" and other build tools. Once Xcode is installed, you'll then need to install "xcode-select" via running in a terminal and following the prompts: + +```bash +xcode-select --install +``` + +If "Homebrew" is not yet installed, it's needed to install "autoconf" and others. You can install it using the script at http://brew.sh and following the directions at https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Installation.md And then run in a terminal: + +```bash +brew install autoconf automake libtool openssl pkg-config +``` + +If Node.js v0.12 and associated commands "node", "npm" and "nvm" are not already installed, you can use "nvm" by running the script at https://github.com/creationix/nvm#install-script And then run this command to install Node.js v0.12 + +```bash +nvm install v0.12 +``` + +Clone the bitcore-node repository locally: + +```bash +git clone https://github.com/bitpay/bitcore-node.git +cd bitcore-node +``` + +And finally run the build which will take several minutes. A script in the "bin" directory will download Bitcoin Core v0.11, apply a patch (see more info below), and compile the static library and Node.js bindings. You can start this by running: + +```bash +npm install +``` +Once everything is built, you can run bitcore-node via: + +```bash +npm start +``` + +This will then start the syncing process for Bitcoin Core and the extended capabilities as provided by the built-in Address Module (details below). \ No newline at end of file diff --git a/docs/bus.md b/docs/bus.md new file mode 100644 index 00000000..38e3a108 --- /dev/null +++ b/docs/bus.md @@ -0,0 +1,32 @@ +# Bus + +The bus provides a way to subscribe to events from any of the services running. It's implemented abstract from transport specific implementation. The primary use of the bus in Bitcore Node is for subscribing to events via a web socket. + +## Opening/Closing + +```javascript + +// a node is needed to be able to open a bus +var node = new Node(configuration); + +// will create a new bus that is ready to subscribe to events +var bus = node.openBus(); + +// will remove all event listeners +bus.close(); +``` + +## Subscribing/Unsubscribing + +```javascript + +// subscribe to all transaction events +bus.subscribe('transaction'); + +// only subscribe to events relevant to a bitcoin address +bus.subscribe('address/transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); + +// unsubscribe +bus.unsubscribe('transaction'); +``` + diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 00000000..eeb9b487 --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,17 @@ +# Errors + +Many times there are cases where an error condition can be gracefully handled depending on a particular use. To assist in better error handling, errors will have different types so that it's possible to determine the type of error and handle appropriatly. + +```js +node.services.address.getUnspentOutputs('00000000839a8...', function(err, outputs) { + + if (err instanceof errors.NoOutputs) { + // the address hasn't received any transactions + } + + // otherwise the address has outputs (which may be unspent/spent) + +}); +``` + +For more information about different types of errors, please see `lib/errors.js`. \ No newline at end of file diff --git a/docs/node.md b/docs/node.md new file mode 100644 index 00000000..4cc9ef93 --- /dev/null +++ b/docs/node.md @@ -0,0 +1,41 @@ +# Node + +A node represents a collection of services that are loaded together. For more information about services, please see the [Services Documentation](services.md). + +## API Documentation + +- `start()` - Will start the node's services in the correct order based on the dependencies of a service. +- `stop()` - Will stop the node's services. +- `openBus()` - Will create a new event bus to subscribe to events. +- `getAllAPIMethods()` - Returns information about all of the API methods from the services. +- `getAllPublishEvents()` - Returns information about publish events. +- `getServiceOrder()` - Returns an array of service modules. +- `services..` - Additional API methods exposed by each service. The services for the node are defined when the node instance is constructed. + +## Example Usage + +```js + +var BitcoinNode = require('bitcore-node').Node; + +var configuration = { + datadir: '~/.bitcoin', + network: 'testnet' +}; + +var node = new BitcoinNode(configuration); + +node.on('ready', function() { + console.log('Bitcoin Node Ready'); +}); + +node.on('error', function(err) { + console.error(err); +}); + +// shutdown the node +node.stop(function() { + // the shutdown is complete +}); + +``` diff --git a/docs/patch.md b/docs/patch.md new file mode 100644 index 00000000..ef954572 --- /dev/null +++ b/docs/patch.md @@ -0,0 +1,7 @@ +# Static Library Patch + +To provide native bindings to JavaScript *(or any other language for that matter)*, Bitcoin code, itself, must be linkable. Currently, Bitcoin Core provides a JSON RPC interface to bitcoind as well as a shared library for script validation *(and hopefully more)* called libbitcoinconsensus. There is a node module, [node-libbitcoinconsensus](https://github.com/bitpay/node-libbitcoinconsensus), that exposes these methods. While these interfaces are useful for several use cases, there are additional use cases that are not fulfilled, and being able to implement customized interfaces is necessary. To be able to do this a few simple changes need to be made to Bitcoin Core to compile as a static library. + +The patch is located at `etc/bitcoin.patch` and adds a configure option `--enable-daemonlib` to compile all object files with `-fPIC` (Position Independent Code - needed to create a shared object), exposes leveldb variables and objects, exposes the threadpool to the bindings, and conditionally includes the main function. + +Every effort will be made to ensure that this patch stays up-to-date with the latest release of Bitcoin. At the very least, this project began supporting Bitcoin Core v0.11. \ No newline at end of file diff --git a/RELEASE.md b/docs/release.md similarity index 97% rename from RELEASE.md rename to docs/release.md index 0df761c4..0f1f4f71 100644 --- a/RELEASE.md +++ b/docs/release.md @@ -1,8 +1,8 @@ -## Release Process +# Release Process Binaries for the C++ binding file (which includes libbitcoind statically linked in) are distributed for convenience. The binary binding file `bitcoind.node` is signed and published to S3 for later download and installation. Source files can also be built if binaries are not desired. -### How to Verify Signatures +## How to Verify Signatures ``` cd build/Release @@ -15,7 +15,7 @@ To verify signatures, use the following PGP keys: - @kleetus: https://pgp.mit.edu/pks/lookup?op=get&search=0x33195D27EF6BDB7F - @pnagurny: https://pgp.mit.edu/pks/lookup?op=get&search=0x0909B33F0AA53013 -### How to Release +## How to Release Ensure you've followed the instructions in the README.md for building the project from source. When building for any platform, be sure to keep in mind the minimum supported C and C++ system libraries and build from source using this library. Example, Ubuntu 12.04 has the earliest system library for Linux that we support, so it would be easiest to build the Linux artifact using this version. You will be using node-gyp to build the C++ bindings. A script will then upload the bindings to S3 for later use. You will also need credentials for BitPay's bitcore-node S3 bucket and be listed as an author for the bitcore-node's npm module. diff --git a/docs/scaffold.md b/docs/scaffold.md new file mode 100644 index 00000000..f3eccf2d --- /dev/null +++ b/docs/scaffold.md @@ -0,0 +1,32 @@ +# Scaffold + +A collection of functions for creating, managing, starting, stopping and interacting with a Bitcore Node. + +## Create + +This function will create a new directory and the initial configuration files/directories, including 'bitcore-node.json', 'package.json', 'bitcoin.conf', install the necessary Node.js modules, and create a data directory. + +## Add + +This function will add a service to a node by installing the necessary dependencies and modifying the `bitcore-node.json` configuration. + +## Start + +This function will load a configuration file `bitcore-node.json` and instantiate and start a node based on the configuration. + +## Find Config + +This function will recursively find a configuration `bitcore-node.json` file in parent directories and return the result. + +## Default Config + +This function will return a default configuration with the default services based on environment variables, and will default to using the standard `~/.bitcoin` data directory. + +## Remove + +This function will remove a service from a node by uninstalling the necessary dependencies and modifying the `bitcore-node.json` configuration. + +## Call Method + +This function will call an API method on a node via the JSON-RPC interface. + diff --git a/docs/services.md b/docs/services.md new file mode 100644 index 00000000..a4c04bb9 --- /dev/null +++ b/docs/services.md @@ -0,0 +1,56 @@ +# Services + +## Available Services + +- [Bitcoin Daemon](services/bitcoind.md) +- [DB](services/db.md) +- [Address](services/address.md) + +## Overview + +Bitcore Node has a service module system that can start up additional services that can include additional: + +- Blockchain indexes (e.g. querying balances for addresses) +- API methods +- HTTP routes +- Event types to publish and subscribe + +The `bitcore-node.json` file describes which services will load for a node: + +```json +{ + "services": [ + "bitcoind", "db", "address", "insight-api" + ] +} +``` + +Services correspond with a Node.js module as described in 'package.json', for example: + +```json +{ + "dependencies": { + "bitcore": "^0.13.1", + "bitcore-node": "^0.2.0", + "insight-api": "^3.0.0" + } +} +``` + +*Note:* If you already have a bitcore-node database, and you want to query data from previous blocks in the blockchain, you will need to reindex. Reindexing right now means deleting your bitcore-node database and resyncing. + +## Writing a Service + +A new service can be created by inheriting from `Node.Service` and implementing these methods and properties: + +- `Service.dependencies` - An array of services that are needed, this will determine the order that services are started on the node. +- `Service.prototype.start()` - Called to start up the service. +- `Service.prototype.stop()` - Called to stop the service. +- `Service.prototype.blockHandler()` - Will be called when a block is added or removed from the chain, and is useful for updating a database view/index. +- `Service.prototype.getAPIMethods()` - Describes which API methods that this service includes, these methods can then be called over the JSON-RPC API, as well as the command-line utility. +- `Service.prototype.getPublishEvents()` - Describes which events can be subscribed to for this service, useful to subscribe to events over the included web socket API. +- `Service.prototype.setupRoutes()` - A service can extend HTTP routes on an express application by implementing this method. + +The `package.json` for the service module can either export the `Node.Service` directly, or specify a specific module to load by including `"bitcoreNode": "lib/bitcore-node.js"`. + +Please take a look at some of the existing services for implemenation specifics. diff --git a/docs/services/address.md b/docs/services/address.md new file mode 100644 index 00000000..63c4f0ff --- /dev/null +++ b/docs/services/address.md @@ -0,0 +1,35 @@ +# Address Service + +The address service builds on the [Bitcoin Service](bitcoind.md) and the [Database Service](db.md) to add additional functionality for querying and subscribing to information based on bitcoin addresses. + +## API Documentation + +Get Unspent Outputs + +```js +var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; +var includeMempool = true; +node.getUnspentOutputs(address, includeMempool, function(err, unspentOutputs) { + //... +}); +``` + +View Balances + +```js +var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; +var includeMempool = true; +node.getBalance(address, includeMempool, function(err, balance) { + //... +}); +``` + +Get Outputs + +```js +var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; +var includeMempool = true; +node.getOutputs(address, includeMempool, function(err, outputs) { + //... +}); +``` diff --git a/docs/services/bitcoind.md b/docs/services/bitcoind.md new file mode 100644 index 00000000..39b469f0 --- /dev/null +++ b/docs/services/bitcoind.md @@ -0,0 +1,20 @@ +# Bitcoin Service + +The bitcoin service adds a native interface to Bitcoin Core for querying information about the Bitcoin blockchain. Bindings are linked to Bitcoin Core compiled as a static library. + +## API Documentation + +- `bitcoind.start([options], [callback])` - Start the JavaScript Bitcoin node. +- `bitcoind.getBlock(blockHash|blockHeight, callback)` - Get any block asynchronously by block hash or height as a node buffer. +- `bitcoind.isSpent(txid, outputIndex)` - Returns a boolean if a txid and outputIndex is already spent. +- `bitcoind.getBlockIndex(blockHash)` - Will return the block chain work and previous hash. +- `bitcoind.estimateFee(blocks)` - Estimates the fees required to have a transaction included in the number of blocks specified as the first argument. +- `bitcoind.sendTransaction(transaction, allowAbsurdFees)` - Will attempt to add a transaction to the mempool and broadcast to peers. +- `bitcoind.getTransaction(txid, queryMempool, callback)` - Get any tx asynchronously by reading it from disk, with an argument to optionally not include the mempool. +- `bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback)` - Similar to getTransaction but will also include the block timestamp and height. +- `bitcoind.getMempoolOutputs(address)` - Will return an array of outputs that match an address from the mempool. +- `bitcoind.getInfo()` - Basic information about the chain including total number of blocks. +- `bitcoind.isSynced()` - Returns a boolean if the daemon is fully synced (not the initial block download) +- `bitcoind.syncPercentage()` - Returns the current estimate of blockchain download as a percentage. +- `bitcoind.stop([callback])` - Stop the JavaScript bitcoin node safely, the callback will be called when bitcoind is closed. This will also be done automatically on `process.exit`. It also takes the bitcoind node off the libuv event loop. If the daemon object is the only thing on the event loop. Node will simply close. + diff --git a/docs/services/db.md b/docs/services/db.md new file mode 100644 index 00000000..9a8d1eba --- /dev/null +++ b/docs/services/db.md @@ -0,0 +1,34 @@ +# Database Service + +An extensible interface to the bitcoin block chain. The service builds on the [Bitcoin Service](bitcoind.md), and includes additional methods for working with the block chain. + +## API Documentation + +Get Transaction + +```js +var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; +var includeMempool = true; +node.getTransaction(txid, includeMempool, function(err, transaction) { + //... +}); +``` + +Get Transaction with Block Info + +```js +var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; +var includeMempool = true; +node.getTransactionWithBlockInfo(txid, includeMempool, function(err, transaction) { + //... +}); +``` + +Get Block + +```js +var blockHash = '00000000d17332a156a807b25bc5a2e041d2c730628ceb77e75841056082a2c2'; +node.getBlock(blockHash, function(err, block) { + //... +}); +``` diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..aba45658 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,52 @@ +## Development & Testing + +To run all of the JavaScript tests: + +```bash +npm run test +``` + +To run tests against the bindings, as defined in `bindings.gyp` the regtest feature of Bitcoin Core is used, and to enable this feature we currently need to build with the wallet enabled *(not a part of the regular build)*. To do this, export an environment variable and recompile: + +```bash +export BITCORENODE_ENV=test +npm run build +``` + +If you do not already have mocha installed: + +```bash +npm install mocha -g +``` + +To run the integration tests: + +```bash +mocha -R spec integration/regtest.js +``` + +If any changes have been made to the bindings in the "src" directory, manually compile the Node.js bindings, as defined in `bindings.gyp`, you can run (-d for debug): + +```bash +$ node-gyp -d rebuild +``` + +Note: `node-gyp` can be installed with `npm install node-gyp -g` + +To be able to debug you'll need to have `gdb` and `node` compiled for debugging with gdb using `--gdb` (sometimes called node_g), and you can then run: + +```bash +$ gdb --args node examples/node.js +``` + +To run mocha from within gdb (notice `_mocha` and not `mocha` so that the tests run in the same process): +```bash +$ gdb --args node /path/to/_mocha -R spec integration/regtest.js +``` + +To run the benchmarks: + +```bash +$ cd benchmarks +$ node index.js +``` \ No newline at end of file