diff --git a/lib/http/server.js b/lib/http/server.js index 60189f11..a22ad7b8 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -1329,7 +1329,12 @@ HTTPServer.prototype._initIO = function _initIO() { if (!socket.api) return callback({ error: 'Not authorized.' }); - socket.watchChain(); + try { + socket.watchChain(); + } catch (e) { + return callback({ error: e.message }); + } + callback(); }); @@ -1337,7 +1342,12 @@ HTTPServer.prototype._initIO = function _initIO() { if (!socket.api) return callback({ error: 'Not authorized.' }); - socket.unwatchChain(); + try { + socket.unwatchChain(); + } catch (e) { + return callback({ error: e.message }); + } + callback(); }); @@ -1345,7 +1355,7 @@ HTTPServer.prototype._initIO = function _initIO() { var data = args[0]; var filter; - if (!util.isHex(data)) + if (!util.isHex(data) && !Buffer.isBuffer(data)) return callback({ error: 'Invalid parameter.' }); if (!socket.api) @@ -1446,7 +1456,7 @@ HTTPServer.prototype._initIO = function _initIO() { var data = args[0]; var tx; - if (!util.isHex(data)) + if (!util.isHex(data) && !Buffer.isBuffer(data)) return callback({ error: 'Invalid parameter.' }); try { @@ -1615,6 +1625,7 @@ function ClientSocket(server, socket) { this.filter = null; this.api = false; this.raw = false; + this.watching = false; this.network = this.server.network; this.node = this.server.node; @@ -1682,7 +1693,7 @@ ClientSocket.prototype.addFilter = function addFilter(chunks) { for (i = 0; i < chunks.length; i++) { data = chunks[i]; - if (!util.isHex(data)) + if (!util.isHex(data) && !Buffer.isBuffer(data)) throw new Error('Not a hex string.'); this.filter.add(data, 'hex'); @@ -1731,6 +1742,11 @@ ClientSocket.prototype.watchChain = function watchChain() { var self = this; var pool = this.mempool || this.pool; + if (this.watching) + throw new Error('Already watching chain.'); + + this.watching = true; + this.bind(this.chain, 'connect', function(entry, block) { self.connectBlock(entry, block); }); @@ -1755,6 +1771,12 @@ ClientSocket.prototype.onError = function onError(err) { ClientSocket.prototype.unwatchChain = function unwatchChain() { var pool = this.mempool || this.pool; + + if (!this.watching) + throw new Error('Not watching chain.'); + + this.watching = false; + this.unbind(this.chain, 'connect'); this.unbind(this.chain, 'disconnect'); this.unbind(this.chain, 'reset'); @@ -1875,13 +1897,13 @@ ClientSocket.prototype.scanner = function scanner(entry, txs) { ClientSocket.prototype.frameEntry = function frameEntry(entry) { if (this.raw) - return entry.toRaw().toString('hex'); + return entry.toRaw(); return entry.toJSON(); }; ClientSocket.prototype.frameTX = function frameTX(tx) { if (this.raw) - return tx.toRaw().toString('hex'); + return tx.toRaw(); return tx.toJSON(this.network); }; diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index a6b40329..4af1ba92 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -237,6 +237,10 @@ FullNode.prototype._open = co(function* open() { yield this.mempool.open(); yield this.miner.open(); yield this.pool.open(); + + if (this.http) + yield this.http.open(); + yield this.walletdb.open(); // Ensure primary wallet. @@ -245,9 +249,6 @@ FullNode.prototype._open = co(function* open() { if (this.options.listen) yield this.pool.listen(); - if (this.http) - yield this.http.open(); - this.logger.info('Node is loaded.'); }); diff --git a/lib/wallet/client.js b/lib/wallet/client.js new file mode 100644 index 00000000..ea4b864f --- /dev/null +++ b/lib/wallet/client.js @@ -0,0 +1,432 @@ +/*! + * client.js - http client for wallets + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var Network = require('../protocol/network'); +var AsyncObject = require('../utils/async'); +var TX = require('../primitives/tx'); +var BlockMeta = require('./records').BlockMeta; +var Headers = require('../primitives/headers'); +var Amount = require('../btc/amount'); +var util = require('../utils/util'); +var BufferReader = require('../utils/reader'); +var co = require('../utils/co'); +var IOClient = require('socket.io-client'); + +/** + * BCoin HTTP client. + * @exports WalletClient + * @constructor + * @param {String} uri + * @param {Object?} options + */ + +function WalletClient(options) { + if (!(this instanceof WalletClient)) + return new WalletClient(options); + + if (!options) + options = {}; + + if (typeof options === 'string') + options = { uri: options }; + + AsyncObject.call(this); + + this.options = options; + this.network = Network.get(options.network); + + this.uri = options.uri || 'http://localhost:' + this.network.rpcPort; + this.apiKey = options.apiKey; + + this.socket = null; +} + +util.inherits(WalletClient, AsyncObject); + +/** + * Open the client, wait for socket to connect. + * @alias WalletClient#open + * @returns {Promise} + */ + +WalletClient.prototype._open = co(function* _open() { + var self = this; + + this.socket = new IOClient(this.uri, { + transports: ['websocket'], + forceNew: true + }); + + this.socket.on('error', function(err) { + self.emit('error', err); + }); + + this.socket.on('version', function(info) { + if (info.network !== self.network.type) + self.emit('error', new Error('Wrong network.')); + }); + + this.socket.on('block connect', function(entry, txs) { + var data; + + try { + data = parseBlock(entry, txs); + } catch (e) { + self.emit('error', e); + return; + } + + self.emit('block connect', data.entry, data.txs); + }); + + this.socket.on('block disconnect', function(entry) { + var block; + + try { + block = parseEntry(entry); + } catch (e) { + self.emit('error', e); + return; + } + + self.emit('block disconnect', block); + }); + + this.socket.on('block rescan', function(entry, txs, cb) { + var data; + + try { + data = parseBlock(entry, txs); + } catch (e) { + self.emit('error', e); + return cb(); + } + + self.emit('block rescan', data.entry, data.txs, cb); + }); + + this.socket.on('chain reset', function(tip) { + var block; + + try { + block = parseEntry(tip); + } catch (e) { + self.emit('error', e); + return; + } + + self.emit('chain reset', block); + }); + + this.socket.on('tx', function(tx) { + tx = parseTX(tx); + self.emit('tx', tx); + }); + + yield this.onConnect(); + yield this.sendAuth(); + yield this.sendOptions({ raw: true }); + yield this.watchChain(); +}); + +/** + * Close the client, wait for the socket to close. + * @alias WalletClient#close + * @returns {Promise} + */ + +WalletClient.prototype._close = function close() { + if (!this.socket) + return Promise.resolve(); + + this.socket.disconnect(); + this.socket = null; + + return Promise.resolve(); +}; + +/** + * Wait for websocket connection. + * @private + * @returns {Promise} + */ + +WalletClient.prototype.onConnect = function onConnect() { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.once('connect', resolve); + }); +}; + +/** + * Wait for websocket auth. + * @private + * @returns {Promise} + */ + +WalletClient.prototype.sendAuth = function sendAuth() { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.emit('auth', self.apiKey, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/** + * Wait for websocket options. + * @private + * @returns {Promise} + */ + +WalletClient.prototype.sendOptions = function sendOptions(options) { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.emit('options', options, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/** + * Wait for websocket options. + * @private + * @returns {Promise} + */ + +WalletClient.prototype.watchChain = function watchChain() { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.emit('watch chain', function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/** + * Get chain tip. + * @returns {Promise} + */ + +WalletClient.prototype.getTip = function getTip() { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.emit('get tip', function(err, tip) { + if (err) + return reject(new Error(err.error)); + resolve(parseEntry(tip)); + }); + }); +}; + +/** + * Get chain entry. + * @param {Hash} hash + * @returns {Promise} + */ + +WalletClient.prototype.getEntry = function getEntry(block) { + var self = this; + return new Promise(function(resolve, reject) { + if (typeof block === 'string') + block = util.revHex(block); + + self.socket.emit('get entry', block, function(err, entry) { + if (err) + return reject(new Error(err.error)); + + if (!entry) + return resolve(null); + + resolve(parseEntry(entry)); + }); + }); +}; + +/** + * Send a transaction. Do not wait for promise. + * @param {TX} tx + * @returns {Promise} + */ + +WalletClient.prototype.send = function send(tx) { + var self = this; + return new Promise(function(resolve, reject) { + var raw = tx.toRaw(); + self.socket.emit('send', raw, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/** + * Set bloom filter. + * @param {Bloom} filter + * @returns {Promise} + */ + +WalletClient.prototype.setFilter = function setFilter(filter) { + var self = this; + return new Promise(function(resolve, reject) { + var raw = filter.toRaw(); + self.socket.emit('set filter', raw, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/** + * Add data to filter. + * @param {Buffer} data + * @returns {Promise} + */ + +WalletClient.prototype.addFilter = function addFilter(chunks) { + var self = this; + var out = []; + var i; + + if (!Array.isArray(chunks)) + chunks = [chunks]; + + for (i = 0; i < chunks.length; i++) + out.push(chunks[i]); + + return new Promise(function(resolve, reject) { + self.socket.emit('add filter', out, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/** + * Reset filter. + * @returns {Promise} + */ + +WalletClient.prototype.resetFilter = function resetFilter() { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.emit('reset filter', function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/** + * Esimate smart fee. + * @param {Number?} blocks + * @returns {Promise} + */ + +WalletClient.prototype.estimateFee = function estimateFee(blocks) { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.emit('estimate fee', blocks, function(err, rate) { + if (err) + return reject(new Error(err.error)); + resolve(Amount.value(rate)); + }); + }); +}; + +/** + * Rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + +WalletClient.prototype.rescan = function rescan(start) { + var self = this; + return new Promise(function(resolve, reject) { + if (typeof start === 'string') + start = util.revHex(start); + + self.socket.emit('rescan', start, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); +}; + +/* + * Helpers + */ + +function parseEntry(data, enc) { + var p, block, hash; + + if (typeof data === 'string') + data = new Buffer(data, 'hex'); + + p = new BufferReader(data); + + block = Headers.fromAbbr(p); + block.height = p.readU32(); + + hash = block.hash('hex'); + + return new BlockMeta(hash, block.height, block.ts); +} + +function parseBlock(entry, txs) { + var block = parseEntry(entry); + var out = []; + var i, tx; + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + tx = parseTX(tx); + tx.block = block.hash; + tx.height = block.height; + tx.ts = block.ts; + tx.index = -1; + out.push(tx); + } + + return new BlockResult(block, out); +} + +function parseTX(data) { + return TX.fromRaw(data, 'hex'); +} + +function toHex(data) { + if (typeof data !== 'string') + return data.toString('hex'); + return data; +} + +function BlockResult(entry, txs) { + this.entry = entry; + this.txs = txs; +} + +/* + * Expose + */ + +module.exports = WalletClient; diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 741b5b7a..f967d523 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -236,6 +236,7 @@ WalletDB.prototype.connect = co(function* connect() { this.bind(); yield this.client.open({ raw: true }); + yield this.setFilter(); }); /** @@ -346,7 +347,7 @@ WalletDB.prototype.watch = co(function* watch() { this.logger.info('Added %d hashes to WalletDB filter.', hashes); this.logger.info('Added %d outpoints to WalletDB filter.', outpoints); - yield this.loadFilter(); + yield this.setFilter(); }); /** @@ -395,7 +396,6 @@ WalletDB.prototype.sync = co(function* sync() { */ WalletDB.prototype.scan = co(function* scan(height) { - var iter = this._addBlock.bind(this); var tip; if (!this.client) @@ -414,10 +414,9 @@ WalletDB.prototype.scan = co(function* scan(height) { tip = yield this.getTip(); - this.rescanning = true; - try { - yield this.client.rescan(tip.hash, iter); + this.rescanning = true; + yield this.client.rescan(tip.hash); } finally { this.rescanning = false; } @@ -483,13 +482,13 @@ WalletDB.prototype.estimateFee = co(function* estimateFee(blocks) { * @returns {Promise} */ -WalletDB.prototype.loadFilter = function loadFilter() { +WalletDB.prototype.setFilter = function setFilter() { if (!this.client) { - this.emit('load filter', this.filter); + this.emit('set filter', this.filter); return Promise.resolve(); } - return this.client.loadFilter(this.filter); + return this.client.setFilter(this.filter); }; /** diff --git a/test/chain-test.js b/test/chain-test.js index be92f4ed..dcc30e22 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -9,14 +9,16 @@ var assert = require('assert'); var opcodes = constants.opcodes; var co = require('../lib/utils/co'); var cob = co.cob; +// var Client = require('../lib/wallet/client'); describe('Chain', function() { var chain, wallet, node, miner, walletdb; var tip1, tip2, cb1, cb2; - this.timeout(15000); + this.timeout(5000); - node = new bcoin.fullnode({ db: 'memory' }); + node = new bcoin.fullnode({ db: 'memory', apiKey: 'foo' }); + // node.walletdb.client = new Client({ apiKey: 'foo', network: 'regtest' }); chain = node.chain; walletdb = node.walletdb; walletdb.options.resolution = false;