/*! * rpcbase.js - json rpc for bcoin. * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('assert'); const EventEmitter = require('events'); const Lock = require('../utils/lock'); const 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 = Object.create(null); this.mounts = []; this.locker = new Lock(); } Object.setPrototypeOf(RPCBase.prototype, EventEmitter.prototype); /** * RPC errors. * @enum {Number} * @default */ RPCBase.errors = { // Standard JSON-RPC 2.0 errors INVALID_REQUEST: -32600, METHOD_NOT_FOUND: -32601, INVALID_PARAMS: -32602, INTERNAL_ERROR: -32603, PARSE_ERROR: -32700, // General application defined errors MISC_ERROR: -1, FORBIDDEN_BY_SAFE_MODE: -2, TYPE_ERROR: -3, INVALID_ADDRESS_OR_KEY: -5, OUT_OF_MEMORY: -7, INVALID_PARAMETER: -8, DATABASE_ERROR: -20, DESERIALIZATION_ERROR: -22, VERIFY_ERROR: -25, VERIFY_REJECTED: -26, VERIFY_ALREADY_IN_CHAIN: -27, IN_WARMUP: -28, // Aliases for backward compatibility TRANSACTION_ERROR: -25, TRANSACTION_REJECTED: -26, TRANSACTION_ALREADY_IN_CHAIN: -27, // P2P client errors CLIENT_NOT_CONNECTED: -9, CLIENT_IN_INITIAL_DOWNLOAD: -10, CLIENT_NODE_ALREADY_ADDED: -23, CLIENT_NODE_NOT_ADDED: -24, CLIENT_NODE_NOT_CONNECTED: -29, CLIENT_INVALID_IP_OR_SUBNET: -30, CLIENT_P2P_DISABLED: -31, // Wallet errors WALLET_ERROR: -4, WALLET_INSUFFICIENT_FUNDS: -6, WALLET_INVALID_ACCOUNT_NAME: -11, WALLET_KEYPOOL_RAN_OUT: -12, WALLET_UNLOCK_NEEDED: -13, WALLET_PASSPHRASE_INCORRECT: -14, WALLET_WRONG_ENC_STATE: -15, WALLET_ENCRYPTION_FAILED: -16, WALLET_ALREADY_UNLOCKED: -17 }; /** * 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 = async function call(body, query) { let cmds = body; let out = []; let array = true; if (!Array.isArray(cmds)) { cmds = [cmds]; array = false; } for (const cmd of cmds) { if (!cmd || typeof cmd !== 'object') { out.push({ result: null, error: { message: 'Invalid request.', code: RPCBase.errors.INVALID_REQUEST }, id: null }); continue; } if (cmd.id && typeof cmd.id === 'object') { out.push({ result: null, error: { message: 'Invalid ID.', code: RPCBase.errors.INVALID_REQUEST }, id: null }); continue; } if (cmd.id == null) cmd.id = null; if (!cmd.params) cmd.params = []; if (typeof cmd.method !== 'string') { out.push({ result: null, error: { message: 'Method not found.', code: RPCBase.errors.METHOD_NOT_FOUND }, id: cmd.id }); continue; } if (!Array.isArray(cmd.params)) { out.push({ result: null, error: { message: 'Invalid params.', code: RPCBase.errors.INVALID_PARAMS }, id: cmd.id }); continue; } 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'; } let result; try { result = await this.execute(cmd); } catch (err) { let code; switch (err.type) { case 'RPCError': code = err.code; break; case 'ValidationError': code = RPCBase.errors.TYPE_ERROR; break; case 'EncodingError': code = RPCBase.errors.DESERIALIZATION_ERROR; break; case 'FundingError': code = RPCBase.errors.WALLET_INSUFFICIENT_FUNDS; break; default: code = RPCBase.errors.INTERNAL_ERROR; this.logger.error('RPC internal error.'); this.logger.error(err); break; } out.push({ result: null, error: { message: err.message, code: code }, 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 = async function execute(json, help) { const func = this.calls[json.method]; if (!func) { for (const mount of this.mounts) { if (mount.calls[json.method]) return await mount.execute(json, help); } throw new RPCError(RPCBase.errors.METHOD_NOT_FOUND, `Method not found: ${json.method}.`); } return await 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(code, msg) { Error.call(this); assert(typeof code === 'number'); assert(typeof msg === 'string'); this.type = 'RPCError'; this.message = msg; this.code = code; if (Error.captureStackTrace) Error.captureStackTrace(this, RPCError); } Object.setPrototypeOf(RPCError.prototype, Error.prototype); /* * Expose */ exports = RPCBase; exports.RPCError = RPCError; module.exports = exports;