walletdb: add experimental client.

This commit is contained in:
Christopher Jeffrey 2016-11-19 21:40:48 -08:00
parent d75b5d80cc
commit 6c61ad976d
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 476 additions and 20 deletions

View File

@ -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);
};

View File

@ -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.');
});

432
lib/wallet/client.js Normal file
View File

@ -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;

View File

@ -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);
};
/**

View File

@ -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;