diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index 5e5bdaa0..816724cf 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -233,6 +233,7 @@ Address.fromRaw = function fromRaw(data) { */ Address.prototype.fromBase58 = function fromBase58(data) { + assert(typeof data === 'string'); return this.fromRaw(utils.fromBase58(data)); }; diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index b81e15ab..975f051b 100644 --- a/lib/bcoin/fullnode.js +++ b/lib/bcoin/fullnode.js @@ -306,19 +306,8 @@ Fullnode.prototype._open = function open(callback) { self.walletdb.rescan(self.chain.db, next); }, function(next) { - var i; - self.wallet.getUnconfirmed(function(err, txs) { - if (err) - return next(err); - - if (txs.length > 0) - self.logger.info('Rebroadcasting %d transactions.', txs.length); - - for (i = 0; i < txs.length; i++) - self.pool.broadcast(txs[i]); - - next(); - }); + // Rebroadcast pending transactions. + self.wallet.resend(next); }, function(next) { if (!self.http) diff --git a/lib/bcoin/http/client.js b/lib/bcoin/http/client.js index bdef1aaf..20f9ba18 100644 --- a/lib/bcoin/http/client.js +++ b/lib/bcoin/http/client.js @@ -287,7 +287,7 @@ HTTPClient.prototype.getInfo = function getInfo(callback) { */ HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address, callback) { - var body = { addresses: address }; + var body = { address: address }; return this._post('/coin/address', body, callback); }; @@ -312,7 +312,7 @@ HTTPClient.prototype.getCoin = function getCoin(hash, index, callback) { */ HTTPClient.prototype.getTXByAddress = function getTXByAddress(address, callback) { - var body = { addresses: address }; + var body = { address: address }; return this._post('/tx/address', body, callback); }; diff --git a/lib/bcoin/http/server.js b/lib/bcoin/http/server.js index 23116a67..62a352f6 100644 --- a/lib/bcoin/http/server.js +++ b/lib/bcoin/http/server.js @@ -43,9 +43,13 @@ function HTTPServer(options) { assert(this.node, 'HTTP requires a Node.'); this.network = this.node.network; - this.walletdb = this.node.walletdb; + this.chain = this.node.chain; this.mempool = this.node.mempool; this.pool = this.node.pool; + this.fees = this.node.fees; + this.miner = this.node.miner; + this.wallet = this.node.wallet; + this.walletdb = this.node.walletdb; this.logger = options.logger || this.node.logger; this.loaded = false; this.apiKey = options.apiKey; @@ -102,8 +106,8 @@ HTTPServer.prototype._init = function _init() { res.setHeader('X-Bcoin-Version', constants.USER_VERSION); res.setHeader('X-Bcoin-Agent', constants.USER_AGENT); res.setHeader('X-Bcoin-Network', self.network.type); - res.setHeader('X-Bcoin-Height', self.node.chain.height + ''); - res.setHeader('X-Bcoin-Tip', utils.revHex(self.node.chain.tip.hash)); + res.setHeader('X-Bcoin-Height', self.chain.height + ''); + res.setHeader('X-Bcoin-Tip', utils.revHex(self.chain.tip.hash)); next(); }); @@ -157,109 +161,184 @@ HTTPServer.prototype._init = function _init() { }); this.use(function(req, res, next, send) { - var params, options; + var i, params, options, output, address; - if (req.method === 'POST' - && req.pathname === '/') { + if (req.method === 'POST' && req.pathname === '/') { + assert(typeof req.body.method === 'string', 'Method must be a string.'); + assert(Array.isArray(req.body.params), 'Params must be an array.'); req.options = {}; return next(); } - params = utils.merge({}, req.params, req.query, req.body); + params = {}; options = {}; + softMerge(params, req.params, true); + softMerge(params, req.query, true); + softMerge(params, req.body); + self.logger.debug('Params:'); self.logger.debug(params); if (params.id) { - assert(params.id !== '!all'); + assert(typeof params.id === 'string', 'ID must be a string.'); options.id = params.id; } if (params.hash) { - if (params.hash.length !== 64) - options.height = params.hash >>> 0; - else + assert(typeof params.hash === 'string', 'Hash must be a string.'); + if (params.hash.length !== 64) { + options.height = Number(params.hash); + assert(utils.isUInt32(options.height), 'Hash must be a number.'); + } else { options.hash = utils.revHex(params.hash); + } } - if (params.index != null) - options.index = params.index >>> 0; + if (params.index != null) { + options.index = Number(params.index); + assert(utils.isUInt32(options.index), 'Index must be a number.'); + } - if (params.height != null) - options.height = params.height >>> 0; + if (params.height != null) { + options.height = Number(params.height); + assert(utils.isUInt32(options.height), 'Height must be a number.'); + } - if (params.start != null) - options.start = params.start >>> 0; + if (params.start != null) { + options.start = Number(params.start); + assert(utils.isUInt32(options.height), 'Start must be a number.'); + } - if (params.end != null) - options.end = params.end >>> 0; + if (params.end != null) { + options.end = Number(params.end); + assert(utils.isUInt32(options.end), 'End must be a number.'); + } - if (params.limit != null) - options.limit = params.limit >>> 0; + if (params.limit != null) { + options.limit = Number(params.limit); + assert(utils.isUInt32(options.limit), 'Limit must be a number.'); + } - if (params.address) { - params.addresses = params.address; - options.address = params.address; + if (params.age != null) { + options.age = Number(params.age); + assert(utils.isUInt32(options.age), 'Age must be a number.'); } if (params.rate) options.rate = utils.satoshi(params.rate); - if (params.subtractFee) - options.subtractFee = params.subtractFee; - - if (Array.isArray(params.outputs)) { - options.outputs = params.outputs.map(function(output) { - return { - address: output.address, - script: decodeScript(output.script), - value: utils.satoshi(output.value) - }; - }); + if (params.m != null) { + options.m = Number(params.m); + assert(utils.isUInt32(options.m), 'm must be a number.'); } - if (params.addresses) { - if (typeof params.addresses === 'string') - options.addresses = params.addresses.split(','); - else - options.addresses = params.addresses; + if (params.n != null) { + options.n = Number(params.n); + assert(utils.isUInt32(options.n), 'n must be a number.'); } - if (params.tx) { - try { - if (typeof params.tx === 'object') - options.tx = bcoin.tx.fromJSON(params.tx); - else - options.tx = bcoin.tx.fromRaw(params.tx, 'hex'); - } catch (e) { - return next(e); + if (params.blocks != null) { + options.blocks = Number(params.blocks); + assert(utils.isUInt32(options.blocks), 'Blocks must be a number.'); + } + + if (params.subtractFee != null) { + if (typeof params.subtractFee === 'number') { + options.subtractFee = params.subtractFee; + assert(utils.isUInt32(options.subtractFee), 'subtractFee must be a number.'); + } else if (params.subtractFee === 'true') { + options.subtractFee = true; + } else { + assert(typeof options.subtractFee === 'boolean', 'subtractFee must be a boolean.'); + options.subtractFee = params.subtractFee; } } - if (typeof params.account === 'string') - options.account = params.account || null; - else if (typeof params.account === 'number') - options.account = params.account; + if (params.outputs) { + assert(Array.isArray(params.outputs), 'Outputs must be an array.'); + options.outputs = []; + for (i = 0; i < params.outputs.length; i++) { + output = params.outputs[i]; - if (params.name) - options.name = params.name; + if (output.address) + assert(typeof output.address === 'string', 'Address must be a string.'); + else if (output.script) + assert(typeof output.script === 'string', 'Script must be a string.'); + else + assert(false, 'No address or script present.'); - if (params.age) - options.age = params.age >>> 0; - - if (params.key) - params.keys = params.key; - - if (params.keys) { - if (typeof params.keys === 'string') - options.keys = params.keys.split(','); - else - options.keys = params.keys; + options.outputs.push({ + address: output.address + ? bcoin.address.fromBase58(output.address) + : null, + script: output.script + ? bcoin.script.fromRaw(output.script, 'hex') + : null, + value: utils.satoshi(output.value) + }); + } } - if (params.passphrase) + if (params.address) { + if (Array.isArray(options.address)) { + options.address = []; + for (i = 0; i < params.address.length; i++) { + address = params.address[i]; + assert(typeof address === 'string', 'Address must be a string.'); + address = bcoin.address.fromBase58(address); + } + } else { + assert(typeof params.address === 'string', 'Address must be a string.'); + options.address = bcoin.address.fromBase58(params.address); + } + } + + if (params.tx) { + if (typeof params.tx === 'object') { + options.tx = bcoin.tx.fromJSON(params.tx); + } else { + assert(typeof params.tx === 'string', 'TX must be a hex string.'); + options.tx = bcoin.tx.fromRaw(params.tx, 'hex'); + } + } + + if (params.account != null) { + if (typeof params.account === 'number') { + options.account = params.account; + assert(utils.isUInt32(options.account), 'Account must be a number.'); + } else { + assert(typeof params.account === 'string', 'Account must be a string.'); + options.account = params.account; + } + } + + if (params.type) { + assert(typeof params.type === 'string', 'Type must be a string.'); + options.type = params.type; + } + + if (params.name) { + assert(typeof params.name === 'string', 'Name must be a string.'); + options.name = params.name; + } + + if (params.key) { + assert(typeof params.key === 'string', 'Key must be a string.'); + options.key = params.key; + } + + if (params.old) { + assert(typeof params.old === 'string', 'Passphrase must be a string.'); + assert(params.old.length > 0, 'Passphrase must be a string.'); + options.old = params.old; + } + + if (params.passphrase) { + assert(typeof params.passphrase === 'string', 'Passphrase must be a string.'); + assert(params.passphrase.length > 0, 'Passphrase must be a string.'); options.passphrase = params.passphrase; + } if (params.token) { assert(utils.isHex(params.token), 'API key must be a hex string.'); @@ -307,19 +386,8 @@ HTTPServer.prototype._init = function _init() { }); }); - function decodeScript(script) { - if (!script) - return; - if (typeof script === 'string') - return bcoin.script.fromRaw(script, 'hex'); - return new bcoin.script(script); - } - // JSON RPC this.post('/', function(req, res, next, send) { - if (!(req.body.method && req.body.params)) - return next(new Error('Method not found.')); - if (!self.rpc) { RPC = require('./rpc'); self.rpc = new RPC(self.node); @@ -359,23 +427,21 @@ HTTPServer.prototype._init = function _init() { send(200, { version: constants.USER_VERSION, agent: constants.USER_AGENT, + services: self.pool.services, network: self.network.type, - height: self.node.chain.height, - tip: utils.revHex(self.node.chain.tip.hash), - peers: self.node.pool.peers.all.length, - progress: self.node.chain.getProgress() + height: self.chain.height, + tip: self.chain.tip.rhash, + peers: self.pool.peers.all.length, + progress: self.chain.getProgress() }); }); // UTXO by address this.get('/coin/address/:address', function(req, res, next, send) { - self.node.getCoinsByAddress(req.options.addresses, function(err, coins) { + self.node.getCoinsByAddress(req.options.address, function(err, coins) { if (err) return next(err); - if (!coins.length) - return send(404); - send(200, coins.map(function(coin) { return coin.toJSON(); })); @@ -397,13 +463,10 @@ HTTPServer.prototype._init = function _init() { // Bulk read UTXOs this.post('/coin/address', function(req, res, next, send) { - self.node.getCoinsByAddress(req.options.addresses, function(err, coins) { + self.node.getCoinsByAddress(req.options.address, function(err, coins) { if (err) return next(err); - if (!coins.length) - return send(404); - send(200, coins.map(function(coin) { return coin.toJSON(); })); @@ -430,7 +493,7 @@ HTTPServer.prototype._init = function _init() { // TX by address this.get('/tx/address/:address', function(req, res, next, send) { - self.node.getTXByAddress(req.options.addresses, function(err, txs) { + self.node.getTXByAddress(req.options.address, function(err, txs) { if (err) return next(err); @@ -449,13 +512,10 @@ HTTPServer.prototype._init = function _init() { // Bulk read TXs this.post('/tx/address', function(req, res, next, send) { - self.node.getTXByAddress(req.options.addresses, function(err, txs) { + self.node.getTXByAddress(req.options.address, function(err, txs) { if (err) return next(err); - if (!txs.length) - return send(404); - utils.forEachSerial(txs, function(tx, next) { self.node.fillHistory(tx, next); }, function(err) { @@ -485,13 +545,10 @@ HTTPServer.prototype._init = function _init() { // Mempool snapshot this.get('/mempool', function(req, res, next, send) { - self.node.mempool.getHistory(function(err, txs) { + self.mempool.getHistory(function(err, txs) { if (err) return next(err); - if (!txs.length) - return send(404); - utils.forEachSerial(txs, function(tx, next) { self.node.fillHistory(tx, next); }, function(err) { @@ -515,6 +572,12 @@ HTTPServer.prototype._init = function _init() { }); }); + // Estimate fee + this.get('/fee', function(req, res, next, send) { + var fee = self.fees.estimateFee(req.options.blocks); + send(200, { rate: utils.btc(fee) }); + }); + // Get wallet this.get('/wallet/:id', function(req, res, next, send) { send(200, req.wallet.toJSON()); @@ -645,6 +708,19 @@ HTTPServer.prototype._init = function _init() { }); }); + // Abandon Wallet TX + this.del('/wallet/:id/tx/:hash', function(req, res, next, send) { + var hash = req.options.hash; + var account = req.options.account; + + req.wallet.abandon(hash, function(err) { + if (err) + return next(err); + + send(200, { success: true }); + }); + }); + // Add key this.put('/wallet/:id/key', function(req, res, next, send) { var account = req.options.account; @@ -672,7 +748,7 @@ HTTPServer.prototype._init = function _init() { // Create address this.post('/wallet/:id/address', function(req, res, next, send) { var account = req.options.account; - req.wallet.createAddress(account, false, function(err, address) { + req.wallet.createReceive(account, function(err, address) { if (err) return next(err); @@ -832,29 +908,32 @@ HTTPServer.prototype._initIO = function _initIO() { if (!this.server.io) return; - this.server.on('websocket', function(socket) { - socket.bcoin = new ClientSocket(self, socket); - socket.bcoin.startTimeout(); + this.server.on('websocket', function(ws) { + var socket = new ClientSocket(self, ws); + + socket.start(); socket.on('error', function(err) { self.emit('error', err); }); - socket.on('auth', function(apiKey, callback) { - callback = utils.ensure(callback); + socket.once('auth', function(apiKey, callback) { + if (typeof callback !== 'function') + return socket.destroy(); - if (!self.apiHash) { - self.logger.info('Successful auth.'); - socket.bcoin.stopTimeout(); - self.emit('websocket', socket); - return callback(); + socket.stop(); + + if (self.apiHash) { + if (!utils.ccmp(hash256(apiKey), self.apiHash)) { + socket.destroy(); + return callback({ error: 'Bad key.' }); + } } - if (!utils.ccmp(hash256(apiKey), self.apiHash)) - return callback({ error: 'Bad key.' }); + socket.auth = true; + + self.logger.info('Successful auth from %s.', socket.host); - self.logger.info('Successful auth.'); - socket.bcoin.stopTimeout(); self.emit('websocket', socket); return callback(); @@ -869,13 +948,20 @@ HTTPServer.prototype._initIO = function _initIO() { this.on('websocket', function(socket) { socket.on('wallet join', function(id, token, callback) { - callback = utils.ensure(callback); + if (typeof callback !== 'function') + return socket.destroy(); + + if (typeof id !== 'string') + return callback({ error: 'Invalid parameter.' }); if (!self.options.walletAuth) { socket.join(id); return callback(); } + if (typeof token !== 'string') + return callback({ error: 'Invalid parameter.' }); + self.walletdb.auth(id, token, function(err, wallet) { if (err) { self.logger.info('Wallet auth failure for %s: %s.', id, err.message); @@ -886,14 +972,22 @@ HTTPServer.prototype._initIO = function _initIO() { return callback({ error: 'Wallet does not exist.' }); self.logger.info('Successful wallet auth for %s.', id); + socket.join(id); + return callback(); }); }); socket.on('wallet leave', function(id, callback) { - callback = utils.ensure(callback); + if (typeof callback !== 'function') + return socket.destroy(); + + if (typeof id !== 'string') + return callback({ error: 'Invalid parameter.' }); + socket.leave(id); + return callback(); }); }); @@ -1034,20 +1128,57 @@ HTTPServer.prototype.listen = function listen(port, host, callback) { */ function ClientSocket(server, socket) { + if (!(this instanceof ClientSocket)) + return new ClientSocket(server, socket); + + EventEmitter.call(this); + this.server = server; this.socket = socket; + this.host = socket.conn.remoteAddress; this.timeout = null; + this.auth = false; + + this._init(); } -ClientSocket.prototype.startTimeout = function startTimeout() { +utils.inherits(ClientSocket, EventEmitter); + +ClientSocket.prototype._init = function _init() { var self = this; - this.stopTimeout(); + var socket = this.socket; + var emit = EventEmitter.prototype.emit; + var onevent = socket.onevent.bind(socket); + socket.onevent = function(packet) { + var result = onevent(packet); + var args = packet.data || []; + emit.apply(self, args); + return result; + }; +}; + +ClientSocket.prototype.join = function join(id) { + this.socket.join(id); +}; + +ClientSocket.prototype.leave = function leave(id) { + this.socket.leave(id); +}; + +ClientSocket.prototype.emit = function emit() { + this.socket.emit.apply(this.socket, arguments); +}; + +ClientSocket.prototype.start = function start() { + var self = this; + this.stop(); this.timeout = setTimeout(function() { + self.timeout = null; self.destroy(); }, 60000); }; -ClientSocket.prototype.stopTimeout = function stopTimeout() { +ClientSocket.prototype.stop = function stop() { if (this.timeout != null) { clearTimeout(this.timeout); this.timeout = null; @@ -1055,6 +1186,7 @@ ClientSocket.prototype.stopTimeout = function stopTimeout() { }; ClientSocket.prototype.destroy = function() { + this.stop(); this.socket.disconnect(); }; @@ -1070,6 +1202,17 @@ function hash256(data) { return utils.hash256(new Buffer(data, 'utf8')); } +function softMerge(a, b, soft) { + var keys = Object.keys(b); + var i, key, value; + for (i = 0; i < keys.length; i++) { + key = keys[i]; + value = b[key]; + if (!soft || value) + a[key] = value; + } +} + /* * Expose */ diff --git a/lib/bcoin/spvnode.js b/lib/bcoin/spvnode.js index 6d45c10f..5c5b465b 100644 --- a/lib/bcoin/spvnode.js +++ b/lib/bcoin/spvnode.js @@ -212,19 +212,8 @@ SPVNode.prototype._open = function open(callback) { }); }, function(next) { - var i; - self.wallet.getUnconfirmed(function(err, txs) { - if (err) - return next(err); - - if (txs.length > 0) - self.logger.info('Rebroadcasting %d transactions.', txs.length); - - for (i = 0; i < txs.length; i++) - self.pool.broadcast(txs[i]); - - next(); - }); + // Rebroadcast pending transactions. + self.wallet.resend(next); }, function(next) { if (!self.http) diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 0ca763d5..23b3d16c 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -915,11 +915,34 @@ Wallet.prototype.send = function send(options, callback) { }, true); }; +/** + * Resend pending wallet transactions. + * @param {Function} callback + */ + +Wallet.prototype.resend = function resend(callback) { + var self = this; + var i; + + this.getUnconfirmed(function(err, txs) { + if (err) + return callback(err); + + if (txs.length > 0) + self.logger.info('Rebroadcasting %d transactions.', txs.length); + + for (i = 0; i < txs.length; i++) + self.db.emit('send', txs[i]); + + return callback(); + }); +}; + /** * Derive necessary addresses for signing a transaction. * @param {TX|Input} tx * @param {Number?} index - Input index. - * @returns {KeyRing[]} + * @param {Function} callback - Returns [Error, {@link KeyRing}[]]. */ Wallet.prototype.deriveInputs = function deriveInputs(tx, callback) {