From 90349b82443825ebee8c99c80bdda4ebf7a658b8 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 17 Aug 2016 05:17:43 -0700 Subject: [PATCH] rpc: add more calls. refactor validation. --- lib/bcoin/http/rpc.js | 875 ++++++++++++++++++++++++++++++------------ 1 file changed, 625 insertions(+), 250 deletions(-) diff --git a/lib/bcoin/http/rpc.js b/lib/bcoin/http/rpc.js index 5a220b52..368cc0fd 100644 --- a/lib/bcoin/http/rpc.js +++ b/lib/bcoin/http/rpc.js @@ -374,8 +374,8 @@ RPC.prototype.addnode = function addnode(args, callback) { if (args.help || args.length !== 2) return callback(new RPCError('addnode "node" "add|remove|onetry"')); - node = String(args[0]); - cmd = String(args[1]); + node = toString(args[0]); + cmd = toString(args[1]); host = bcoin.packets.NetworkAddress.fromHostname(node, this.network); switch (cmd) { @@ -404,7 +404,7 @@ RPC.prototype.disconnectnode = function disconnectnode(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('disconnectnode "node"')); - node = String(args[0]); + node = toString(args[0]); node = IP.normalize(node); peer = this.pool.getPeer(node); @@ -416,13 +416,14 @@ RPC.prototype.disconnectnode = function disconnectnode(args, callback) { RPC.prototype.getaddednodeinfo = function getaddednodeinfo(args, callback) { var out = []; - var i, peer, peers; + var i, host, peer, peers; if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('getaddednodeinfo dummy ( "node" )')); if (args.length === 2) { - peer = this.pool.getPeer(String(args[1])); + host = toString(args[1]); + peer = this.pool.getPeer(host); if (!peer) return callback(new RPCError('Node has not been added.')); peers = [peer]; @@ -456,23 +457,24 @@ RPC.prototype.getconnectioncount = function getconnectioncount(args, callback) { }; RPC.prototype.getnettotals = function getnettotals(args, callback) { + var i, sent, recv, peer; + if (args.help || args.length > 0) return callback(new RPCError('getnettotals')); + sent = 0; + recv = 0; + + for (i = 0; i < this.pool.peers.all.length; i++) { + peer = this.pool.peers.all[i]; + sent += peer.socket.bytesWritten; + recv += peer.socket.bytesRead; + } + callback(null, { - totalbytesrecv: 0, - totalbytessent: 0, - timemillis: utils.ms(), - uploadtarget: { - timeframe: 86400, - target: 0, - target_reached: false, - serve_historical_blocks: !this.pool.options.selfish - && !this.pool.options.spv - && !this.chain.db.options.prune, - bytes_left_in_cycle: 0, - time_left_in_cycle: 0 - } + totalbytesrecv: recv, + totalbytessent: sent, + timemillis: utils.ms() }); }; @@ -490,21 +492,19 @@ RPC.prototype.getpeerinfo = function getpeerinfo(args, callback) { addr: peer.hostname, addrlocal: peer.hostname, relaytxes: peer.type !== bcoin.peer.types.LEECH, - lastsend: 0, - lastrecv: 0, - bytessent: 0, - bytesrecv: 0, - conntime: utils.now() - peer.ts, - timeoffset: bcoin.time.known[peer.host] || 0, - pingtime: peer.lastPing, - minping: peer.minPing, + lastsend: peer.lastSend / 1000 | 0, + lastrecv: peer.lastRecv / 1000 | 0, + bytessent: peer.socket.bytesWritten, + bytesrecv: peer.socket.bytesRead, + conntime: peer.ts !== 0 ? utils.now() - peer.ts : 0, + timeoffset: peer.version ? peer.version.ts - utils.now() : 0, + pingtime: peer.lastPing !== -1 ? peer.lastPing / 1000 : 0, + minping: peer.minPing !== -1 ? peer.minPing / 1000 : 0, version: peer.version ? peer.version.version : 0, subver: peer.version ? peer.version.agent : '', inbound: peer.type === bcoin.peer.types.LEECH, startingheight: peer.version ? peer.version.height : -1, banscore: peer.banScore, - synced_headers: 0, - synced_blocks: 0, inflight: [], whitelisted: false }); @@ -526,7 +526,7 @@ RPC.prototype.ping = function ping(args, callback) { }; RPC.prototype.setban = function setban(args, callback) { - var peer, ip; + var host, peer, ip; if (args.help || args.length < 2 @@ -535,7 +535,8 @@ RPC.prototype.setban = function setban(args, callback) { 'setban "ip(/netmask)" "add|remove" (bantime) (absolute)')); } - ip = IP.normalize(args[0]); + host = toString(args[0]); + ip = IP.normalize(host); switch (args[1]) { case 'add': @@ -739,12 +740,15 @@ RPC.prototype.getblock = function getblock(args, callback) { if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('getblock "hash" ( verbose )')); - hash = utils.revHex(String(args[0])); + hash = toHash(args[0]); + + if (!hash) + return callback(new RPCError('Invalid parameter.')); verbose = true; if (args.length > 1) - verbose = Boolean(args[1]); + verbose = toBool(args[1]); this.chain.db.get(hash, function(err, entry) { if (err) @@ -841,7 +845,7 @@ RPC.prototype.getblockhash = function getblockhash(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('getblockhash index')); - height = args[0]; + height = toNumber(args[0]); if (height < 0 || height > this.chain.height) return callback(new RPCError('Block height out of range.')); @@ -864,12 +868,15 @@ RPC.prototype.getblockheader = function getblockheader(args, callback) { if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('getblockheader "hash" ( verbose )')); - hash = utils.revHex(String(args[0])); + hash = toHash(args[0]); + + if (!hash) + return callback(new RPCError('Invalid parameter.')); verbose = true; if (args.length > 1) - verbose = Boolean(args[1]); + verbose = toBool(args[1], true); this.chain.db.get(hash, function(err, entry) { if (err) @@ -1060,21 +1067,60 @@ RPC.prototype.getmempoolinfo = function getmempoolinfo(args, callback) { }; RPC.prototype.getmempoolancestors = function getmempoolancestors(args, callback) { + var hash, entry; + if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('getmempoolancestors txid (verbose)')); + + hash = toHash(args[0]); + + if (!hash) + return callback(new RPCError('Invalid parameter.')); + + entry = this.mempool.getEntry(hash); + + if (!entry) + return callback(new RPCError('Transaction not in mempool.')); + callback(new Error('Not implemented.')); }; RPC.prototype.getmempooldescendants = function getmempooldescendants(args, callback) { + var hash, entry; + if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('getmempooldescendants txid (verbose)')); + + hash = toHash(args[0]); + + if (!hash) + return callback(new RPCError('Invalid parameter.')); + + entry = this.mempool.getEntry(hash); + + if (!entry) + return callback(new RPCError('Transaction not in mempool.')); + callback(new Error('Not implemented.')); }; RPC.prototype.getmempoolentry = function getmempoolentry(args, callback) { + var hash, entry; + if (args.help || args.length !== 1) return callback(new RPCError('getmempoolentry txid')); - callback(new Error('Not implemented.')); + + hash = toHash(args[0]); + + if (!hash) + return callback(new RPCError('Invalid parameter.')); + + entry = this.mempool.getEntry(hash); + + if (!entry) + return callback(new RPCError('Transaction not in mempool.')); + + callback(null, this.entryToJSON(entry)); }; RPC.prototype.getrawmempool = function getrawmempool(args, callback) { @@ -1086,15 +1132,14 @@ RPC.prototype.getrawmempool = function getrawmempool(args, callback) { verbose = false; if (args.length > 0) - verbose = Boolean(args[0]); + verbose = toBool(args[0], false); return this.mempoolToJSON(verbose, callback); }; RPC.prototype.mempoolToJSON = function mempoolToJSON(verbose, callback) { - var self = this; var out = {}; - var i, tx, hashes, hash, entry; + var i, hashes, hash, entry; if (verbose) { hashes = this.mempool.getSnapshot(); @@ -1106,21 +1151,7 @@ RPC.prototype.mempoolToJSON = function mempoolToJSON(verbose, callback) { if (!entry) continue; - tx = entry.tx; - - out[tx.rhash] = { - size: entry.size, - fee: entry.fee, - modifiedfee: entry.fees, - time: entry.ts, - height: entry.height, - startingpriority: entry.priority, - currentpriority: entry.getPriority(self.chain.height), - descendantcount: self.mempool.countDescendants(tx), - descendantsize: entry.size, - descendantfees: entry.fees, - depends: self.mempool.getDepends(tx) - }; + out[entry.tx.rhash] = this.entryToJSON(entry); } return callback(null, out); @@ -1131,6 +1162,26 @@ RPC.prototype.mempoolToJSON = function mempoolToJSON(verbose, callback) { return callback(null, hashes.map(utils.revHex)); }; +RPC.prototype.entryToJSON = function entryToJSON(entry) { + var tx = entry.tx; + return { + size: entry.size, + fee: +utils.btc(entry.fee), + modifiedfee: +utils.btc(entry.fees), + time: entry.ts, + height: entry.height, + startingpriority: entry.priority, + currentpriority: entry.getPriority(this.chain.height), + descendantcount: this.mempool.countDescendants(tx), + descendantsize: entry.sizes, + descendantfees: +utils.btc(entry.fees), + ancestorcount: this.mempool.countAncestors(tx), + ancestorsize: entry.sizes, + ancestorfees: +utils.btc(entry.fees), + depends: this.mempool.getDepends(tx).map(utils.revHex) + }; +}; + RPC.prototype.gettxout = function gettxout(args, callback) { var self = this; var hash, index, mempool; @@ -1138,12 +1189,15 @@ RPC.prototype.gettxout = function gettxout(args, callback) { if (args.help || args.length < 2 || args.length > 3) return callback(new RPCError('gettxout "txid" n ( includemempool )')); - hash = utils.revHex(String(args[0])); - index = Number(args[1]); + hash = toHash(args[0]); + index = toNumber(args[1]); mempool = true; if (args.length > 2) - mempool = Boolean(args[2]); + mempool = toBool(args[2], true); + + if (!hash || index < 0) + return callback(new RPCError('Invalid parameter.')); function getCoin(callback) { if (mempool) @@ -1179,23 +1233,22 @@ RPC.prototype.gettxoutproof = function gettxoutproof(args, callback) { + ' ["txid",...] ( blockhash )')); } - txids = args[0]; + txids = toArray(args[0]); block = args[1]; - if (!Array.isArray(txids) || txids.length === 0) + if (!txids || txids.length === 0) return callback(new RPCError('Invalid parameter.')); if (block) { - if (!utils.isHex(block) || block.length !== 64) + block = toHash(block); + if (!block) return callback(new RPCError('Invalid parameter.')); - block = utils.revHex(block); } for (i = 0; i < txids.length; i++) { - hash = txids[i]; - if (!utils.isHex(hash) || hash.length !== 64) + hash = toHash(txids[i]); + if (!hash) return callback(new RPCError('Invalid parameter.')); - hash = utils.revHex(hash); if (uniq[hash]) return callback(new RPCError('Duplicate txid.')); uniq[hash] = true; @@ -1253,7 +1306,7 @@ RPC.prototype.verifytxoutproof = function verifytxoutproof(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('verifytxoutproof "proof"')); - block = bcoin.merkleblock.fromRaw(String(args[0]), 'hex'); + block = bcoin.merkleblock.fromRaw(toString(args[0]), 'hex'); if (!block.verify()) return callback(null, res); @@ -1474,6 +1527,8 @@ RPC.prototype._poll = function _poll(lpid, callback) { return callback(); if (typeof lpid === 'string') { + if (lpid.length < 65) + return callback(new RPCError('Invalid parameter.')); watched = lpid.slice(0, 64); lastTX = +lpid.slice(64); if (!utils.isHex(watched) || !utils.isNumber(lastTX)) @@ -1548,16 +1603,17 @@ RPC.prototype.getmininginfo = function getmininginfo(args, callback) { }; RPC.prototype.getnetworkhashps = function getnetworkhashps(args, callback) { - var lookup, height; + var lookup = 120; + var height = -1; if (args.help || args.length > 2) return callback(new RPCError('getnetworkhashps ( blocks height )')); - lookup = args.length > 0 ? Number(args[0]) : 120; - height = args.length > 1 ? Number(args[1]) : -1; + if (args.length > 0) + lookup = toNumber(args[0], 120); - if (!utils.isNumber(lookup) || !utils.isNumber(height)) - return callback(new RPCError('Invalid parameter.')); + if (args.length > 1) + height = toNumber(args[1], -1); return this._hashps(lookup, height, callback); }; @@ -1570,18 +1626,16 @@ RPC.prototype.prioritisetransaction = function prioritisetransaction(args, callb + ' ')); } - hash = args[0]; + hash = toHash(args[0]); pri = args[1]; fee = args[2]; - if (!utils.isHex(hash) || hash.length !== 64) + if (!hash) return callback(new RPCError('Invalid parameter')); if (!utils.isNumber(pri) || !utils.isNumber(fee)) return callback(new RPCError('Invalid parameter')); - hash = utils.revHex(hash); - entry = this.mempool.getEntry(hash); if (!entry) @@ -1607,7 +1661,7 @@ RPC.prototype.submitblock = function submitblock(args, callback) { + ' ( "jsonparametersobject" )')); } - block = bcoin.block.fromRaw(String(args[0]), 'hex'); + block = bcoin.block.fromRaw(toString(args[0]), 'hex'); this.chain.add(block, function(err, total) { if (err) @@ -1679,12 +1733,17 @@ RPC.prototype._hashps = function _hashps(lookup, height, callback) { */ RPC.prototype.getgenerate = function getgenerate(args, callback) { + if (args.help || args.length !== 0) + return callback(new RPCError('getgenerate')); callback(null, this.mining); }; RPC.prototype.setgenerate = function setgenerate(args, callback) { - this.mining = Boolean(args[0]); - this.proclimit = Number(args[1]); + if (args.help || args.length < 1 || args.length > 2) + return callback(new RPCError('setgenerate mine ( proclimit )')); + + this.mining = toBool(args[0]); + this.proclimit = toNumber(args[1], 0); if (this.mining) this.miner.start(); @@ -1701,7 +1760,7 @@ RPC.prototype.generate = function generate(args, callback) { if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('generate numblocks ( maxtries )')); - numblocks = Number(args[0]); + numblocks = toNumber(args[0], 1); hashes = []; utils.forRangeSerial(0, numblocks, function(i, next) { @@ -1728,7 +1787,7 @@ RPC.prototype.generatetoaddress = function generatetoaddress(args, callback) { } address = this.miner.address; - this.miner.address = bcoin.address.fromBase58(args[1]); + this.miner.address = bcoin.address.fromBase58(toString(args[1])); args = args.slice(); args.splice(1, 1); @@ -1757,19 +1816,16 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac + ' ( locktime )')); } - if (!args[0] || !args[1]) - return callback(new RPCError('Invalid parameter')); + inputs = toArray(args[0]); + sendTo = toObject(args[1]); - inputs = args[0]; - sendTo = args[1]; - - if (!Array.isArray(inputs) || typeof sendTo !== 'object') + if (!inputs || !sendTo) return callback(new RPCError('Invalid parameter')); tx = bcoin.tx(); if (args.length > 2 && args[2] != null) { - locktime = Number(args[2]); + locktime = toNumber(args[2]); if (locktime < 0 || locktime > 0xffffffff) return callback(new RPCError('Invalid parameter, locktime out of range')); tx.locktime = locktime; @@ -1781,24 +1837,23 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac if (!input) return callback(new RPCError('Invalid parameter')); - hash = input.txid; + hash = toHash(input.txid); index = input.vout; sequence = 0xffffffff; if (tx.locktime) sequence--; - if (!utils.isHex(hash) - || hash.length !== 64 + if (!hash || !utils.isNumber(index) || index < 0) { return callback(new RPCError('Invalid parameter')); } if (utils.isNumber(input.sequence)) { + sequence = toNumber(input.sequence); if (input.sequence < 0 || input.sequence > 0xffffffff) return callback(new RPCError('Invalid parameter')); - sequence = input.sequence; } input = new bcoin.input({ @@ -1838,7 +1893,7 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac addrs[b58] = true; output = new bcoin.output({ - value: utils.satoshi(value), + value: toSatoshi(value), address: address }); @@ -1854,10 +1909,7 @@ RPC.prototype.decoderawtransaction = function decoderawtransaction(args, callbac if (args.help || args.length !== 1) return callback(new RPCError('decoderawtransaction "hexstring"')); - if (!utils.isHex(args[0])) - return callback(new RPCError('Invalid parameter')); - - tx = bcoin.tx.fromRaw(args[0], 'hex'); + tx = bcoin.tx.fromRaw(toString(args[0]), 'hex'); callback(null, this.txToJSON(tx)); }; @@ -1868,7 +1920,7 @@ RPC.prototype.decodescript = function decodescript(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('decodescript "hex"')); - data = String(args[0]); + data = toString(args[0]); script = new bcoin.script(); if (data.length > 0) @@ -1890,9 +1942,9 @@ RPC.prototype.getrawtransaction = function getrawtransaction(args, callback) { if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('getrawtransaction "txid" ( verbose )')); - hash = args[0]; + hash = toHash(args[0]); - if (!utils.isHex(hash) || hash.length !== 64) + if (!hash) return callback(new RPCError('Invalid parameter')); verbose = false; @@ -1900,7 +1952,7 @@ RPC.prototype.getrawtransaction = function getrawtransaction(args, callback) { if (args.length > 1) verbose = Boolean(args[1]); - this.node.getTX(utils.revHex(hash), function(err, tx) { + this.node.getTX(hash, function(err, tx) { if (err) return callback(err); @@ -2003,12 +2055,11 @@ RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, arg prev = prevout[i]; if (!prev) return callback(new RPCError('Invalid parameter')); - hash = prev.txid; + hash = toHash(prev.txid); index = prev.vout; script = prev.scriptPubKey; - value = utils.satoshi(prev.amount || 0); - if (!utils.isHex(hash) - || hash.length !== 64 + value = toSatoshi(prev.amount); + if (!hash || !utils.isNumber(index) || index < 0 || !utils.isHex(script)) { @@ -2115,22 +2166,19 @@ RPC.prototype.fundrawtransaction = function fundrawtransaction(args, callback) { + ' "hexstring" ( options )')); } - if (!utils.isHex(args[0])) - return callback(new RPCError('Invalid parameter.')); - - tx = bcoin.mtx.fromRaw(args[0], 'hex'); + tx = bcoin.mtx.fromRaw(toString(args[0]), 'hex'); if (tx.outputs.length === 0) return callback(new RPCError('TX must have at least one output.')); if (args.length === 2 && args[1]) { options = args[1]; - changeAddress = options.changeAddress; + changeAddress = toString(options.changeAddress); if (changeAddress) - changeAddress = bcoin.address.fromBase58(String(changeAddress)); + changeAddress = bcoin.address.fromBase58(changeAddress); feeRate = options.feeRate; if (feeRate != null) - feeRate = utils.satoshi(feeRate + ''); + feeRate = toSatoshi(feeRate); } options = { @@ -2178,14 +2226,14 @@ RPC.prototype._createRedeem = function _createRedeem(args, callback) { if (!hash) return next(new RPCError('Invalid key.')); - self.node.wallet.getKeyring(hash, function(err, address) { + self.node.wallet.getKeyring(hash, function(err, ring) { if (err) return next(err); - if (!address) + if (!ring) return next(new RPCError('Invalid key.')); - keys[i] = address.getPublicKey(); + keys[i] = ring.getPublicKey(); next(); }); @@ -2247,11 +2295,7 @@ RPC.prototype.createwitnessaddress = function createwitnessaddress(args, callbac if (args.help || args.length !== 1) return callback(new RPCError('createwitnessaddress "script"')); - raw = args[1]; - - if (!utils.isHex(raw)) - return callback(new RPCError('Invalid parameter')); - + raw = toString(args[1]); script = bcoin.script.fromRaw(raw, 'hex'); program = this._scriptForWitness(script); @@ -2268,7 +2312,7 @@ RPC.prototype.validateaddress = function validateaddress(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('validateaddress "bitcoinaddress"')); - b58 = String(args[0]); + b58 = toString(args[0]); try { address = bcoin.address.fromBase58(b58); @@ -2278,7 +2322,7 @@ RPC.prototype.validateaddress = function validateaddress(args, callback) { }); } - this.wallet.getKeyring(address.getHash('hex'), function(err, ring) { + this.wallet.getAddressPath(address.getHash('hex'), function(err, path) { if (err) return callback(err); @@ -2286,18 +2330,15 @@ RPC.prototype.validateaddress = function validateaddress(args, callback) { isvalid: true, address: address.toBase58(self.network), scriptPubKey: address.toScript().toJSON(), - ismine: ring ? true : false, + ismine: path ? true : false, iswatchonly: false }; - if (!ring) + if (!path) return callback(null, json); - json.account = ring.name; - json.hdkeypath = 'm' - + '/' + ring.account + '\'' - + '/' + ring.change - + '/' + ring.index; + json.account = path.name; + json.hdkeypath = path.toPath(); callback(null, json); }); @@ -2313,9 +2354,9 @@ RPC.prototype.verifymessage = function verifymessage(args, callback) { + ' "bitcoinaddress" "signature" "message"')); } - address = String(args[0]); - sig = String(args[1]); - msg = String(args[2]); + address = toString(args[0]); + sig = toString(args[1]); + msg = toString(args[2]); address = bcoin.address.getHash(address); @@ -2342,8 +2383,8 @@ RPC.prototype.signmessagewithprivkey = function signmessagewithprivkey(args, cal if (args.help || args.length !== 2) return callback(new RPCError('signmessagewithprivkey "privkey" "message"')); - key = String(args[0]); - msg = String(args[1]); + key = toString(args[0]); + msg = toString(args[1]); key = bcoin.keypair.fromSecret(key).getPrivateKey(); msg = new Buffer(RPC.magic + msg, 'utf8'); @@ -2360,10 +2401,7 @@ RPC.prototype.estimatefee = function estimatefee(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('estimatefee nblocks')); - blocks = Number(args[0]); - - if (!utils.isNumber(blocks)) - blocks = -1; + blocks = toNumber(args[0], 1); if (blocks < 1) blocks = 1; @@ -2384,10 +2422,7 @@ RPC.prototype.estimatepriority = function estimatepriority(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('estimatepriority nblocks')); - blocks = Number(args[0]); - - if (!utils.isNumber(blocks)) - blocks = -1; + blocks = toNumber(args[0], 1); if (blocks < 1) blocks = 1; @@ -2403,10 +2438,7 @@ RPC.prototype.estimatesmartfee = function estimatesmartfee(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('estimatesmartfee nblocks')); - blocks = Number(args[0]); - - if (!utils.isNumber(blocks)) - blocks = -1; + blocks = toNumber(args[0], 1); if (blocks < 1) blocks = 1; @@ -2430,10 +2462,7 @@ RPC.prototype.estimatesmartpriority = function estimatesmartpriority(args, callb if (args.help || args.length !== 1) return callback(new RPCError('estimatesmartpriority nblocks')); - blocks = Number(args[0]); - - if (!utils.isNumber(blocks)) - blocks = -1; + blocks = toNumber(args[0], 1); if (blocks < 1) blocks = 1; @@ -2452,13 +2481,11 @@ RPC.prototype.invalidateblock = function invalidateblock(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('invalidateblock "hash"')); - hash = args[0]; + hash = toHash(args[0]); - if (!utils.isHex(hash) || hash.length !== 64) + if (!hash) return callback(new RPCError('Block not found.')); - hash = utils.revHex(hash); - this.chain.invalid[hash] = true; callback(null, null); @@ -2470,13 +2497,11 @@ RPC.prototype.reconsiderblock = function reconsiderblock(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('reconsiderblock "hash"')); - hash = args[0]; + hash = toHash(args[0]); - if (!utils.isHex(hash) || hash.length !== 64) + if (!hash) return callback(new RPCError('Block not found.')); - hash = utils.revHex(hash); - delete this.chain.invalid[hash]; callback(null, null); @@ -2488,9 +2513,9 @@ RPC.prototype.setmocktime = function setmocktime(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('setmocktime timestamp')); - time = args[0]; + time = toNumber(args[0]); - if (!utils.isNumber(time)) + if (time < 0) return callback(new RPCError('Invalid parameter.')); delta = bcoin.now() - time; @@ -2555,7 +2580,7 @@ RPC.prototype.dumpprivkey = function dumpprivkey(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('dumpprivkey "bitcoinaddress"')); - hash = bcoin.address.getHash(String(args[0]), 'hex'); + hash = bcoin.address.getHash(toString(args[0]), 'hex'); if (!hash) return callback(new RPCError('Invalid address.')); @@ -2585,7 +2610,10 @@ RPC.prototype.dumpwallet = function dumpwallet(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('dumpwallet "filename"')); - file = utils.normalize(String(args[0])); + if (!args[0] || typeof args[0] !== 'string') + return callback(new RPCError('Invalid parameter.')); + + file = utils.normalize(args[0]); time = utils.date(); out = [ utils.fmt('# Wallet Dump created by BCoin %s', constants.USER_VERSION), @@ -2597,7 +2625,7 @@ RPC.prototype.dumpwallet = function dumpwallet(args, callback) { '' ]; - this.wallet.getAddresses(function(err, hashes) { + this.wallet.getAddressHashes(function(err, hashes) { if (err) return callback(err); @@ -2651,9 +2679,9 @@ RPC.prototype.encryptwallet = function encryptwallet(args, callback) { if (this.wallet.master.encrypted) return callback(new RPCError('Already running with an encrypted wallet')); - passphrase = args[0]; + passphrase = toString(args[0]); - if (typeof passphrase !== 'string' || passphrase.length < 1) + if (passphrase.length < 1) return callback(new RPCError('encryptwallet "passphrase"')); this.wallet.setPassphrase(passphrase, function(err) { @@ -2669,7 +2697,7 @@ RPC.prototype.getaccountaddress = function getaccountaddress(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('getaccountaddress "account"')); - account = String(args[0]); + account = toString(args[0]); if (!account) account = 'default'; @@ -2696,51 +2724,41 @@ RPC.prototype.getaccount = function getaccount(args, callback) { if (!hash) return callback(new RPCError('Invalid address.')); - this.wallet.getKeyring(hash, function(err, address) { + this.wallet.getAddressPath(hash, function(err, path) { if (err) return callback(err); - if (!address) + if (!path) return callback(null, ''); - return callback(null, address.name); + return callback(null, path.name); }); }; RPC.prototype.getaddressesbyaccount = function getaddressesbyaccount(args, callback) { var self = this; - var account, addrs; + var i, path, account, addrs; if (args.help || args.length !== 1) return callback(new RPCError('getaddressesbyaccount "account"')); - account = String(args[0]); + account = toString(args[0]); if (!account) account = 'default'; addrs = []; - this.wallet.getAddresses(function(err, hashes) { + this.wallet.getPaths(account, function(err, paths) { if (err) return callback(err); - utils.forEachSerial(hashes, function(hash, next) { - self.wallet.getKeyring(hash, function(err, address) { - if (err) - return callback(err); + for (i = 0; i < paths.length; i++) { + path = paths[i]; + addrs.push(path.toAddress().toBase58(self.network)); + } - if (address && address.name === account) - addrs.push(address.getAddress('base58')); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, addrs); - }); + callback(null, addrs); }); }; @@ -2754,7 +2772,7 @@ RPC.prototype.getbalance = function getbalance(args, callback) { } if (args.length >= 1) { - account = String(args[0]); + account = toString(args[0]); if (!account) account = 'default'; if (account === '*') @@ -2762,13 +2780,13 @@ RPC.prototype.getbalance = function getbalance(args, callback) { } if (args.length >= 2) - minconf = Number(args[1]); + minconf = toNumber(args[1], 0); this.wallet.getBalance(account, function(err, balance) { if (err) return callback(err); - if (minconf >= 1) + if (minconf) value = balance.confirmed; else value = balance.total; @@ -2784,7 +2802,7 @@ RPC.prototype.getnewaddress = function getnewaddress(args, callback) { return callback(new RPCError('getnewaddress ( "account" )')); if (args.length === 1) - account = String(args[0]); + account = toString(args[0]); if (!account) account = 'default'; @@ -2812,25 +2830,28 @@ RPC.prototype.getreceivedbyaccount = function getreceivedbyaccount(args, callbac var minconf = 0; var total = 0; var filter = {}; - var i, j, account, tx, output; + var lastConf = -1; + var i, j, path, tx, output, conf, hash, account; if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('getreceivedbyaccount "account" ( minconf )')); - account = String(args[0]); + account = toString(args[0]); if (!account) account = 'default'; if (args.length === 2) - minconf = Number(args[1]); + minconf = toNumber(args[1], 0); - this.wallet.getAddresses(function(err, hashes) { + this.wallet.getPaths(account, function(err, paths) { if (err) return callback(err); - for (i = 0; i < hashes.length; i++) - filter[hashes[i]] = true; + for (i = 0; i < paths.length; i++) { + path = paths[i]; + filter[path.hash] = true; + } self.wallet.getHistory(account, function(err, txs) { if (err) @@ -2838,15 +2859,23 @@ RPC.prototype.getreceivedbyaccount = function getreceivedbyaccount(args, callbac for (i = 0; i < txs.length; i++) { tx = txs[i]; + if (minconf) { if (tx.height === -1) continue; if (!(self.chain.height - tx.height + 1 >= minconf)) continue; } + + conf = tx.getConfirmations(self.chain.height); + + if (lastConf === -1 || conf < lastConf) + lastConf = conf; + for (j = 0; j < tx.outputs.length; j++) { output = tx.outputs[j]; - if (filter[output.getHash('hex')]) + hash = output.getHash('hex'); + if (filter[hash]) total += output.value; } } @@ -2867,13 +2896,13 @@ RPC.prototype.getreceivedbyaddress = function getreceivedbyaddress(args, callbac + ' "bitcoinaddress" ( minconf )')); } - hash = bcoin.address.getHash(String(args[0]), 'hex'); + hash = bcoin.address.getHash(toString(args[0]), 'hex'); if (!hash) return callback(new RPCError('Invalid address')); if (args.length === 2) - minconf = Number(args[1]); + minconf = toNumber(args[1], 0); this.wallet.getHistory(function(err, txs) { if (err) @@ -2986,13 +3015,11 @@ RPC.prototype.gettransaction = function gettransaction(args, callback) { if (args.help || args.length < 1 || args.length > 2) return callback(new RPCError('gettransaction "txid" ( includeWatchonly )')); - hash = String(args[0]); + hash = toHash(args[0]); - if (!utils.isHex(hash) || hash.length !== 64) + if (!hash) return callback(new RPCError('Invalid parameter')); - hash = utils.revHex(hash); - this.wallet.getTX(hash, function(err, tx) { if (err) return callback(err); @@ -3010,13 +3037,11 @@ RPC.prototype.abandontransaction = function abandontransaction(args, callback) { if (args.help || args.length !== 1) return callback(new RPCError('abandontransaction "txid"')); - hash = args[0]; + hash = toHash(args[0]); - if (!utils.isHex(hash) || hash.length !== 64) + if (!hash) return callback(new RPCError('Invalid parameter.')); - hash = utils.revHex(hash); - this.wallet.abandon(hash, function(err, result) { if (err) return callback(err); @@ -3145,19 +3170,159 @@ RPC.prototype.listaddressgroupings = function listaddressgroupings(args, callbac }; RPC.prototype.listlockunspent = function listlockunspent(args, callback) { - callback(new Error('Not implemented.')); + var i, outpoints, outpoint, out; + + if (args.help || args.length > 0) + return callback(new RPCError('listlockunspent')); + + outpoints = this.wallet.tx.getLocked(); + out = []; + + for (i = 0; i < outpoints.length; i++) { + outpoint = outpoints[i]; + out.push({ + txid: utils.revHex(outpoint.hash), + vout: outpoint.index + }); + } + + callback(null, out); }; RPC.prototype.listreceivedbyaccount = function listreceivedbyaccount(args, callback) { - callback(new Error('Not implemented.')); + var minconf = 0; + var includeEmpty = false; + + if (args.help || args.length > 3) { + return callback(new RPCError('listreceivedbyaccount' + + ' ( minconf includeempty includeWatchonly)')); + } + + if (args.length > 0) + minconf = toNumber(args[0], 0); + + if (args.length > 1) + includeEmpty = toBool(args[1], false); + + this._listReceived(minconf, includeEmpty, true, callback); }; RPC.prototype.listreceivedbyaddress = function listreceivedbyaddress(args, callback) { + var minconf = 0; + var includeEmpty = false; + if (args.help || args.length > 3) { return callback(new RPCError('listreceivedbyaddress' + ' ( minconf includeempty includeWatchonly)')); } - callback(new Error('Not implemented.')); + + if (args.length > 0) + minconf = toNumber(args[0], 0); + + if (args.length > 1) + includeEmpty = toBool(args[1], false); + + this._listReceived(minconf, includeEmpty, false, callback); +}; + +RPC.prototype._listReceived = function _listReceived(minconf, empty, account, callback) { + var self = this; + var out = []; + var result = []; + var map = {}; + var i, j, path, tx, output, conf, hash; + var entry, address, keys, key, item; + + this.wallet.getPaths(function(err, paths) { + if (err) + return callback(err); + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + hash = new Buffer(path.hash, 'hex'); + map[path.hash] = { + involvesWatchonly: false, + address: path.toAddress().toBase58(self.network), + account: path.name, + amount: 0, + confirmations: -1, + label: '', + }; + } + + self.wallet.getHistory(function(err, txs) { + if (err) + return callback(err); + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + + if (minconf) { + if (tx.height === -1) + continue; + if (!(self.chain.height - tx.height + 1 >= minconf)) + continue; + } + + conf = tx.getConfirmations(self.chain.height); + + for (j = 0; j < tx.outputs.length; j++) { + output = tx.outputs[j]; + address = output.getAddress(); + if (!address) + continue; + hash = address.getHash('hex'); + entry = map[hash]; + if (entry) { + if (entry.confirmations === -1 || conf < entry.confirmations) + entry.confirmations = conf; + entry.address = address.toBase58(self.network); + entry.amount += output.value; + } + } + } + + keys = Object.keys(map); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + entry = map[key]; + out.push(entry); + } + + if (account) { + map = {}; + for (i = 0; i < out.length; i++) { + entry = out[i]; + item = map[entry.account]; + if (!item) { + map[entry.account] = entry; + entry.address = undefined; + continue; + } + item.amount += entry.amount; + } + out = []; + keys = Object.keys(map); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + entry = map[key]; + out.push(entry); + } + } + + for (i = 0; i < out.length; i++) { + entry = out[i]; + if (!empty && entry.amount === 0) + continue; + if (entry.confirmations === -1) + entry.confirmations = 0; + entry.amount = +utils.btc(entry.amount); + result.push(entry); + } + + callback(null, result); + }); + }); }; RPC.prototype.listsinceblock = function listsinceblock(args, callback) { @@ -3170,19 +3335,15 @@ RPC.prototype.listsinceblock = function listsinceblock(args, callback) { } if (args.length > 0) { - block = String(args[0]); - if (!utils.isHex(block) || block.length !== 64) + block = toHash(args[0]); + if (!block) return callback(new RPCError('Invalid parameter.')); - block = utils.revHex(block); } conf = 0; - if (args.length > 1) { - conf = Number(args[1]); - if (!utils.isNumber(conf) || conf < 0) - return callback(new RPCError('Invalid parameter.')); - } + if (args.length > 1) + conf = toNumber(args[1], 0); out = []; @@ -3321,7 +3482,7 @@ RPC.prototype.listtransactions = function listtransactions(args, callback) { account = null; if (args.length > 0) { - account = String(args[0]); + account = toString(args[0]); if (!account) account = 'default'; } @@ -3329,9 +3490,9 @@ RPC.prototype.listtransactions = function listtransactions(args, callback) { count = 10; if (args.length > 1) - count = Number(args[1]); + count = toNumber(args[1], 10); - if (!utils.isNumber(count) || count < 0) + if (count < 0) count = 10; this.wallet.getHistory(account, function(err, txs) { @@ -3366,21 +3527,18 @@ RPC.prototype.listunspent = function listunspent(args, callback) { } if (args.length > 0) - minDepth = Number(args[0]); + minDepth = toNumber(args[0], 1); if (args.length > 1) - maxDepth = Number(args[1]); + maxDepth = toNumber(args[1], maxDepth); if (args.length > 2) - addrs = args[2]; + addrs = toArray(args[2]); - if (!utils.isNumber(minDepth) || !utils.isNumber(maxDepth)) - return callback(new RPCError('Invalid parameter.')); - - if (Array.isArray(addrs)) { + if (addrs) { addresses = {}; for (i = 0; i < addrs.length; i++) { - address = String(addrs[i]); + address = toString(addrs[i]); hash = bcoin.address.getHash(address, 'hex'); if (!hash) @@ -3428,7 +3586,7 @@ RPC.prototype.listunspent = function listunspent(args, callback) { scriptPubKey: coin.script.toJSON(), amount: +utils.btc(coin.value), confirmations: depth, - spendable: true, + spendable: !self.wallet.tx.isLocked(coin), solvable: true }); @@ -3443,14 +3601,53 @@ RPC.prototype.listunspent = function listunspent(args, callback) { }; RPC.prototype.lockunspent = function lockunspent(args, callback) { + var i, unlock, outputs, output, outpoint; + if (args.help || args.length < 1 || args.length > 2) { return callback(new RPCError('lockunspent' + ' unlock ([{"txid":"txid","vout":n},...])')); } - callback(new Error('Not implemented.')); + + unlock = toBool(args[0]); + + if (args.length === 1) { + if (unlock) + this.wallet.tx.unlockCoins(); + return callback(null, true); + } + + outputs = toArray(args[1]); + + if (!outputs) + return callback(new RPCError('Invalid paremeter.')); + + for (i = 0; i < outputs.length; i++) { + output = outputs[i]; + + if (!output || typeof output !== 'object') + return callback(new RPCError('Invalid paremeter.')); + + outpoint = new bcoin.outpoint(); + outpoint.hash = toHash(output.txid); + outpoint.index = toNumber(output.vout); + + if (!outpoint.txid) + return callback(new RPCError('Invalid paremeter.')); + + if (outpoint.index < 0) + return callback(new RPCError('Invalid paremeter.')); + + if (unlock) + this.wallet.unlockCoin(outpoint); + else + this.wallet.lockCoin(outpoint); + } + + callback(null, true); }; RPC.prototype.move = function move(args, callback) { + // Not implementing: stupid and deprecated. callback(new Error('Not implemented.')); }; @@ -3477,9 +3674,9 @@ RPC.prototype.sendfrom = function sendfrom(args, callback) { + ' amount ( minconf "comment" "comment-to" )')); } - account = String(args[0]); - address = bcoin.address.fromBase58(String(args[1])); - amount = utils.satoshi(String(args[2])); + account = toString(args[0]); + address = bcoin.address.fromBase58(toString(args[1])); + amount = toSatoshi(args[2]); if (!account) account = 'default'; @@ -3492,7 +3689,78 @@ RPC.prototype.sendfrom = function sendfrom(args, callback) { }; RPC.prototype.sendmany = function sendmany(args, callback) { - callback(new Error('Not implemented.')); + var account, sendTo, minDepth, comment, subtractFee; + var i, j, outputs, subtractIndex, keys, uniq; + var key, value, address, hash, output, options; + + if (args.help || args.length < 2 || args.length > 5) { + return callback(new RPCError('sendmany' + + ' "fromaccount" {"address":amount,...}' + + ' ( minconf "comment" ["address",...] )')); + } + + account = toString(args[0]); + sendTo = toObject(args[1]); + minDepth = 1; + + if (!account) + account = 'default'; + + if (!sendTo) + return callback(new RPCError('Invalid parameter.')); + + if (args.length > 2) + minDepth = toNumber(args[2], 1); + + if (args.length > 3) + comment = toString(args[3]); + + if (args.length > 4) + subtractFee = toArray(args[4]); + + outputs = []; + subtractIndex = null; + keys = Object.keys(sendTo); + uniq = {}; + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + value = toSatoshi(sendTo[key]); + address = bcoin.address.fromBase58(key); + hash = address.getHash('hex'); + + if (uniq[hash]) + return callback(new RPCError('Invalid parameter.')); + + uniq[hash] = true; + + for (j = 0; j < subtractFee.length; j++) { + if (subtractFee[j] === key) + subtractIndex = i; + } + + output = new bcoin.output(); + output.value = value; + output.script.fromAddress(address); + outputs.push(output); + } + + // Don't do this for now. We sort the outputs. + if (subtractIndex != null) + return callback(new RPCError('Cannot subtract.')); + + options = { + outputs: outputs, + subtractFee: subtractIndex, + account: account, + confirmations: minDepth // todo: addme + }; + + this.wallet.send(options, function(err, tx) { + if (err) + return callback(err); + callback(null, tx.rhash); + }); }; RPC.prototype.sendtoaddress = function sendtoaddress(args, callback) { @@ -3505,9 +3773,9 @@ RPC.prototype.sendtoaddress = function sendtoaddress(args, callback) { + ' subtractfeefromamount )')); } - address = bcoin.address.fromBase58(String(args[0])); - amount = utils.satoshi(String(args[1])); - subtractFee = Boolean(args[4]); + address = bcoin.address.fromBase58(toString(args[0])); + amount = toSatoshi(args[1]); + subtractFee = toBool(args[4]); this._send(null, address, amount, subtractFee, function(err, tx) { if (err) @@ -3527,7 +3795,7 @@ RPC.prototype.settxfee = function settxfee(args, callback) { if (args.help || args.length < 1 || args.length > 1) return callback(new RPCError('settxfee amount')); - this.feeRate = utils.satoshi(args[0]); + this.feeRate = toSatoshi(args[0]); callback(null, true); }; @@ -3539,8 +3807,8 @@ RPC.prototype.signmessage = function signmessage(args, callback) { if (args.help || args.length !== 2) return callback(new RPCError('signmessage "bitcoinaddress" "message"')); - address = String(args[0]); - msg = String(args[1]); + address = toString(args[0]); + msg = toString(args[1]); address = bcoin.address.getHash(address, 'hex'); @@ -3592,11 +3860,8 @@ RPC.prototype.walletpassphrasechange = function walletpassphrasechange(args, cal if (!this.wallet.master.encrypted) return callback(new RPCError('Wallet is not encrypted.')); - if (typeof args[0] !== 'string' || typeof args[1] !== 'string') - return callback(new RPCError('Invalid parameter')); - - old = args[0]; - new_ = args[1]; + old = toString(args[0]); + new_ = toString(args[1]); if (old.length < 1 || new_.length < 1) return callback(new RPCError('Invalid parameter')); @@ -3610,19 +3875,24 @@ RPC.prototype.walletpassphrasechange = function walletpassphrasechange(args, cal }; RPC.prototype.walletpassphrase = function walletpassphrase(args, callback) { + var passphrase, timeout; + if (args.help || (this.wallet.master.encrypted && args.length !== 2)) return callback(new RPCError('walletpassphrase "passphrase" timeout')); if (!this.wallet.master.encrypted) return callback(new RPCError('Wallet is not encrypted.')); - if (typeof args[0] !== 'string' || args[0].length < 1) + passphrase = toString(args[0]); + timeout = toNumber(args[1]); + + if (passphrase.length < 1) return callback(new RPCError('Invalid parameter')); - if (!utils.isNumber(args[1]) || args[1] < 0) + if (timeout < 0) return callback(new RPCError('Invalid parameter')); - this.wallet.unlock(args[0], args[1], function(err) { + this.wallet.unlock(passphrase, timeout, function(err) { if (err) return callback(err); callback(null, null); @@ -3630,17 +3900,76 @@ RPC.prototype.walletpassphrase = function walletpassphrase(args, callback) { }; RPC.prototype.importprunedfunds = function importprunedfunds(args, callback) { + var self = this; + var tx, block, label; + if (args.help || args.length < 2 || args.length > 3) { return callback(new RPCError('importprunedfunds' + ' "rawtransaction" "txoutproof" ( "label" )')); } - callback(new Error('Not implemented.')); + + tx = args[0]; + block = args[1]; + + if (!utils.isHex(tx) || !utils.isHex(block)) + return callback(new RPCError('Invalid parameter.')); + + tx = bcoin.tx.fromRaw(tx, 'hex'); + block = bcoin.merkleblock.fromRaw(block, 'hex'); + + if (args.length === 3) + label = toString(args[2]); + + if (!block.verify()) + return callback(new RPCError('Invalid proof.')); + + if (!block.hasTX(tx)) + return callback(new RPCError('Invalid proof.')); + + this.chain.db.getHeight(block.hash('hex'), function(err, height) { + if (err) + return callback(err); + + if (height === -1) + return callback(new RPCError('Invalid proof.')); + + tx.index = block.indexOf(tx); + tx.block = block.hash('hex'); + tx.ts = block.ts; + tx.height = height; + + self.wallet.addTX(tx, function(err, added) { + if (err) + return callback(err); + + if (!added) + return callback(new RPCError('No tracked address for TX.')); + + callback(null, null); + }); + }); }; RPC.prototype.removeprunedfunds = function removeprunedfunds(args, callback) { + var hash; + if (args.help || args.length !== 1) return callback(new RPCError('removeprunedfunds "txid"')); - callback(new Error('Not implemented.')); + + hash = toHash(args[0]); + + if (!hash) + return callback(new RPCError('Invalid parameter.')); + + this.wallet.tx.remove(hash, function(err, removed) { + if (err) + return callback(err); + + if (!removed) + return callback(new RPCError('Transaction not in wallet.')); + + callback(null, null); + }); }; RPC.prototype.getmemory = function getmemory(args, callback) { @@ -3675,6 +4004,52 @@ function RPCError(msg) { utils.inherits(RPCError, Error); +function toBool(obj, def) { + if (typeof obj === 'boolean' || typeof obj === 'number') + return !!obj; + return def || false; +} + +function toNumber(obj, def) { + if (!utils.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 utils.revHex(obj); +} + +function isHash(obj) { + return utils.isHex(obj) && obj.length === 64; +} + +function toSatoshi(obj) { + if (typeof obj !== 'number') + throw new RPCError('Bad BTC amount.'); + return utils.satoshi(obj + ''); +} + /* * Expose */