From 59e9ef277ae1dd1cbded11944e35ac33fabbfa2a Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 9 Mar 2017 18:15:35 -0800 Subject: [PATCH] http: make rpc more hookable. --- lib/http/base.js | 30 ++ lib/http/rpc.js | 1048 +++++++++++++++------------------------- lib/http/rpcbase.js | 226 +++++++++ lib/http/server.js | 12 +- lib/node/fullnode.js | 3 + lib/node/node.js | 2 +- lib/node/spvnode.js | 3 + lib/pkg.js | 9 - lib/utils/validator.js | 30 +- lib/wallet/common.js | 28 +- lib/wallet/http.js | 53 +- lib/wallet/rpc.js | 1011 +++++++++++++++----------------------- lib/wallet/wallet.js | 2 +- lib/wallet/walletdb.js | 8 +- 14 files changed, 1133 insertions(+), 1332 deletions(-) create mode 100644 lib/http/rpcbase.js diff --git a/lib/http/base.js b/lib/http/base.js index ee22d9de..9db826ee 100644 --- a/lib/http/base.js +++ b/lib/http/base.js @@ -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 diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 86c73269..5ad47a40 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -6,7 +6,6 @@ 'use strict'; -var EventEmitter = require('events').EventEmitter; var util = require('../utils/util'); var co = require('../utils/co'); var crypto = require('../crypto/crypto'); @@ -27,11 +26,14 @@ var MTX = require('../primitives/mtx'); var Network = require('../protocol/network'); var Output = require('../primitives/output'); var TX = require('../primitives/tx'); -var Logger = require('../node/logger'); var IP = require('../utils/ip'); var encoding = require('../utils/encoding'); var consensus = require('../protocol/consensus'); +var Validator = require('../utils/validator'); +var RPCBase = require('./rpcbase'); var pkg = require('../pkg'); +var RPCError = RPCBase.RPCError; +var MAGIC_STRING = RPCBase.MAGIC_STRING; /** * Bitcoin Core RPC @@ -44,7 +46,7 @@ function RPC(node) { if (!(this instanceof RPC)) return new RPC(node); - EventEmitter.call(this); + RPCBase.call(this); assert(node, 'RPC requires a Node.'); @@ -68,279 +70,90 @@ function RPC(node) { this.start = 0; this._boundChain = false; this.coinbase = {}; + + this.init(); } -util.inherits(RPC, EventEmitter); +util.inherits(RPC, RPCBase); -RPC.magic = 'Bitcoin Signed Message:\n'; +RPC.prototype.init = function init() { + this.add('stop', this.stop); + this.add('help', this.help); -RPC.prototype.call = co(function* call(body, query) { - var cmds = body; - var out = []; - var array = true; - var i, cmd, result; + this.add('getblockchaininfo', this.getblockchaininfo); + this.add('getbestblockhash', this.getbestblockhash); + this.add('getblockcount', this.getblockcount); + this.add('getblock', this.getblock); + this.add('getblockhash', this.getblockhash); + this.add('getblockheader', this.getblockheader); + this.add('getchaintips', this.getchaintips); + this.add('getdifficulty', this.getdifficulty); + this.add('getmempoolancestors', this.getmempoolancestors); + this.add('getmempooldescendants', this.getmempooldescendants); + this.add('getmempoolentry', this.getmempoolentry); + this.add('getmempoolinfo', this.getmempoolinfo); + this.add('getrawmempool', this.getrawmempool); + this.add('gettxout', this.gettxout); + this.add('gettxoutsetinfo', this.gettxoutsetinfo); + this.add('verifychain', this.verifychain); - if (!Array.isArray(cmds)) { - cmds = [cmds]; - array = false; - } + this.add('invalidateblock', this.invalidateblock); + this.add('reconsiderblock', this.reconsiderblock); - for (i = 0; i < cmds.length; i++) { - cmd = cmds[i]; + this.add('getnetworkhashps', this.getnetworkhashps); + this.add('getmininginfo', this.getmininginfo); + this.add('prioritisetransaction', this.prioritisetransaction); + this.add('getwork', this.getwork); + this.add('getworklp', this.getworklp); + this.add('getblocktemplate', this.getblocktemplate); + this.add('submitblock', this.submitblock); + this.add('verifyblock', this.verifyblock); - assert(cmd && typeof cmd === 'object', 'Command must be an object.'); - assert(typeof cmd.method === 'string', 'Method must be a string.'); + this.add('setgenerate', this.setgenerate); + this.add('getgenerate', this.getgenerate); + this.add('generate', this.generate); + this.add('generatetoaddress', this.generatetoaddress); - if (!cmd.params) - cmd.params = []; + this.add('estimatefee', this.estimatefee); + this.add('estimatepriority', this.estimatepriority); + this.add('estimatesmartfee', this.estimatesmartfee); + this.add('estimatesmartpriority', this.estimatesmartpriority); - assert(Array.isArray(cmd.params), 'Params must be an array.'); + this.add('getinfo', this.getinfo); + this.add('validateaddress', this.validateaddress); + this.add('createmultisig', this.createmultisig); + this.add('createwitnessaddress', this.createwitnessaddress); + this.add('verifymessage', this.verifymessage); + this.add('signmessagewithprivkey', this.signmessagewithprivkey); - if (!cmd.id) - cmd.id = 0; + this.add('setmocktime', this.setmocktime); - assert(typeof cmd.id === 'number', 'ID must be a number.'); - } + this.add('getconnectioncount', this.getconnectioncount); + this.add('ping', this.ping); + this.add('getpeerinfo', this.getpeerinfo); + this.add('addnode', this.addnode); + this.add('disconnectnode', this.disconnectnode); + this.add('getaddednodeinfo', this.getaddednodeinfo); + this.add('getnettotals', this.getnettotals); + this.add('getnetworkinfo', this.getnetworkinfo); + this.add('setban', this.setban); + this.add('listbanned', this.listbanned); + this.add('clearbanned', this.clearbanned); - for (i = 0; i < cmds.length; i++) { - cmd = cmds[i]; + this.add('getrawtransaction', this.getrawtransaction); + this.add('createrawtransaction', this.createrawtransaction); + this.add('decoderawtransaction', this.decoderawtransaction); + this.add('decodescript', this.decodescript); + this.add('sendrawtransaction', this.sendrawtransaction); + this.add('signrawtransaction', this.signrawtransaction); - 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); - } - } + this.add('gettxoutproof', this.gettxoutproof); + this.add('verifytxoutproof', this.verifytxoutproof); - 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; -}); - -RPC.prototype.execute = function execute(json, help) { - switch (json.method) { - case 'stop': - return this.stop(json.params, help); - case 'help': - return this.help(json.params, help); - - case 'getblockchaininfo': - return this.getblockchaininfo(json.params, help); - case 'getbestblockhash': - return this.getbestblockhash(json.params, help); - case 'getblockcount': - return this.getblockcount(json.params, help); - case 'getblock': - return this.getblock(json.params, help); - case 'getblockhash': - return this.getblockhash(json.params, help); - case 'getblockheader': - return this.getblockheader(json.params, help); - case 'getchaintips': - return this.getchaintips(json.params, help); - case 'getdifficulty': - return this.getdifficulty(json.params, help); - case 'getmempoolancestors': - return this.getmempoolancestors(json.params, help); - case 'getmempooldescendants': - return this.getmempooldescendants(json.params, help); - case 'getmempoolentry': - return this.getmempoolentry(json.params, help); - case 'getmempoolinfo': - return this.getmempoolinfo(json.params, help); - case 'getrawmempool': - return this.getrawmempool(json.params, help); - case 'gettxout': - return this.gettxout(json.params, help); - case 'gettxoutsetinfo': - return this.gettxoutsetinfo(json.params, help); - case 'verifychain': - return this.verifychain(json.params, help); - - case 'invalidateblock': - return this.invalidateblock(json.params, help); - case 'reconsiderblock': - return this.reconsiderblock(json.params, help); - - case 'getnetworkhashps': - return this.getnetworkhashps(json.params, help); - case 'getmininginfo': - return this.getmininginfo(json.params, help); - case 'prioritisetransaction': - return this.prioritisetransaction(json.params, help); - case 'getwork': - return this.getwork(json.params, help); - case 'getworklp': - return this.getworklp(json.params, help); - case 'getblocktemplate': - return this.getblocktemplate(json.params, help); - case 'submitblock': - return this.submitblock(json.params, help); - case 'verifyblock': - return this.verifyblock(json.params, help); - - case 'setgenerate': - return this.setgenerate(json.params, help); - case 'getgenerate': - return this.getgenerate(json.params, help); - case 'generate': - return this.generate(json.params, help); - case 'generatetoaddress': - return this.generatetoaddress(json.params, help); - - case 'estimatefee': - return this.estimatefee(json.params, help); - case 'estimatepriority': - return this.estimatepriority(json.params, help); - case 'estimatesmartfee': - return this.estimatesmartfee(json.params, help); - case 'estimatesmartpriority': - return this.estimatesmartpriority(json.params, help); - - case 'getinfo': - return this.getinfo(json.params, help); - case 'validateaddress': - return this.validateaddress(json.params, help); - case 'createmultisig': - return this.createmultisig(json.params, help); - case 'createwitnessaddress': - return this.createwitnessaddress(json.params, help); - case 'verifymessage': - return this.verifymessage(json.params, help); - case 'signmessagewithprivkey': - return this.signmessagewithprivkey(json.params, help); - - case 'setmocktime': - return this.setmocktime(json.params, help); - - case 'getconnectioncount': - return this.getconnectioncount(json.params, help); - case 'ping': - return this.ping(json.params, help); - case 'getpeerinfo': - return this.getpeerinfo(json.params, help); - case 'addnode': - return this.addnode(json.params, help); - case 'disconnectnode': - return this.disconnectnode(json.params, help); - case 'getaddednodeinfo': - return this.getaddednodeinfo(json.params, help); - case 'getnettotals': - return this.getnettotals(json.params, help); - case 'getnetworkinfo': - return this.getnetworkinfo(json.params, help); - case 'setban': - return this.setban(json.params, help); - case 'listbanned': - return this.listbanned(json.params, help); - case 'clearbanned': - return this.clearbanned(json.params, help); - - case 'getrawtransaction': - return this.getrawtransaction(json.params, help); - case 'createrawtransaction': - return this.createrawtransaction(json.params, help); - case 'decoderawtransaction': - return this.decoderawtransaction(json.params, help); - case 'decodescript': - return this.decodescript(json.params, help); - case 'sendrawtransaction': - return this.sendrawtransaction(json.params, help); - case 'signrawtransaction': - return this.signrawtransaction(json.params, help); - - case 'gettxoutproof': - return this.gettxoutproof(json.params, help); - case 'verifytxoutproof': - return this.verifytxoutproof(json.params, help); - - case 'getmemory': - return this.getmemory(json.params, help); - case 'setloglevel': - return this.setloglevel(json.params, help); - - default: - return this.custom(json, help); - } + this.add('getmemory', this.getmemory); + this.add('setloglevel', this.setloglevel); }; -/** - * Add a custom RPC call. - * @param {String} name - * @param {Function} func - * @param {Object?} ctx - */ - -RPC.prototype.add = function add(name, func, ctx) { - assert(!this.calls[name], 'Duplicate RPC call.'); - this.calls[name] = func.bind(ctx); -}; - -/** - * Execute a custom RPC call. - * @private - * @param {Object} json - * @param {Boolean} help - * @returns {Promise} - */ - -RPC.prototype.custom = co(function* custom(json, help) { - var call = this.calls[json.method]; - - if (!call) - throw new RPCError('Not found: ' + json.method + '.'); - - return yield call(json.params, help); -}); - /* * Overall control/query calls */ @@ -415,13 +228,14 @@ RPC.prototype.getnetworkinfo = co(function* getnetworkinfo(args, help) { }); RPC.prototype.addnode = co(function* addnode(args, help) { - var node, cmd, addr, peer; + var valid = new Validator([args]); + var node = valid.str(0, ''); + var cmd = valid.str(1, ''); + var addr, peer; if (help || args.length !== 2) throw new RPCError('addnode "node" "add|remove|onetry"'); - node = toString(args[0]); - cmd = toString(args[1]); addr = NetAddress.fromHostname(node, this.network); switch (cmd) { @@ -443,14 +257,14 @@ RPC.prototype.addnode = co(function* addnode(args, help) { }); RPC.prototype.disconnectnode = co(function* disconnectnode(args, help) { - var addr, peer; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var peer; if (help || args.length !== 1) throw new RPCError('disconnectnode "node"'); - addr = toString(args[0]); addr = IP.fromHostname(addr, this.network.port); - peer = this.pool.peers.get(addr.hostname); if (peer) @@ -460,14 +274,15 @@ RPC.prototype.disconnectnode = co(function* disconnectnode(args, help) { }); RPC.prototype.getaddednodeinfo = co(function* getaddednodeinfo(args, help) { + var valid = new Validator([args]); + var addr = valid.str(1, ''); var out = []; - var addr, peer; + var peer; if (help || args.length < 1 || args.length > 2) throw new RPCError('getaddednodeinfo dummy ( "node" )'); if (args.length === 2) { - addr = toString(args[1]); addr = IP.fromHostname(addr, this.network.port); peer = this.pool.peers.get(addr.hostname); if (!peer) @@ -582,19 +397,20 @@ RPC.prototype.ping = co(function* ping(args, help) { }); RPC.prototype.setban = co(function* setban(args, help) { - var addr; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var action = valid.str(1, ''); if (help || args.length < 2 - || (args[1] !== 'add' && args[1] !== 'remove')) { + || (action !== 'add' && action !== 'remove')) { throw new RPCError('setban "ip(/netmask)"' + ' "add|remove" (bantime) (absolute)'); } - addr = toString(args[0]); addr = NetAddress.fromHostname(addr, this.network); - switch (args[1]) { + switch (action) { case 'add': this.pool.ban(addr); break; @@ -760,21 +576,17 @@ RPC.prototype.getblockcount = co(function* getblockcount(args, help) { }); RPC.prototype.getblock = co(function* getblock(args, help) { - var hash, verbose, entry, block; + var valid = new Validator([args]); + var hash = valid.hash(0); + var verbose = valid.bool(1, true); + var entry, block; if (help || args.length < 1 || args.length > 2) throw new RPCError('getblock "hash" ( verbose )'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter.'); - verbose = true; - - if (args.length > 1) - verbose = toBool(args[1]); - entry = yield this.chain.db.getEntry(hash); if (!entry) @@ -876,13 +688,13 @@ RPC.prototype._scriptToJSON = function scriptToJSON(script, hex) { }; RPC.prototype.getblockhash = co(function* getblockhash(args, help) { - var height, hash; + var valid = new Validator([args]); + var height = valid.num(0, -1); + var hash; if (help || args.length !== 1) throw new RPCError('getblockhash index'); - height = toNumber(args[0]); - if (height < 0 || height > this.chain.height) throw new RPCError('Block height out of range.'); @@ -895,21 +707,17 @@ RPC.prototype.getblockhash = co(function* getblockhash(args, help) { }); RPC.prototype.getblockheader = co(function* getblockheader(args, help) { - var hash, verbose, entry; + var valid = new Validator([args]); + var hash = valid.hash(0); + var verbose = valid.bool(1, true); + var entry; if (help || args.length < 1 || args.length > 2) throw new RPCError('getblockheader "hash" ( verbose )'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter.'); - verbose = true; - - if (args.length > 1) - verbose = toBool(args[1], true); - entry = yield this.chain.db.getEntry(hash); if (!entry) @@ -1035,8 +843,11 @@ RPC.prototype.getmempoolinfo = co(function* getmempoolinfo(args, help) { }); RPC.prototype.getmempoolancestors = co(function* getmempoolancestors(args, help) { + var valid = new Validator([args]); + var hash = valid.hash(0); + var verbose = valid.bool(1, false); var out = []; - var i, hash, verbose, entry, entries; + var i, entry, entries; if (help || args.length < 1 || args.length > 2) throw new RPCError('getmempoolancestors txid (verbose)'); @@ -1044,14 +855,9 @@ RPC.prototype.getmempoolancestors = co(function* getmempoolancestors(args, help) if (!this.mempool) throw new RPCError('No mempool available.'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter.'); - if (args.length > 1) - verbose = toBool(args[1], false); - entry = this.mempool.getEntry(hash); if (!entry) @@ -1075,8 +881,11 @@ RPC.prototype.getmempoolancestors = co(function* getmempoolancestors(args, help) }); RPC.prototype.getmempooldescendants = co(function* getmempooldescendants(args, help) { + var valid = new Validator([args]); + var hash = valid.hash(0); + var verbose = valid.bool(1, false); var out = []; - var i, hash, verbose, entry, entries; + var i, entry, entries; if (help || args.length < 1 || args.length > 2) throw new RPCError('getmempooldescendants txid (verbose)'); @@ -1084,14 +893,9 @@ RPC.prototype.getmempooldescendants = co(function* getmempooldescendants(args, h if (!this.mempool) throw new RPCError('No mempool available.'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter.'); - if (args.length > 1) - verbose = toBool(args[1], false); - entry = this.mempool.getEntry(hash); if (!entry) @@ -1115,7 +919,9 @@ RPC.prototype.getmempooldescendants = co(function* getmempooldescendants(args, h }); RPC.prototype.getmempoolentry = co(function* getmempoolentry(args, help) { - var hash, entry; + var valid = new Validator([args]); + var hash = valid.hash(0); + var entry; if (help || args.length !== 1) throw new RPCError('getmempoolentry txid'); @@ -1123,8 +929,6 @@ RPC.prototype.getmempoolentry = co(function* getmempoolentry(args, help) { if (!this.mempool) throw new RPCError('No mempool available.'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter.'); @@ -1137,23 +941,14 @@ RPC.prototype.getmempoolentry = co(function* getmempoolentry(args, help) { }); RPC.prototype.getrawmempool = co(function* getrawmempool(args, help) { - var verbose; + var valid = new Validator([args]); + var verbose = valid.bool(0, false); + var out = {}; + var i, hashes, hash, entry; if (help || args.length > 1) throw new RPCError('getrawmempool ( verbose )'); - verbose = false; - - if (args.length > 0) - verbose = toBool(args[0], false); - - return this._mempoolToJSON(verbose); -}); - -RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose) { - var out = {}; - var i, hashes, hash, entry; - if (verbose) { hashes = this.mempool.getSnapshot(); @@ -1164,7 +959,7 @@ RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose) { if (!entry) continue; - out[entry.tx.txid()] = this._entryToJSON(entry); + out[entry.txid()] = this._entryToJSON(entry); } return out; @@ -1173,7 +968,7 @@ RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose) { hashes = this.mempool.getSnapshot(); return hashes.map(util.revHex); -}; +}); RPC.prototype._entryToJSON = function _entryToJSON(entry) { return { @@ -1195,7 +990,11 @@ RPC.prototype._entryToJSON = function _entryToJSON(entry) { }; RPC.prototype.gettxout = co(function* gettxout(args, help) { - var hash, index, mempool, coin; + var valid = new Validator([args]); + var hash = valid.hash(0); + var index = valid.num(1); + var mempool = valid.bool(2, true); + var coin; if (help || args.length < 2 || args.length > 3) throw new RPCError('gettxout "txid" n ( includemempool )'); @@ -1206,13 +1005,6 @@ RPC.prototype.gettxout = co(function* gettxout(args, help) { if (this.chain.options.prune) throw new RPCError('Cannot get coins when pruned.'); - hash = toHash(args[0]); - index = toNumber(args[1]); - mempool = true; - - if (args.length > 2) - mempool = toBool(args[2], true); - if (!hash || index < 0) throw new RPCError('Invalid parameter.'); @@ -1236,8 +1028,11 @@ RPC.prototype.gettxout = co(function* gettxout(args, help) { }); RPC.prototype.gettxoutproof = co(function* gettxoutproof(args, help) { + var valid = new Validator([args]); + var txids = valid.array(0); + var hash = valid.hash(1); var uniq = {}; - var i, txids, block, hash, txid, tx, coins; + var i, block, txid, tx, coins; if (help || (args.length !== 1 && args.length !== 2)) throw new RPCError('gettxoutproof ["txid",...] ( blockhash )'); @@ -1248,20 +1043,13 @@ RPC.prototype.gettxoutproof = co(function* gettxoutproof(args, help) { if (this.chain.options.prune) throw new RPCError('Cannot get coins when pruned.'); - txids = toArray(args[0]); - hash = args[1]; - if (!txids || txids.length === 0) throw new RPCError('Invalid parameter.'); - if (hash) { - hash = toHash(hash); - if (!hash) - throw new RPCError('Invalid parameter.'); - } + valid = new Validator([txids]); for (i = 0; i < txids.length; i++) { - txid = toHash(txids[i]); + txid = valid.hash(i); if (!txid) throw new RPCError('Invalid parameter.'); @@ -1302,13 +1090,18 @@ RPC.prototype.gettxoutproof = co(function* gettxoutproof(args, help) { }); RPC.prototype.verifytxoutproof = co(function* verifytxoutproof(args, help) { + var valid = new Validator([args]); + var data = valid.buf(0); var out = []; var i, block, hash, entry; if (help || args.length !== 1) throw new RPCError('verifytxoutproof "proof"'); - block = MerkleBlock.fromRaw(toString(args[0]), 'hex'); + if (!data) + throw new RPCError('Invalid hex string.'); + + block = MerkleBlock.fromRaw(data); if (!block.verify()) return out; @@ -1345,9 +1138,16 @@ RPC.prototype.gettxoutsetinfo = co(function* gettxoutsetinfo(args, help) { }); RPC.prototype.verifychain = co(function* verifychain(args, help) { + var valid = new Validator([args]); + var level = valid.num(0); + var blocks = valid.num(1); + if (help || args.length > 2) throw new RPCError('verifychain ( checklevel numblocks )'); + if (level == null || blocks == null) + throw new RPCError('Invalid parameter.'); + if (this.chain.options.spv) throw new RPCError('Cannot verify chain in SPV mode.'); @@ -1381,8 +1181,7 @@ RPC.prototype.__submitwork = co(function* _submitwork(data) { return false; data = data.slice(0, 80); - - reverseEndian(data); + data = swap32(data); header = Headers.fromAbbr(data); block = attempt.block; @@ -1456,7 +1255,7 @@ RPC.prototype.__creatework = co(function* _creatework() { data[80] = 0x80; data.writeUInt32BE(80 * 8, data.length - 4, true); - reverseEndian(data); + data = swap32(data); return { data: data.toString('hex'), @@ -1471,17 +1270,16 @@ RPC.prototype.getworklp = co(function* getworklp(args, help) { }); RPC.prototype.getwork = co(function* getwork(args, help) { - var data; + var valid = new Validator([args]); + var data = valid.buf(0); if (args.length > 1) throw new RPCError('getwork ( "data" )'); if (args.length === 1) { - if (!util.isHex(args[0])) + if (!data) throw new RPCError('Invalid parameter.'); - data = new Buffer(args[0], 'hex'); - return yield this._submitwork(data); } @@ -1489,12 +1287,14 @@ RPC.prototype.getwork = co(function* getwork(args, help) { }); RPC.prototype.submitblock = co(function* submitblock(args, help) { + var valid = new Validator([args]); + var data = valid.buf(0); var block, tx; if (help || args.length < 1 || args.length > 2) throw new RPCError('submitblock "hexdata" ( "jsonparametersobject" )'); - block = Block.fromRaw(toString(args[0]), 'hex'); + block = Block.fromRaw(data); // Fix eloipool bug (witness nonce is not present). if (this.chain.state.hasWitness() && block.getCommitmentHash()) { @@ -1550,61 +1350,53 @@ RPC.prototype.__submitblock = co(function* submitblock(block) { }); RPC.prototype.getblocktemplate = co(function* getblocktemplate(args, help) { - var mode = 'template'; - var version = -1; + var validator = new Validator([args]); + var options = validator.obj(0, {}); + var valid = new Validator([options]); + var mode = valid.str('mode', 'template'); + var lpid = valid.str('longpollid'); + var data = valid.buf('data'); + var rules = valid.array('rules'); + var capabilities = valid.array('capabilities'); + var version = valid.num('maxversion', -1); var coinbase = false; - var rules = []; - var i, opt, lpid, cap, block; - var coinbasevalue, coinbasetxn; + var i, cap, block, value, txn; if (help || args.length > 1) throw new RPCError('getblocktemplate ( "jsonrequestobject" )'); - if (args.length === 1) { - opt = args[0] || {}; + if (mode !== 'template' && mode !== 'proposal') + throw new RPCError('Invalid mode.'); - if (opt.mode != null) { - mode = opt.mode; - if (mode !== 'template' && mode !== 'proposal') - throw new RPCError('Invalid mode.'); - } + if (mode === 'proposal') { + if (!data) + throw new RPCError('Invalid parameter.'); - lpid = opt.longpollid; + block = Block.fromRaw(data); - if (mode === 'proposal') { - if (!util.isHex(opt.data)) - throw new RPCError('Invalid parameter.'); + return yield this._submitblock(block); + } - block = Block.fromRaw(opt.data, 'hex'); + if (rules) + version = -1; - return yield this._submitblock(block); - } - - if (Array.isArray(opt.rules)) { - for (i = 0; i < opt.rules.length; i++) - rules.push(toString(opt.rules[i])); - } else if (util.isNumber(opt.maxversion)) { - version = opt.maxversion; - } - - if (Array.isArray(opt.capabilities)) { - for (i = 0; i < opt.capabilities.length; i++) { - cap = toString(opt.capabilities[i]); - switch (cap) { - case 'coinbasetxn': - coinbasetxn = true; - break; - case 'coinbasevalue': - coinbasevalue = true; - break; - case 'coinbase/append': - break; - } + if (capabilities) { + for (i = 0; i < capabilities.length; i++) { + cap = capabilities[i]; + switch (cap) { + case 'coinbasetxn': + txn = true; + break; + case 'coinbasevalue': + value = true; + break; + case 'coinbase/append': + break; } - - if (coinbasetxn) - coinbase = true; } + + if (txn) + coinbase = true; } if (!this.network.selfConnect) { @@ -1615,7 +1407,8 @@ RPC.prototype.getblocktemplate = co(function* getblocktemplate(args, help) { throw new RPCError('Bitcoin is downloading blocks...'); } - yield this._poll(lpid); + if (lpid) + yield this._poll(lpid); return yield this._template(version, coinbase, rules); }); @@ -1784,9 +1577,6 @@ RPC.prototype.__template = co(function* _template(version, coinbase, rules) { RPC.prototype._poll = co(function* _poll(lpid) { var watched, lastTX; - if (typeof lpid !== 'string') - return; - if (lpid.length !== 74) throw new RPCError('Invalid parameter.'); @@ -1899,23 +1689,22 @@ RPC.prototype.getmininginfo = co(function* getmininginfo(args, help) { }); RPC.prototype.getnetworkhashps = co(function* getnetworkhashps(args, help) { - var lookup = 120; - var height = -1; + var valid = new Validator([args]); + var lookup = valid.num(0, 120); + var height = valid.num(1, -1); if (help || args.length > 2) throw new RPCError('getnetworkhashps ( blocks height )'); - if (args.length > 0) - lookup = toNumber(args[0], 120); - - if (args.length > 1) - height = toNumber(args[1], -1); - return yield this._hashps(lookup, height); }); RPC.prototype.prioritisetransaction = co(function* prioritisetransaction(args, help) { - var hash, pri, fee, entry; + var valid = new Validator([args]); + var hash = valid.hash(0); + var pri = valid.num(1); + var fee = valid.btc(2); + var entry; if (help || args.length !== 3) { throw new RPCError('prioritisetransaction' @@ -1925,14 +1714,10 @@ RPC.prototype.prioritisetransaction = co(function* prioritisetransaction(args, h if (!this.mempool) throw new RPCError('No mempool available.'); - hash = toHash(args[0]); - pri = args[1]; - fee = args[2]; - if (!hash) throw new RPCError('Invalid parameter'); - if (!util.isNumber(pri) || !util.isNumber(fee)) + if (pri == null || fee == null) throw new RPCError('Invalid parameter'); entry = this.mempool.getEntry(hash); @@ -1993,18 +1778,20 @@ RPC.prototype._hashps = co(function* _hashps(lookup, height) { }); RPC.prototype.verifyblock = co(function* verifyblock(args, help) { + var valid = new Validator([args]); + var data = valid.buf(0); var block; if (help || args.length !== 1) throw new RPCError('verifyblock "block-hex"'); - if (typeof args[0] !== 'string') + if (!data) throw new RPCError('Invalid parameters.'); if (this.chain.options.spv) throw new RPCError('Cannot verify block in SPV mode.'); - block = Block.fromRaw(args[0], 'hex'); + block = Block.fromRaw(data); try { yield this.chain.verifyBlock(block); @@ -2028,18 +1815,24 @@ RPC.prototype.getgenerate = co(function* getgenerate(args, help) { }); RPC.prototype.setgenerate = co(function* setgenerate(args, help) { + var valid = new Validator([args]); + var mine = valid.bool(0, false); + var limit = valid.num(0, 0); + if (help || args.length < 1 || args.length > 2) throw new RPCError('setgenerate mine ( proclimit )'); - this.mining = toBool(args[0]); - this.proclimit = toNumber(args[1], 0); + this.mining = mine; + this.proclimit = limit; - if (this.mining) + if (mine) { this.miner.start().catch(util.nop); - else - yield this.miner.stop(); + return true; + } - return this.mining; + yield this.miner.stop(); + + return false; }); RPC.prototype.generate = co(function* generate(args, help) { @@ -2052,17 +1845,17 @@ RPC.prototype.generate = co(function* generate(args, help) { }); RPC.prototype._generate = co(function* generate(args, help) { - var numblocks; + var valid = new Validator([args]); + var blocks = valid.num(0, 1); + var tries = valid.num(1); if (help || args.length < 1 || args.length > 2) throw new RPCError('generate numblocks ( maxtries )'); - numblocks = toNumber(args[0], 1); - - return yield this._generateBlocks(numblocks); + return yield this._generateBlocks(blocks, null, tries); }); -RPC.prototype._generateBlocks = co(function* _generateBlocks(blocks, address) { +RPC.prototype._generateBlocks = co(function* _generateBlocks(blocks, address, tries) { var hashes = []; var i, block; @@ -2085,15 +1878,20 @@ RPC.prototype.generatetoaddress = co(function* generatetoaddress(args, help) { }); RPC.prototype._generatetoaddress = co(function* generatetoaddress(args, help) { - var numblocks, address; + var valid = new Validator([args]); + var blocks = valid.num(0, 1); + var addr = valid.str(1, ''); + var tries = valid.num(2, 1000000); if (help || args.length < 2 || args.length > 3) throw new RPCError('generatetoaddress numblocks address ( maxtries )'); - numblocks = toNumber(args[0], 1); - address = Address.fromBase58(toString(args[1]), this.network); + if (tries < 0) + throw new RPCError('Invalid parameter.'); - return yield this._generateBlocks(numblocks, address); + addr = Address.fromBase58(addr, this.network); + + return yield this._generateBlocks(blocks, addr); }); /* @@ -2101,8 +1899,11 @@ RPC.prototype._generatetoaddress = co(function* generatetoaddress(args, help) { */ RPC.prototype.createrawtransaction = co(function* createrawtransaction(args, help) { - var inputs, sendTo, tx, locktime; - var i, input, output, hash, index, sequence; + var valid = new Validator([args]); + var inputs = valid.array(0); + var sendTo = valid.obj(1); + var locktime = valid.num(2, -1); + var i, tx, input, output, hash, index, sequence; var keys, addrs, key, value, address, b58; if (help || args.length < 2 || args.length > 3) { @@ -2112,43 +1913,28 @@ RPC.prototype.createrawtransaction = co(function* createrawtransaction(args, hel + ' ( locktime )'); } - inputs = toArray(args[0]); - sendTo = toObject(args[1]); - if (!inputs || !sendTo) throw new RPCError('Invalid parameter'); tx = new TX(); - if (args.length > 2 && args[2] != null) { - locktime = toNumber(args[2]); - if (!util.isUInt32(locktime)) - throw new RPCError('Locktime out of range'); - tx.locktime = locktime; - } + if (locktime !== -1) + tx.setLocktime(locktime); for (i = 0; i < inputs.length; i++) { input = inputs[i]; + valid = new Validator([input]); - if (!input) - throw new RPCError('Invalid parameter'); - - hash = toHash(input.txid); - index = input.vout; - sequence = 0xffffffff; + hash = valid.hash('txid'); + index = valid.num('vout'); + sequence = valid.num('sequence'); if (tx.locktime) sequence--; - if (!hash || !util.isUInt32(index)) + if (!hash || !util.isUInt32(index) || !util.isUInt32(sequence)) throw new RPCError('Invalid parameter'); - if (util.isNumber(input.sequence)) { - sequence = toNumber(input.sequence); - if (!util.isUInt32(sequence)) - throw new RPCError('Invalid parameter'); - } - input = new Input(); input.prevout.hash = hash; input.prevout.index = index; @@ -2158,18 +1944,23 @@ RPC.prototype.createrawtransaction = co(function* createrawtransaction(args, hel } keys = Object.keys(sendTo); + valid = new Validator([sendTo]); addrs = {}; for (i = 0; i < keys.length; i++) { key = keys[i]; - value = sendTo[key]; if (key === 'data') { - value = new Buffer(value, 'hex'); + value = valid.buf(key); + + if (!value) + throw new RPCError('Invalid parameter.'); + output = new Output(); output.value = 0; output.script.fromNulldata(value); tx.outputs.push(output); + continue; } @@ -2181,8 +1972,13 @@ RPC.prototype.createrawtransaction = co(function* createrawtransaction(args, hel addrs[b58] = true; + value = valid.btc(key); + + if (value == null) + throw new RPCError('Invalid parameter.'); + output = new Output(); - output.value = toSatoshi(value); + output.value = value; output.script.fromAddress(address); tx.outputs.push(output); @@ -2192,27 +1988,33 @@ RPC.prototype.createrawtransaction = co(function* createrawtransaction(args, hel }); RPC.prototype.decoderawtransaction = co(function* decoderawtransaction(args, help) { + var valid = new Validator([args]); + var data = valid.buf(0); var tx; if (help || args.length !== 1) throw new RPCError('decoderawtransaction "hexstring"'); - tx = TX.fromRaw(toString(args[0]), 'hex'); + if (!data) + throw new RPCError('Invalid parameter.'); + + tx = TX.fromRaw(data); return this._txToJSON(tx); }); RPC.prototype.decodescript = co(function* decodescript(args, help) { - var data, script, address; + var valid = new Validator([args]); + var data = valid.buf(0); + var script, address; if (help || args.length !== 1) throw new RPCError('decodescript "hex"'); - data = toString(args[0]); script = new Script(); - if (data.length > 0) - script.fromRaw(new Buffer(data, 'hex')); + if (data) + script.fromRaw(data); address = Address.fromScripthash(script.hash160()); @@ -2223,21 +2025,17 @@ RPC.prototype.decodescript = co(function* decodescript(args, help) { }); RPC.prototype.getrawtransaction = co(function* getrawtransaction(args, help) { - var hash, verbose, json, meta, tx, entry; + var valid = new Validator([args]); + var hash = valid.hash(0); + var verbose = valid.bool(1, false); + var json, meta, tx, entry; if (help || args.length < 1 || args.length > 2) throw new RPCError('getrawtransaction "txid" ( verbose )'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter'); - verbose = false; - - if (args.length > 1) - verbose = toBool(args[1]); - meta = yield this.node.getMeta(hash); if (!meta) @@ -2259,15 +2057,17 @@ RPC.prototype.getrawtransaction = co(function* getrawtransaction(args, help) { }); RPC.prototype.sendrawtransaction = co(function* sendrawtransaction(args, help) { + var valid = new Validator([args]); + var data = valid.buf(0); var tx; if (help || args.length < 1 || args.length > 2) throw new RPCError('sendrawtransaction "hexstring" ( allowhighfees )'); - if (!util.isHex(args[0])) + if (!data) throw new RPCError('Invalid parameter'); - tx = TX.fromRaw(args[0], 'hex'); + tx = TX.fromRaw(data); this.node.relay(tx); @@ -2275,6 +2075,8 @@ RPC.prototype.sendrawtransaction = co(function* sendrawtransaction(args, help) { }); RPC.prototype.signrawtransaction = co(function* signrawtransaction(args, help) { + var valid = new Validator([args]); + var data = valid.buf(0); var tx; if (help || args.length < 1 || args.length > 4) { @@ -2285,59 +2087,52 @@ RPC.prototype.signrawtransaction = co(function* signrawtransaction(args, help) { + ' sighashtype )'); } - if (!util.isHex(args[0])) + if (!data) throw new RPCError('Invalid parameter'); - tx = MTX.fromRaw(args[0], 'hex'); + tx = MTX.fromRaw(data); tx.view = yield this.mempool.getSpentView(tx); return yield this._signrawtransaction(tx, args); }); RPC.prototype._signrawtransaction = co(function* signrawtransaction(tx, args) { + var valid = new Validator([args]); + var prevout = valid.array(1); + var secrets = valid.array(2); + var sighash = valid.str(3); var type = Script.hashType.ALL; var keys = []; - var keyMap = {}; - var i, j, k, secret, key; - var coin, prevout, prev; + var map = {}; + var i, j, secret, key; + var coin, prev; var hash, index, script, value; var redeem, op, parts; - if (args.length > 2 && Array.isArray(args[2])) { - k = args[2]; - for (i = 0; i < k.length; i++) { - secret = k[i]; - - if (typeof secret !== 'string') - throw new RPCError('Invalid parameter'); - + if (secrets) { + valid = new Validator([secrets]); + for (i = 0; i < secrets.length; i++) { + secret = valid.str(i, ''); key = KeyRing.fromSecret(secret, this.network); - keyMap[key.getPublicKey('hex')] = key; + map[key.getPublicKey('hex')] = key; keys.push(key); } } - if (args.length > 1 && Array.isArray(args[1])) { - prevout = args[1]; - + if (prevout) { for (i = 0; i < prevout.length; i++) { prev = prevout[i]; + valid = new Validator([prev]); + hash = valid.hash('txid'); + index = valid.num('index'); + script = valid.buf('scriptPubKey'); + value = valid.btc('amount'); + redeem = valid.buf('redeemScript'); - if (!prev || typeof prev !== 'object') + if (!hash || index == null || !script || value == null) throw new RPCError('Invalid parameter'); - hash = toHash(prev.txid); - index = prev.vout; - script = prev.scriptPubKey; - value = toSatoshi(prev.amount); - - if (!hash - || !util.isUInt32(index) - || !util.isHex(script)) { - throw new RPCError('Invalid parameter'); - } - - script = Script.fromRaw(script, 'hex'); + script = Script.fromRaw(script); coin = new Output(); coin.script = script; @@ -2345,32 +2140,34 @@ RPC.prototype._signrawtransaction = co(function* signrawtransaction(tx, args) { tx.view.addOutput(hash, index, coin); - if (keys.length === 0 || !util.isHex(prev.redeemScript)) + if (keys.length === 0 || !redeem) continue; - if (script.isScripthash() || script.isWitnessScripthash()) { - redeem = Script.fromRaw(prev.redeemScript, 'hex'); - for (j = 0; j < redeem.code.length; j++) { - op = redeem.code[j]; + if (!script.isScripthash() && !script.isWitnessScripthash()) + continue; - if (!op.data) - continue; + redeem = Script.fromRaw(redeem); - key = keyMap[op.data.toString('hex')]; + for (j = 0; j < redeem.code.length; j++) { + op = redeem.code[j]; - if (key) { - key.script = redeem; - key.witness = script.isWitnessScripthash(); - key.refresh(); - break; - } + if (!op.data) + continue; + + key = map[op.data.toString('hex')]; + + if (key) { + key.script = redeem; + key.witness = script.isWitnessScripthash(); + key.refresh(); + break; } } } } - if (args.length > 3) { - parts = toString(args[3]).split('|'); + if (sighash) { + parts = sighash.split('|'); type = Script.hashType[parts[0]]; if (type == null) @@ -2398,14 +2195,58 @@ RPC.prototype._signrawtransaction = co(function* signrawtransaction(tx, args) { * Utility Functions */ +RPC.prototype.createmultisig = co(function* createmultisig(args, help) { + var valid = new Validator([args]); + var keys = valid.array(1, []); + var m = valid.num(0, 0); + var n = keys.length; + var i, script, key, address; + + if (help || args.length < 2 || args.length > 2) + throw new RPCError('createmultisig nrequired ["key",...]'); + + if (m < 1 || n < m || n > 16) + throw new RPCError('Invalid parameter.'); + + valid = new Validator([keys]); + + for (i = 0; i < keys.length; i++) { + key = valid.buf(i); + + if (!key) + throw new RPCError('Invalid key.'); + + if (!ec.publicKeyVerify(key)) + throw new RPCError('Invalid key.'); + + keys[i] = key; + } + + script = Script.fromMultisig(m, n, keys); + + if (script.getSize() > consensus.MAX_SCRIPT_PUSH) + throw new RPCError('Redeem script exceeds size limit.'); + + address = script.getAddress(); + + return { + address: address.toBase58(this.network), + redeemScript: script.toJSON() + }; +}); + RPC.prototype.createwitnessaddress = co(function* createwitnessaddress(args, help) { - var raw, script, program, address; + var valid = new Validator([args]); + var raw = valid.buf(0); + var script, program, address; if (help || args.length !== 1) throw new RPCError('createwitnessaddress "script"'); - raw = toString(args[1]); - script = Script.fromRaw(raw, 'hex'); + if (!raw) + throw new RPCError('Invalid parameter.'); + + script = Script.fromRaw(raw); program = script.forWitness(); address = program.getAddress(); @@ -2416,13 +2257,13 @@ RPC.prototype.createwitnessaddress = co(function* createwitnessaddress(args, hel }); RPC.prototype.validateaddress = co(function* validateaddress(args, help) { - var b58, address, script; + var valid = new Validator([args]); + var b58 = valid.str(0, ''); + var address, script; if (help || args.length !== 1) throw new RPCError('validateaddress "bitcoinaddress"'); - b58 = toString(args[0]); - try { address = Address.fromBase58(b58, this.network); } catch (e) { @@ -2443,22 +2284,20 @@ RPC.prototype.validateaddress = co(function* validateaddress(args, help) { }); RPC.prototype.verifymessage = co(function* verifymessage(args, help) { - var address, sig, msg, hash, key; + var valid = new Validator([args]); + var b58 = valid.str(0, ''); + var sig = valid.buf(1, null, 'base64'); + var msg = valid.str(2); + var hash = Address.getHash(b58); + var key; if (help || args.length !== 3) throw new RPCError('verifymessage "bitcoinaddress" "signature" "message"'); - address = toString(args[0]); - sig = toString(args[1]); - msg = toString(args[2]); + if (!hash || !sig || !msg) + throw new RPCError('Invalid parameter.'); - hash = Address.getHash(address); - - if (!hash) - throw new RPCError('Invalid address.'); - - sig = new Buffer(sig, 'base64'); - msg = new Buffer(RPC.magic + msg, 'utf8'); + msg = new Buffer(MAGIC_STRING + msg, 'utf8'); msg = crypto.hash256(msg); key = ec.recover(msg, sig, 0, true); @@ -2472,16 +2311,16 @@ RPC.prototype.verifymessage = co(function* verifymessage(args, help) { }); RPC.prototype.signmessagewithprivkey = co(function* signmessagewithprivkey(args, help) { - var key, msg, sig; + var valid = new Validator([args]); + var key = valid.str(0, ''); + var msg = valid.str(1, ''); + var sig; if (help || args.length !== 2) throw new RPCError('signmessagewithprivkey "privkey" "message"'); - key = toString(args[0]); - msg = toString(args[1]); - key = KeyRing.fromSecret(key, this.network); - msg = new Buffer(RPC.magic + msg, 'utf8'); + msg = new Buffer(MAGIC_STRING + msg, 'utf8'); msg = crypto.hash256(msg); sig = key.sign(msg); @@ -2490,7 +2329,9 @@ RPC.prototype.signmessagewithprivkey = co(function* signmessagewithprivkey(args, }); RPC.prototype.estimatefee = co(function* estimatefee(args, help) { - var blocks, fee; + var valid = new Validator([args]); + var blocks = valid.num(0, 1); + var fee; if (help || args.length !== 1) throw new RPCError('estimatefee nblocks'); @@ -2498,23 +2339,20 @@ RPC.prototype.estimatefee = co(function* estimatefee(args, help) { if (!this.fees) throw new RPCError('Fee estimation not available.'); - blocks = toNumber(args[0], 1); - if (blocks < 1) blocks = 1; fee = this.fees.estimateFee(blocks, false); if (fee === 0) - fee = -1; - else - fee = Amount.btc(fee, true); + return -1; - return fee; + return Amount.btc(fee, true); }); RPC.prototype.estimatepriority = co(function* estimatepriority(args, help) { - var blocks, pri; + var valid = new Validator([args]); + var blocks = valid.num(0, 1); if (help || args.length !== 1) throw new RPCError('estimatepriority nblocks'); @@ -2522,18 +2360,16 @@ RPC.prototype.estimatepriority = co(function* estimatepriority(args, help) { if (!this.fees) throw new RPCError('Priority estimation not available.'); - blocks = toNumber(args[0], 1); - if (blocks < 1) blocks = 1; - pri = this.fees.estimatePriority(blocks, false); - - return pri; + return this.fees.estimatePriority(blocks, false); }); RPC.prototype.estimatesmartfee = co(function* estimatesmartfee(args, help) { - var blocks, fee; + var valid = new Validator([args]); + var blocks = valid.num(0, 1); + var fee; if (help || args.length !== 1) throw new RPCError('estimatesmartfee nblocks'); @@ -2541,8 +2377,6 @@ RPC.prototype.estimatesmartfee = co(function* estimatesmartfee(args, help) { if (!this.fees) throw new RPCError('Fee estimation not available.'); - blocks = toNumber(args[0], 1); - if (blocks < 1) blocks = 1; @@ -2560,7 +2394,9 @@ RPC.prototype.estimatesmartfee = co(function* estimatesmartfee(args, help) { }); RPC.prototype.estimatesmartpriority = co(function* estimatesmartpriority(args, help) { - var blocks, pri; + var valid = new Validator([args]); + var blocks = valid.num(0, 1); + var pri; if (help || args.length !== 1) throw new RPCError('estimatesmartpriority nblocks'); @@ -2568,8 +2404,6 @@ RPC.prototype.estimatesmartpriority = co(function* estimatesmartpriority(args, h if (!this.fees) throw new RPCError('Priority estimation not available.'); - blocks = toNumber(args[0], 1); - if (blocks < 1) blocks = 1; @@ -2582,13 +2416,12 @@ RPC.prototype.estimatesmartpriority = co(function* estimatesmartpriority(args, h }); RPC.prototype.invalidateblock = co(function* invalidateblock(args, help) { - var hash; + var valid = new Validator([args]); + var hash = valid.hash(0); if (help || args.length !== 1) throw new RPCError('invalidateblock "hash"'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Block not found.'); @@ -2598,13 +2431,12 @@ RPC.prototype.invalidateblock = co(function* invalidateblock(args, help) { }); RPC.prototype.reconsiderblock = co(function* reconsiderblock(args, help) { - var hash; + var valid = new Validator([args]); + var hash = valid.hash(0); if (help || args.length !== 1) throw new RPCError('reconsiderblock "hash"'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Block not found.'); @@ -2614,14 +2446,14 @@ RPC.prototype.reconsiderblock = co(function* reconsiderblock(args, help) { }); RPC.prototype.setmocktime = co(function* setmocktime(args, help) { - var ts, delta; + var valid = new Validator([args]); + var ts = valid.num(0); + var delta; if (help || args.length !== 1) throw new RPCError('setmocktime timestamp'); - ts = toNumber(args[0]); - - if (ts < 0) + if (ts == null) throw new RPCError('Invalid parameter.'); this.network.time.offset = 0; @@ -2634,37 +2466,20 @@ RPC.prototype.setmocktime = co(function* setmocktime(args, help) { }); RPC.prototype.getmemory = co(function* getmemory(args, help) { - var mem; - if (help || args.length !== 0) throw new RPCError('getmemory'); - if (!process.memoryUsage) - return {}; - - mem = process.memoryUsage(); - - return { - rss: util.mb(mem.rss), - jsheap: util.mb(mem.heapUsed), - jsheaptotal: util.mb(mem.heapTotal), - nativeheap: util.mb(mem.rss - mem.heapTotal) - }; + return util.memoryUsage(); }); RPC.prototype.setloglevel = co(function* setloglevel(args, help) { - var name, level; + var valid = new Validator([args]); + var level = valid.str(0, ''); if (help || args.length !== 1) throw new RPCError('setloglevel "level"'); - name = toString(args[0]); - level = Logger.levels[name]; - - if (level == null) - throw new RPCError('Bad log level.'); - - this.logger.level = level; + this.logger.setLevel(level); return null; }); @@ -2673,70 +2488,13 @@ RPC.prototype.setloglevel = co(function* setloglevel(args, help) { * Helpers */ -function RPCError(msg) { - Error.call(this); - - if (Error.captureStackTrace) - Error.captureStackTrace(this, RPCError); - - this.type = 'RPCError'; - this.message = msg; -} - -util.inherits(RPCError, Error); - -function toBool(obj, def) { - if (typeof obj === 'boolean' || typeof obj === 'number') - return !!obj; - return def || false; -} - -function toNumber(obj, def) { - if (util.isNumber(obj)) - return obj; - return def != null ? def : -1; -} - -function toString(obj, def) { - if (typeof obj === 'string') - return obj; - return def != null ? def : ''; -} - -function toArray(obj, def) { - if (Array.isArray(obj)) - return obj; - return def != null ? def : null; -} - -function toObject(obj, def) { - if (obj && typeof obj === 'object') - return obj; - return def != null ? def : null; -} - -function toHash(obj) { - if (!isHash(obj)) - return null; - return util.revHex(obj); -} - -function isHash(obj) { - return util.isHex(obj) && obj.length === 64; -} - -function toSatoshi(obj) { - if (typeof obj !== 'number') - throw new RPCError('Bad BTC amount.'); - return Amount.value(obj, true); -} - -function reverseEndian(data) { +function swap32(data) { var i, field; for (i = 0; i < data.length; i += 4) { field = data.readUInt32LE(i, true); data.writeUInt32BE(field, i, true); } + return data; } /* diff --git a/lib/http/rpcbase.js b/lib/http/rpcbase.js new file mode 100644 index 00000000..0d209800 --- /dev/null +++ b/lib/http/rpcbase.js @@ -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; diff --git a/lib/http/server.js b/lib/http/server.js index e6001c82..c8f9e940 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -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; diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 15cd8f89..8fa61a7b 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -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, diff --git a/lib/node/node.js b/lib/node/node.js index d0841a64..d4bda8d7 100644 --- a/lib/node/node.js +++ b/lib/node/node.js @@ -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(); } diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 2d2c552f..59f90f2b 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -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, diff --git a/lib/pkg.js b/lib/pkg.js index fc02c54a..7c1c856e 100644 --- a/lib/pkg.js +++ b/lib/pkg.js @@ -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'; diff --git a/lib/utils/validator.js b/lib/utils/validator.js index b9c17b5d..5b9bfcd0 100644 --- a/lib/utils/validator.js +++ b/lib/utils/validator.js @@ -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; }; diff --git a/lib/wallet/common.js b/lib/wallet/common.js index ba85dd92..183ddda6 100644 --- a/lib/wallet/common.js +++ b/lib/wallet/common.js @@ -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 = []; diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 13ff7202..c19daacd 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -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 */ diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index 77385ffe..4ed35c3a 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -22,214 +22,123 @@ var Outpoint = require('../primitives/outpoint'); var Output = require('../primitives/output'); var TX = require('../primitives/tx'); var encoding = require('../utils/encoding'); -var consensus = require('../protocol/consensus'); +var RPCBase = require('../http/rpcbase'); var pkg = require('../pkg'); +var Validator = require('../utils/validator'); +var common = require('./common'); +var RPCError = RPCBase.RPCError; +var MAGIC_STRING = RPCBase.MAGIC_STRING; /** * Bitcoin Core RPC * @alias module:wallet.RPC * @constructor - * @param {Node} node + * @param {WalletDB} wdb */ -function RPC(walletdb) { +function RPC(wdb) { if (!(this instanceof RPC)) - return new RPC(walletdb); + return new RPC(wdb); - assert(walletdb, 'RPC requires a WalletDB.'); + RPCBase.call(this); - this.network = walletdb.network; - this.logger = walletdb.logger; - this.walletdb = walletdb; + assert(wdb, 'RPC requires a WalletDB.'); + + this.network = wdb.network; + this.logger = wdb.logger; + this.client = wdb.client; + this.wdb = wdb; this.wallet = null; this.locker = new Lock(); this.feeRate = null; + + this.init(); } -RPC.magic = 'Bitcoin Signed Message:\n'; +util.inherits(RPC, RPCBase); -RPC.prototype.attach = function attach(rpc) { - rpc.add('fundrawtransaction', this.fundrawtransaction, this); - rpc.add('resendwallettransactions', this.resendwallettransactions, this); - rpc.add('abandontransaction', this.abandontransaction, this); - rpc.add('addmultisigaddress', this.addmultisigaddress, this); - rpc.add('addwitnessaddress', this.addwitnessaddress, this); - rpc.add('backupwallet', this.backupwallet, this); - rpc.add('dumpprivkey', this.dumpprivkey, this); - rpc.add('dumpwallet', this.dumpwallet, this); - rpc.add('encryptwallet', this.encryptwallet, this); - rpc.add('getaccountaddress', this.getaccountaddress, this); - rpc.add('getaccount', this.getaccount, this); - rpc.add('getaddressesbyaccount', this.getaddressesbyaccount, this); - rpc.add('getbalance', this.getbalance, this); - rpc.add('getnewaddress', this.getnewaddress, this); - rpc.add('getrawchangeaddress', this.getrawchangeaddress, this); - rpc.add('getreceivedbyaccount', this.getreceivedbyaccount, this); - rpc.add('getreceivedbyaddress', this.getreceivedbyaddress, this); - rpc.add('gettransaction', this.gettransaction, this); - rpc.add('getunconfirmedbalance', this.getunconfirmedbalance, this); - rpc.add('getwalletinfo', this.getwalletinfo, this); - rpc.add('importprivkey', this.importprivkey, this); - rpc.add('importwallet', this.importwallet, this); - rpc.add('importaddress', this.importaddress, this); - rpc.add('importprunedfunds', this.importprunedfunds, this); - rpc.add('importpubkey', this.importpubkey, this); - rpc.add('keypoolrefill', this.keypoolrefill, this); - rpc.add('listaccounts', this.listaccounts, this); - rpc.add('listaddressgroupings', this.listaddressgroupings, this); - rpc.add('listlockunspent', this.listlockunspent, this); - rpc.add('listreceivedbyaccount', this.listreceivedbyaccount, this); - rpc.add('listreceivedbyaddress', this.listreceivedbyaddress, this); - rpc.add('listsinceblock', this.listsinceblock, this); - rpc.add('listtransactions', this.listtransactions, this); - rpc.add('listunspent', this.listunspent, this); - rpc.add('lockunspent', this.lockunspent, this); - rpc.add('move', this.move, this); - rpc.add('sendfrom', this.sendfrom, this); - rpc.add('sendmany', this.sendmany, this); - rpc.add('sendtoaddress', this.sendtoaddress, this); - rpc.add('setaccount', this.setaccount, this); - rpc.add('settxfee', this.settxfee, this); - rpc.add('signmessage', this.signmessage, this); - rpc.add('walletlock', this.walletlock, this); - rpc.add('walletpassphrasechange', this.walletpassphrasechange, this); - rpc.add('walletpassphrase', this.walletpassphrase, this); - rpc.add('removeprunedfunds', this.removeprunedfunds, this); -}; - -RPC.prototype.execute = function execute(json, help) { - switch (json.method) { - case 'fundrawtransaction': - return this.fundrawtransaction(json.params, help); - case 'resendwallettransactions': - return this.resendwallettransactions(json.params, help); - case 'abandontransaction': - return this.abandontransaction(json.params, help); - case 'addmultisigaddress': - return this.addmultisigaddress(json.params, help); - case 'addwitnessaddress': - return this.addwitnessaddress(json.params, help); - case 'backupwallet': - return this.backupwallet(json.params, help); - case 'dumpprivkey': - return this.dumpprivkey(json.params, help); - case 'dumpwallet': - return this.dumpwallet(json.params, help); - case 'encryptwallet': - return this.encryptwallet(json.params, help); - case 'getaccountaddress': - return this.getaccountaddress(json.params, help); - case 'getaccount': - return this.getaccount(json.params, help); - case 'getaddressesbyaccount': - return this.getaddressesbyaccount(json.params, help); - case 'getbalance': - return this.getbalance(json.params, help); - case 'getnewaddress': - return this.getnewaddress(json.params, help); - case 'getrawchangeaddress': - return this.getrawchangeaddress(json.params, help); - case 'getreceivedbyaccount': - return this.getreceivedbyaccount(json.params, help); - case 'getreceivedbyaddress': - return this.getreceivedbyaddress(json.params, help); - case 'gettransaction': - return this.gettransaction(json.params, help); - case 'getunconfirmedbalance': - return this.getunconfirmedbalance(json.params, help); - case 'getwalletinfo': - return this.getwalletinfo(json.params, help); - case 'importprivkey': - return this.importprivkey(json.params, help); - case 'importwallet': - return this.importwallet(json.params, help); - case 'importaddress': - return this.importaddress(json.params, help); - case 'importprunedfunds': - return this.importprunedfunds(json.params, help); - case 'importpubkey': - return this.importpubkey(json.params, help); - case 'keypoolrefill': - return this.keypoolrefill(json.params, help); - case 'listaccounts': - return this.listaccounts(json.params, help); - case 'listaddressgroupings': - return this.listaddressgroupings(json.params, help); - case 'listlockunspent': - return this.listlockunspent(json.params, help); - case 'listreceivedbyaccount': - return this.listreceivedbyaccount(json.params, help); - case 'listreceivedbyaddress': - return this.listreceivedbyaddress(json.params, help); - case 'listsinceblock': - return this.listsinceblock(json.params, help); - case 'listtransactions': - return this.listtransactions(json.params, help); - case 'listunspent': - return this.listunspent(json.params, help); - case 'lockunspent': - return this.lockunspent(json.params, help); - case 'move': - return this.move(json.params, help); - case 'sendfrom': - return this.sendfrom(json.params, help); - case 'sendmany': - return this.sendmany(json.params, help); - case 'sendtoaddress': - return this.sendtoaddress(json.params, help); - case 'setaccount': - return this.setaccount(json.params, help); - case 'settxfee': - return this.settxfee(json.params, help); - case 'signmessage': - return this.signmessage(json.params, help); - case 'walletlock': - return this.walletlock(json.params, help); - case 'walletpassphrasechange': - return this.walletpassphrasechange(json.params, help); - case 'walletpassphrase': - return this.walletpassphrase(json.params, help); - case 'removeprunedfunds': - return this.removeprunedfunds(json.params, help); - default: - return Promise.reject(new Error('Unknown RPC call: ' + json.method)); - } +RPC.prototype.init = function init() { + this.add('fundrawtransaction', this.fundrawtransaction); + this.add('resendwallettransactions', this.resendwallettransactions); + this.add('abandontransaction', this.abandontransaction); + this.add('addmultisigaddress', this.addmultisigaddress); + this.add('addwitnessaddress', this.addwitnessaddress); + this.add('backupwallet', this.backupwallet); + this.add('dumpprivkey', this.dumpprivkey); + this.add('dumpwallet', this.dumpwallet); + this.add('encryptwallet', this.encryptwallet); + this.add('getaccountaddress', this.getaccountaddress); + this.add('getaccount', this.getaccount); + this.add('getaddressesbyaccount', this.getaddressesbyaccount); + this.add('getbalance', this.getbalance); + this.add('getnewaddress', this.getnewaddress); + this.add('getrawchangeaddress', this.getrawchangeaddress); + this.add('getreceivedbyaccount', this.getreceivedbyaccount); + this.add('getreceivedbyaddress', this.getreceivedbyaddress); + this.add('gettransaction', this.gettransaction); + this.add('getunconfirmedbalance', this.getunconfirmedbalance); + this.add('getwalletinfo', this.getwalletinfo); + this.add('importprivkey', this.importprivkey); + this.add('importwallet', this.importwallet); + this.add('importaddress', this.importaddress); + this.add('importprunedfunds', this.importprunedfunds); + this.add('importpubkey', this.importpubkey); + this.add('keypoolrefill', this.keypoolrefill); + this.add('listaccounts', this.listaccounts); + this.add('listaddressgroupings', this.listaddressgroupings); + this.add('listlockunspent', this.listlockunspent); + this.add('listreceivedbyaccount', this.listreceivedbyaccount); + this.add('listreceivedbyaddress', this.listreceivedbyaddress); + this.add('listsinceblock', this.listsinceblock); + this.add('listtransactions', this.listtransactions); + this.add('listunspent', this.listunspent); + this.add('lockunspent', this.lockunspent); + this.add('move', this.move); + this.add('sendfrom', this.sendfrom); + this.add('sendmany', this.sendmany); + this.add('sendtoaddress', this.sendtoaddress); + this.add('setaccount', this.setaccount); + this.add('settxfee', this.settxfee); + this.add('signmessage', this.signmessage); + this.add('walletlock', this.walletlock); + this.add('walletpassphrasechange', this.walletpassphrasechange); + this.add('walletpassphrase', this.walletpassphrase); + this.add('removeprunedfunds', this.removeprunedfunds); }; RPC.prototype.fundrawtransaction = co(function* fundrawtransaction(args, help) { + var valid = new Validator([args]); + var data = valid.buf(0); + var options = valid.obj(1); var wallet = this.wallet; - var feeRate = this.feeRate; - var tx, options, changeAddress; + var rate = this.feeRate; + var change, tx; if (help || args.length < 1 || args.length > 2) throw new RPCError('fundrawtransaction "hexstring" ( options )'); - if (!util.isHex(args[0])) + if (!data) throw new RPCError('Invalid parameter.'); - tx = MTX.fromRaw(args[0], 'hex'); + tx = MTX.fromRaw(data); if (tx.outputs.length === 0) throw new RPCError('TX must have at least one output.'); - if (args.length > 1) { - options = toObject(args[1]); - changeAddress = toString(options.changeAddress); + if (options) { + valid = new Validator([options]); + change = valid.str('changeAddress'); + rate = valid.btc('feeRate'); - if (changeAddress) - changeAddress = Address.fromBase58(changeAddress, this.network); - - feeRate = options.feeRate; - - if (feeRate != null) - feeRate = toSatoshi(feeRate); + if (change) + change = Address.fromBase58(change, this.network); } options = { - rate: feeRate, - changeAddress: changeAddress + rate: rate, + changeAddress: change }; yield wallet.fund(tx, options); @@ -241,72 +150,6 @@ RPC.prototype.fundrawtransaction = co(function* fundrawtransaction(args, help) { }; }); -RPC.prototype._createRedeem = co(function* _createRedeem(args, help) { - var wallet = this.wallet; - var i, m, n, keys, hash, script, key, ring; - - if (!util.isNumber(args[0]) - || !Array.isArray(args[1]) - || args[0] < 1 - || args[1].length < args[0] - || args[1].length > 16) { - throw new RPCError('Invalid parameter.'); - } - - m = args[0]; - n = args[1].length; - keys = args[1]; - - for (i = 0; i < keys.length; i++) { - key = keys[i]; - - if (!util.isBase58(key)) { - if (!util.isHex(key)) - throw new RPCError('Invalid key.'); - keys[i] = new Buffer(key, 'hex'); - continue; - } - - hash = Address.getHash(key, 'hex'); - - if (!hash) - throw new RPCError('Invalid key.'); - - ring = yield wallet.getKey(hash); - - if (!ring) - throw new RPCError('Invalid key.'); - - keys[i] = ring.publicKey; - } - - try { - script = Script.fromMultisig(m, n, keys); - } catch (e) { - throw new RPCError('Invalid parameters.'); - } - - if (script.getSize() > consensus.MAX_SCRIPT_PUSH) - throw new RPCError('Redeem script exceeds size limit.'); - - return script; -}); - -RPC.prototype.createmultisig = co(function* createmultisig(args, help) { - var script, address; - - if (help || args.length < 2 || args.length > 2) - throw new RPCError('createmultisig nrequired ["key",...]'); - - script = yield this._createRedeem(args); - address = script.getAddress(); - - return { - address: address.toBase58(this.network), - redeemScript: script.toJSON() - }; -}); - /* * Wallet */ @@ -348,27 +191,27 @@ RPC.prototype.addwitnessaddress = co(function* addwitnessaddress(args, help) { }); RPC.prototype.backupwallet = co(function* backupwallet(args, help) { - var dest; + var valid = new Validator([args]); + var dest = valid.str(0); - if (help || args.length !== 1) + if (help || args.length !== 1 || !dest) throw new RPCError('backupwallet "destination"'); - dest = toString(args[0]); - - yield this.walletdb.backup(dest); + yield this.wdb.backup(dest); return null; }); RPC.prototype.dumpprivkey = co(function* dumpprivkey(args, help) { var wallet = this.wallet; - var hash, ring; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var hash = Address.getHash(addr, 'hex'); + var ring; if (help || args.length !== 1) throw new RPCError('dumpprivkey "bitcoinaddress"'); - hash = Address.getHash(toString(args[0]), 'hex'); - if (!hash) throw new RPCError('Invalid address.'); @@ -382,22 +225,24 @@ RPC.prototype.dumpprivkey = co(function* dumpprivkey(args, help) { RPC.prototype.dumpwallet = co(function* dumpwallet(args, help) { var wallet = this.wallet; - var i, file, time, address, fmt, str, out, hash, hashes, ring; + var valid = new Validator([args]); + var file = valid.str(0); + var time = util.date(); + var i, tip, addr, fmt, str, out, hash, hashes, ring; if (help || args.length !== 1) throw new RPCError('dumpwallet "filename"'); - if (!args[0] || typeof args[0] !== 'string') + if (!file) throw new RPCError('Invalid parameter.'); - file = toString(args[0]); - time = util.date(); + tip = yield this.wdb.getTip(); + out = [ util.fmt('# Wallet Dump created by Bcoin %s', pkg.version), util.fmt('# * Created on %s', time), - util.fmt('# * Best block at time of backup was %d (%s),', - this.chain.height, this.chain.tip.rhash()), - util.fmt('# mined on %s', util.date(this.chain.tip.ts)), + util.fmt('# * Best block at time of backup was %d (%s).', + tip.height, util.revHex(tip.hash)), util.fmt('# * File: %s', file), '' ]; @@ -411,13 +256,13 @@ RPC.prototype.dumpwallet = co(function* dumpwallet(args, help) { if (!ring) continue; - address = ring.getAddress('base58'); + addr = ring.getAddress('base58'); fmt = '%s %s label= addr=%s'; if (ring.branch === 1) fmt = '%s %s change=1 addr=%s'; - str = util.fmt(fmt, ring.toSecret(), time, address); + str = util.fmt(fmt, ring.toSecret(), time, addr); out.push(str); } @@ -433,12 +278,13 @@ RPC.prototype.dumpwallet = co(function* dumpwallet(args, help) { yield fs.writeFile(file, out, 'utf8'); - return out; + return null; }); RPC.prototype.encryptwallet = co(function* encryptwallet(args, help) { var wallet = this.wallet; - var passphrase; + var valid = new Validator([args]); + var passphrase = valid.str(0, ''); if (!wallet.master.encrypted && (help || args.length !== 1)) throw new RPCError('encryptwallet "passphrase"'); @@ -446,8 +292,6 @@ RPC.prototype.encryptwallet = co(function* encryptwallet(args, help) { if (wallet.master.encrypted) throw new RPCError('Already running with an encrypted wallet'); - passphrase = toString(args[0]); - if (passphrase.length < 1) throw new RPCError('encryptwallet "passphrase"'); @@ -457,18 +301,18 @@ RPC.prototype.encryptwallet = co(function* encryptwallet(args, help) { }); RPC.prototype.getaccountaddress = co(function* getaccountaddress(args, help) { + var valid = new Validator([args]); var wallet = this.wallet; + var name = valid.str(0, ''); var account; if (help || args.length !== 1) throw new RPCError('getaccountaddress "account"'); - account = toString(args[0]); + if (!name) + name = 'default'; - if (!account) - account = 'default'; - - account = yield wallet.getAccount(account); + account = yield wallet.getAccount(name); if (!account) return ''; @@ -478,13 +322,14 @@ RPC.prototype.getaccountaddress = co(function* getaccountaddress(args, help) { RPC.prototype.getaccount = co(function* getaccount(args, help) { var wallet = this.wallet; - var hash, path; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var hash = Address.getHash(addr, 'hex'); + var path; if (help || args.length !== 1) throw new RPCError('getaccount "bitcoinaddress"'); - hash = Address.getHash(args[0], 'hex'); - if (!hash) throw new RPCError('Invalid address.'); @@ -498,19 +343,19 @@ RPC.prototype.getaccount = co(function* getaccount(args, help) { RPC.prototype.getaddressesbyaccount = co(function* getaddressesbyaccount(args, help) { var wallet = this.wallet; - var i, path, account, address, addrs, paths; + var valid = new Validator([args]); + var name = valid.str(0, ''); + var i, path, address, addrs, paths; if (help || args.length !== 1) throw new RPCError('getaddressesbyaccount "account"'); - account = toString(args[0]); - - if (!account) - account = 'default'; + if (name === '') + name = 'default'; addrs = []; - paths = yield wallet.getPaths(account); + paths = yield wallet.getPaths(name); for (i = 0; i < paths.length; i++) { path = paths[i]; @@ -523,26 +368,25 @@ RPC.prototype.getaddressesbyaccount = co(function* getaddressesbyaccount(args, h RPC.prototype.getbalance = co(function* getbalance(args, help) { var wallet = this.wallet; - var minconf = 0; - var account, value, balance; + var valid = new Validator([args]); + var name = valid.str(0); + var minconf = valid.num(1, 0); + var watchOnly = valid.bool(2, false); + var value, balance; if (help || args.length > 3) throw new RPCError('getbalance ( "account" minconf includeWatchonly )'); - if (args.length >= 1) { - account = toString(args[0]); + if (name === '') + name = 'default'; - if (!account) - account = 'default'; + if (name === '*') + name = null; - if (account === '*') - account = null; - } + if (wallet.watchOnly !== watchOnly) + return 0; - if (args.length >= 2) - minconf = toNumber(args[1], 0); - - balance = yield wallet.getBalance(account); + balance = yield wallet.getBalance(name); if (minconf) value = balance.confirmed; @@ -554,18 +398,17 @@ RPC.prototype.getbalance = co(function* getbalance(args, help) { RPC.prototype.getnewaddress = co(function* getnewaddress(args, help) { var wallet = this.wallet; - var account, address; + var valid = new Validator([args]); + var name = valid.str(0); + var address; if (help || args.length > 1) throw new RPCError('getnewaddress ( "account" )'); - if (args.length === 1) - account = toString(args[0]); + if (name === '') + name = 'default'; - if (!account) - account = 'default'; - - address = yield wallet.createReceive(account); + address = yield wallet.createReceive(name); return address.getAddress('base58'); }); @@ -584,36 +427,34 @@ RPC.prototype.getrawchangeaddress = co(function* getrawchangeaddress(args, help) RPC.prototype.getreceivedbyaccount = co(function* getreceivedbyaccount(args, help) { var wallet = this.wallet; - var minconf = 0; + var valid = new Validator([args]); + var name = valid.str(0); + var minconf = valid.num(0, 0); + var height = this.wdb.state.height; var total = 0; var filter = {}; var lastConf = -1; - var i, j, path, wtx, output, conf, hash, account, paths, txs; + var i, j, path, wtx, output, conf, hash, paths, txs; if (help || args.length < 1 || args.length > 2) throw new RPCError('getreceivedbyaccount "account" ( minconf )'); - account = toString(args[0]); + if (name === '') + name = 'default'; - if (!account) - account = 'default'; - - if (args.length === 2) - minconf = toNumber(args[1], 0); - - paths = yield wallet.getPaths(account); + paths = yield wallet.getPaths(name); for (i = 0; i < paths.length; i++) { path = paths[i]; filter[path.hash] = true; } - txs = yield wallet.getHistory(account); + txs = yield wallet.getHistory(name); for (i = 0; i < txs.length; i++) { wtx = txs[i]; - conf = wtx.getDepth(this.chain.height); + conf = wtx.getDepth(height); if (conf < minconf) continue; @@ -634,27 +475,26 @@ RPC.prototype.getreceivedbyaccount = co(function* getreceivedbyaccount(args, hel RPC.prototype.getreceivedbyaddress = co(function* getreceivedbyaddress(args, help) { var wallet = this.wallet; - var minconf = 0; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var minconf = valid.num(1, 0); + var hash = Address.getHash(addr, 'hex'); + var height = this.wdb.state.height; var total = 0; - var i, j, hash, wtx, output, txs; + var i, j, wtx, output, txs; if (help || args.length < 1 || args.length > 2) throw new RPCError('getreceivedbyaddress "bitcoinaddress" ( minconf )'); - hash = Address.getHash(toString(args[0]), 'hex'); - if (!hash) throw new RPCError('Invalid address'); - if (args.length === 2) - minconf = toNumber(args[1], 0); - txs = yield wallet.getHistory(); for (i = 0; i < txs.length; i++) { wtx = txs[i]; - if (wtx.getDepth(this.chain.height) < minconf) + if (wtx.getDepth(height) < minconf) continue; for (j = 0; j < wtx.tx.outputs.length; j++) { @@ -743,13 +583,14 @@ RPC.prototype._toWalletTX = co(function* _toWalletTX(wtx) { RPC.prototype.gettransaction = co(function* gettransaction(args, help) { var wallet = this.wallet; - var hash, wtx; + var valid = new Validator([args]); + var hash = valid.hash(0); + var watchOnly = valid.bool(1, false); + var wtx; if (help || args.length < 1 || args.length > 2) throw new RPCError('gettransaction "txid" ( includeWatchonly )'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter'); @@ -758,18 +599,18 @@ RPC.prototype.gettransaction = co(function* gettransaction(args, help) { if (!wtx) throw new RPCError('TX not found.'); - return yield this._toWalletTX(wtx); + return yield this._toWalletTX(wtx, watchOnly); }); RPC.prototype.abandontransaction = co(function* abandontransaction(args, help) { var wallet = this.wallet; - var hash, result; + var valid = new Validator([args]); + var hash = valid.hash(0); + var result; if (help || args.length !== 1) throw new RPCError('abandontransaction "txid"'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter.'); @@ -819,37 +660,33 @@ RPC.prototype.getwalletinfo = co(function* getwalletinfo(args, help) { RPC.prototype.importprivkey = co(function* importprivkey(args, help) { var wallet = this.wallet; - var secret, label, rescan, key; + var valid = new Validator([args]); + var secret = valid.str(0); + var rescan = valid.bool(2, false); + var key; if (help || args.length < 1 || args.length > 3) throw new RPCError('importprivkey "bitcoinprivkey" ( "label" rescan )'); - secret = toString(args[0]); - - if (args.length > 1) - label = toString(args[1]); - - if (args.length > 2) - rescan = toBool(args[2]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - key = KeyRing.fromSecret(secret, this.network); yield wallet.importKey(0, key); if (rescan) - yield this.walletdb.rescan(0); + yield this.wdb.rescan(0); return null; }); RPC.prototype.importwallet = co(function* importwallet(args, help) { var wallet = this.wallet; - var file, keys, lines, line, parts; - var i, secret, time, label, addr; - var data, key, rescan; + var valid = new Validator([args]); + var file = valid.str(0); + var rescan = valid.bool(1, false); + var keys = []; + var i, lines, line, parts; + var secret, time, label, addr; + var data, key; if (help || args.length !== 1) throw new RPCError('importwallet "filename" ( rescan )'); @@ -857,18 +694,9 @@ RPC.prototype.importwallet = co(function* importwallet(args, help) { if (fs.unsupported) throw new RPCError('FS not available.'); - file = toString(args[0]); - - if (args.length > 1) - rescan = toBool(args[1]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - data = yield fs.readFile(file, 'utf8'); lines = data.split(/\n+/); - keys = []; for (i = 0; i < lines.length; i++) { line = lines[i].trim(); @@ -899,71 +727,63 @@ RPC.prototype.importwallet = co(function* importwallet(args, help) { } if (rescan) - yield this.walletdb.rescan(0); + yield this.wdb.rescan(0); return null; }); RPC.prototype.importaddress = co(function* importaddress(args, help) { var wallet = this.wallet; - var addr, label, rescan, p2sh; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var rescan = valid.bool(2, false); + var p2sh = valid.bool(3, false); + var script; if (help || args.length < 1 || args.length > 4) throw new RPCError('importaddress "address" ( "label" rescan p2sh )'); - addr = toString(args[0]); + if (p2sh) { + script = valid.buf(0); - if (args.length > 1) - label = toString(args[1]); + if (!script) + throw new RPCError('Invalid parameters.'); - if (args.length > 2) - rescan = toBool(args[2]); + script = Script.fromRaw(script); + script = Script.fromScripthash(script.hash160()); - if (args.length > 3) - p2sh = toBool(args[3]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - - addr = Address.fromBase58(addr, this.network); + addr = script.getAddress(); + } else { + addr = Address.fromBase58(addr, this.network); + } yield wallet.importAddress(0, addr); if (rescan) - yield this.walletdb.rescan(0); + yield this.wdb.rescan(0); return null; }); RPC.prototype.importpubkey = co(function* importpubkey(args, help) { var wallet = this.wallet; - var pubkey, label, rescan, key; + var valid = new Validator([args]); + var data = valid.buf(0); + var rescan = valid.bool(2, false); + var key; if (help || args.length < 1 || args.length > 4) throw new RPCError('importpubkey "pubkey" ( "label" rescan )'); - pubkey = toString(args[0]); - - if (!util.isHex(pubkey)) + if (!data) throw new RPCError('Invalid parameter.'); - if (args.length > 1) - label = toString(args[1]); - - if (args.length > 2) - rescan = toBool(args[2]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - - pubkey = new Buffer(pubkey, 'hex'); - - key = KeyRing.fromPublic(pubkey, this.network); + key = KeyRing.fromPublic(data, this.network); yield wallet.importKey(0, key); if (rescan) - yield this.walletdb.rescan(0); + yield this.wdb.rescan(0); return null; }); @@ -976,18 +796,30 @@ RPC.prototype.keypoolrefill = co(function* keypoolrefill(args, help) { RPC.prototype.listaccounts = co(function* listaccounts(args, help) { var wallet = this.wallet; - var i, map, accounts, account, balance; + var valid = new Validator([args]); + var minconf = valid.num(0, 0); + var watchOnly = valid.bool(1, false); + var map = {}; + var i, accounts, account, balance, value; if (help || args.length > 2) throw new RPCError('listaccounts ( minconf includeWatchonly)'); - map = {}; accounts = yield wallet.getAccounts(); for (i = 0; i < accounts.length; i++) { account = accounts[i]; balance = yield wallet.getBalance(account); - map[account] = Amount.btc(balance.unconfirmed, true); + + value = balance.unconfirmed; + + if (minconf > 0) + value = balance.confirmed; + + if (wallet.watchOnly !== watchOnly) + value = 0; + + map[account] = Amount.btc(value, true); } return map; @@ -1021,47 +853,40 @@ RPC.prototype.listlockunspent = co(function* listlockunspent(args, help) { }); RPC.prototype.listreceivedbyaccount = co(function* listreceivedbyaccount(args, help) { - var minconf = 0; - var includeEmpty = false; + var valid = new Validator([args]); + var minconf = valid.num(0, 0); + var includeEmpty = valid.bool(1, false); + var watchOnly = valid.bool(2, false); if (help || args.length > 3) { throw new RPCError('listreceivedbyaccount' + ' ( minconf includeempty includeWatchonly )'); } - if (args.length > 0) - minconf = toNumber(args[0], 0); - - if (args.length > 1) - includeEmpty = toBool(args[1], false); - - return yield this._listReceived(minconf, includeEmpty, true); + return yield this._listReceived(minconf, includeEmpty, watchOnly, true); }); RPC.prototype.listreceivedbyaddress = co(function* listreceivedbyaddress(args, help) { - var minconf = 0; - var includeEmpty = false; + var valid = new Validator([args]); + var minconf = valid.num(0, 0); + var includeEmpty = valid.bool(1, false); + var watchOnly = valid.bool(2, false); if (help || args.length > 3) { throw new RPCError('listreceivedbyaddress' + ' ( minconf includeempty includeWatchonly )'); } - if (args.length > 0) - minconf = toNumber(args[0], 0); - - if (args.length > 1) - includeEmpty = toBool(args[1], false); - - return yield this._listReceived(minconf, includeEmpty, false); + return yield this._listReceived(minconf, includeEmpty, watchOnly, false); }); -RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, account) { +RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, watchOnly, account) { var wallet = this.wallet; + var paths = yield wallet.getPaths(); + var height = this.wdb.state.height; var out = []; var result = []; var map = {}; - var paths = yield wallet.getPaths(); var i, j, path, wtx, output, conf, hash; var entry, address, keys, key, item, txs; @@ -1083,7 +908,7 @@ RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, account for (i = 0; i < txs.length; i++) { wtx = txs[i]; - conf = wtx.getDepth(this.chain.height); + conf = wtx.getDepth(height); if (conf < minconf) continue; @@ -1157,27 +982,29 @@ RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, account RPC.prototype.listsinceblock = co(function* listsinceblock(args, help) { var wallet = this.wallet; - var minconf = 0; + var chainHeight = this.wdb.state.height; + var valid = new Validator([args]); + var block = valid.hash(0); + var minconf = valid.num(1, 0); + var watchOnly = valid.bool(2, false); + var height = -1; var out = []; - var i, block, highest, height; - var txs, wtx, json; + var i, entry, highest, txs, wtx, json; if (help) { throw new RPCError('listsinceblock' + ' ( "blockhash" target-confirmations includeWatchonly)'); } - if (args.length > 0) { - block = toHash(args[0]); - if (!block) - throw new RPCError('Invalid parameter.'); + if (wallet.watchOnly !== watchOnly) + return out; + + if (block) { + entry = yield this.client.getEntry(block); + if (entry) + height = entry.height; } - if (args.length > 1) - minconf = toNumber(args[1], 0); - - height = yield this.chain.db.getHeight(block); - if (height === -1) height = this.chain.height; @@ -1189,7 +1016,7 @@ RPC.prototype.listsinceblock = co(function* listsinceblock(args, help) { if (wtx.height < height) continue; - if (wtx.getDepth(this.chain.height) < minconf) + if (wtx.getDepth(chainHeight) < minconf) continue; if (!highest || wtx.height > highest) @@ -1210,12 +1037,12 @@ RPC.prototype.listsinceblock = co(function* listsinceblock(args, help) { RPC.prototype._toListTX = co(function* _toListTX(wtx) { var wallet = this.wallet; + var details = yield wallet.toDetails(wtx); var sent = 0; var received = 0; var receive = true; var sendMember, recMember, sendIndex, recIndex; var i, member, index; - var details = yield wallet.toDetails(wtx); if (!details) throw new RPCError('TX not found.'); @@ -1283,8 +1110,13 @@ RPC.prototype._toListTX = co(function* _toListTX(wtx) { RPC.prototype.listtransactions = co(function* listtransactions(args, help) { var wallet = this.wallet; - var account = null; - var count = 10; + var valid = new Validator([args]); + var name = valid.str(0); + var count = valid.num(1, 10); + var from = valid.num(2, 0); + var watchOnly = valid.bool(3, false); + var end = from + count; + var out = []; var i, txs, wtx, json; if (help || args.length > 4) { @@ -1292,75 +1124,69 @@ RPC.prototype.listtransactions = co(function* listtransactions(args, help) { 'listtransactions ( "account" count from includeWatchonly)'); } - if (args.length > 0) { - account = toString(args[0]); - if (!account) - account = 'default'; - } + if (wallet.watchOnly !== watchOnly) + return out; - if (args.length > 1) { - count = toNumber(args[1], 10); - if (count < 0) - count = 10; - } + if (name === '') + name = 'default'; + + if (count < 0) + count = 10; txs = yield wallet.getHistory(); - sortTX(txs); + common.sortTX(txs); - for (i = 0; i < txs.length; i++) { + end = Math.min(end, txs.length); + + for (i = from; i < end; i++) { wtx = txs[i]; json = yield this._toListTX(wtx); - txs[i] = json; + out.push(json); } - return txs; + return out; }); RPC.prototype.listunspent = co(function* listunspent(args, help) { var wallet = this.wallet; - var minDepth = 1; - var maxDepth = 9999999; + var valid = new Validator([args]); + var minDepth = valid.num(0, 1); + var maxDepth = valid.num(1, 9999999); + var addrs = valid.array(2); + var height = this.wdb.state.height; var out = []; - var i, addresses, addrs, depth, address, hash, coins, coin, ring; + var map = {}; + var i, depth, address, hash, coins, coin, ring; if (help || args.length > 3) { throw new RPCError('listunspent' + ' ( minconf maxconf ["address",...] )'); } - if (args.length > 0) - minDepth = toNumber(args[0], 1); - - if (args.length > 1) - maxDepth = toNumber(args[1], maxDepth); - - if (args.length > 2) - addrs = toArray(args[2]); - if (addrs) { - addresses = {}; + valid = new Validator([addrs]); for (i = 0; i < addrs.length; i++) { - address = toString(addrs[i]); + address = valid.str(i, ''); hash = Address.getHash(address, 'hex'); if (!hash) throw new RPCError('Invalid address.'); - if (addresses[hash]) + if (map[hash]) throw new RPCError('Duplicate address.'); - addresses[hash] = true; + map[hash] = true; } } coins = yield wallet.getCoins(); - sortCoins(coins); + common.sortCoins(coins); for (i = 0; i < coins.length; i++ ) { coin = coins[i]; - depth = coin.getDepth(this.chain.height); + depth = coin.getDepth(height); if (!(depth >= minDepth && depth <= maxDepth)) continue; @@ -1372,8 +1198,8 @@ RPC.prototype.listunspent = co(function* listunspent(args, help) { hash = coin.getHash('hex'); - if (addresses) { - if (!hash || !addresses[hash]) + if (addrs) { + if (!hash || !map[hash]) continue; } @@ -1400,41 +1226,37 @@ RPC.prototype.listunspent = co(function* listunspent(args, help) { RPC.prototype.lockunspent = co(function* lockunspent(args, help) { var wallet = this.wallet; - var i, unlock, outputs, output, outpoint; + var valid = new Validator([args]); + var unlock = valid.bool(0, false); + var outputs = valid.array(1); + var i, output, outpoint, hash, index; if (help || args.length < 1 || args.length > 2) { throw new RPCError('lockunspent' + ' unlock ([{"txid":"txid","vout":n},...])'); } - unlock = toBool(args[0]); - if (args.length === 1) { if (unlock) wallet.unlockCoins(); return true; } - outputs = toArray(args[1]); - if (!outputs) throw new RPCError('Invalid parameter.'); for (i = 0; i < outputs.length; i++) { output = outputs[i]; + valid = new Validator([output]); + hash = valid.hash('txid'); + index = valid.num('vout'); - if (!output || typeof output !== 'object') + if (hash == null || index == null || index < 0) throw new RPCError('Invalid parameter.'); outpoint = new Outpoint(); - outpoint.hash = toHash(output.txid); - outpoint.index = toNumber(output.vout); - - if (!outpoint.hash) - throw new RPCError('Invalid parameter.'); - - if (outpoint.index < 0) - throw new RPCError('Invalid parameter.'); + outpoint.hash = hash; + outpoint.index = index; if (unlock) { wallet.unlockCoin(outpoint); @@ -1452,17 +1274,37 @@ RPC.prototype.move = co(function* move(args, help) { throw new Error('Not implemented.'); }); -RPC.prototype._send = co(function* _send(account, address, amount, subtractFee) { +RPC.prototype.sendfrom = co(function* sendfrom(args, help) { var wallet = this.wallet; - var tx, options; + var valid = new Validator([args]); + var name = valid.str(0); + var addr = valid.str(1, ''); + var value = valid.btc(2, -1); + var minconf = valid.num(3, 0); + var options, tx; + + if (help || args.length < 3 || args.length > 6) { + throw new RPCError('sendfrom' + + ' "fromaccount" "tobitcoinaddress"' + + ' amount ( minconf "comment" "comment-to" )'); + } + + if (!addr || value < 0) + throw new RPCError('Invalid parameter.'); + + addr = Address.fromBase58(addr, this.network); + + if (name === '') + name = 'default'; options = { - account: account, - subtractFee: subtractFee, + account: name, + subtractFee: false, rate: this.feeRate, + depth: minconf, outputs: [{ - address: address, - value: amount + address: addr, + value: value }] }; @@ -1471,31 +1313,15 @@ RPC.prototype._send = co(function* _send(account, address, amount, subtractFee) return tx.txid(); }); -RPC.prototype.sendfrom = co(function* sendfrom(args, help) { - var account, address, amount; - - if (help || args.length < 3 || args.length > 6) { - throw new RPCError('sendfrom' - + ' "fromaccount" "tobitcoinaddress"' - + ' amount ( minconf "comment" "comment-to" )'); - } - - account = toString(args[0]); - address = Address.fromBase58(toString(args[1]), this.network); - amount = toSatoshi(args[2]); - - if (!account) - account = 'default'; - - return yield this._send(account, address, amount, false); -}); - RPC.prototype.sendmany = co(function* sendmany(args, help) { var wallet = this.wallet; - var minconf = 1; + var valid = new Validator([args]); + var name = valid.str(0); + var sendTo = valid.obj(1); + var minconf = valid.num(2, 1); + var subtractFee = valid.bool(4, false); var outputs = []; var uniq = {}; - var account, sendTo, comment, subtractFee; var i, keys, tx, key, value, address; var hash, output, options; @@ -1505,37 +1331,24 @@ RPC.prototype.sendmany = co(function* sendmany(args, help) { + ' ( minconf "comment" ["address",...] )'); } - account = toString(args[0]); - sendTo = toObject(args[1]); - - if (!account) - account = 'default'; + if (name === '') + name = 'default'; if (!sendTo) throw new RPCError('Invalid parameter.'); - if (args.length > 2) - minconf = toNumber(args[2], 1); - - if (args.length > 3) - comment = toString(args[3]); - - if (args.length > 4) { - subtractFee = args[4]; - if (typeof subtractFee !== 'boolean') { - if (!util.isNumber(subtractFee)) - throw new RPCError('Invalid parameter.'); - } - } - keys = Object.keys(sendTo); + valid = new Validator([sendTo]); for (i = 0; i < keys.length; i++) { key = keys[i]; - value = toSatoshi(sendTo[key]); + value = valid.btc(key); address = Address.fromBase58(key, this.network); hash = address.getHash('hex'); + if (value == null) + throw new RPCError('Invalid parameter.'); + if (uniq[hash]) throw new RPCError('Invalid parameter.'); @@ -1550,7 +1363,7 @@ RPC.prototype.sendmany = co(function* sendmany(args, help) { options = { outputs: outputs, subtractFee: subtractFee, - account: account, + account: name, depth: minconf }; @@ -1560,7 +1373,12 @@ RPC.prototype.sendmany = co(function* sendmany(args, help) { }); RPC.prototype.sendtoaddress = co(function* sendtoaddress(args, help) { - var address, amount, subtractFee; + var wallet = this.wallet; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var value = valid.btc(1, -1); + var subtractFee = valid.bool(4, false); + var options, tx; if (help || args.length < 2 || args.length > 5) { throw new RPCError('sendtoaddress' @@ -1569,11 +1387,23 @@ RPC.prototype.sendtoaddress = co(function* sendtoaddress(args, help) { + ' subtractfeefromamount )'); } - address = Address.fromBase58(toString(args[0]), this.network); - amount = toSatoshi(args[1]); - subtractFee = toBool(args[4]); + addr = Address.fromBase58(addr, this.network); - return yield this._send(null, address, amount, subtractFee); + if (value < 0) + throw new RPCError('Invalid parameter.'); + + options = { + subtractFee: subtractFee, + rate: this.feeRate, + outputs: [{ + address: addr, + value: value + }] + }; + + tx = yield wallet.send(options); + + return tx.txid(); }); RPC.prototype.setaccount = co(function* setaccount(args, help) { @@ -1585,30 +1415,36 @@ RPC.prototype.setaccount = co(function* setaccount(args, help) { }); RPC.prototype.settxfee = co(function* settxfee(args, help) { + var valid = new Validator([args]); + var rate = valid.btc(0, -1); + if (help || args.length < 1 || args.length > 1) throw new RPCError('settxfee amount'); - this.feeRate = toSatoshi(args[0]); + if (rate < 0) + throw new RPCError('Invalid parameter.'); + + this.feeRate = rate; return true; }); RPC.prototype.signmessage = co(function* signmessage(args, help) { var wallet = this.wallet; - var address, msg, sig, ring; + var valid = new Validator([args]); + var addr = valid.str(0, ''); + var msg = valid.str(1, ''); + var sig, ring; if (help || args.length !== 2) throw new RPCError('signmessage "bitcoinaddress" "message"'); - address = toString(args[0]); - msg = toString(args[1]); + addr = Address.getHash(addr, 'hex'); - address = Address.getHash(address, 'hex'); - - if (!address) + if (!addr) throw new RPCError('Invalid address.'); - ring = yield wallet.getKey(address); + ring = yield wallet.getKey(addr); if (!ring) throw new RPCError('Address not found.'); @@ -1616,7 +1452,7 @@ RPC.prototype.signmessage = co(function* signmessage(args, help) { if (!wallet.master.key) throw new RPCError('Wallet is locked.'); - msg = new Buffer(RPC.magic + msg, 'utf8'); + msg = new Buffer(MAGIC_STRING + msg, 'utf8'); msg = crypto.hash256(msg); sig = ring.sign(msg); @@ -1640,7 +1476,9 @@ RPC.prototype.walletlock = co(function* walletlock(args, help) { RPC.prototype.walletpassphrasechange = co(function* walletpassphrasechange(args, help) { var wallet = this.wallet; - var old, new_; + var valid = new Validator([args]); + var old = valid.str(0, ''); + var new_ = valid.str(1, ''); if (help || (wallet.master.encrypted && args.length !== 2)) { throw new RPCError('walletpassphrasechange' @@ -1650,9 +1488,6 @@ RPC.prototype.walletpassphrasechange = co(function* walletpassphrasechange(args, if (!wallet.master.encrypted) throw new RPCError('Wallet is not encrypted.'); - old = toString(args[0]); - new_ = toString(args[1]); - if (old.length < 1 || new_.length < 1) throw new RPCError('Invalid parameter'); @@ -1663,7 +1498,9 @@ RPC.prototype.walletpassphrasechange = co(function* walletpassphrasechange(args, RPC.prototype.walletpassphrase = co(function* walletpassphrase(args, help) { var wallet = this.wallet; - var passphrase, timeout; + var valid = new Validator([args]); + var passphrase = valid.str(0, ''); + var timeout = valid.num(1, -1); if (help || (wallet.master.encrypted && args.length !== 2)) throw new RPCError('walletpassphrase "passphrase" timeout'); @@ -1671,9 +1508,6 @@ RPC.prototype.walletpassphrase = co(function* walletpassphrase(args, help) { if (!wallet.master.encrypted) throw new RPCError('Wallet is not encrypted.'); - passphrase = toString(args[0]); - timeout = toNumber(args[1]); - if (passphrase.length < 1) throw new RPCError('Invalid parameter'); @@ -1686,33 +1520,30 @@ RPC.prototype.walletpassphrase = co(function* walletpassphrase(args, help) { }); RPC.prototype.importprunedfunds = co(function* importprunedfunds(args, help) { - var tx, block, hash, label, height; + var valid = new Validator([args]); + var tx = valid.buf(0); + var block = valid.buf(1); + var hash, height; if (help || args.length < 2 || args.length > 3) { throw new RPCError('importprunedfunds' + ' "rawtransaction" "txoutproof" ( "label" )'); } - tx = args[0]; - block = args[1]; - - if (!util.isHex(tx) || !util.isHex(block)) + if (!tx || !block) throw new RPCError('Invalid parameter.'); - tx = TX.fromRaw(tx, 'hex'); - block = MerkleBlock.fromRaw(block, 'hex'); + tx = TX.fromRaw(tx); + block = MerkleBlock.fromRaw(block); hash = block.hash('hex'); - if (args.length === 3) - label = toString(args[2]); - if (!block.verify()) throw new RPCError('Invalid proof.'); if (!block.hasTX(tx.hash('hex'))) throw new RPCError('Invalid proof.'); - height = yield this.chain.db.getHeight(hash); + height = yield this.client.getEntry(hash); if (height === -1) throw new RPCError('Invalid proof.'); @@ -1723,7 +1554,7 @@ RPC.prototype.importprunedfunds = co(function* importprunedfunds(args, help) { height: height }; - if (!(yield this.walletdb.addTX(tx, block))) + if (!(yield this.wdb.addTX(tx, block))) throw new RPCError('No tracked address for TX.'); return null; @@ -1731,13 +1562,12 @@ RPC.prototype.importprunedfunds = co(function* importprunedfunds(args, help) { RPC.prototype.removeprunedfunds = co(function* removeprunedfunds(args, help) { var wallet = this.wallet; - var hash; + var valid = new Validator([args]); + var hash = valid.hash(0); if (help || args.length !== 1) throw new RPCError('removeprunedfunds "txid"'); - hash = toHash(args[0]); - if (!hash) throw new RPCError('Invalid parameter.'); @@ -1748,13 +1578,14 @@ RPC.prototype.removeprunedfunds = co(function* removeprunedfunds(args, help) { }); RPC.prototype.selectwallet = co(function* selectwallet(args, help) { - var id, wallet; + var valid = new Validator([args]); + var id = valid.str(0); + var wallet; if (help || args.length !== 1) throw new RPCError('selectwallet "id"'); - id = toString(args[0]); - wallet = yield this.walletdb.get(id); + wallet = yield this.wdb.get(id); if (!wallet) throw new RPCError('Wallet not found.'); @@ -1764,82 +1595,6 @@ RPC.prototype.selectwallet = co(function* selectwallet(args, help) { return null; }); -/* - * Helpers - */ - -function RPCError(msg) { - Error.call(this); - - if (Error.captureStackTrace) - Error.captureStackTrace(this, RPCError); - - this.type = 'RPCError'; - this.message = msg; -} - -util.inherits(RPCError, Error); - -function toBool(obj, def) { - if (typeof obj === 'boolean' || typeof obj === 'number') - return !!obj; - return def || false; -} - -function toNumber(obj, def) { - if (util.isNumber(obj)) - return obj; - return def != null ? def : -1; -} - -function toString(obj, def) { - if (typeof obj === 'string') - return obj; - return def != null ? def : ''; -} - -function toArray(obj, def) { - if (Array.isArray(obj)) - return obj; - return def != null ? def : null; -} - -function toObject(obj, def) { - if (obj && typeof obj === 'object') - return obj; - return def != null ? def : null; -} - -function toHash(obj) { - if (!isHash(obj)) - return null; - return util.revHex(obj); -} - -function isHash(obj) { - return util.isHex(obj) && obj.length === 64; -} - -function toSatoshi(obj) { - if (typeof obj !== 'number') - throw new RPCError('Bad BTC amount.'); - return Amount.value(obj, true); -} - -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 */ diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index d3f96133..21bba7a8 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -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]); diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 63a97b5e..1e7f83fa 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -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];