From 598cf64a5f6f31643bbbe2a92b883695436dc346 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Wed, 29 Jul 2015 20:34:44 -0400 Subject: [PATCH 1/4] Start a node and expose API methods and events over a socket. --- bin/start-node.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++ lib/node.js | 18 +++++++++++ package.json | 6 ++-- 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 bin/start-node.js diff --git a/bin/start-node.js b/bin/start-node.js new file mode 100644 index 00000000..bfcafcc3 --- /dev/null +++ b/bin/start-node.js @@ -0,0 +1,78 @@ +'use strict'; + +var BitcoinNode = require('..').Node; +var chainlib = require('chainlib'); +var io = require('socket.io'); +var log = chainlib.log; +log.debug = function() {}; + +var configuration = { + datadir: process.env.BITCOINDJS_DIR || '~/.bitcoin', + network: process.env.BITCOINDJS_NETWORK || 'livenet' +}; + +var node = new BitcoinNode(configuration); + +var count = 0; +var interval; + +node.on('ready', function() { + + 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]; + methodsMap[name] = function() { + return method.apply(instance, arguments); + }; + }); + + socket.on('message', function(message) { + if (methodsMap[message.command]) { + methodsMap[message.command](message.params); + } + }); + + 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(data) { + socket.emit(event.name, data); + }); + }); + + socket.on('disconnect', function() { + bus.close(); + }); + + }); + +}); + +node.on('error', function(err) { + log.error(err); +}); + +node.chain.on('addblock', function(block) { + count++; +}); diff --git a/lib/node.js b/lib/node.js index 00a929bd..7c82de52 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 = []; + 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..466d302f 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,9 @@ "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", + "tiny": "0.0.10" }, "devDependencies": { "benchmark": "1.0.0", From a2962dc7f331d7289160f130a5cbea9cc0472788 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Mon, 3 Aug 2015 16:35:38 -0400 Subject: [PATCH 2/4] get rpc over socket io to work --- bin/client.js | 24 +++++++++++++++++++++++ bin/start-node.js | 50 +++++++++++++++++++++++++++++++++++++++++------ lib/bus.js | 4 ++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 bin/client.js diff --git a/bin/client.js b/bin/client.js new file mode 100644 index 00000000..30b61c88 --- /dev/null +++ b/bin/client.js @@ -0,0 +1,24 @@ +'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 = { + command: 'getOutputs', + params: ['1HTxCVrXuthad6YW5895K98XmVsdMvvBSw', true] +}; + +socket.emit('message', message, function(response) { + if(response.error) { + console.log('Error', response.error); + return; + } + + console.log(response.result); +}); \ No newline at end of file diff --git a/bin/start-node.js b/bin/start-node.js index bfcafcc3..3f18e8fc 100644 --- a/bin/start-node.js +++ b/bin/start-node.js @@ -2,7 +2,7 @@ var BitcoinNode = require('..').Node; var chainlib = require('chainlib'); -var io = require('socket.io'); +var io = require('socket.io')(3000); var log = chainlib.log; log.debug = function() {}; @@ -34,14 +34,50 @@ node.on('ready', function() { var name = data[0]; var instance = data[1]; var method = data[2]; - methodsMap[name] = function() { - return method.apply(instance, arguments); + var args = data[3]; + methodsMap[name] = { + fn: function() { + return method.apply(instance, arguments); + }, + args: args }; }); - socket.on('message', function(message) { + socket.on('message', function(message, socketCallback) { if (methodsMap[message.command]) { - methodsMap[message.command](message.params); + var params = message.params; + + if(!params || !params.length) { + params = []; + } + + if(params.length !== methodsMap[message.command].args) { + return socketCallback({ + error: 'Expected ' + methodsMap[message.command].args + ' parameters' + }); + } + + var callback = function(err, result) { + console.log('callback called'); + console.log(err, result); + var response = {}; + if(err) { + response.error = err; + } + + if(result) { + response.result = result; + } + + socketCallback(response); + }; + + params = params.concat(callback); + methodsMap[message.command].fn.apply(this, params); + } else { + socketCallback({ + error: 'Method Not Found' + }); } }); @@ -57,7 +93,9 @@ node.on('ready', function() { events.forEach(function(event) { bus.on(event.name, function(data) { - socket.emit(event.name, data); + if(socket.connected) { + socket.emit(event.name, data); + } }); }); diff --git a/lib/bus.js b/lib/bus.js index aaf1da33..d1d0ecca 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -40,4 +40,8 @@ Bus.prototype.unsubscribe = function(name) { } }; +Bus.prototype.close = function() { + // TODO Unsubscribe from all events +}; + module.exports = Bus; From e95d4c865f70fc769868bea773acf7ddf2bbd047 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Tue, 4 Aug 2015 16:34:53 -0400 Subject: [PATCH 3/4] finish getting everything to work. add tests --- bin/client.js | 24 ------------------ bin/{start-node.js => start.js} | 30 ++++++++++++++++------ example/client.js | 44 +++++++++++++++++++++++++++++++++ lib/block.js | 4 +++ lib/bus.js | 10 +++++++- lib/modules/address.js | 17 ++++++++++++- lib/node.js | 2 +- test/bus.unit.js | 26 +++++++++++++++++++ test/modules/address.unit.js | 14 +++++++++++ test/node.unit.js | 39 +++++++++++++++++++++++++++++ 10 files changed, 176 insertions(+), 34 deletions(-) delete mode 100644 bin/client.js rename bin/{start-node.js => start.js} (77%) create mode 100644 example/client.js diff --git a/bin/client.js b/bin/client.js deleted file mode 100644 index 30b61c88..00000000 --- a/bin/client.js +++ /dev/null @@ -1,24 +0,0 @@ -'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 = { - command: 'getOutputs', - params: ['1HTxCVrXuthad6YW5895K98XmVsdMvvBSw', true] -}; - -socket.emit('message', message, function(response) { - if(response.error) { - console.log('Error', response.error); - return; - } - - console.log(response.result); -}); \ No newline at end of file diff --git a/bin/start-node.js b/bin/start.js similarity index 77% rename from bin/start-node.js rename to bin/start.js index 3f18e8fc..0c4f6204 100644 --- a/bin/start-node.js +++ b/bin/start.js @@ -2,13 +2,14 @@ var BitcoinNode = require('..').Node; var chainlib = require('chainlib'); -var io = require('socket.io')(3000); +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' + network: process.env.BITCOINDJS_NETWORK || 'livenet', + port: 3000 }; var node = new BitcoinNode(configuration); @@ -18,6 +19,8 @@ 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; @@ -58,15 +61,17 @@ node.on('ready', function() { } var callback = function(err, result) { - console.log('callback called'); - console.log(err, result); var response = {}; if(err) { response.error = err; } if(result) { - response.result = result; + if(result.toJSON) { + response.result = result.toJSON(); + } else { + response.result = result; + } } socketCallback(response); @@ -92,9 +97,20 @@ node.on('ready', function() { var events = node.getAllPublishEvents(); events.forEach(function(event) { - bus.on(event.name, function(data) { + bus.on(event.name, function() { if(socket.connected) { - socket.emit(event.name, data); + 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); } }); }); diff --git a/example/client.js b/example/client.js new file mode 100644 index 00000000..2acb35b5 --- /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 = { + command: 'getOutputs', + params: ['1HTxCVrXuthad6YW5895K98XmVsdMvvBSw', true] +}; + +socket.send(message, function(response) { + if(response.error) { + console.log('Error', response.error); + return; + } + + console.log(response.result); +}); + +var message2 = { + command: '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 d1d0ecca..8772f3f8 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -41,7 +41,15 @@ Bus.prototype.unsubscribe = function(name) { }; Bus.prototype.close = function() { - // TODO Unsubscribe from all events + // 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 c4a582f8..93863b21 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -172,7 +172,10 @@ 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'); + + if(!addresses) { + return this.unsubscribeAll(name, emitter); + } for(var i = 0; i < addresses.length; i++) { if(this.subscriptions[name][addresses[i]]) { @@ -185,6 +188,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 7c82de52..16d9fb43 100644 --- a/lib/node.js +++ b/lib/node.js @@ -30,7 +30,7 @@ Node.prototype.openBus = function() { }; Node.prototype.getAllAPIMethods = function() { - var methods = []; + 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()); 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 71c32478..03482823 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 2417f1dc..efab912d 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({}); From 10843182c122ed726764ff1d0aab48896c16766e Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Tue, 4 Aug 2015 17:04:25 -0400 Subject: [PATCH 4/4] fixes --- bin/start.js | 12 +++++++----- example/client.js | 4 ++-- lib/modules/address.js | 2 ++ package.json | 3 +-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bin/start.js b/bin/start.js index 0c4f6204..24d2a122 100644 --- a/bin/start.js +++ b/bin/start.js @@ -47,23 +47,25 @@ node.on('ready', function() { }); socket.on('message', function(message, socketCallback) { - if (methodsMap[message.command]) { + if (methodsMap[message.method]) { var params = message.params; if(!params || !params.length) { params = []; } - if(params.length !== methodsMap[message.command].args) { + if(params.length !== methodsMap[message.method].args) { return socketCallback({ - error: 'Expected ' + methodsMap[message.command].args + ' parameters' + error: 'Expected ' + methodsMap[message.method].args + ' parameters' }); } var callback = function(err, result) { var response = {}; if(err) { - response.error = err; + response.error = { + message: err.toString() + }; } if(result) { @@ -78,7 +80,7 @@ node.on('ready', function() { }; params = params.concat(callback); - methodsMap[message.command].fn.apply(this, params); + methodsMap[message.method].fn.apply(this, params); } else { socketCallback({ error: 'Method Not Found' diff --git a/example/client.js b/example/client.js index 2acb35b5..88a1cf9c 100644 --- a/example/client.js +++ b/example/client.js @@ -10,7 +10,7 @@ socket.on('disconnect', function(){ }); var message = { - command: 'getOutputs', + method: 'getOutputs', params: ['1HTxCVrXuthad6YW5895K98XmVsdMvvBSw', true] }; @@ -24,7 +24,7 @@ socket.send(message, function(response) { }); var message2 = { - command: 'getTransaction', + method: 'getTransaction', params: ['4f793f67fc7465f14fa3a8d3727fa7d133cdb2f298234548b94a5f08b6f4103e', true] }; diff --git a/lib/modules/address.js b/lib/modules/address.js index 93863b21..d20e8bab 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; @@ -172,6 +173,7 @@ 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) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined'); if(!addresses) { return this.unsubscribeAll(name, emitter); diff --git a/package.json b/package.json index 466d302f..e7418d4b 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,7 @@ "memdown": "^1.0.0", "mkdirp": "0.5.0", "nan": "1.3.0", - "socket.io": "^1.3.6", - "tiny": "0.0.10" + "socket.io": "^1.3.6" }, "devDependencies": { "benchmark": "1.0.0",