diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 0a4abd52..1eb9edc1 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -270,7 +270,7 @@ Chain.prototype._preload = function _preload(callback) { var self = this; var url = 'https://headers.electrum.org/blockchain_headers'; var buf, height, stream; - var request; + var request = require('./http/request'); if (!this.options.preload) return callback(); @@ -281,19 +281,13 @@ Chain.prototype._preload = function _preload(callback) { if (network.type !== 'main') return callback(new Error('Electrum.org only offers `main` headers.')); - try { - request = require('request'); - } catch (e) { - return callback(e); - } - utils.debug('Loading %s', url); this.db.getChainHeight(function(err, chainHeight) { if (err) return callback(err); - stream = request.get(url); + stream = request({ method: 'GET', uri: url }); height = 0; buf = { data: [], @@ -386,7 +380,7 @@ Chain.prototype._preload = function _preload(callback) { // redundant blocks to disk! if (height <= chainHeight) { self.db.addCache(entry); - self.db.bloom(entry.hash, 'hex'); + // self.db.bloom(entry.hash, 'hex'); } else { self.db.save(entry); } diff --git a/lib/bcoin/http/client.js b/lib/bcoin/http/client.js new file mode 100644 index 00000000..311d0750 --- /dev/null +++ b/lib/bcoin/http/client.js @@ -0,0 +1,370 @@ +/** + * client.js - http client for wallets + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var EventEmitter = require('events').EventEmitter; +var bcoin = require('../../bcoin'); +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var utils = bcoin.utils; +var assert = utils.assert; +var request = require('./request'); + +/** + * Client + */ + +function Client(uri) { + if (!(this instanceof Client)) + return new Client(uri); + + EventEmitter.call(this); + + this.uri = uri; + this.id = null; + this._init(); +} + +utils.inherits(Client, EventEmitter); + +Client.prototype._init = function _init() { + var self = this; + var io; + + try { + io = require('socket.io'); + } catch (e) { + ; + } + + if (!io) + return; + + this.socket = new io.Socket(this.uri); + + this.socket.on('open', function() { + self.socket.on('tx', function(tx, map) { + self.emit('tx', bcoin.tx.fromJSON(tx), map); + }); + + self.socket.on('confirmed', function(tx, map) { + self.emit('confirmed', bcoin.tx.fromJSON(tx), map); + }); + + self.socket.on('updated', function(tx, map) { + self.emit('updated', bcoin.tx.fromJSON(tx), map); + }); + + self.socket.on('balance', function(balance, id) { + self.emit('balance', utils.satoshi(balance), id); + }); + + self.socket.on('balances', function(balances) { + Object.keys(balances).forEach(function(id) { + balances[id] = utils.satoshi(balances[id]); + }); + self.emit('balances', balances); + }); + + self.socket.on('error', function(err) { + self.emit('error', err); + }); + }); +}; + +Client.prototype.listenWallet = function listenWallet(id) { + this.socket.join(id); +}; + +Client.prototype.unlistenWallet = function unlistenWallet(id) { + this.socket.leave(id); +}; + +Client.prototype.listenAll = function listenAll(id) { + this.listenWallet('!all'); +}; + +Client.prototype.unlistenAll = function unlistenAll(id) { + this.unlistenWallet('!all'); +}; + +Client.prototype.destroy = function destroy() { + this.socket.destroy(); + this.socket = null; +}; + +Client.prototype._request = function _request(method, endpoint, json, callback) { + var query; + + if (!callback) { + callback = json; + json = null; + } + + if (json && method === 'get') { + json = null; + query = json; + } + + request({ + method: method, + uri: this.uri + '/' + endpoint, + query: query, + json: json, + expect: 'json' + }, function(err, res, body) { + if (err) + return callback(err); + + if (res.statusCode === 404) + return callback(); + + if (!body) + return callback(new Error('No body.')); + + if (res.statusCode !== 200) + return callback(new Error('Status code: ' + res.statusCode)); + + try { + return callback(null, body); + } catch (e) { + return callback(e); + } + }); +}; + +Client.prototype._get = function _get(endpoint, json, callback) { + return this._request('get', endpoint, json, callback); +}; + +Client.prototype._post = function _post(endpoint, json, callback) { + return this._request('post', endpoint, json, callback); +}; + +Client.prototype._put = function _put(endpoint, json, callback) { + return this._request('put', endpoint, json, callback); +}; + +Client.prototype._del = function _del(endpoint, json, callback) { + return this._request('delete', endpoint, json, callback); +}; + +Client.prototype.getWalletAll = function getWalletAll(id, callback) { + return this._get('/wallet/' + id + '/tx/all', function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, body.map(function(data) { + return bcoin.tx.fromJSON(data); + })); + }); +}; + +Client.prototype.getWalletCoins = function getWalletCoins(id, callback) { + return this._get('/wallet/' + id + '/coin', function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, body.map(function(data) { + return bcoin.coin.fromJSON(data); + })); + }); +}; + +Client.prototype.getWalletPending = function getPending(id, callback) { + return this._get('/wallet/' + id + '/tx/pending', function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, body.map(function(data) { + return bcoin.coin.fromJSON(data); + })); + }); +}; + +Client.prototype.getWalletBalance = function getBalance(id, callback) { + return this._get('/wallet/' + id + '/balance', function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(new Error('Not found.')); + + return callback(null, utils.satoshi(body.balance)); + }); +}; + +Client.prototype.getWalletLast = function getLast(id, limit, callback) { + return this._get('/wallet/' + id + '/tx/last', options, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, body.map(function(data) { + return bcoin.tx.fromJSON(data); + })); + }); +}; + +Client.prototype.getWalletRange = function getWalletRange(id, options, callback) { + return this._get('/wallet/' + id + '/tx/range', options, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, body.map(function(data) { + return bcoin.tx.fromJSON(data); + })); + }); +}; + +Client.prototype.getWalletTX = function getTX(id, hash, callback) { + hash = utils.revHex(hash); + + return this._get('/wallet/' + id + '/tx/' + hash, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, bcoin.tx.fromJSON(body)); + }); +}; + +Client.prototype.getWalletCoin = function getCoin(id, hash, index, callback) { + hash = utils.revHex(hash); + + return this._get('/wallet/' + id + '/coin/' + hash + '/' + index, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, bcoin.coin.fromJSON(body)); + }); +}; + +Client.prototype.syncWallet = function syncWallet(id, options, callback) { + var self = this; + + var body = { + receiveDepth: options.receiveDepth, + changeDepth: options.changeDepth + }; + + return this._put('/wallet/' + id, body, function(err) { + if (err) + return callback(err); + return callback(); + }); +}; + +Client.prototype.getCoinByAddress = function getCoinByAddress(address, callback) { + var body = { addresses: address }; + + return this._post('/coin/address', body, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, body.map(function(data) { + return bcoin.coin.fromJSON(data); + })); + }); +}; + +Client.prototype.getCoin = function getCoin(hash, index, callback) { + hash = utils.revHex(hash); + + return this._get('/coin/' + hash + '/' + index, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, bcoin.coin.fromJSON(body)); + }); +}; + +Client.prototype.getTXByAddress = function getTXByAddress(address, callback) { + var body = { addresses: address }; + + return this._post('/tx/address', body, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, body.map(function(data) { + return bcoin.tx.fromJSON(data); + })); + }); +}; + +Client.prototype.getTX = function getTX(hash, callback) { + hash = utils.revHex(hash); + + return this._get('/tx/' + hash, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, bcoin.tx.fromJSON(body)); + }); +}; + +Client.prototype.getBlock = function getBlock(hash, callback) { + if (typeof hash !== 'number') + hash = utils.revHex(hash); + + return this._get('/block/' + hash, function(err, body) { + if (err) + return callback(err); + + if (!body) + return callback(null, []); + + return callback(null, bcoin.block.fromJSON(body)); + }); +}; + +Client.prototype.broadcast = function broadcast(tx, callback) { + var body = { tx: utils.toHex(tx.toRaw()) }; + + callback = utils.ensure(callback); + + return this._post('/broadcast', body, function(err, body) { + if (err) + return callback(err); + return callback(); + }); +}; + +/** + * Expose + */ + +module.exports = Client; diff --git a/lib/bcoin/http/http.js b/lib/bcoin/http/http.js index fcc3af88..88afe252 100644 --- a/lib/bcoin/http/http.js +++ b/lib/bcoin/http/http.js @@ -5,35 +5,24 @@ */ var EventEmitter = require('events').EventEmitter; -var StringDecoder = require('string_decoder').StringDecoder; -var url = require('url'); -var engine; - -try { - engine = require('engine.io'); -} catch (e) { - ; -} - -var bcoin = require('../bcoin'); -var bn = require('bn.js'); -var constants = bcoin.protocol.constants; -var network = bcoin.protocol.network; +var bcoin = require('../../bcoin'); var utils = bcoin.utils; -var assert = utils.assert; /** * HTTPServer */ -function HTTPServer(node, options) { +function HTTPServer(options) { var self = this; + if (!(this instanceof HTTPServer)) + return new HTTPServer(options); + if (!options) options = {}; this.options = options; - this.node = node; + this.io = null; this.routes = { get: [], @@ -42,6 +31,8 @@ function HTTPServer(node, options) { del: [] }; + this.stack = []; + this.server = options.key ? require('https').createServer(options) : require('http').createServer(); @@ -55,7 +46,7 @@ HTTPServer.prototype._init = function _init() { var self = this; this._initRouter(); - this._initEngine(); + this._initIO(); this.server.on('connection', function(socket) { socket.on('error', function(err) { @@ -67,348 +58,30 @@ HTTPServer.prototype._init = function _init() { }); }); - this.get('/', function(req, res, next, send) { - send(200, { - version: require('../../package.json').version, - network: self.node.network.type - }); - }); - - // UTXO by address - this.get('/coin/address/:address', function(req, res, next, send) { - var addresses = req.params.address.split(','); - self.node.getCoinByAddress(addresses, function(err, coins) { - if (err) - return next(err); - - if (!coins.length) - return send(404); - - send(200, coins.map(function(coin) { return coin.toJSON(); })); - }); - }); - - // UTXO by id - this.get('/coin/:hash/:index', function(req, res, next, send) { - req.params.hash = utils.revHex(req.params.hash); - self.node.getCoin(req.params.hash, +req.params.index, function(err, coin) { - if (err) - return next(err); - - if (!coin) - return send(404); - - send(200, coin.toJSON()); - }); - }); - - // Bulk read UTXOs - this.post('/coin/address', function(req, res, next, send) { - self.node.getCoinByAddress(req.body.addresses, function(err, coins) { - if (err) - return next(err); - - if (!coins.length) - return send(404); - - send(200, coins.map(function(coin) { return coin.toJSON(); })); - }); - }); - - // TX by hash - this.get('/tx/:hash', function(req, res, next, send) { - req.params.hash = utils.revHex(req.params.hash); - self.node.getTX(req.params.hash, function(err, tx) { - if (err) - return next(err); - - if (!tx) - return send(404); - - send(200, tx.toJSON()); - }); - }); - - // TX by address - this.get('/tx/address/:address', function(req, res, next, send) { - var addresses = req.params.address.split(','); - self.node.getTXByAddress(addresses, function(err, txs) { - if (err) - return next(err); - - if (!txs.length) - return send(404); - - send(200, txs.map(function(tx) { return tx.toJSON(); })); - }); - }); - - // Bulk read TXs - this.post('/tx/address', function(req, res, next, send) { - self.node.getTXByAddress(req.params.addresses, function(err, txs) { - if (err) - return next(err); - if (!txs.length) - return send(404); - send(200, txs.map(function(tx) { return tx.toJSON(); })); - }); - }); - - // Block by hash/height - this.get('/block/:hash', function(req, res, next, send) { - var hash = req.params.hash; - - if (utils.isInt(hash)) - hash = +hash; - else - hash = utils.revHex(hash); - - self.node.getFullBlock(hash, function(err, block) { - if (err) - return next(err); - - if (!block) - return send(404); - - send(200, block.toJSON()); - }); - }); - - // Get wallet - this.get('/wallet/:id', function(req, res, next, send) { - self.node.walletdb.getJSON(req.params.id, function(err, json) { - if (err) - return next(err); - - if (!json) - return send(404); - - send(200, json); - }); - }); - - // Create/get wallet - this.post('/wallet/:id', function(req, res, next, send) { - req.body.id = req.params.id; - self.node.walletdb.create(req.body, function(err, wallet) { - var wallet; - - if (err) - return next(err); - - if (!wallet) - return send(404); - - json = wallet.toJSON(); - wallet.destroy(); - - send(200, json); - }); - }); - - // Update wallet / sync address depth - this.put('/wallet/:id', function(req, res, next, send) { - var id = req.params.id; - var receive = req.body.receiveDepth >>> 0; - var change = req.body.changeDepth >>> 0; - self.node.walletdb.setDepth(id, receive, change, function(err) { - if (err) - return next(err); - - if (!json) - return send(404); - - send(200, { success: true }); - }); - }); - - // Wallet Balance - this.get('/wallet/:id/balance', function(req, res, next, send) { - var id = req.params.id; - if (id === '_all') - id = null; - self.node.walletdb.getBalance(req.params.id, function(err, balance) { - if (err) - return next(err); - - if (!coins.length) - return send(404); - - send(200, { balance: utils.btc(balance) }); - }); - }); - - // Wallet UTXOs - this.get('/wallet/:id/coin', function(req, res, next, send) { - var id = req.params.id; - if (id === '_all') - id = null; - self.node.walletdb.getCoins(req.params.id, function(err, coins) { - if (err) - return next(err); - - if (!coins.length) - return send(404); - - send(200, coins.map(function(coin) { return coin.toJSON(); })); - }); - }); - - // Wallet TX - this.get('/wallet/:id/coin/:hash/:index', function(req, res, next, send) { - var id = req.params.id; - if (id === '_all') - id = null; - self.node.walletdb.getCoin(req.params.hash, +req.params.index, function(err, coin) { - if (err) - return next(err); - - if (!coin) - return send(404); - - send(200, coin.toJSON()); - }); - }); - - // Wallet TXs - this.get('/wallet/:id/tx/all', function(req, res, next, send) { - var id = req.params.id; - if (id === '_all') - id = null; - self.node.walletdb.getAll(req.params.id, function(err, txs) { - if (err) - return next(err); - - if (!txs.length) - return send(404); - - send(200, txs.map(function(tx) { return tx.toJSON(); })); - }); - }); - - // Wallet Pending TXs - this.get('/wallet/:id/tx/pending', function(req, res, next, send) { - var id = req.params.id; - if (id === '_all') - id = null; - self.node.walletdb.getPending(req.params.id, function(err, txs) { - if (err) - return next(err); - - if (!txs.length) - return send(404); - - send(200, txs.map(function(tx) { return tx.toJSON(); })); - }); - }); - - // Wallet TXs within time range - this.get('/wallet/:id/tx/range', function(req, res, next, send) { - var id = req.params.id; - if (id === '_all') - id = null; - - var options = { - start: +req.query.start, - end: +req.query.end, - limit: +req.query.limit - }; - - self.node.walletdb.getTimeRange(req.params.id, options, function(err, txs) { - if (err) - return next(err); - - if (!txs.length) - return send(404); - - send(200, txs.map(function(tx) { return tx.toJSON(); })); - }); - }); - - // Wallet TXs within time range - this.get('/wallet/:id/tx/last', function(req, res, next, send) { - var id = req.params.id; - if (id === '_all') - id = null; - - self.node.walletdb.getTimeRange(id, +req.query.limit, function(err, txs) { - if (err) - return next(err); - - if (!txs.length) - return send(404); - - send(200, txs.map(function(tx) { return tx.toJSON(); })); - }); - }); - - // Wallet TX - this.get('/wallet/:id/tx/:hash', function(req, res, next, send) { - self.node.walletdb.getTX(req.params.hash, function(err, tx) { - if (err) - return next(err); - - if (!tx) - return send(404); - - send(200, tx.toJSON()); - }); - }); - - this.post('/broadcast', function(req, res, next, send) { - self.node.pool.broadcast(bcoin.tx.fromRaw(req.body.tx, 'hex')); - send(200, { success: true }); + this.server.on('error', function(err) { + self.emit('error', err); }); }; -HTTPServer.prototype._initEngine = function _initEngine() { +HTTPServer.prototype._initIO = function _initIO() { var self = this; + var io; - if (!engine) + try { + io = require('socket.io'); + } catch (e) { + ; + } + + if (!io) return; - this.clients = []; + this.io = new io.Server(); - this.engine = new engine.Server(); + this.io.attach(this.server); - this.engine.attach(this.server); - - this.engine.on('connection', function(socket) { - var s = new Socket(self, socket); - - socket.on('message', function(data) { - s.parse(data); - }); - - socket.on('close', function() { - s.destroy(); - }); - - socket.on('error', function(err) { - self.emit('error', err); - }); - - s.on('error', function(err) { - self.emit('error', err); - }); - }); -}; - -HTTPServer.prototype.listen = function listen(port, host, callback) { - var self = this; - this.server.listen(port, host, function(err) { - var address; - - if (err) - throw err; - - address = self.server.address(); - - utils.debug('Listening - host=%s port=%d', - address.address, address.port); - - if (callback) - callback(); + this.io.on('connection', function(socket) { + self.emit('websocket', socket); }); }; @@ -485,19 +158,60 @@ HTTPServer.prototype._initRouter = function _initRouter() { req.params[i] = item; }); - // Avoid stack overflows - utils.nextTick(function() { - try { - callback(req, res, next, _send); - } catch (e) { - done(e); - } + self._handle(req, res, _send, function(err) { + if (err) + return done(err); + + // Avoid stack overflows + utils.nextTick(function() { + try { + callback(req, res, next, _send); + } catch (e) { + done(e); + } + }); }); })(); }); }); }; +HTTPServer.prototype._handle = function _handle(req, res, _send, callback) { + var self = this; + var i = 0; + var handler; + + (function next(err) { + if (err) + return callback(err); + + if (i === self.stack.length) + return callback(); + + handler = self.stack[i++]; + + utils.nextTick(function() { + if (handler.path && req.pathname.indexOf(handler.path) === -1) + return next(); + + try { + handler.callback(req, res, next, _send); + } catch (e) { + next(e); + } + }); + })(); +}; + +HTTPServer.prototype.use = function use(path, callback) { + if (!callback) { + callback = path; + path = null; + } + + this.stack.push({ path: path, callback: callback }); +}; + HTTPServer.prototype.get = function get(path, callback) { this.routes.get.push({ path: path, callback: callback }); }; @@ -514,37 +228,21 @@ HTTPServer.prototype.del = function del(path, callback) { this.routes.del.push({ path: path, callback: callback }); }; -HTTPServer.prototype.sendWebhook = function sendWebhook(msg, callback) { - var request, body, secret, hmac; +HTTPServer.prototype.listen = function listen(port, host, callback) { + var self = this; + this.server.listen(port, host, function(err) { + var address; - callback = utils.ensure(callback); - - if (!this.options.webhook) - return callback(); - - try { - request = require('request'); - } catch (e) { - return callback(e); - } - - body = new Buffer(JSON.stringify(msg) + '\n', 'utf8'); - secret = new Buffer(this.options.webhook.secret || '', 'utf8'); - hmac = utils.sha512hmac(body, secret); - - request({ - method: 'POST', - uri: this.options.webhook.endpoint, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'Content-Length': body.length + '', - 'X-Bcoin-Hmac': hmac.toString('hex') - }, - body: body - }, function(err, res, body) { if (err) - return callback(err); - return callback(); + throw err; + + address = self.server.address(); + + utils.debug('Listening - host=%s port=%d', + address.address, address.port); + + if (callback) + callback(); }); }; @@ -558,8 +256,10 @@ function send(res, code, msg) { try { res.statusCode = code; - msg = JSON.stringify(msg, null, 2) + '\n'; - res.setHeader('Content-Type', 'application/json; charset=utf-8'); + if (typeof msg === 'object') { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + msg = JSON.stringify(msg, null, 2) + '\n'; + } res.setHeader('Content-Length', Buffer.byteLength(msg) + ''); res.write(msg); res.end(); @@ -594,6 +294,7 @@ function compilePath(path) { } function parseBody(req, callback) { + var StringDecoder = require('string_decoder').StringDecoder; var decode = new StringDecoder('utf8'); var total = 0; var body = ''; @@ -661,6 +362,7 @@ function parsePairs(str, del, eq) { } function parsePath(req) { + var url = require('url'); var uri = url.parse(req.url); var pathname = uri.pathname || '/'; @@ -706,215 +408,6 @@ function unescape(str) { } } -function Socket(server, socket) { - this.server = server; - this.engine = server.engine; - this.node = server.node; - this.walletdb = server.node.walletdb; - this.socket = socket; - this.listeners = {}; - this._init(); -} - -Socket.prototype._init = function _init() { - this.server.clients.push(this); -}; - -Socket.prototype.parse = function parse(msg) { - var size, off, header, payload; - - size = utils.readIntv(msg, 0); - off = size.off; - size = size.r; - - if (off + size > msg.length) - return this.send({ event: 'error', msg: 'Size larger than message.' }); - - try { - header = JSON.parse(msg.slice(off, off + size).toString('utf8')); - off += size; - } catch (e) { - return this.send({ event: 'error', msg: 'Header malformed.' }); - } - - payload = data.slice(off); - - try { - return this._handle(header, payload); - } catch (e) { - return this.send({ event: 'error', msg: e.message + '' }); - } -}; - -Socket.prototype._handle = function _handle(header, payload) { - if (header.cmd === 'handshake') - return this._handleHandshake(header, payload); - - if (header.cmd === 'listen') - return this._handleListen(header, payload); - - if (header.cmd === 'unlisten') - return this._handleUnlisten(header, payload); - - if (header.event) - return this.emit(header.event, header, payload); - - throw new Error('Not a valid command.'); -}; - -Socket.prototype.destroy = function destroy() { - this.remove(); - this.cleanup(); - return this.clients.destroy(); -}; - -Socket.prototype.remove = function remove() { - var i = this.server.clients.indexOf(this); - if (i !== -1) - this.server.clients.splice(i, 1); -}; - -Socket.prototype.cleanup = function cleanup() { - Object.keys(this.listeners).forEach(function(id) { - this.unlistenWallet(id); - }, this); -}; - -Socket.prototype.unlistenWallet = function unlistenWallet(id) { - this.listeners[id].forEach(function(listener) { - this.walletdb.removeListener(listener[0], listener[1]); - }, this); - - delete this.listeners[id]; -}; - -Socket.prototype._listenWallet = function _listenWallet(id, event, listener) { - if (!this.listeners[id]) - this.listeners[id] = []; - - this.listeners[id].push([event, listener]); - this.walletdb.on(event, listener); -}; - -Socket.prototype.listenWallet = function listenWallet(event, id, listener) { - if (id === '_all') { - this._listenWallet(id, 'tx', function(tx, map) { - self.send({ event: 'tx', map: map, id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, 'updated', function(tx, map) { - self.send({ event: 'updated', map: map, id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, 'confirmed', function(tx, map) { - self.send({ event: 'confirmed', map: map, id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, 'unconfirmed', function(tx, map) { - self.send({ event: 'unconfirmed', map: map, id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, 'balances', function(balances) { - Object.keys(balances).forEach(function(key) { - balances[key] = utils.btc(balances[key]); - }); - self.send({ event: 'balances', id: id, balances: balances }); - }); - - return; - } - - this._listenWallet(id, id + ' tx', function(tx) { - self.send({ event: 'tx', id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, id + ' updated', function(tx) { - self.send({ event: 'updated', id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, id + ' confirmed', function(tx) { - self.send({ event: 'confirmed', id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, id + ' unconfirmed', function(tx) { - self.send({ event: 'unconfirmed', id: id, _payload: tx.toExtended() }); - }); - - this._listenWallet(id, id + ' balance', function(balance) { - self.send({ event: 'balance', id: id, balance: utils.btc(balance) }); - }); -}; - -Socket.prototype._onListen = function _onListen(header, payload) { - var self = this; - var id = header.id; - - if (typeof id !== 'string') - throw new Error('Wallet ID is not a string.'); - - if (id.length > 1000) - throw new Error('Wallet ID too large.'); - - if (!/^[a-zA-Z0-9]+$/.test(id)) - throw new Error('Wallet ID must be alphanumeric.'); - - if (this.listeners[id]) - throw new Error('Already listening.'); - - this.listenWallet(id); -}; - -Socket.prototype._onUnlisten = function _onUnlisten(header, payload) { - var self = this; - var id = header.id; - - if (typeof id !== 'string') - throw new Error('Wallet ID is not a string.'); - - if (id.length > 1000) - throw new Error('Wallet ID too large.'); - - if (!/^[a-zA-Z0-9]+$/.test(id)) - throw new Error('Wallet ID must be alphanumeric.'); - - if (!this.listeners[id]) - throw new Error('Not listening.'); - - this.unlistenWallet(id); -}; - -Socket.prototype._handleHandshake = function _handleHandshake(header, payload) { - if (this._activated) - throw new Error('Handshake already completed.'); - - if (payload.length > 0) - throw new Error('Unexpected payload.'); - - this._activated = true; -}; - -Socket.prototype.send = function send(data) { - var header, payload, size, msg; - - if (data._payload) { - payload = data._payload; - delete data._payload; - } else { - payload = new Buffer([]); - } - - assert(Buffer.isBuffer(payload)); - - header = new Buffer(JSON.stringify(data), 'utf8'); - - size = new Buffer(utils.sizeIntv(header.length)); - utils.writeIntv(size, header.length, 0); - - msg = Buffer.concat([size, header, payload]); - - return this.socket.send(msg); -}; - /** * Expose */ diff --git a/lib/bcoin/http/index.js b/lib/bcoin/http/index.js new file mode 100644 index 00000000..7aa7e755 --- /dev/null +++ b/lib/bcoin/http/index.js @@ -0,0 +1,4 @@ +exports.http = require('./http'); +exports.server = require('./server'); +exports.client = require('./client'); +exports.request = require('./request'); diff --git a/lib/bcoin/http/provider.js b/lib/bcoin/http/provider.js new file mode 100644 index 00000000..e4dd5793 --- /dev/null +++ b/lib/bcoin/http/provider.js @@ -0,0 +1,125 @@ +/** + * provider.js - http provider for wallets + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var EventEmitter = require('events').EventEmitter; + +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var utils = bcoin.utils; +var assert = utils.assert; +var Client = require('./client'); + +/** + * Provider + */ + +function Provider(url) { + if (!(this instanceof Provider)) + return new Provider(url); + + EventEmitter.call(this); + + this.client = new Client(url); + this.url = url; + this.id = null; + this._init(); +} + +utils.inherits(Provider, EventEmitter); + +Provider.prototype._init = function _init() { + var self = this; + + this.client.on('tx', function(tx) { + self.emit('tx', tx); + }); + + this.client.on('confirmed', function(tx) { + self.emit('confirmed', tx); + }); + + this.client.on('updated', function(tx) { + self.emit('updated', tx); + }); + + this.client.on('balance', function(balance) { + self.emit('balance', balance); + }); + + this.client.on('error', function(err) { + self.emit('error', err); + }); +}; + +Provider.prototype.setID = function setID(id) { + assert(!this.id) + this.id = id; + this.client.listenWallet(id); +}; + +Provider.prototype.destroy = function destroy() { + this.client.destroy(); + this.client = null; +}; + +Provider.prototype.getAll = function getAll(callback) { + return this.client.getWalletAll(this.id, callback); +}; + +Provider.prototype.getCoins = function getCoins(callback) { + return this.client.getWalletCoins(this.id, callback); +}; + +Provider.prototype.getPending = function getPending(callback) { + return this.client.getWalletPending(this.id, callback); +}; + +Provider.prototype.getBalance = function getBalance(callback) { + return this.client.getWalletBalance(this.id, callback); +}; + +Provider.prototype.getLastTime = function getLastTime(callback) { + assert(false); +}; + +Provider.prototype.getLast = function getLast(limit, callback) { + return this.client.getWalletLast(this.id, limit, callback); +}; + +Provider.prototype.getRange = function getRange(options, callback) { + return this.client.getWalletRange(this.id, options, callback); +}; + +Provider.prototype.getTX = function getTX(hash, callback) { + return this.client.getWalletTX(this.id, hash, callback); +}; + +Provider.prototype.getCoin = function getCoin(hash, index, callback) { + return this.client.getWalletCoin(this.id, hash, index, callback); +}; + +Provider.prototype.fillTX = function fillTX(tx, callback) { + assert(false); +}; + +Provider.prototype.fillCoin = function fillCoin(tx, callback) { + assert(false); +}; + +Provider.prototype.sync = function sync(wallet, address) { + return this.client.syncWallet(this.id, wallet, function(err) { + if (err) + self.emit('error', err); + }); +}; + +/** + * Expose + */ + +module.exports = WalletDB; diff --git a/lib/bcoin/http/request.js b/lib/bcoin/http/request.js index cb464e54..99b5f8ff 100644 --- a/lib/bcoin/http/request.js +++ b/lib/bcoin/http/request.js @@ -1,13 +1,24 @@ -var StringDecoder = require('string_decoder').StringDecoder; +/** + * request.js - http request for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + var Stream = require('stream').Stream; -var qs = require('querystring'); -var url = require('url'); // Spoof by default var USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)' + ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36'; +/** + * Request + */ + function request(options, callback, stream) { + var StringDecoder = require('string_decoder').StringDecoder; + var qs = require('querystring'); + var url = require('url'); + var uri = options.uri; var query = options.query; var body = options.body; @@ -57,7 +68,8 @@ function request(options, callback, stream) { path: uri.pathname + query, headers: { 'User-Agent': options.agent || USER_AGENT - } + }, + rejectUnauthorized: options.strictSSL !== false }; if (body) { @@ -99,6 +111,14 @@ function request(options, callback, stream) { opt.method = options.method || 'GET'; } + if (options.auth) + uri.auth = options.auth.username + ':' + options.auth.password; + + if (uri.auth) { + opt.headers['Authorization'] = + 'Basic ' + new Buffer(uri.auth, 'utf8').toString('base64'); + } + opt.method = opt.method.toUpperCase(); req = http.request(opt); @@ -246,8 +266,16 @@ request._buffer = function(options, callback) { return stream; }; +/** + * ReqStream + */ + function ReqStream(options) { + if (!(this instanceof ReqStream)) + return new ReqStream(options); + Stream.call(this); + this.req = null; this.res = null; this.headers = null; @@ -277,3 +305,9 @@ ReqStream.prototype.destroy = function destroy() { ; } }; + +/** + * Expose + */ + +module.exports = request; diff --git a/lib/bcoin/http/server.js b/lib/bcoin/http/server.js new file mode 100644 index 00000000..80b7cdbb --- /dev/null +++ b/lib/bcoin/http/server.js @@ -0,0 +1,459 @@ +/** + * server.js - http server for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var EventEmitter = require('events').EventEmitter; +var bcoin = require('../../bcoin'); +var HTTPServer = require('./http'); +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var utils = bcoin.utils; +var assert = utils.assert; + +/** + * NodeServer + */ + +function NodeServer(node, options) { + var self = this; + + if (!options) + options = {}; + + this.options = options; + this.node = node; + this.walletdb = node.walletdb; + this.pool = node.pool; + + this.server = new HTTPServer(options); + this.io = null; + + this._init(); +} + +utils.inherits(NodeServer, EventEmitter); + +NodeServer.prototype._init = function _init() { + var self = this; + + this.use(function(req, res, next, send) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET,HEAD,PUT,PATCH,POST,DELETE'); + + if (req.method === 'OPTIONS') + return send(200); + + next(); + }); + + this.use(function(req, res, next, send) { + var params = utils.merge({}, req.query, req.body, req.params); + var options = {}; + + if (params.id) { + assert(params.id !== '!all'); + options.id = params.id; + } + + if (params.hash) { + if (utils.isInt(params.hash)) + options.height = params.hash >>> 0; + else + options.hash = utils.revHex(params.hash); + } + + if (params.index != null) + options.index = params.index >>> 0; + + if (params.height != null) + options.height = params.height >>> 0; + + if (params.start != null) + options.start = params.start >>> 0; + + if (params.end != null) + options.end = params.end >>> 0; + + if (params.limit != null) + options.limit = params.limit >>> 0; + + if (params.changeDepth) + options.changeDepth = params.changeDepth >>> 0; + + if (params.receiveDepth) + options.receiveDepth = params.receiveDepth >>> 0; + + if (params.address) + params.addresses = params.address; + + if (params.addresses) { + if (typeof params.addresses === 'string') + options.addresses = params.addresses.split(','); + else + options.addresses = params.addresses; + } + + if (params.tx) { + try { + options.tx = bcoin.tx.fromRaw(params.tx, 'hex'); + } catch (e) { + return next(e); + } + } + + if (params.bin) + options.bin = true; + + req.options = options; + + next(); + }); + + this.get('/', function(req, res, next, send) { + send(200, { + version: require('../../../package.json').version, + network: self.node.network.type + }); + }); + + // UTXO by address + this.get('/coin/address/:address', function(req, res, next, send) { + self.node.getCoinByAddress(req.options.addresses, function(err, coins) { + if (err) + return next(err); + + if (!coins.length) + return send(404); + + send(200, coins.map(function(coin) { return coin.toJSON(); })); + }); + }); + + // UTXO by id + this.get('/coin/:hash/:index', function(req, res, next, send) { + self.node.getCoin(req.options.hash, req.options.index, function(err, coin) { + if (err) + return next(err); + + if (!coin) + return send(404); + + send(200, coin.toJSON()); + }); + }); + + // Bulk read UTXOs + this.post('/coin/address', function(req, res, next, send) { + self.node.getCoinByAddress(req.body.addresses, function(err, coins) { + if (err) + return next(err); + + if (!coins.length) + return send(404); + + send(200, coins.map(function(coin) { return coin.toJSON(); })); + }); + }); + + // TX by hash + this.get('/tx/:hash', function(req, res, next, send) { + self.node.getTX(req.options.hash, function(err, tx) { + if (err) + return next(err); + + if (!tx) + return send(404); + + send(200, tx.toJSON()); + }); + }); + + // TX by address + this.get('/tx/address/:address', function(req, res, next, send) { + self.node.getTXByAddress(req.options.addresses, function(err, txs) { + if (err) + return next(err); + + if (!txs.length) + return send(404); + + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Bulk read TXs + this.post('/tx/address', function(req, res, next, send) { + self.node.getTXByAddress(req.body.addresses, function(err, txs) { + if (err) + return next(err); + + if (!txs.length) + return send(404); + + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Block by hash/height + this.get('/block/:hash', function(req, res, next, send) { + var hash = req.options.hash || req.options.height; + self.node.getFullBlock(hash, function(err, block) { + if (err) + return next(err); + + if (!block) + return send(404); + + send(200, block.toJSON()); + }); + }); + + // Get wallet + this.get('/wallet/:id', function(req, res, next, send) { + self.walletdb.getJSON(req.options.id, function(err, json) { + if (err) + return next(err); + + if (!json) + return send(404); + + send(200, json); + }); + }); + + // Create/get wallet + this.post('/wallet/:id', function(req, res, next, send) { + self.walletdb.create(req.options, function(err, wallet) { + var json; + + if (err) + return next(err); + + if (!wallet) + return send(404); + + json = wallet.toJSON(); + wallet.destroy(); + + send(200, json); + }); + }); + + // Update wallet / sync address depth + this.put('/wallet/:id', function(req, res, next, send) { + var id = req.options.id; + var receive = req.options.receiveDepth; + var change = req.options.changeDepth; + + self.walletdb.setDepth(id, receive, change, function(err) { + if (err) + return next(err); + + if (!json) + return send(404); + + send(200, { success: true }); + }); + }); + + // Wallet Balance + this.get('/wallet/:id/balance', function(req, res, next, send) { + self.walletdb.getBalance(req.options.id, function(err, balance) { + if (err) + return next(err); + + if (!coins.length) + return send(404); + + send(200, { balance: utils.btc(balance) }); + }); + }); + + // Wallet UTXOs + this.get('/wallet/:id/coin', function(req, res, next, send) { + self.walletdb.getCoins(req.options.id, function(err, coins) { + if (err) + return next(err); + + if (!coins.length) + return send(404); + + send(200, coins.map(function(coin) { return coin.toJSON(); })); + }); + }); + + // Wallet TX + this.get('/wallet/:id/coin/:hash/:index', function(req, res, next, send) { + self.walletdb.getCoin(req.options.hash, req.options.index, function(err, coin) { + if (err) + return next(err); + + if (!coin) + return send(404); + + send(200, coin.toJSON()); + }); + }); + + // Wallet TXs + this.get('/wallet/:id/tx/all', function(req, res, next, send) { + self.walletdb.getAll(req.options.id, function(err, txs) { + if (err) + return next(err); + + if (!txs.length) + return send(404); + + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Wallet Pending TXs + this.get('/wallet/:id/tx/pending', function(req, res, next, send) { + self.walletdb.getPending(req.options.id, function(err, txs) { + if (err) + return next(err); + + if (!txs.length) + return send(404); + + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Wallet TXs within time range + this.get('/wallet/:id/tx/range', function(req, res, next, send) { + self.walletdb.getRange(req.options.id, req.options, function(err, txs) { + if (err) + return next(err); + + if (!txs.length) + return send(404); + + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Wallet TXs within time range + this.get('/wallet/:id/tx/last', function(req, res, next, send) { + self.walletdb.getRange(req.options.id, req.options.limit, function(err, txs) { + if (err) + return next(err); + + if (!txs.length) + return send(404); + + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Wallet TX + this.get('/wallet/:id/tx/:hash', function(req, res, next, send) { + self.walletdb.getTX(req.options.hash, function(err, tx) { + if (err) + return next(err); + + if (!tx) + return send(404); + + send(200, tx.toJSON()); + }); + }); + + // Broadcast TX + this.post('/broadcast', function(req, res, next, send) { + var tx = req.options.tx; + self.pool.broadcast(tx); + send(200, { success: true }); + }); + + this.server.on('error', function(err) { + self.emit('error', err); + }); + + this._initIO(); +}; + +NodeServer.prototype._initIO = function _initIO() { + if (!this.server.io) + return; + + this.server.on('websocket', function(socket) { + socket.on('error', function(err) { + self.emit('error', err); + }); + self.emit('websocket', socket); + }); + + this.walletdb.on('tx', function(tx, map) { + tx = tx.toJSON(); + map.all.forEach(function(id) { + self.server.io.to(id).emit('tx', tx); + }); + self.server.io.to('!all').emit('tx', tx, map); + }); + + this.walletdb.on('confirmed', function(tx, map) { + tx = tx.toJSON(); + map.all.forEach(function(id) { + self.server.io.to(id).emit('confirmed', tx); + }); + self.server.io.to('!all').emit('confirmed', tx, map); + }); + + this.walletdb.on('updated', function(tx, map) { + tx = tx.toJSON(); + map.all.forEach(function(id) { + self.server.io.to(id).emit('updated', tx); + }); + self.server.io.to('!all').emit('updated', tx, map); + }); + + this.walletdb.on('balance', function(balance, id) { + balance = utils.btc(balance); + self.server.io.to(id).emit('balance', balance); + self.server.io.to('!all').emit('balance', balance, id); + }); + + this.walletdb.on('balances', function(balances) { + Object.keys(balances).forEach(function(id) { + balances[id] = utils.btc(balances[id]); + }); + self.server.io.to('!all').emit('balances', balances); + }); +}; + +NodeServer.prototype.use = function use(path, callback) { + return this.server.use(path, callback); +}; + +NodeServer.prototype.get = function get(path, callback) { + return this.server.get(path, callback); +}; + +NodeServer.prototype.post = function post(path, callback) { + return this.server.post(path, callback); +}; + +NodeServer.prototype.put = function put(path, callback) { + return this.server.put(path, callback); +}; + +NodeServer.prototype.del = function del(path, callback) { + return this.server.del(path, callback); +}; + +NodeServer.prototype.listen = function listen(port, host, callback) { + return this.server.listen(port, host, callback); +}; + +/** + * Expose + */ + +module.exports = NodeServer; diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index 05fa3fed..4b17b342 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -75,16 +75,13 @@ Fullnode.prototype._init = function _init() { }); // WalletDB needs access to the network type. - this.walletdb = new bcoin.walletdb(this, { - type: this.options.walletdb - }); + this.walletdb = new bcoin.walletdb(this); // HTTP needs access to the mempool // and blockdb. - this.http = new bcoin.http(this, { - key: this.options.httpKey, - cert: this.options.httpCert, - webhook: this.options.webhook + this.http = new bcoin.http.server(this, { + key: this.options.sslKey, + cert: this.options.sslCert }); // Bind to errors @@ -100,6 +97,10 @@ Fullnode.prototype._init = function _init() { self.emit('error', err); }); + this.http.on('error', function(err) { + self.emit('error', err); + }); + this.walletdb.on('error', function(err) { self.emit('error', err); }); diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index e9046816..f13fbb17 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -1106,7 +1106,7 @@ TX.prototype.toExtended = function toExtended(coins) { return; } - coin = bcoin.protocol.framer.coin(input.output, true); + coin = bcoin.protocol.framer.coin(input.output); off += utils.writeIntv(buf, coin.length, off); off += utils.copy(coin, buf, off); }); @@ -1166,7 +1166,11 @@ TX._fromExtended = function _fromExtended(buf, coins) { off += chunkSize; if (coin.length === 0) continue; - tx.inputs[i].output = bcoin.protocol.parser.parseCoin(coin, true); + coin = bcoin.protocol.parser.parseCoin(coin); + coin.hash = tx.inputs[i].prevout.hash; + coin.index = tx.inputs[i].prevout.index; + coin.spent = false; + tx.inputs[i].output = coin; } } diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 894d2bc7..9b633f84 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -1048,7 +1048,7 @@ TXPool.prototype.getHeightHashes = function getHeightHashes(height, callback) { return this.getHeightRangeHashes({ start: height, end: height }, callback); }; -TXPool.prototype.getTimeRangeHashes = function getTimeRangeHashes(address, options, callback) { +TXPool.prototype.getRangeHashes = function getRangeHashes(address, options, callback) { var prefix = this.prefix + '/'; var self = this; var txs = []; @@ -1097,11 +1097,11 @@ TXPool.prototype.getTimeRangeHashes = function getTimeRangeHashes(address, optio })(); }; -TXPool.prototype.getTimeRange = function getLast(address, options, callback) { +TXPool.prototype.getRange = function getLast(address, options, callback) { var self = this; var txs = []; - return this.getTimeRangeHashes(address, options, function(err, hashes) { + return this.getRangeHashes(address, options, function(err, hashes) { if (err) return callback(err); @@ -1127,7 +1127,7 @@ TXPool.prototype.getTimeRange = function getLast(address, options, callback) { }; TXPool.prototype.getLast = function getLast(address, limit, callback) { - return this.getTimeRange(address, { + return this.getRange(address, { start: 0, end: 0xffffffff, reverse: true, diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 3404d11e..9a740b9e 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -134,6 +134,10 @@ Wallet.prototype._init = function _init() { this.provider.setID(this.id); + this.provider.on('error', function(err) { + utils.debug('Wallet Error: %s', err.message); + }); + this.provider.on('tx', function(tx) { self.emit('tx', tx); }); diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 08e10d04..fdc2422b 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -161,13 +161,14 @@ WalletDB.prototype._init = function _init() { balances[id] = balance; + self.emit('balance', balance, id); self.emit(id + ' balance', balance); }); }, function(err) { if (err) self.emit('error', err); - self.emit('balances', balances); + self.emit('balances', balances, map); }); // Only sync for confirmed txs. @@ -551,10 +552,10 @@ WalletDB.prototype.getLast = function getLast(id, limit, callback) { return this.tx.getLast(id, limit, callback); }; -WalletDB.prototype.getTimeRange = function getTimeRange(id, options, callback) { +WalletDB.prototype.getRange = function getRange(id, options, callback) { var self = this; id = id.id || id; - return this.tx.getTimeRange(id, options, callback); + return this.tx.getRange(id, options, callback); }; WalletDB.prototype.fillTX = function fillTX(tx, callback) { @@ -689,8 +690,8 @@ Provider.prototype.getLast = function getLast(limit, callback) { return this.db.getLast(this.id, limit, callback); }; -Provider.prototype.getTimeRange = function getTimeRange(options, callback) { - return this.db.getTimeRange(this.id, options, callback); +Provider.prototype.getRange = function getRange(options, callback) { + return this.db.getRange(this.id, options, callback); }; Provider.prototype.getTX = function getTX(hash, callback) { diff --git a/package.json b/package.json index 19b248d5..6ff579da 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "test": "rm -rf ~/.bcoin-test && BCOIN_PREFIX=~/.bcoin-test mocha --reporter spec test/*-test.js" }, - "repository": "git://github.com/indutny/bcoin.git", + "repository": "git://github.com/bcoin-org/bcoin.git", "keywords": [ "bitcoin", "blockchain", @@ -35,7 +35,7 @@ }, "optionalDependencies": { "secp256k1": "3.0.0", - "request": "2.67.0" + "socket.io": "1.45.0" }, "devDependencies": { "hash.js": "1.0.3",