http: make rpc more hookable.

This commit is contained in:
Christopher Jeffrey 2017-03-09 18:15:35 -08:00
parent a47316c264
commit 59e9ef277a
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
14 changed files with 1133 additions and 1332 deletions

View File

@ -401,6 +401,36 @@ HTTPBase.prototype._readBody = function _readBody(req, enc, opt, resolve, reject
req.on('end', onEnd);
};
/**
* JSON rpc middleware.
* @param {RPCBase} rpc
* @returns {Function}
*/
HTTPBase.prototype.jsonRPC = function jsonRPC(rpc) {
return co(function* (req, res) {
var json;
if (req.method !== 'POST')
return;
if (req.pathname !== '/')
return;
if (typeof req.body.method !== 'string')
return;
json = yield rpc.call(req.body, req.query);
json = JSON.stringify(json);
json += '\n';
res.setHeader('X-Long-Polling', '/?longpoll=1');
res.send(200, json, 'json');
});
};
/**
* Handle mount stack.
* @private

File diff suppressed because it is too large Load Diff

226
lib/http/rpcbase.js Normal file
View File

@ -0,0 +1,226 @@
/*!
* rpcbase.js - json rpc for bcoin.
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var util = require('../utils/util');
var co = require('../utils/co');
var Lock = require('../utils/lock');
var Logger = require('../node/logger');
/**
* JSON RPC
* @alias module:http.RPCBase
* @constructor
*/
function RPCBase() {
if (!(this instanceof RPCBase))
return new RPCBase();
EventEmitter.call(this);
this.logger = Logger.global;
this.calls = {};
this.mounts = [];
this.locker = new Lock();
}
util.inherits(RPCBase, EventEmitter);
/**
* Magic string for signing.
* @const {String}
* @default
*/
RPCBase.MAGIC_STRING = 'Bitcoin Signed Message:\n';
/**
* Execute batched RPC calls.
* @param {Object|Object[]} body
* @param {Object} query
* @returns {Promise}
*/
RPCBase.prototype.call = co(function* call(body, query) {
var cmds = body;
var out = [];
var array = true;
var i, cmd, result;
if (!Array.isArray(cmds)) {
cmds = [cmds];
array = false;
}
for (i = 0; i < cmds.length; i++) {
cmd = cmds[i];
assert(cmd && typeof cmd === 'object', 'Command must be an object.');
assert(typeof cmd.method === 'string', 'Method must be a string.');
if (!cmd.params)
cmd.params = [];
assert(Array.isArray(cmd.params), 'Params must be an array.');
if (!cmd.id)
cmd.id = 0;
assert(typeof cmd.id === 'number', 'ID must be a number.');
}
for (i = 0; i < cmds.length; i++) {
cmd = cmds[i];
if (cmd.method !== 'getwork'
&& cmd.method !== 'getblocktemplate'
&& cmd.method !== 'getbestblockhash') {
this.logger.debug('Handling RPC call: %s.', cmd.method);
if (cmd.method !== 'submitblock'
&& cmd.method !== 'getmemorypool') {
this.logger.debug(cmd.params);
}
}
if (cmd.method === 'getwork') {
if (query.longpoll)
cmd.method = 'getworklp';
}
try {
result = yield this.execute(cmd);
} catch (err) {
if (err.type === 'RPCError') {
out.push({
result: null,
error: {
message: err.message,
code: -1
},
id: cmd.id
});
continue;
}
this.logger.error(err);
out.push({
result: null,
error: {
message: err.message,
code: 1
},
id: cmd.id
});
continue;
}
if (result === undefined)
result = null;
out.push({
result: result,
error: null,
id: cmd.id
});
}
if (!array)
out = out[0];
return out;
});
/**
* Execute an RPC call.
* @private
* @param {Object} json
* @param {Boolean} help
* @returns {Promise}
*/
RPCBase.prototype.execute = co(function* execute(json, help) {
var func = this.calls[json.method];
var i, mount;
if (!func) {
for (i = 0; i < this.mounts.length; i++) {
mount = this.mounts[i];
if (mount.call[json.method])
return yield mount.execute(json, help);
}
throw new RPCError('Method not found: ' + json.method + '.');
}
return yield func.call(this, json.params, help);
});
/**
* Add a custom RPC call.
* @param {String} name
* @param {Function} func
*/
RPCBase.prototype.add = function add(name, func) {
assert(typeof func === 'function', 'Handler must be a function.');
assert(!this.calls[name], 'Duplicate RPC call.');
this.calls[name] = func;
};
/**
* Mount another RPC object.
* @param {Object} rpc
*/
RPCBase.prototype.mount = function mount(rpc) {
assert(rpc, 'RPC must be an object.');
assert(typeof rpc.execute === 'function', 'Execute must be a method.');
this.mounts.push(rpc);
};
/**
* Attach to another RPC object.
* @param {Object} rpc
*/
RPCBase.prototype.attach = function attach(rpc) {
assert(rpc, 'RPC must be an object.');
assert(typeof rpc.execute === 'function', 'Execute must be a method.');
rpc.mount(this);
};
/**
* RPC Error
* @constructor
* @ignore
*/
function RPCError(msg, code) {
Error.call(this);
if (Error.captureStackTrace)
Error.captureStackTrace(this, RPCError);
this.type = 'RPCError';
this.message = msg;
this.code = code != null ? code : -1;
}
util.inherits(RPCError, Error);
/*
* Expose
*/
exports = RPCBase;
exports.RPCError = RPCError;
module.exports = exports;

View File

@ -102,17 +102,7 @@ HTTPServer.prototype.initRouter = function initRouter() {
contentType: 'json'
}));
// JSON RPC
this.post('/', co(function* (req, res) {
var json = yield this.rpc.call(req.body, req.query);
json = JSON.stringify(json);
json += '\n';
res.setHeader('X-Long-Polling', '/?longpoll=1');
res.send(200, json, 'json');
}));
this.use(this.jsonRPC(this.rpc));
this.get('/', co(function* (req, res) {
var totalTX = this.mempool ? this.mempool.totalTX : 0;

View File

@ -44,6 +44,9 @@ function FullNode(options) {
Node.call(this, options);
// SPV flag.
this.spv = false;
// Instantiate blockchain.
this.chain = new Chain({
network: this.network,

View File

@ -41,6 +41,7 @@ function Node(options) {
this.plugins = {};
this.stack = [];
this.spv = false;
this.logger = new Logger();
this.chain = null;
this.fees = null;
@ -48,7 +49,6 @@ function Node(options) {
this.pool = null;
this.miner = null;
this.http = null;
this.client = null;
this.init();
}

View File

@ -41,6 +41,9 @@ function SPVNode(options) {
Node.call(this, options);
// SPV flag.
this.spv = true;
this.chain = new Chain({
network: this.network,
logger: this.logger,

View File

@ -19,12 +19,3 @@ exports.version = 'v1.0.0-beta.9';
*/
exports.url = 'https://github.com/bcoin-org/bcoin';
/**
* Default prefix.
* @const {String}
*/
exports.prefix = process.platform === 'win32'
? 'C:/Temp/.bcoin'
: '/tmp/.bcoin';

View File

@ -187,6 +187,25 @@ Validator.prototype.amt = function amt(key, fallback) {
return value * 1e8;
};
/**
* Get a config option (as a number).
* @param {String} key
* @param {Object?} fallback
* @returns {Number|null}
*/
Validator.prototype.btc = function btc(key, fallback) {
var value = this.num(key);
if (fallback === undefined)
fallback = null;
if (value === null)
return fallback;
return value * 1e8;
};
/**
* Get a config option (as a number).
* @param {String} key
@ -313,10 +332,13 @@ Validator.prototype.bool = function bool(key, fallback) {
* @returns {Buffer|null}
*/
Validator.prototype.buf = function buf(key, fallback) {
Validator.prototype.buf = function buf(key, fallback, enc) {
var value = this.get(key);
var data;
if (!enc)
enc = 'hex';
if (fallback === undefined)
fallback = null;
@ -329,10 +351,10 @@ Validator.prototype.buf = function buf(key, fallback) {
return value;
}
data = new Buffer(value, 'hex');
data = new Buffer(value, enc);
if (data.length !== value.length / 2)
throw new Error(key + ' must be a hex string.');
if (data.length !== Buffer.byteLength(value, enc))
throw new Error(key + ' must be a ' + enc + ' string.');
return data;
};

View File

@ -36,12 +36,38 @@ common.isName = function isName(key) {
};
/**
* Sort an array of transactions in dependency order.
* Sort an array of transactions by time.
* @param {TX[]} txs
* @returns {TX[]}
*/
common.sortTX = function sortTX(txs) {
return txs.sort(function(a, b) {
return a.ps - b.ps;
});
};
/**
* Sort an array of coins by height.
* @param {Coin[]} txs
* @returns {Coin[]}
*/
common.sortCoins = function sortCoins(coins) {
return coins.sort(function(a, b) {
a = a.height === -1 ? 0x7fffffff : a.height;
b = b.height === -1 ? 0x7fffffff : b.height;
return a - b;
});
};
/**
* Sort an array of transactions in dependency order.
* @param {TX[]} txs
* @returns {TX[]}
*/
common.sortDeps = function sortDeps(txs) {
var depMap = {};
var count = {};
var result = [];

View File

@ -18,6 +18,7 @@ var Script = require('../script/script');
var crypto = require('../crypto/crypto');
var Network = require('../protocol/network');
var Validator = require('../utils/validator');
var common = require('./common');
var RPC = require('./rpc');
/**
@ -105,6 +106,8 @@ HTTPServer.prototype.initRouter = function initRouter() {
contentType: 'json'
}));
this.use(this.jsonRPC(this.rpc));
this.hook(co(function* (req, res) {
var valid = req.valid();
var id, token, wallet;
@ -583,7 +586,7 @@ HTTPServer.prototype.initRouter = function initRouter() {
var result = [];
var i, coin;
sortCoins(coins);
common.sortCoins(coins);
for (i = 0; i < coins.length; i++) {
coin = coins[i];
@ -665,7 +668,7 @@ HTTPServer.prototype.initRouter = function initRouter() {
var result = [];
var i, details, item;
sortTX(txs);
common.sortTX(txs);
details = yield req.wallet.toDetails(txs);
@ -685,7 +688,7 @@ HTTPServer.prototype.initRouter = function initRouter() {
var result = [];
var i, details, item;
sortTX(txs);
common.sortTX(txs);
details = yield req.wallet.toDetails(txs);
@ -787,35 +790,41 @@ HTTPServer.prototype.initSockets = function initSockets() {
this.walletdb.on('tx', function(id, tx, details) {
var json = details.toJSON();
self.to(id, 'wallet tx', json);
var channel = 'w:' + id;
self.to(channel, 'wallet tx', json);
self.to('!all', 'wallet tx', id, json);
});
this.walletdb.on('confirmed', function(id, tx, details) {
var json = details.toJSON();
self.to(id, 'wallet confirmed', json);
var channel = 'w:' + id;
self.to(channel, 'wallet confirmed', json);
self.to('!all', 'wallet confirmed', id, json);
});
this.walletdb.on('unconfirmed', function(id, tx, details) {
var json = details.toJSON();
self.to(id, 'wallet unconfirmed', json);
var channel = 'w:' + id;
self.to(channel, 'wallet unconfirmed', json);
self.to('!all', 'wallet unconfirmed', id, json);
});
this.walletdb.on('conflict', function(id, tx, details) {
var json = details.toJSON();
self.to(id, 'wallet conflict', json);
var channel = 'w:' + id;
self.to(channel, 'wallet conflict', json);
self.to('!all', 'wallet conflict', id, json);
});
this.walletdb.on('balance', function(id, balance) {
var json = balance.toJSON();
self.to(id, 'wallet balance', json);
var channel = 'w:' + id;
self.to(channel, 'wallet balance', json);
self.to('!all', 'wallet balance', id, json);
});
this.walletdb.on('address', function(id, receive) {
var channel = 'w:' + id;
var json = [];
var i, address;
@ -824,7 +833,7 @@ HTTPServer.prototype.initSockets = function initSockets() {
json.push(address.toJSON());
}
self.to(id, 'wallet address', json);
self.to(channel, 'wallet address', json);
self.to('!all', 'wallet address', id, json);
});
};
@ -873,15 +882,16 @@ HTTPServer.prototype.handleAuth = function handleAuth(socket) {
socket.hook('wallet join', co(function* (args) {
var valid = new Validator([args]);
var id = valid.str(0);
var id = valid.str(0, '');
var token = valid.buf(1);
var channel = 'w:' + id;
var wallet;
if (!id)
throw new Error('Invalid parameter.');
if (!self.options.walletAuth) {
socket.join(id);
socket.join(channel);
return;
}
@ -900,19 +910,20 @@ HTTPServer.prototype.handleAuth = function handleAuth(socket) {
self.logger.info('Successful wallet auth for %s.', id);
socket.join(id);
socket.join(channel);
return null;
}));
socket.hook('wallet leave', function(args) {
var valid = new Validator([args]);
var id = valid.str(0);
var id = valid.str(0, '');
var channel = 'w:' + id;
if (!id)
throw new Error('Invalid parameter.');
socket.leave(id);
socket.leave(channel);
return null;
});
@ -1066,20 +1077,6 @@ function enforce(value, msg) {
}
}
function sortTX(txs) {
return txs.sort(function(a, b) {
return a.ps - b.ps;
});
}
function sortCoins(coins) {
return coins.sort(function(a, b) {
a = a.height === -1 ? 0x7fffffff : a.height;
b = b.height === -1 ? 0x7fffffff : b.height;
return a - b;
});
}
/*
* Expose
*/

File diff suppressed because it is too large Load Diff

View File

@ -1774,7 +1774,7 @@ Wallet.prototype.resend = co(function* resend() {
txs.push(wtx.tx);
}
txs = common.sortTX(txs);
txs = common.sortDeps(txs);
for (i = 0; i < txs.length; i++)
yield this.db.send(txs[i]);

View File

@ -130,12 +130,12 @@ WalletDB.init = function init(node) {
wipeNoReally: config.bool('wallet-wipe-no-really'),
apiKey: config.str(['wallet-api-key', 'api-key']),
walletAuth: config.bool('wallet-auth'),
noAuth: config.bool('no-auth'),
noAuth: config.bool(['wallet-no-auth', 'no-auth']),
ssl: config.str('wallet-ssl'),
host: config.str('wallet-host'),
port: config.num('wallet-port'),
spv: node.chain.options.spv,
verify: node.chain.options.spv
spv: node.spv,
verify: node.spv
});
if (node.http) {
@ -1571,7 +1571,7 @@ WalletDB.prototype.resendPending = co(function* resendPending(wid) {
txs.push(wtx.tx);
}
txs = common.sortTX(txs);
txs = common.sortDeps(txs);
for (i = 0; i < txs.length; i++) {
tx = txs[i];