Merge pull request #97 from pnagurny/startnode

Simple RPC/websockets API
This commit is contained in:
Braydon Fuller 2015-08-04 17:33:35 -04:00
commit c21ff322b7
10 changed files with 312 additions and 3 deletions

134
bin/start.js Normal file
View File

@ -0,0 +1,134 @@
'use strict';
var BitcoinNode = require('..').Node;
var chainlib = require('chainlib');
var socketio = require('socket.io');
var log = chainlib.log;
log.debug = function() {};
var configuration = {
datadir: process.env.BITCOINDJS_DIR || '~/.bitcoin',
network: process.env.BITCOINDJS_NETWORK || 'livenet',
port: 3000
};
var node = new BitcoinNode(configuration);
var count = 0;
var interval;
node.on('ready', function() {
var io = socketio(configuration.port);
interval = setInterval(function() {
log.info('Sync Status: Tip:', node.chain.tip.hash, 'Height:', node.chain.tip.__height, 'Rate:', count/10, 'blocks per second');
count = 0;
}, 10000);
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: 'Expected ' + methodsMap[message.method].args + ' parameters'
});
}
var callback = function(err, result) {
var response = {};
if(err) {
response.error = {
message: err.toString()
};
}
if(result) {
if(result.toJSON) {
response.result = result.toJSON();
} else {
response.result = result;
}
}
socketCallback(response);
};
params = params.concat(callback);
methodsMap[message.method].fn.apply(this, params);
} else {
socketCallback({
error: '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++) {
if(arguments[i].toJSON) {
results.push(arguments[i].toJSON());
} else {
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++;
});

44
example/client.js Normal file
View File

@ -0,0 +1,44 @@
'use strict';
var socket = require('socket.io-client')('http://localhost:3000');
socket.on('connect', function(){
console.log('connected');
});
socket.on('disconnect', function(){
console.log('disconnected');
});
var message = {
method: 'getOutputs',
params: ['1HTxCVrXuthad6YW5895K98XmVsdMvvBSw', true]
};
socket.send(message, function(response) {
if(response.error) {
console.log('Error', response.error);
return;
}
console.log(response.result);
});
var message2 = {
method: 'getTransaction',
params: ['4f793f67fc7465f14fa3a8d3727fa7d133cdb2f298234548b94a5f08b6f4103e', true]
};
socket.send(message2, function(response) {
if(response.error) {
console.log('Error', response.error);
return;
}
console.log(response.result);
});
socket.on('transaction', function(address, block) {
console.log(address, block);
});
socket.emit('subscribe', 'transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']);

View File

@ -60,6 +60,10 @@ Block.prototype.toObject = function() {
};
};
Block.prototype.toJSON = function() {
return JSON.stringify(this.toObject());
};
Block.prototype.headerToBufferWriter = function(bw) {
/* jshint maxstatements: 20 */

View File

@ -40,4 +40,16 @@ Bus.prototype.unsubscribe = function(name) {
}
};
Bus.prototype.close = function() {
// Unsubscribe from all events
for (var i = 0; i < this.db.modules.length; i++) {
var mod = this.db.modules[i];
var events = mod.getPublishEvents();
for (var j = 0; j < events.length; j++) {
var event = events[j];
event.unsubscribe.call(event.scope, this);
}
}
};
module.exports = Bus;

View File

@ -9,6 +9,7 @@ var levelup = chainlib.deps.levelup;
var errors = chainlib.errors;
var bitcore = require('bitcore');
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
var EventEmitter = require('events').EventEmitter;
var PublicKey = bitcore.PublicKey;
var Address = bitcore.Address;
@ -171,7 +172,11 @@ AddressModule.prototype.subscribe = function(name, emitter, addresses) {
AddressModule.prototype.unsubscribe = 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');
$.checkArgument(Array.isArray(addresses) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined');
if(!addresses) {
return this.unsubscribeAll(name, emitter);
}
for(var i = 0; i < addresses.length; i++) {
if(this.subscriptions[name][addresses[i]]) {
@ -184,6 +189,18 @@ AddressModule.prototype.unsubscribe = function(name, emitter, addresses) {
}
};
AddressModule.prototype.unsubscribeAll = function(name, emitter) {
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
for(var address in this.subscriptions[name]) {
var emitters = this.subscriptions[name][address];
var index = emitters.indexOf(emitter);
if(index > -1) {
emitters.splice(index, 1);
}
}
};
AddressModule.prototype.getBalance = function(address, queryMempool, callback) {
this.getUnspentOutputs(address, queryMempool, function(err, outputs) {
if(err) {

View File

@ -29,6 +29,24 @@ Node.prototype.openBus = function() {
return new Bus({db: this.db});
};
Node.prototype.getAllAPIMethods = function() {
var methods = this.db.getAPIMethods();
for (var i = 0; i < this.db.modules.length; i++) {
var mod = this.db.modules[i];
methods = methods.concat(mod.getAPIMethods());
}
return methods;
};
Node.prototype.getAllPublishEvents = function() {
var events = [];
for (var i = 0; i < this.db.modules.length; i++) {
var mod = this.db.modules[i];
events = events.concat(mod.getPublishEvents());
}
return events;
};
Node.prototype._loadConfiguration = function(config) {
var self = this;
this._loadBitcoinConf(config);

View File

@ -29,7 +29,7 @@
"scripts": {
"preinstall": "./bin/build-libbitcoind",
"install": "./bin/build-bindings",
"start": "node example",
"start": "node bin/start.js",
"test": "NODE_ENV=test mocha --recursive",
"coverage": "istanbul cover _mocha -- --recursive",
"libbitcoind": "node bin/start-libbitcoind.js"
@ -45,7 +45,8 @@
"errno": "^0.1.2",
"memdown": "^1.0.0",
"mkdirp": "0.5.0",
"nan": "1.3.0"
"nan": "1.3.0",
"socket.io": "^1.3.6"
},
"devDependencies": {
"benchmark": "1.0.0",

View File

@ -58,4 +58,30 @@ describe('Bus', function() {
});
});
describe('#close', function() {
it('will unsubscribe from all events', function() {
var unsubscribe = sinon.spy();
var db = {
modules: [
{
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
unsubscribe: unsubscribe
}
])
}
]
};
var bus = new Bus({db: db});
bus.close();
unsubscribe.callCount.should.equal(1);
unsubscribe.args[0].length.should.equal(1);
unsubscribe.args[0][0].should.equal(bus);
});
});
});

View File

@ -295,6 +295,20 @@ describe('AddressModule', function() {
am.unsubscribe(name, emitter, [address]);
am.subscriptions.balance[address].should.deep.equal([emitter2]);
});
it('should unsubscribe from all addresses if no addresses are specified', function() {
var am = new AddressModule({});
var emitter = new EventEmitter();
var emitter2 = new EventEmitter();
am.subscriptions.balance = {
'1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W': [emitter, emitter2],
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter2, emitter]
};
am.unsubscribe('balance', emitter);
am.subscriptions.balance.should.deep.equal({
'1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W': [emitter2],
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter2]
});
});
});
describe('#getBalance', function() {

View File

@ -40,6 +40,45 @@ describe('Bitcoind Node', function() {
bus.db.should.equal(db);
});
});
describe('#getAllAPIMethods', function() {
it('should return db methods and modules methods', function() {
var node = new Node({});
var db = {
getAPIMethods: sinon.stub().returns(['db1', 'db2']),
modules: [
{
getAPIMethods: sinon.stub().returns(['mda1', 'mda2'])
},
{
getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2'])
}
]
};
node.db = db;
var methods = node.getAllAPIMethods();
methods.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']);
});
});
describe('#getAllPublishEvents', function() {
it('should return modules publish events', function() {
var node = new Node({});
var db = {
modules: [
{
getPublishEvents: sinon.stub().returns(['mda1', 'mda2'])
},
{
getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2'])
}
]
};
node.db = db;
var events = node.getAllPublishEvents();
events.should.deep.equal(['mda1', 'mda2', 'mdb1', 'mdb2']);
});
});
describe('#_loadConfiguration', function() {
it('should call the necessary methods', function() {
var node = new Node({});