diff --git a/bin/start.js b/bin/start.js new file mode 100644 index 00000000..24d2a122 --- /dev/null +++ b/bin/start.js @@ -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++; +}); diff --git a/example/client.js b/example/client.js new file mode 100644 index 00000000..88a1cf9c --- /dev/null +++ b/example/client.js @@ -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']); \ No newline at end of file diff --git a/lib/block.js b/lib/block.js index 6e5e9c13..e8499c31 100644 --- a/lib/block.js +++ b/lib/block.js @@ -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 */ diff --git a/lib/bus.js b/lib/bus.js index aaf1da33..8772f3f8 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -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; diff --git a/lib/modules/address.js b/lib/modules/address.js index 7069cd65..7b9464cb 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -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) { diff --git a/lib/node.js b/lib/node.js index 42d0cf4b..9b88a9e2 100644 --- a/lib/node.js +++ b/lib/node.js @@ -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); diff --git a/package.json b/package.json index a5cd18da..e7418d4b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/bus.unit.js b/test/bus.unit.js index 3dc09dc5..a0d4f249 100644 --- a/test/bus.unit.js +++ b/test/bus.unit.js @@ -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); + }); + }); + }); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index edf0c888..83fed199 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -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() { diff --git a/test/node.unit.js b/test/node.unit.js index f65d796f..438fae88 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -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({});