Merge pull request #135 from braydonf/cli

CLI and Module Architecture
This commit is contained in:
Patrick Nagurny 2015-08-25 16:51:14 -04:00
commit 216830976f
16 changed files with 1077 additions and 192 deletions

View File

@ -5,16 +5,31 @@ A Node.js module that adds a native interface to Bitcoin Core for querying infor
## Install
Here is how you can you install and start your node:
```bash
git clone https://github.com/bitpay/bitcore-node.git
cd bitcore-node
npm install
npm install -g bitcore-node@0.2.0-beta.4
bitcore-node start
```
Note: For 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. This greatly speeds up the process of using this project. If you don't want to compile the project for yourself, then please skip down to "Example Usage" section for next steps. Please see detailed instructions below for complete build details and dependencies needed for installation if you choose 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. 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.
## 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.
```bash
bitcore-node create -d <bitcoin-data-dir> mynode "My Node"
cd mynode
bitcore-node add <module>
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).
## Build & Install
There are two main parts of the build, compiling Bitcoin Core as a static library and the Node.js bindings.
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)

View File

@ -32,7 +32,7 @@ Ensure you've followed the instructions in the README.md for building the projec
When publishing to npm, the .gitignore file is used to exclude files from the npm publishing process. Be sure that the bitcore-node directory has only the directories and files that you would like to publish to npm. You might need to run the commands below on each platform that you intend to publish (e.g. Mac and Linux).
To make a release, bump the version of the package.json:
To make a release, bump the `version` and `lastBuild` of the `package.json`:
```bash
git checkout master
@ -44,7 +44,7 @@ npm run upload
npm publish
```
And then update the version of the package.json for development (e.g. "0.3.2-dev"):
And then update the `version` of the `package.json` for development (e.g. "0.3.2-dev"):
```bash
git commit -a -m "Bump development version to <version>"

View File

@ -1,175 +1,6 @@
'use strict';
var BitcoinNode = require('..').Node;
var chainlib = require('chainlib');
var socketio = require('socket.io');
var log = chainlib.log;
log.debug = function() {};
var start = require('../lib/scaffold/start');
var defaultConfig = require('../lib/scaffold/default-config');
var configuration = {
datadir: process.env.BITCORENODE_DIR || '~/.bitcoin',
network: process.env.BITCORENODE_NETWORK || 'livenet',
port: 3000
};
var node = new BitcoinNode(configuration);
var count = 0;
var interval = false;
function logSyncStatus() {
log.info('Sync Status: Tip:', node.chain.tip.hash, 'Height:', node.chain.tip.__height, 'Rate:', count/10, 'blocks per second');
}
node.on('synced', function() {
// Stop logging of sync status
clearInterval(interval);
interval = false;
logSyncStatus();
});
node.on('ready', function() {
var io = socketio(configuration.port);
io.on('connection', function(socket) {
var bus = node.openBus();
var methods = node.getAllAPIMethods();
var methodsMap = {};
methods.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
var args = data[3];
methodsMap[name] = {
fn: function() {
return method.apply(instance, arguments);
},
args: args
};
});
socket.on('message', function(message, socketCallback) {
if (methodsMap[message.method]) {
var params = message.params;
if(!params || !params.length) {
params = [];
}
if(params.length !== methodsMap[message.method].args) {
return socketCallback({
error: {
message: 'Expected ' + methodsMap[message.method].args + ' parameters'
}
});
}
var callback = function(err, result) {
var response = {};
if(err) {
response.error = {
message: err.toString()
};
}
if(result) {
response.result = result;
}
socketCallback(response);
};
params = params.concat(callback);
methodsMap[message.method].fn.apply(this, params);
} else {
socketCallback({
error: {
message: 'Method Not Found'
}
});
}
});
socket.on('subscribe', function(name, params) {
bus.subscribe(name, params);
});
socket.on('unsubscribe', function(name, params) {
bus.unsubscribe(name, params);
});
var events = node.getAllPublishEvents();
events.forEach(function(event) {
bus.on(event.name, function() {
if(socket.connected) {
var results = [];
for(var i = 0; i < arguments.length; i++) {
results.push(arguments[i]);
}
var params = [event.name].concat(results);
socket.emit.apply(socket, params);
}
});
});
socket.on('disconnect', function() {
bus.close();
});
});
});
node.on('error', function(err) {
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('stopping', function() {
clearInterval(interval);
});
function exitHandler(options, err) {
if (err) {
log.error('uncaught exception:', err);
if(err.stack) {
console.log(err.stack);
}
process.exit(-1);
}
if (options.sigint) {
node.stop(function(err) {
if(err) {
log.error('Failed to stop services: ' + err);
return process.exit(1);
}
log.info('Halted');
process.exit(0);
});
}
}
//catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(null, {exit:true}));
//catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, {sigint:true}));
start(defaultConfig());

85
cli/bitcore-node.js Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env node
'use strict';
var program = require('commander');
var version = require(__dirname + '/../package.json').version;
var bitcore = require('bitcore');
var $ = bitcore.util.preconditions;
var path = require('path');
var create = require('../lib/scaffold/create');
var add = require('../lib/scaffold/add');
var start = require('../lib/scaffold/start');
var findConfig = require('../lib/scaffold/find-config');
var defaultConfig = require('../lib/scaffold/default-config');
program
.version(version);
program
.command('create <directory> [name]')
.description('Create a new node')
.option('-d, --datadir <dir>', 'Specify the bitcoin database directory')
.action(function(dirname, name, cmd){
if (cmd.datadir) {
cmd.datadir = path.resolve(process.cwd(), cmd.datadir);
}
var opts = {
cwd: process.cwd(),
dirname: dirname,
name: name,
datadir: cmd.datadir || './data',
isGlobal: false
};
create(opts, function(err) {
if (err) {
throw err;
}
console.log('Successfully created node in directory: ', dirname);
});
});
program
.command('start')
.description('Start the current node')
.option('-c, --config <dir>', 'Specify the directory with Bitcore Node configuration')
.action(function(cmd){
if (cmd.config) {
cmd.config = path.resolve(process.cwd(), cmd.config);
}
var configInfo = findConfig(cmd.config || process.cwd());
if (!configInfo) {
configInfo = defaultConfig();
}
start(configInfo);
});
program
.command('add <modules...>')
.alias('install')
.description('Install a module for the current node')
.action(function(modules){
var configInfo = findConfig(process.cwd());
if (!configInfo) {
throw new Error('Could not find configuration, see `bitcore-node create --help`');
}
var opts = {
path: configInfo.path,
modules: modules
};
add(opts, function() {
console.log('Successfully added modules: ', modules.join(', '));
});
}).on('--help', function() {
console.log(' Examples:');
console.log();
console.log(' $ bitcore-node add wallet-service');
console.log(' $ bitcore-node add insight-api');
console.log();
});
program.parse(process.argv);
if (process.argv.length === 2) {
program.help();
}

View File

@ -57,12 +57,11 @@ 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 datadir = config.datadir.replace(/^~/, process.env.HOME);
var configPath = datadir + '/bitcoin.conf';
var configPath = config.datadir + '/bitcoin.conf';
this.bitcoinConfiguration = {};
if (!fs.existsSync(datadir)) {
mkdirp.sync(datadir);
if (!fs.existsSync(config.datadir)) {
mkdirp.sync(config.datadir);
}
if (!fs.existsSync(configPath)) {
@ -329,13 +328,12 @@ Node.prototype._loadDB = function(config) {
$.checkArgument(config.datadir, 'Please specify "datadir" in configuration options');
$.checkState(this.network, 'Network property not defined');
var regtest = Networks.get('regtest');
var datadir = config.datadir.replace(/^~/, process.env.HOME);
if (this.network === Networks.livenet) {
options.path = datadir + '/bitcore-node.db';
options.path = config.datadir + '/bitcore-node.db';
} else if (this.network === Networks.testnet) {
options.path = datadir + '/testnet3/bitcore-node.db';
options.path = config.datadir + '/testnet3/bitcore-node.db';
} else if (this.network === regtest) {
options.path = datadir + '/regtest/bitcore-node.db';
options.path = config.datadir + '/regtest/bitcore-node.db';
} else {
throw new Error('Unknown network: ' + this.network);
}

104
lib/scaffold/add.js Normal file
View File

@ -0,0 +1,104 @@
'use strict';
var async = require('async');
var fs = require('fs');
var path = require('path');
var spawn = require('child_process').spawn;
var bitcore = require('bitcore');
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
/**
* @param {String} configFilePath - The absolute path to the configuration file
* @param {String} module - The name of the module
* @param {Function} done
*/
function addConfig(configFilePath, module, done) {
$.checkState(path.isAbsolute(configFilePath), 'An absolute path is expected');
fs.readFile(configFilePath, function(err, data) {
if (err) {
return done(err);
}
var config = JSON.parse(data);
$.checkState(
Array.isArray(config.modules),
'Configuration file is expected to have a modules array.'
);
config.modules.push(module);
config.modules = _.unique(config.modules);
config.modules.sort(function(a, b) {
return a > b;
});
fs.writeFile(configFilePath, JSON.stringify(config, null, 2), done);
});
}
/**
* @param {String} configDir - The absolute configuration directory path
* @param {String} module - The name of the module
* @param {Function} done
*/
function addModule(configDir, module, done) {
$.checkState(path.isAbsolute(configDir), 'An absolute path is expected');
var npm = spawn('npm', ['install', module, '--save'], {cwd: configDir});
npm.stdout.on('data', function(data) {
process.stdout.write(data);
});
npm.stderr.on('data', function(data) {
process.stderr.write(data);
});
npm.on('close', function(code) {
if (code !== 0) {
return done(new Error('There was an error installing module: ' + module));
} else {
return 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 {Function} done - A callback function called when finished
*/
function add(options, done) {
$.checkArgument(_.isObject(options));
$.checkArgument(_.isFunction(done));
$.checkArgument(
_.isString(options.path) && path.isAbsolute(options.path),
'An absolute path is expected'
);
$.checkArgument(Array.isArray(options.modules));
var configPath = options.path;
var modules = options.modules;
var bitcoreConfigPath = path.resolve(configPath, 'bitcore-node.json');
var packagePath = path.resolve(configPath, 'package.json');
if (!fs.existsSync(bitcoreConfigPath) || !fs.existsSync(packagePath)) {
return done(
new Error('Directory does not have a bitcore-node.json and/or package.json file.')
);
}
async.eachSeries(
modules,
function(module, next) {
// npm install <module_name> --save
addModule(configPath, module, function(err) {
if (err) {
return next(err);
}
// add module to bitcore-node.json
addConfig(bitcoreConfigPath, module, next);
});
}, done
);
}
module.exports = add;

168
lib/scaffold/create.js Normal file
View File

@ -0,0 +1,168 @@
'use strict';
var spawn = require('child_process').spawn;
var bitcore = require('bitcore');
var async = require('async');
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
var path = require('path');
var packageFile = require('../../package.json');
var mkdirp = require('mkdirp');
var fs = require('fs');
var BASE_CONFIG = {
name: 'My Node',
modules: [
'address'
],
datadir: './data',
network: 'livenet',
port: 3001
};
var version;
if (packageFile.version.match('-dev')) {
version = '^' + packageFile.lastBuild;
} else {
version = '^' + packageFile.version;
}
var BASE_PACKAGE = {
dependencies: {
'bitcore': '^' + bitcore.version,
'bitcore-node': version
}
};
var BASE_BITCOIN_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n';
/**
* Will create a directory and bitcoin.conf file for Bitcoin.
* @param {String} dataDir - The absolute path
* @param {Function} done - The callback function called when finished
*/
function createBitcoinDirectory(datadir, done) {
mkdirp(datadir, function(err) {
if (err) {
throw err;
}
try {
fs.writeFileSync(datadir + '/bitcoin.conf', BASE_BITCOIN_CONFIG);
} catch(e) {
done(e);
}
done();
});
}
/**
* Will create a base Bitcore Node configuration directory and files.
* @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 {Function} done - The callback function called when finished
*/
function createConfigDirectory(configDir, name, datadir, isGlobal, done) {
mkdirp(configDir, function(err) {
if (err) {
throw err;
}
var config = BASE_CONFIG;
config.name = name || 'Bitcore Node';
config.datadir = datadir;
var configJSON = JSON.stringify(config, null, 2);
var packageJSON = JSON.stringify(BASE_PACKAGE, null, 2);
try {
fs.writeFileSync(configDir + '/bitcore-node.json', configJSON);
if (!isGlobal) {
fs.writeFileSync(configDir + '/package.json', packageJSON);
}
} catch(e) {
done(e);
}
done();
});
}
/**
* Will setup a directory with a Bitcore Node directory, configuration file,
* bitcoin configuration, and will install all necessary dependencies.
*
* @param {Object} options
* @param {String} options.cwd - The current working directory
* @param {String} options.dirname - The name of the bitcore node configuration directory
* @param {String} options.name - The name of the bitcore node
* @param {String} options.datadir - The path to the bitcoin datadir
* @param {Function} done - A callback function called when finished
*/
function create(options, done) {
/* jshint maxstatements:20 */
$.checkArgument(_.isObject(options));
$.checkArgument(_.isFunction(done));
$.checkArgument(_.isString(options.cwd));
$.checkArgument(_.isString(options.dirname));
$.checkArgument(_.isString(options.name) || _.isUndefined(options.name));
$.checkArgument(_.isBoolean(options.isGlobal));
$.checkArgument(_.isString(options.datadir));
var cwd = options.cwd;
var dirname = options.dirname;
var name = options.name;
var datadir = options.datadir;
var isGlobal = options.isGlobal;
var absConfigDir = path.resolve(cwd, dirname);
var absDataDir = path.resolve(absConfigDir, datadir);
async.series([
function(next) {
// Setup the the bitcore-node directory and configuration
if (!fs.existsSync(absConfigDir)) {
createConfigDirectory(absConfigDir, name, datadir, isGlobal, next);
} else {
next(new Error('Directory "' + absConfigDir+ '" already exists.'));
}
},
function(next) {
// Setup the bitcoin directory and configuration
if (!fs.existsSync(absDataDir)) {
createBitcoinDirectory(absDataDir, next);
} else {
next();
}
},
function(next) {
// Install all of the necessary dependencies
if (!isGlobal) {
var npm = spawn('npm', ['install'], {cwd: absConfigDir});
npm.stdout.on('data', function (data) {
process.stdout.write(data);
});
npm.stderr.on('data', function (data) {
process.stderr.write(data);
});
npm.on('close', function (code) {
if (code !== 0) {
return next(new Error('There was an error installing dependencies.'));
} else {
return next();
}
});
} else {
next();
}
}
], done);
}
module.exports = create;

View File

@ -0,0 +1,20 @@
'use strict';
var path = require('path');
/**
* Will return the path and default bitcore-node configuration on environment variables
* or default locations.
*/
function getDefaultConfig() {
return {
path: process.cwd(),
config: {
datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'),
network: process.env.BITCORENODE_NETWORK || 'livenet',
port: process.env.BITCORENODE_PORT || 3001
}
};
}
module.exports = getDefaultConfig;

View File

@ -0,0 +1,29 @@
'use strict';
var bitcore = require('bitcore');
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
var path = require('path');
var fs = require('fs');
/**
* Will return the path and bitcore-node configuration
* @param {String} cwd - The absolute path to the current working directory
*/
function findConfig(cwd) {
$.checkArgument(_.isString(cwd), 'Argument should be a string');
$.checkArgument(path.isAbsolute(cwd), 'Argument should be an absolute path');
var directory = String(cwd);
while (!fs.existsSync(path.resolve(directory, 'bitcore-node.json'))) {
directory = path.resolve(directory, '../');
if (directory === '/') {
return false;
}
}
return {
path: directory,
config: require(path.resolve(directory, 'bitcore-node.json'))
};
}
module.exports = findConfig;

218
lib/scaffold/start.js Normal file
View File

@ -0,0 +1,218 @@
'use strict';
var path = require('path');
var socketio = require('socket.io');
var BitcoreNode = require('../node');
var chainlib = require('chainlib');
var bitcore = require('bitcore');
var _ = bitcore.deps._;
var log = chainlib.log;
log.debug = function() {};
var count = 0;
var interval = false;
function start(options) {
/* jshint maxstatements: 100 */
var modules = [];
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 module;
try {
// first try in the built-in bitcore-node modules directory
module = require(path.resolve(__dirname, '../modules/' + moduleName));
} catch(e) {
// then try loading external modules
module = require(moduleName);
}
modules.push(module);
}
}
var fullConfig = _.clone(config);
// expand to the full path
fullConfig.datadir = path.resolve(configPath, config.datadir);
// delete until modules move to the node
delete fullConfig.modules;
// load the modules
fullConfig.db = {
modules: modules
};
var node = new BitcoreNode(fullConfig);
function logSyncStatus() {
log.info(
'Sync Status: Tip:', node.chain.tip.hash,
'Height:', node.chain.tip.__height,
'Rate:', count/10, 'blocks per second'
);
}
node.on('synced', function() {
// Stop logging of sync status
clearInterval(interval);
interval = false;
logSyncStatus();
});
node.on('ready', function() {
var io = socketio(fullConfig.port);
io.on('connection', function(socket) {
var bus = node.openBus();
var methods = node.getAllAPIMethods();
var methodsMap = {};
methods.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
var args = data[3];
methodsMap[name] = {
fn: function() {
return method.apply(instance, arguments);
},
args: args
};
});
socket.on('message', function(message, socketCallback) {
if (methodsMap[message.method]) {
var params = message.params;
if(!params || !params.length) {
params = [];
}
if(params.length !== methodsMap[message.method].args) {
return socketCallback({
error: {
message: 'Expected ' + methodsMap[message.method].args + ' parameters'
}
});
}
var callback = function(err, result) {
var response = {};
if(err) {
response.error = {
message: err.toString()
};
}
if(result) {
response.result = result;
}
socketCallback(response);
};
params = params.concat(callback);
methodsMap[message.method].fn.apply(this, params);
} else {
socketCallback({
error: {
message: 'Method Not Found'
}
});
}
});
socket.on('subscribe', function(name, params) {
bus.subscribe(name, params);
});
socket.on('unsubscribe', function(name, params) {
bus.unsubscribe(name, params);
});
var events = node.getAllPublishEvents();
events.forEach(function(event) {
bus.on(event.name, function() {
if(socket.connected) {
var results = [];
for(var i = 0; i < arguments.length; i++) {
results.push(arguments[i]);
}
var params = [event.name].concat(results);
socket.emit.apply(socket, params);
}
});
});
socket.on('disconnect', function() {
bus.close();
});
});
});
node.on('error', function(err) {
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('stopping', function() {
clearInterval(interval);
});
function exitHandler(options, err) {
if (err) {
log.error('uncaught exception:', err);
if(err.stack) {
console.log(err.stack);
}
process.exit(-1);
}
if (options.sigint) {
node.stop(function(err) {
if(err) {
log.error('Failed to stop services: ' + err);
return process.exit(1);
}
log.info('Halted');
process.exit(0);
});
}
}
//catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(null, {exit:true}));
//catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, {sigint:true}));
return node;
}
module.exports = start;

View File

@ -3,7 +3,9 @@
"description": "Full node with extended capabilities using Bitcore and Bitcoin Core",
"author": "BitPay <dev@bitpay.com>",
"version": "0.2.0-dev",
"lastBuild": "0.2.0-beta.3",
"main": "./index.js",
"bin": "./cli/bitcore-node.js",
"repository": "git://github.com/bitpay/bitcore-node.git",
"homepage": "https://github.com/bitpay/bitcore-node.js",
"bugs": {
@ -46,6 +48,7 @@
"bindings": "^1.2.1",
"bitcore": "^0.13.0",
"chainlib": "^0.2.0",
"commander": "^2.8.1",
"errno": "^0.1.2",
"memdown": "^1.0.0",
"mkdirp": "0.5.0",

View File

@ -101,7 +101,7 @@ describe('Bitcoind Node', function() {
describe('#_loadBitcoinConf', function() {
it('will parse a bitcoin.conf file', function() {
var node = new Node({});
node._loadBitcoinConf({datadir: '~/.bitcoin'});
node._loadBitcoinConf({datadir: process.env.HOME + '/.bitcoin'});
should.exist(node.bitcoinConfiguration);
node.bitcoinConfiguration.should.deep.equal({
server: 1,
@ -349,7 +349,7 @@ describe('Bitcoind Node', function() {
};
var config = {
DB: DB,
datadir: '~/.bitcoin'
datadir: process.env.HOME + '/.bitcoin'
};
var node = new Node(config);
@ -363,7 +363,7 @@ describe('Bitcoind Node', function() {
};
var config = {
DB: DB,
datadir: '~/.bitcoin'
datadir: process.env.HOME + '/.bitcoin'
};
var node = new Node(config);
@ -373,7 +373,7 @@ describe('Bitcoind Node', function() {
});
it('error with unknown network', function() {
var config = {
datadir: '~/.bitcoin'
datadir: process.env.HOME + '/.bitcoin'
};
var node = new Node(config);
@ -388,7 +388,7 @@ describe('Bitcoind Node', function() {
};
var config = {
DB: DB,
datadir: '~/.bitcoin'
datadir: process.env.HOME + '/.bitcoin'
};
var node = new Node(config);

View File

@ -0,0 +1,122 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var path = require('path');
var fs = require('fs');
var proxyquire = require('proxyquire');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var add = require('../../lib/scaffold/add');
describe('#add', function() {
var basePath = path.resolve(__dirname, '..');
var testDir = path.resolve(basePath, 'temporary-test-data');
var startConfig = {
name: 'My Node',
modules: []
};
var startPackage = {};
before(function(done) {
mkdirp(testDir + '/s0/s1', function(err) {
if (err) {
throw err;
}
fs.writeFile(
testDir + '/s0/s1/bitcore-node.json',
JSON.stringify(startConfig),
function(err) {
if (err) {
throw err;
}
fs.writeFile(
testDir + '/s0/s1/package.json',
JSON.stringify(startPackage),
done
);
}
);
});
});
after(function(done) {
// cleanup testing directories
rimraf(testDir, function(err) {
if (err) {
throw err;
}
done();
});
});
describe('will modify scaffold files', function() {
it('will give an error if expected files do not exist', function(done) {
add({
path: path.resolve(testDir, 's0'),
modules: ['a', 'b', 'c']
}, function(err) {
should.exist(err);
err.message.match(/^Invalid state/);
done();
});
});
it('will receive error from `npm install`', function(done) {
var spawn = sinon.stub().returns({
stdout: {
on: sinon.stub()
},
stderr: {
on: sinon.stub()
},
on: sinon.stub().callsArgWith(1, 1)
});
var addtest = proxyquire('../../lib/scaffold/add', {
'child_process': {
spawn: spawn
}
});
addtest({
path: path.resolve(testDir, 's0/s1/'),
modules: ['a', 'b', 'c']
}, function(err) {
should.exist(err);
err.message.should.equal('There was an error installing module: a');
done();
});
});
it('will update bitcore-node.json modules', function(done) {
var spawn = sinon.stub().returns({
stdout: {
on: sinon.stub()
},
stderr: {
on: sinon.stub()
},
on: sinon.stub().callsArgWith(1, 0)
});
var addtest = proxyquire('../../lib/scaffold/add', {
'child_process': {
spawn: spawn
}
});
addtest({
path: path.resolve(testDir, 's0/s1/'),
modules: ['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']);
done();
});
});
});
});

View File

@ -0,0 +1,174 @@
'use strict';
var should = require('chai').should();
var proxyquire = require('proxyquire');
var sinon = require('sinon');
var create = proxyquire('../../lib/scaffold/create', {
'child_process': {
spawn: sinon.stub().returns({
stdout: {
on: sinon.stub()
},
stderr: {
on: sinon.stub()
},
on: function(event, cb) {
cb(0);
}
})
}
});
var fs = require('fs');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
describe('#create', function() {
var basePath = __dirname + '/../';
var testDir = basePath + 'temporary-test-data';
before(function(done) {
// setup testing directories
mkdirp(testDir, function(err) {
if (err) {
throw err;
}
mkdirp(testDir + '/.bitcoin', function(err) {
if (err) {
throw err;
}
done();
});
});
});
after(function(done) {
// cleanup testing directories
rimraf(testDir, function(err) {
if (err) {
throw err;
}
done();
});
});
it('will create scaffold files', function() {
create({
cwd: testDir,
dirname: 'mynode',
name: 'My Node 1',
isGlobal: false,
datadir: './data'
}, function(err) {
if (err) {
throw err;
}
var configPath = testDir + '/mynode/bitcore-node.json';
var packagePath = testDir + '/mynode/package.json';
var bitcoinConfig = testDir + '/mynode/data/bitcoin.conf';
should.equal(fs.existsSync(configPath), true);
should.equal(fs.existsSync(packagePath), true);
should.equal(fs.existsSync(bitcoinConfig), true);
var config = JSON.parse(fs.readFileSync(configPath));
config.name.should.equal('My Node 1');
config.modules.should.deep.equal(['address']);
config.datadir.should.equal('./data');
config.network.should.equal('livenet');
var pack = JSON.parse(fs.readFileSync(packagePath));
should.exist(pack.dependencies);
});
});
it('will error if directory already exists', function() {
create({
cwd: testDir,
dirname: 'mynode',
name: 'My Node 2',
isGlobal: false,
datadir: './data'
}, function(err) {
should.exist(err);
err.message.should.match(/^Directory/);
});
});
it('will not create bitcoin.conf if it already exists', function() {
create({
cwd: testDir,
dirname: 'mynode2',
name: 'My Node 2',
isGlobal: false,
datadir: '../.bitcoin'
}, function(err) {
if (err) {
throw err;
}
var bitcoinConfig = testDir + '/.bitcoin/bitcoin.conf';
should.equal(fs.existsSync(bitcoinConfig), false);
});
});
it('will not create a package.json if globally installed', function() {
create({
cwd: testDir,
dirname: 'mynode3',
name: 'My Node 3',
isGlobal: true,
datadir: '../.bitcoin'
}, function(err) {
if (err) {
throw err;
}
var packagePath = testDir + '/mynode3/package.json';
should.equal(fs.existsSync(packagePath), false);
});
});
it('will receieve an error from npm', function() {
var createtest = proxyquire('../../lib/scaffold/create', {
'child_process': {
spawn: sinon.stub().returns({
stdout: {
on: sinon.stub()
},
stderr: {
on: sinon.stub()
},
on: function(event, cb) {
cb(1);
}
})
}
});
createtest({
cwd: testDir,
dirname: 'mynode4',
name: 'My Node 4',
isGlobal: false,
datadir: '../.bitcoin'
}, function(err) {
should.exist(err);
err.message.should.equal('There was an error installing dependencies.');
});
});
});

View File

@ -0,0 +1,79 @@
'use strict';
var fs = require('fs');
var path = require('path');
var should = require('chai').should();
var sinon = require('sinon');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var findConfig = require('../../lib/scaffold/find-config');
describe('#findConfig', function() {
var testDir = path.resolve(__dirname, '../temporary-test-data');
var expectedConfig = {
name: 'My Node'
};
before(function(done) {
// setup testing directories
mkdirp(testDir + '/p2/p1/p0', function(err) {
if (err) {
throw err;
}
fs.writeFile(
testDir + '/p2/bitcore-node.json',
JSON.stringify(expectedConfig),
function() {
mkdirp(testDir + '/e0', function(err) {
if (err) {
throw err;
}
done();
});
}
);
});
});
after(function(done) {
// cleanup testing directories
rimraf(testDir, function(err) {
if (err) {
throw err;
}
done();
});
});
describe('will find a configuration file', function() {
it('in the current directory', function() {
var config = findConfig(path.resolve(testDir, 'p2'));
config.path.should.equal(path.resolve(testDir, 'p2'));
config.config.should.deep.equal(expectedConfig);
});
it('in a parent directory', function() {
var config = findConfig(path.resolve(testDir, 'p2/p1'));
config.path.should.equal(path.resolve(testDir, 'p2'));
config.config.should.deep.equal(expectedConfig);
});
it('recursively find in parent directories', function() {
var config = findConfig(path.resolve(testDir, 'p2/p1/p0'));
config.path.should.equal(path.resolve(testDir, 'p2'));
config.config.should.deep.equal(expectedConfig);
});
});
it('will return false if missing a configuration', function() {
var config = findConfig(path.resolve(testDir, 'e0'));
config.should.equal(false);
});
});

View File

@ -0,0 +1,39 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var AddressModule = require('../../lib/modules/address');
describe('#start', function() {
describe('will dynamically create a node from a configuration', function() {
it('require each bitcore-node module', function(done) {
var node;
var TestNode = function(options) {
options.db.modules.should.deep.equal([AddressModule]);
};
TestNode.prototype.on = sinon.stub();
TestNode.prototype.chain = {
on: sinon.stub()
};
var starttest = proxyquire('../../lib/scaffold/start', {
'../node': TestNode
});
node = starttest({
path: __dirname,
config: {
modules: [
'address'
],
datadir: './data'
}
});
node.should.be.instanceof(TestNode);
done();
});
});
});