diff --git a/lib/transport/index.js b/lib/transport/index.js index f1cbcbf..f219f81 100644 --- a/lib/transport/index.js +++ b/lib/transport/index.js @@ -2,7 +2,8 @@ * @namespace Transport */ module.exports = { - Peer: require('./peer'), Messages: require('./messages'), - Pool: require('./pool') + Peer: require('./peer'), + Pool: require('./pool'), + RPC: require('./rpc') }; diff --git a/lib/transport/rpc.js b/lib/transport/rpc.js index 3d1c39f..6fcee3f 100644 --- a/lib/transport/rpc.js +++ b/lib/transport/rpc.js @@ -1,30 +1,118 @@ -// RpcClient.js -// MIT/X11-like license. See LICENSE.txt. -// Copyright 2013 BitPay, Inc. -// +'use strict'; + var http = require('http'); var https = require('https'); -var log = require('../util/log'); -function RpcClient(opts) { +/** + * A JSON RPC client for bitcoind. An instances of RPC connects to a bitcoind + * server and enables simple and batch RPC calls. + * + * @example + * + * var client = new RPC('user', 'pass'); + * client.getInfo(function(err, info) { + * // do something with the info + * }); + * + * @param {String} user - username used to connect bitcoind + * @param {String} password - password used to connect bitcoind + * @param {Object} opts - Connection options: host, port, secure, disableAgent, rejectUnauthorized + * @returns {RPC} + * @constructor + */ +function RPC(user, password, opts) { + if (!(this instanceof RPC)) { + return new RPC(user, password, opts); + } + + this.user = user; + this.pass = password; + opts = opts || {}; this.host = opts.host || '127.0.0.1'; this.port = opts.port || 8332; - this.user = opts.user || 'user'; - this.pass = opts.pass || 'pass'; - this.protocol = (opts.protocol == 'http') ? http : https; + + this.secure = typeof opts.secure === 'undefined' ? true : opts.secure; + this._client = opts.secure ? https : http; + this.batchedCalls = null; this.disableAgent = opts.disableAgent || false; this.rejectUnauthorized = opts.rejectUnauthorized || false; } -RpcClient.prototype.batch = function(batchCallback, resultCallback) { +/** + * Allows to excecute RPC calls in batch. + * + * @param {Function} batchCallback - Function that makes all calls to be excecuted in bach + * @param {Function} resultCallbak - Function to be called on result + */ +RPC.prototype.batch = function(batchCallback, resultCallback) { this.batchedCalls = []; batchCallback(); - rpc.call(this, this.batchedCalls, resultCallback); + this._request(this.batchedCalls, resultCallback); this.batchedCalls = null; } +/** + * Internal function to make an RPC call + * + * @param {Object} request - Object to be serialized and sent to bitcoind + * @param {Function} callbak - Function to be called on result + */ +RPC.prototype._request = function(request, callback) { + var self = this; + + var request = JSON.stringify(request); + var auth = Buffer(self.user + ':' + self.pass).toString('base64'); + + var options = { + host: self.host, + path: '/', + method: 'POST', + port: self.port, + rejectUnauthorized: self.rejectUnauthorized, + agent: self.disableAgent ? false : undefined + }; + + var req = this._client.request(options, function(res) { + var buf = ''; + res.on('data', function(data) { + buf += data; + }); + + res.on('end', function() { + if (res.statusCode == 401) { + var error = new Error('bitcoin JSON-RPC connection rejected: 401 unauthorized'); + return callback(error); + } + + if (res.statusCode == 403) { + var error = new Error('bitcoin JSON-RPC connection rejected: 403 forbidden'); + return callback(error); + } + + try { + var parsedBuf = JSON.parse(buf); + } catch (e) { + return callback(e); + } + + callback(parsedBuf.error, parsedBuf); + }); + }); + + req.on('error', function(e) { + var err = new Error('Could not connect to bitcoin via RPC at host: ' + self.host + ' port: ' + self.port + ' Error: ' + e.message); + callback(err); + }); + + req.setHeader('Content-Length', request.length); + req.setHeader('Content-Type', 'application/json'); + req.setHeader('Authorization', 'Basic ' + auth); + req.write(request); + req.end(); +}; + var callspec = { addMultiSigAddress: '', addNode: '', @@ -94,11 +182,12 @@ var callspec = { walletPassphraseChange: '', }; + var slice = function(arr, start, end) { return Array.prototype.slice.call(arr, start, end); }; -function generateRPCMethods(constructor, apiCalls, rpc) { +function generateRPCMethods(constructor, apiCalls) { function createRPCMethod(methodName, argMap) { return function() { var limit = arguments.length - 1; @@ -113,7 +202,7 @@ function generateRPCMethods(constructor, apiCalls, rpc) { params: slice(arguments) }); } else { - rpc.call(this, { + this._request({ method: methodName, params: slice(arguments, 0, arguments.length - 1) }, arguments[arguments.length - 1]); @@ -153,71 +242,6 @@ function generateRPCMethods(constructor, apiCalls, rpc) { } } -function rpc(request, callback) { - var self = this; - var request; - request = JSON.stringify(request); - var auth = Buffer(self.user + ':' + self.pass).toString('base64'); - - var options = { - host: self.host, - path: '/', - method: 'POST', - port: self.port, - rejectUnauthorized: self.rejectUnauthorized, - agent: self.disableAgent ? false : undefined, - }; - if (self.httpOptions) { - for (var k in self.httpOptions) { - options[k] = self.httpOptions[k]; - } - } - var err = null; - var req = this.protocol.request(options, function(res) { - - var buf = ''; - res.on('data', function(data) { - buf += data; - }); - res.on('end', function() { - if (res.statusCode == 401) { - callback(new Error('bitcoin JSON-RPC connection rejected: 401 unauthorized')); - return; - } - if (res.statusCode == 403) { - callback(new Error('bitcoin JSON-RPC connection rejected: 403 forbidden')); - return; - } - - if (err) { - callback(err); - return; - } - try { - var parsedBuf = JSON.parse(buf); - } catch (e) { - log.err(e.stack); - log.err(buf); - log.err('HTTP Status code:' + res.statusCode); - callback(e); - return; - } - callback(parsedBuf.error, parsedBuf); - }); - }); - req.on('error', function(e) { - var err = new Error('Could not connect to bitcoin via RPC at host: ' + self.host + ' port: ' + self.port + ' Error: ' + e.message); - log.err(err); - callback(err); - }); - - req.setHeader('Content-Length', request.length); - req.setHeader('Content-Type', 'application/json'); - req.setHeader('Authorization', 'Basic ' + auth); - req.write(request); - req.end(); -}; - -generateRPCMethods(RpcClient, callspec, rpc); +generateRPCMethods(RPC, callspec); -module.exports = RpcClient; +module.exports = RPC; diff --git a/test/transport/rpc.js b/test/transport/rpc.js index 13ed435..0b29266 100644 --- a/test/transport/rpc.js +++ b/test/transport/rpc.js @@ -1,28 +1,62 @@ 'use strict'; -var chai = chai || require('chai'); -var bitcore = bitcore || require('../bitcore'); - +var chai = require('chai'); var should = chai.should(); -var RpcClientModule = bitcore.RpcClient; -var RpcClient; - RpcClient = RpcClientModule; +var bitcore = require('../..'); +var RPC = bitcore.transport.RPC; -describe('RpcClient', function() { - it('should initialze the main object', function() { - should.exist(RpcClientModule); - }); - it('should be able to create class', function() { - should.exist(RpcClient); - }); +describe('RPC', function() { it('should be able to create instance', function() { - var s = new RpcClient(); - should.exist(s); + var client = new RPC('user', 'pass'); + should.exist(client); }); -}); + it('should set default config', function() { + var client = new RPC('user', 'pass'); + client.user.should.be.equal('user'); + client.pass.should.be.equal('pass'); + client.host.should.be.equal('127.0.0.1'); + client.port.should.be.equal(8332); + client.secure.should.be.equal(true); + client.disableAgent.should.be.equal(false); + client.rejectUnauthorized.should.be.equal(false); + }); + it('should allow setting custom host and port', function() { + var client = new RPC('user', 'pass', { + host: 'localhost', + port: 18332 + }); + client.host.should.be.equal('localhost'); + client.port.should.be.equal(18332); + }); + + it('should honor request options', function() { + var client = new RPC('user', 'pass', { + host: 'localhost', + port: 18332, + rejectUnauthorized: true, + disableAgent: true + }); + + client._client = {}; + client._client.request = function(options, callback) { + options.host.should.be.equal('localhost'); + options.port.should.be.equal(18332); + options.rejectUnauthorized.should.be.equal(true); + options.agent.should.be.false; + return { + on: function() {}, + setHeader: function() {}, + write: function() {}, + end: function() {} + }; + }; + + client._request({}, function() {}); + }); +});