/*! * client.js - http client for wallets * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const IOClient = require('socket.io-client'); const Network = require('../protocol/network'); const AsyncObject = require('../utils/asyncobject'); const TX = require('../primitives/tx'); const {BlockMeta} = require('./records'); const Headers = require('../primitives/headers'); const util = require('../utils/util'); const BufferReader = require('../utils/reader'); /** * Bcoin HTTP client. * @alias module:wallet.WalletClient * @constructor * @param {Object|String} 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; } Object.setPrototypeOf(WalletClient.prototype, AsyncObject.prototype); /** * Open the client, wait for socket to connect. * @alias WalletClient#open * @returns {Promise} */ WalletClient.prototype._open = async function _open() { this.socket = new IOClient(this.uri, { transports: ['websocket'], forceNew: true }); this.socket.on('error', (err) => { this.emit('error', err); }); this.socket.on('version', (info) => { if (info.network !== this.network.type) this.emit('error', new Error('Wrong network.')); }); this.socket.on('block connect', (entry, txs) => { let block; try { block = parseBlock(entry, txs); } catch (e) { this.emit('error', e); return; } this.emit('block connect', block.entry, block.txs); }); this.socket.on('block disconnect', (entry) => { let block; try { block = parseEntry(entry); } catch (e) { this.emit('error', e); return; } this.emit('block disconnect', block); }); this.socket.on('block rescan', (entry, txs, cb) => { let block; try { block = parseBlock(entry, txs); } catch (e) { this.emit('error', e); cb(); return; } this.fire('block rescan', block.entry, block.txs).then(cb, cb); }); this.socket.on('chain reset', (tip) => { let block; try { block = parseEntry(tip); } catch (e) { this.emit('error', e); return; } this.emit('chain reset', block); }); this.socket.on('tx', (tx) => { try { tx = parseTX(tx); } catch (e) { this.emit('error', e); return; } this.emit('tx', tx); }); await this.onConnect(); await this.sendAuth(); await this.watchChain(); await this.watchMempool(); }; /** * 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() { return new Promise((resolve, reject) => { this.socket.once('connect', resolve); }); }; /** * Wait for websocket auth. * @private * @returns {Promise} */ WalletClient.prototype.sendAuth = function sendAuth() { return new Promise((resolve, reject) => { this.socket.emit('auth', this.apiKey, wrap(resolve, reject)); }); }; /** * Watch the blockchain. * @private * @returns {Promise} */ WalletClient.prototype.watchChain = function watchChain() { return new Promise((resolve, reject) => { this.socket.emit('watch chain', wrap(resolve, reject)); }); }; /** * Watch the blockchain. * @private * @returns {Promise} */ WalletClient.prototype.watchMempool = function watchMempool() { return new Promise((resolve, reject) => { this.socket.emit('watch mempool', wrap(resolve, reject)); }); }; /** * Get chain tip. * @returns {Promise} */ WalletClient.prototype.getTip = function getTip() { return new Promise((resolve, reject) => { this.socket.emit('get tip', wrap(resolve, reject, parseEntry)); }); }; /** * Get chain entry. * @param {Hash} hash * @returns {Promise} */ WalletClient.prototype.getEntry = function getEntry(block) { return new Promise((resolve, reject) => { if (typeof block === 'string') block = util.revHex(block); this.socket.emit('get entry', block, wrap(resolve, reject, parseEntry)); }); }; /** * Send a transaction. Do not wait for promise. * @param {TX} tx * @returns {Promise} */ WalletClient.prototype.send = function send(tx) { return new Promise((resolve, reject) => { this.socket.emit('send', tx.toRaw(), wrap(resolve, reject)); }); }; /** * Set bloom filter. * @param {Bloom} filter * @returns {Promise} */ WalletClient.prototype.setFilter = function setFilter(filter) { return new Promise((resolve, reject) => { this.socket.emit('set filter', filter.toRaw(), wrap(resolve, reject)); }); }; /** * Add data to filter. * @param {Buffer} data * @returns {Promise} */ WalletClient.prototype.addFilter = function addFilter(chunks) { if (!Array.isArray(chunks)) chunks = [chunks]; return new Promise((resolve, reject) => { this.socket.emit('add filter', chunks, wrap(resolve, reject)); }); }; /** * Reset filter. * @returns {Promise} */ WalletClient.prototype.resetFilter = function resetFilter() { return new Promise((resolve, reject) => { this.socket.emit('reset filter', wrap(resolve, reject)); }); }; /** * Esimate smart fee. * @param {Number?} blocks * @returns {Promise} */ WalletClient.prototype.estimateFee = function estimateFee(blocks) { return new Promise((resolve, reject) => { this.socket.emit('estimate fee', blocks, wrap(resolve, reject)); }); }; /** * 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) { return new Promise((resolve, reject) => { if (typeof start === 'string') start = util.revHex(start); this.socket.emit('rescan', start, wrap(resolve, reject)); }); }; /* * Helpers */ function parseEntry(data, enc) { if (typeof data === 'string') data = Buffer.from(data, 'hex'); const block = Headers.fromHead(data); const br = new BufferReader(data); br.seek(80); const height = br.readU32(); const hash = block.hash('hex'); return new BlockMeta(hash, height, block.time); } function parseBlock(entry, txs) { const block = parseEntry(entry); const out = []; for (const raw of txs) { const tx = parseTX(raw); out.push(tx); } return new BlockResult(block, out); } function parseTX(data) { return TX.fromRaw(data, 'hex'); } function BlockResult(entry, txs) { this.entry = entry; this.txs = txs; } function wrap(resolve, reject, parse) { return function(err, result) { if (err) { reject(new Error(err.message)); return; } if (!result) { resolve(null); return; } if (!parse) { resolve(result); return; } try { result = parse(result); } catch (e) { reject(e); return; } resolve(result); }; } /* * Expose */ module.exports = WalletClient;