diff --git a/lib/transport/index.js b/lib/transport/index.js index f1cbcbf1b..f219f81bc 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 new file mode 100644 index 000000000..6fcee3f5c --- /dev/null +++ b/lib/transport/rpc.js @@ -0,0 +1,247 @@ +'use strict'; + +var http = require('http'); +var https = require('https'); + +/** + * 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.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; +} + +/** + * 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(); + 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: '', + backupWallet: '', + createMultiSig: '', + createRawTransaction: '', + decodeRawTransaction: '', + dumpPrivKey: '', + encryptWallet: '', + getAccount: '', + getAccountAddress: 'str', + getAddedNodeInfo: '', + getAddressesByAccount: '', + getBalance: 'str int', + getBestBlockHash: '', + getBlock: '', + getBlockCount: '', + getBlockHash: 'int', + getBlockNumber: '', + getBlockTemplate: '', + getConnectionCount: '', + getDifficulty: '', + getGenerate: '', + getHashesPerSec: '', + getInfo: '', + getMemoryPool: '', + getMiningInfo: '', + getNewAddress: '', + getPeerInfo: '', + getRawMemPool: '', + getRawTransaction: 'str int', + getReceivedByAccount: 'str int', + getReceivedByAddress: 'str int', + getTransaction: '', + getTxOut: 'str int bool', + getTxOutSetInfo: '', + getWork: '', + help: '', + importAddress: 'str str bool', + importPrivKey: 'str str bool', + keyPoolRefill: '', + listAccounts: 'int', + listAddressGroupings: '', + listReceivedByAccount: 'int bool', + listReceivedByAddress: 'int bool', + listSinceBlock: 'str int', + listTransactions: 'str int int', + listUnspent: 'int int', + listLockUnspent: 'bool', + lockUnspent: '', + move: 'str str float int str', + sendFrom: 'str str float int str str', + sendMany: 'str str int str', //not sure this is will work + sendRawTransaction: '', + sendToAddress: 'str float str str', + setAccount: '', + setGenerate: 'bool int', + setTxFee: 'float', + signMessage: '', + signRawTransaction: '', + stop: '', + submitBlock: '', + validateAddress: '', + verifyMessage: '', + walletLock: '', + walletPassPhrase: 'string int', + walletPassphraseChange: '', +}; + + +var slice = function(arr, start, end) { + return Array.prototype.slice.call(arr, start, end); +}; + +function generateRPCMethods(constructor, apiCalls) { + function createRPCMethod(methodName, argMap) { + return function() { + var limit = arguments.length - 1; + if (this.batchedCalls) var limit = arguments.length; + for (var i = 0; i < limit; i++) { + if (argMap[i]) arguments[i] = argMap[i](arguments[i]); + }; + if (this.batchedCalls) { + this.batchedCalls.push({ + jsonrpc: '2.0', + method: methodName, + params: slice(arguments) + }); + } else { + this._request({ + method: methodName, + params: slice(arguments, 0, arguments.length - 1) + }, arguments[arguments.length - 1]); + } + }; + }; + + var types = { + str: function(arg) { + return arg.toString(); + }, + int: function(arg) { + return parseFloat(arg); + }, + float: function(arg) { + return parseFloat(arg); + }, + bool: function(arg) { + return (arg === true || arg == '1' || arg == 'true' || arg.toString().toLowerCase() == 'true'); + }, + }; + + for (var k in apiCalls) { + if (apiCalls.hasOwnProperty(k)) { + var spec = apiCalls[k].split(' '); + for (var i = 0; i < spec.length; i++) { + if (types[spec[i]]) { + spec[i] = types[spec[i]]; + } else { + spec[i] = types.string; + } + } + var methodName = k.toLowerCase(); + constructor.prototype[k] = createRPCMethod(methodName, spec); + constructor.prototype[methodName] = constructor.prototype[k]; + } + } +} + +generateRPCMethods(RPC, callspec); + +module.exports = RPC; diff --git a/test/transport/rpc.js b/test/transport/rpc.js new file mode 100644 index 000000000..0b292666a --- /dev/null +++ b/test/transport/rpc.js @@ -0,0 +1,62 @@ +'use strict'; + +var chai = require('chai'); +var should = chai.should(); + +var bitcore = require('../..'); +var RPC = bitcore.transport.RPC; + +describe('RPC', function() { + it('should be able to create instance', function() { + 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() {}); + }); + +});