From 1a32e664685763f7997642ddd108d9fba359d833 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 30 Jun 2016 17:29:00 -0700 Subject: [PATCH] fix open and close for all async objects. --- lib/bcoin/async.js | 179 +++++++++++++++ lib/bcoin/chain.js | 56 ++--- lib/bcoin/chaindb.js | 85 +++---- lib/bcoin/fullnode.js | 113 +++++----- lib/bcoin/http/base.js | 108 ++++++--- lib/bcoin/http/client.js | 55 +++-- lib/bcoin/http/server.js | 48 ++-- lib/bcoin/http/wallet.js | 28 ++- lib/bcoin/lowlevelup.js | 66 ++++-- lib/bcoin/mempool.js | 111 ++++----- lib/bcoin/miner.js | 55 ++--- lib/bcoin/node.js | 6 +- lib/bcoin/pool.js | 236 +++++++++----------- lib/bcoin/spvnode.js | 109 +++++---- lib/bcoin/txdb.js | 106 +++++++++ lib/bcoin/walletdb.js | 471 +++++++++++++++++++++++++-------------- test/chain-test.js | 9 +- test/mempool-test.js | 8 +- 18 files changed, 1143 insertions(+), 706 deletions(-) create mode 100644 lib/bcoin/async.js diff --git a/lib/bcoin/async.js b/lib/bcoin/async.js new file mode 100644 index 00000000..2e9570a1 --- /dev/null +++ b/lib/bcoin/async.js @@ -0,0 +1,179 @@ +/*! + * async.js - async object class for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var utils = require('./utils'); +var assert = utils.assert; +var EventEmitter = require('events').EventEmitter; + +/** + * An abstract object that handles state and + * provides recallable open and close methods. + * @constructor + * @property {Boolean} loading + * @property {Boolean} closing + * @property {Boolean} loaded + */ + +function AsyncObject() { + assert(this instanceof AsyncObject); + + EventEmitter.call(this); + + this.loading = false; + this.closing = false; + this.loaded = false; + this.locker = null; +} + +utils.inherits(AsyncObject, EventEmitter); + +/** + * Open the object (recallable). + * @param {Function} callback + */ + +AsyncObject.prototype.open = function open(callback) { + var self = this; + var unlock; + + callback = utils.ensure(callback); + + assert(!this.closing, 'Cannot open while closing.'); + + if (this.loaded) + return utils.nextTick(callback); + + if (this.loading) + return this.once('open', callback); + + if (this.locker) { + unlock = this.locker.lock(utils.nop, []); + + assert(unlock, 'Cannot call methods before load.'); + + callback = utils.wrap(callback, unlock); + } + + this.loading = true; + + this._open(function(err) { + utils.nextTick(function() { + if (err) { + self.loading = false; + self._error('open', err); + return callback(err); + } + + self.loading = false; + self.loaded = true; + self.emit('open'); + + callback(); + }); + }); +}; + +/** + * Close the object (recallable). + * @param {Function} callback + */ + +AsyncObject.prototype.close = function close(callback) { + var self = this; + var unlock; + + callback = utils.ensure(callback); + + assert(!this.loading, 'Cannot close while loading.'); + + if (!this.loaded) + return utils.nextTick(callback); + + if (this.closing) + return this.on('close', callback); + + if (this.locker) { + unlock = this.locker.lock(close, [callback]); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + } + + this.closing = true; + this.loaded = false; + + this._close(function(err) { + utils.nextTick(function() { + if (err) { + self.closing = false; + self._error('close', err); + return callback(err); + } + + self.closing = false; + self.emit('close'); + + callback(); + }); + }); +}; + +/** + * Close the object (recallable). + * @method + * @param {Function} callback + */ + +AsyncObject.prototype.destroy = AsyncObject.prototype.close; + +/** + * Emit an error for `open` or `close` listeners. + * @private + * @param {String} event + * @param {Error} err + */ + +AsyncObject.prototype._error = function _error(event, err) { + var listeners = this.listeners(event); + var i, callback; + + this.removeAllListeners(event); + + for (i = 0; i < listeners.length; i++) + listeners[i](err); + + this.emit('error', err); +}; + +/** + * Initialize the object. + * @private + * @param {Function} callback + */ + +AsyncObject.prototype._open = function _open(callback) { + throw new Error('Abstract method.'); +}; + +/** + * Close the object. + * @private + * @param {Function} callback + */ + +AsyncObject.prototype._close = function _close(callback) { + throw new Error('Abstract method.'); +}; + +/* + * Expose + */ + +module.exports = AsyncObject; diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index bf3c76e9..220311ea 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -9,6 +9,7 @@ var bcoin = require('./env'); var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('./async'); var constants = bcoin.protocol.constants; var utils = require('./utils'); var assert = utils.assert; @@ -55,17 +56,18 @@ var VerifyError = bcoin.errors.VerifyError; */ function Chain(options) { + var self = this; + if (!(this instanceof Chain)) return new Chain(options); - EventEmitter.call(this); + AsyncObject.call(this); if (!options) options = {}; this.options = options; - this.loaded = false; this.network = bcoin.network.get(options.network); this.db = new bcoin.chaindb(this, options); this.total = 0; @@ -91,7 +93,12 @@ function Chain(options) { this._init(); } -utils.inherits(Chain, EventEmitter); +utils.inherits(Chain, AsyncObject); + +/** + * Initialize the chain. + * @private + */ Chain.prototype._init = function _init() { var self = this; @@ -192,20 +199,30 @@ Chain.prototype._init = function _init() { this.db.on('remove block', function(block) { self.emit('remove block', block); }); +}; + +/** + * Open the chain, wait for the database to load. + * @alias Chain#open + * @param {Function} callback + */ + +Chain.prototype._open = function open(callback) { + var self = this; bcoin.debug('Chain is loading.'); - self.db.open(function(err) { + this.db.open(function(err) { if (err) - return self.emit('error', err); + return callback(err); self.preload(function(err) { if (err) - return self.emit('error', err); + return callback(err); self.db.getTip(function(err, tip) { if (err) - return self.emit('error', err); + return callback(err); assert(tip); @@ -221,7 +238,7 @@ Chain.prototype._init = function _init() { self.getInitialState(function(err) { if (err) - return self.emit('error', err); + return callback(err); if (self.csvActive) bcoin.debug('CSV is active.'); @@ -231,41 +248,28 @@ Chain.prototype._init = function _init() { bcoin.profiler.snapshot(); - self.loaded = true; - self.emit('open'); self.emit('tip', tip); if (!self.synced && self.isFull()) { self.synced = true; self.emit('full'); } + + callback(); }); }); }); }); }; -/** - * Open the chain, wait for the database to load. - * @param {Function} callback - */ - -Chain.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); -}; - /** * Close the chain, wait for the database to close. - * @method + * @alias Chain#close * @param {Function} callback */ -Chain.prototype.close = -Chain.prototype.destroy = function destroy(callback) { - this.db.close(utils.ensure(callback)); +Chain.prototype._close = function close(callback) { + this.db.close(callback); }; /** diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index e89b9b21..ff7b5d07 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -9,6 +9,7 @@ var bcoin = require('./env'); var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('./async'); var constants = bcoin.protocol.constants; var utils = require('./utils'); var assert = utils.assert; @@ -174,12 +175,22 @@ function ChainDB(chain, options) { if (!options) options = {}; - EventEmitter.call(this); + AsyncObject.call(this); this.options = options; this.chain = chain; this.network = this.chain.network; + this.db = bcoin.ldb({ + network: this.network, + name: this.options.name || (this.options.spv ? 'spvchain' : 'chain'), + location: this.options.location, + db: this.options.db, + compression: true, + cacheSize: 16 << 20, + writeBufferSize: 8 << 20 + }); + this.keepBlocks = options.keepBlocks || 288; this.prune = !!options.prune; @@ -207,83 +218,59 @@ function ChainDB(chain, options) { this.coinCache = new NullCache(this.coinWindow); this.cacheHash = new bcoin.lru(this.cacheWindow, 1); this.cacheHeight = new bcoin.lru(this.cacheWindow, 1); - - this._init(); } -utils.inherits(ChainDB, EventEmitter); +utils.inherits(ChainDB, AsyncObject); -ChainDB.prototype._init = function _init() { +/** + * Open the chain db, wait for the database to load. + * @alias ChainDB#open + * @param {Function} callback + */ + +ChainDB.prototype._open = function open(callback) { var self = this; var genesis, block; - if (this.loaded) - return; - - this.db = bcoin.ldb({ - network: this.network, - name: this.options.name || (this.options.spv ? 'spvchain' : 'chain'), - location: this.options.location, - db: this.options.db, - compression: true, - cacheSize: 16 << 20, - writeBufferSize: 8 << 20 - }); - bcoin.debug('Starting chain load.'); + function done(err) { + if (err) + return callback(err); + + bcoin.debug('Chain successfully loaded.'); + + callback(); + } + this.db.open(function(err) { if (err) - return self.emit('error', err); - - function finish(err) { - if (err) - return self.emit('error', err); - - bcoin.debug('Chain successfully loaded.'); - - self.loaded = true; - self.emit('open'); - } + return done(err); self.db.has(layout.h(self.network.genesis.hash), function(err, exists) { if (err) - return self.emit('error', err); + return done(err); if (exists) - return finish(); + return done(); block = bcoin.block.fromRaw(self.network.genesisBlock, 'hex'); block.setHeight(0); genesis = bcoin.chainentry.fromBlock(self.chain, block); - self.save(genesis, block, null, true, finish); + self.save(genesis, block, null, true, done); }); }); }; /** - * Open the chain, wait for the database to load. + * Close the chain db, wait for the database to close. + * @alias ChainDB#close * @param {Function} callback */ -ChainDB.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); -}; - -/** - * Close the chain, wait for the database to close. - * @method - * @param {Function} callback - */ - -ChainDB.prototype.close = -ChainDB.prototype.destroy = function destroy(callback) { - callback = utils.ensure(callback); +ChainDB.prototype._close = function close(callback) { this.db.close(callback); }; diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index 333bdc61..e86f7f3e 100644 --- a/lib/bcoin/fullnode.js +++ b/lib/bcoin/fullnode.js @@ -10,6 +10,7 @@ var bcoin = require('./env'); var utils = require('./utils'); var assert = utils.assert; +var Node = bcoin.node; /** * Create a fullnode complete with a chain, @@ -47,20 +48,7 @@ function Fullnode(options) { if (!(this instanceof Fullnode)) return new Fullnode(options); - bcoin.node.call(this, options); - - this.loaded = false; - - this._init(); -} - -utils.inherits(Fullnode, bcoin.node); - -Fullnode.prototype._init = function _init() { - var self = this; - var options; - - this.wallet = null; + Node.call(this, options); this.chain = new bcoin.chain({ network: this.network, @@ -119,6 +107,19 @@ Fullnode.prototype._init = function _init() { }); } + this._init(); +} + +utils.inherits(Fullnode, Node); + +/** + * Initialize the node. + * @private + */ + +Fullnode.prototype._init = function _init() { + var self = this; + // Bind to errors this.mempool.on('error', function(err) { self.emit('error', err); @@ -196,14 +197,26 @@ Fullnode.prototype._init = function _init() { this.miner.on('block', function(block) { self.pool.broadcast(block.toInv()); }); +}; - function load(err) { +/** + * Open the node and all its child objects, + * wait for the database to load. + * @alias Fullnode#open + * @param {Function} callback + */ + +Fullnode.prototype._open = function open(callback) { + var self = this; + var options; + + function done(err) { if (err) - return self.emit('error', err); + return callback(err); - self.loaded = true; - self.emit('open'); bcoin.debug('Node is loaded.'); + + callback(); } options = utils.merge({ @@ -256,7 +269,30 @@ Fullnode.prototype._init = function _init() { return next(); self.http.open(next); } - ], load); + ], done); +}; + +/** + * Close the node, wait for the database to close. + * @alias Fullnode#close + * @param {Function} callback + */ + +Fullnode.prototype._close = function close(callback) { + var self = this; + + utils.serial([ + function(next) { + if (!self.http) + return next(); + self.http.close(next); + }, + this.walletdb.close.bind(this.walletdb), + this.pool.close.bind(this.pool), + this.miner.close.bind(this.miner), + this.mempool.close.bind(this.mempool), + this.chain.close.bind(this.chain) + ], callback); }; /** @@ -340,45 +376,6 @@ Fullnode.prototype.stopSync = function stopSync() { return this.pool.stopSync(); }; -/** - * Open the node and all its child objects, - * wait for the database to load. - * @param {Function} callback - */ - -Fullnode.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); -}; - -/** - * Close the node, wait for the database to close. - * @method - * @param {Function} callback - */ - -Fullnode.prototype.close = -Fullnode.prototype.destroy = function destroy(callback) { - var self = this; - - this.wallet.destroy(); - - utils.serial([ - function(next) { - if (!self.http) - return next(); - self.http.close(next); - }, - this.walletdb.close.bind(this.walletdb), - this.pool.close.bind(this.pool), - this.miner.close.bind(this.miner), - this.mempool.close.bind(this.mempool), - this.chain.close.bind(this.chain) - ], callback); -}; - /** * Create a {@link Wallet} in the wallet database. * @param {Object} options - See {@link Wallet}. diff --git a/lib/bcoin/http/base.js b/lib/bcoin/http/base.js index d173b029..8109fc98 100644 --- a/lib/bcoin/http/base.js +++ b/lib/bcoin/http/base.js @@ -8,7 +8,9 @@ 'use strict'; var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('../async'); var utils = require('../utils'); +var assert = utils.assert; /** * HTTPBase @@ -25,6 +27,8 @@ function HTTPBase(options) { if (!options) options = {}; + AsyncObject.call(this); + this.options = options; this.io = null; @@ -44,7 +48,12 @@ function HTTPBase(options) { this._init(); } -utils.inherits(HTTPBase, EventEmitter); +utils.inherits(HTTPBase, AsyncObject); + +/** + * Initialize server. + * @private + */ HTTPBase.prototype._init = function _init() { var self = this; @@ -69,30 +78,10 @@ HTTPBase.prototype._init = function _init() { }); }; -HTTPBase.prototype._initIO = function _initIO() { - var self = this; - var IOServer; - - if (!this.options.sockets) - return; - - try { - IOServer = require('socket.io'); - } catch (e) { - ; - } - - if (!IOServer) - return; - - this.io = new IOServer(); - - this.io.attach(this.server); - - this.io.on('connection', function(socket) { - self.emit('websocket', socket); - }); -}; +/** + * Initialize router. + * @private + */ HTTPBase.prototype._initRouter = function _initRouter() { var self = this; @@ -185,6 +174,66 @@ HTTPBase.prototype._initRouter = function _initRouter() { }); }; +/** + * Initialize websockets. + * @private + */ + +HTTPBase.prototype._initIO = function _initIO() { + var self = this; + var IOServer; + + if (!this.options.sockets) + return; + + try { + IOServer = require('socket.io'); + } catch (e) { + ; + } + + if (!IOServer) + return; + + this.io = new IOServer(); + + this.io.attach(this.server); + + this.io.on('connection', function(socket) { + self.emit('websocket', socket); + }); +}; + +/** + * Open the server. + * @alias HTTPBase#open + * @param {Function} callback + */ + +HTTPBase.prototype._open = function open(callback) { + assert(typeof this.options.port === 'number', 'Port required.'); + this.listen(this.options.port, this.options.host, callback); +}; + +/** + * Close the server. + * @alias HTTPBase#close + * @param {Function} callback + */ + +HTTPBase.prototype._close = function close(callback) { + this.server.close(callback); +}; + +/** + * Handle middleware stack. + * @param {HTTPRequest} req + * @param {HTTPResponse} res + * @param {Function} _send + * @param {Function} callback + * @private + */ + HTTPBase.prototype._handle = function _handle(req, res, _send, callback) { var self = this; var i = 0; @@ -339,15 +388,6 @@ HTTPBase.prototype.listen = function listen(port, host, callback) { }); }; -/** - * Close the the server. - * @param {Function} callback - */ - -HTTPBase.prototype.close = function close(callback) { - this.server.close(callback); -}; - /* * Helpers */ diff --git a/lib/bcoin/http/client.js b/lib/bcoin/http/client.js index 05c098ef..0c203fae 100644 --- a/lib/bcoin/http/client.js +++ b/lib/bcoin/http/client.js @@ -9,6 +9,7 @@ var bcoin = require('../env'); var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('../async'); var utils = require('../utils'); var assert = utils.assert; var request = require('./request'); @@ -31,19 +32,28 @@ function HTTPClient(options) { if (typeof options === 'string') options = { uri: options }; - EventEmitter.call(this); + AsyncObject.call(this); - this.uri = options.uri; - this.loaded = false; - this.id = null; this.options = options; this.network = bcoin.network.get(options.network); - this._init(); + + this.uri = options.uri; + this.id = null; + this.socket = null; + + // Open automatically. + this.open(); } -utils.inherits(HTTPClient, EventEmitter); +utils.inherits(HTTPClient, AsyncObject); -HTTPClient.prototype._init = function _init() { +/** + * Open the client, wait for socket to connect. + * @alias HTTPClient#open + * @param {Function} callback + */ + +HTTPClient.prototype._open = function _open(callback) { var self = this; var IOClient; @@ -132,15 +142,19 @@ HTTPClient.prototype._init = function _init() { }; /** - * Open the client, wait for the socket to load. + * Close the client, wait for the socket to close. + * @alias HTTPClient#close * @param {Function} callback */ -HTTPClient.prototype.open = function open(callback) { - if (this.loaded) +HTTPClient.prototype._close = function close(callback) { + if (!this.socket) return utils.nextTick(callback); - this.once('open', callback); + this.socket.destroy(); + this.socket = null; + + return utils.nextTick(callback); }; /** @@ -183,25 +197,6 @@ HTTPClient.prototype.none = function none() { this.leave('!all'); }; -/** - * Close the client, wait for the socket to close. - * @method - * @param {Function} callback - */ - -HTTPClient.prototype.close = -HTTPClient.prototype.destroy = function destroy(callback) { - callback = utils.ensure(callback); - - if (!this.socket) - return utils.nextTick(callback); - - this.socket.destroy(); - this.socket = null; - - return utils.nextTick(callback); -}; - /** * Make an http request to endpoint. * @private diff --git a/lib/bcoin/http/server.js b/lib/bcoin/http/server.js index 3d815419..159bf4c6 100644 --- a/lib/bcoin/http/server.js +++ b/lib/bcoin/http/server.js @@ -52,6 +52,11 @@ function HTTPServer(options) { utils.inherits(HTTPServer, EventEmitter); +/** + * Initialize routes. + * @private + */ + HTTPServer.prototype._init = function _init() { var self = this; @@ -707,34 +712,13 @@ HTTPServer.prototype._init = function _init() { }); this._initIO(); - - if (this.options.port != null) - this.listen(this.options.port, this.options.host); }; /** - * Open the server, wait for socket. - * @param {Function} callback + * Initialize websockets. + * @private */ -HTTPServer.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); -}; - -/** - * Close the server, wait for server socket to close. - * @method - * @param {Function} callback - */ - -HTTPServer.prototype.close = -HTTPServer.prototype.destroy = function destroy(callback) { - this.server.close(callback); -}; - HTTPServer.prototype._initIO = function _initIO() { var self = this; @@ -829,6 +813,24 @@ HTTPServer.prototype._initIO = function _initIO() { }); }; +/** + * Open the server, wait for socket. + * @param {Function} callback + */ + +HTTPServer.prototype.open = function open(callback) { + this.server.open(callback); +}; + +/** + * Close the server, wait for server socket to close. + * @param {Function} callback + */ + +HTTPServer.prototype.close = function close(callback) { + this.server.close(callback); +}; + /** * @see HTTPBase#use */ diff --git a/lib/bcoin/http/wallet.js b/lib/bcoin/http/wallet.js index b8e659fa..66535e01 100644 --- a/lib/bcoin/http/wallet.js +++ b/lib/bcoin/http/wallet.js @@ -33,14 +33,21 @@ function HTTPWallet(options) { options = { uri: options }; this.options = options; - this.client = new http.client(options); this.network = bcoin.network.get(options.network); + + this.client = new http.client(options); this.uri = options.uri; + this._init(); } utils.inherits(HTTPWallet, EventEmitter); +/** + * Initialize the wallet. + * @private + */ + HTTPWallet.prototype._init = function _init() { var self = this; @@ -72,7 +79,9 @@ HTTPWallet.prototype._init = function _init() { }; /** - * @see Wallet#open + * Open the client and ensure a wallet. + * @alias HTTPWallet#open + * @param {Function} callback */ HTTPWallet.prototype.open = function open(callback) { @@ -85,18 +94,13 @@ HTTPWallet.prototype.open = function open(callback) { }; /** - * @see Wallet#close + * Close the client, wait for the socket to close. + * @alias HTTPWallet#close + * @param {Function} callback */ -HTTPWallet.prototype.close = -HTTPWallet.prototype.destroy = function destroy(callback) { - callback = utils.ensure(callback); - - if (!this.client) - return utils.nextTick(callback); - - this.client.destroy(callback); - this.client = null; +HTTPWallet.prototype.close = function close(callback) { + this.client.close(callback); }; /** diff --git a/lib/bcoin/lowlevelup.js b/lib/bcoin/lowlevelup.js index a1536a93..4ac8086b 100644 --- a/lib/bcoin/lowlevelup.js +++ b/lib/bcoin/lowlevelup.js @@ -10,6 +10,7 @@ var utils = require('./utils'); var assert = utils.assert; var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('./async'); /** * Extremely low-level version of levelup. @@ -33,9 +34,11 @@ function LowlevelUp(file, options) { if (!(this instanceof LowlevelUp)) return new LowlevelUp(file, options); - EventEmitter.call(this); + AsyncObject.call(this); - this.loaded = false; + this.options = options; + this.backend = options.db; + this.location = file; this.db = new options.db(file); @@ -48,39 +51,60 @@ function LowlevelUp(file, options) { if (this.db.binding) this.binding = this.db.binding; - - this.binding.open(options, function(err) { - if (err) - return self.emit('error', err); - - self.loaded = true; - self.emit('open'); - }); } -utils.inherits(LowlevelUp, EventEmitter); +utils.inherits(LowlevelUp, AsyncObject); /** * Open the database (recallable). + * @alias LowlevelUp#open * @param {Function} callback */ -LowlevelUp.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); +LowlevelUp.prototype._open = function open(callback) { + this.binding.open(this.options, callback); }; /** - * Close the database (not recallable). + * Close the database (recallable). + * @alias LowlevelUp#close * @param {Function} callback */ -LowlevelUp.prototype.close = function close(callback) { - assert(this.loaded, 'Cannot use database before it is loaded.'); - this.loaded = false; - return this.binding.close(callback); +LowlevelUp.prototype._close = function close(callback) { + this.binding.close(callback); +}; + +/** + * Destroy the database. + * @param {Function} callback + */ + +LowlevelUp.prototype.destroy = function destroy(callback) { + assert(!this.loading); + assert(!this.closing); + assert(!this.loaded); + + if (!this.backend.destroy) + return utils.nextTick(callback); + + this.backend.destroy(this.location, callback); +}; + +/** + * Repair the database. + * @param {Function} callback + */ + +LowlevelUp.prototype.repair = function repair(callback) { + assert(!this.loading); + assert(!this.closing); + assert(!this.loaded); + + if (!this.backend.repair) + return utils.asyncify(callback)(new Error('Cannot repair.')); + + this.backend.repair(this.location, callback); }; /** diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index c4758ab5..97f6c796 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -14,6 +14,7 @@ var bcoin = require('./env'); var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('./async'); var constants = bcoin.protocol.constants; var utils = require('./utils'); var assert = utils.assert; @@ -63,7 +64,7 @@ function Mempool(options) { if (!(this instanceof Mempool)) return new Mempool(options); - EventEmitter.call(this); + AsyncObject.call(this); if (!options) options = {}; @@ -78,7 +79,13 @@ function Mempool(options) { this.locker = new bcoin.locker(this, this.addTX); - this.db = null; + this.db = bcoin.ldb({ + network: this.network, + name: this.options.name || 'mempool', + location: this.options.location, + db: this.options.db || 'memory' + }); + this.size = 0; this.waiting = {}; this.orphans = {}; @@ -105,64 +112,61 @@ function Mempool(options) { this.minFeeRate = 0; this.minReasonableFee = constants.tx.MIN_RELAY; this.minRelayFee = constants.tx.MIN_RELAY; - - this._init(); } -utils.inherits(Mempool, EventEmitter); +utils.inherits(Mempool, AsyncObject); -Mempool.prototype._lock = function _lock(func, args, force) { - return this.locker.lock(func, args, force); -}; +/** + * Open the chain, wait for the database to load. + * @alias Mempool#open + * @param {Function} callback + */ -Mempool.prototype._init = function _init() { +Mempool.prototype._open = function open(callback) { var self = this; - var unlock = this._lock(utils.nop, []); - var options = { - network: this.network, - name: this.options.name || 'mempool', - location: this.options.location, - db: this.options.db || 'memory' - }; - - assert(unlock); // Clean the database before loading. The only // reason for using an on-disk db for the mempool // is not for persistence, but to keep ~300mb of // txs out of main memory. - bcoin.ldb.destroy(options, function(err) { - if (err) { - unlock(); - return self.emit('error', err); - } - - self.db = bcoin.ldb(options); + this.db.destroy(function(err) { + if (err) + return callback(err); self.db.open(function(err) { - if (err) { - unlock(); - return self.emit('error', err); - } + if (err) + return callback(err); + self.initialMemoryUsage(function(err) { - if (err) { - unlock(); - return self.emit('error', err); - } - self.chain.open(function(err) { - if (err) { - unlock(); - return self.emit('error', err); - } - unlock(); - self.loaded = true; - self.emit('open'); - }); + if (err) + return callback(err); + + self.chain.open(callback); }); }); }); }; +/** + * Close the chain, wait for the database to close. + * @alias Mempool#close + * @param {Function} callback + */ + +Mempool.prototype._close = function destroy(callback) { + this.db.close(callback); +}; + +/** + * Invoke mutex lock. + * @private + * @returns {Function} unlock + */ + +Mempool.prototype._lock = function _lock(func, args, force) { + return this.locker.lock(func, args, force); +}; + /** * Tally up total memory usage from database. * @param {Function} callback - Returns [Error, Number]. @@ -187,29 +191,6 @@ Mempool.prototype.initialMemoryUsage = function initialMemoryUsage(callback) { }); }; -/** - * Open the chain, wait for the database to load. - * @param {Function} callback - */ - -Mempool.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - return this.once('open', callback); -}; - -/** - * Close the chain, wait for the database to close. - * @method - * @param {Function} callback - */ - -Mempool.prototype.close = -Mempool.prototype.destroy = function destroy(callback) { - this.db.close(utils.ensure(callback)); -}; - /** * Notify the mempool that a new block has come * in (removes all transactions contained in the diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index 30fd2d68..a82c34ca 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -13,6 +13,7 @@ var assert = utils.assert; var constants = bcoin.protocol.constants; var bn = require('bn.js'); var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('./async'); var BufferReader = require('./reader'); var BufferWriter = require('./writer'); @@ -33,7 +34,7 @@ function Miner(options) { if (!(this instanceof Miner)) return new Miner(options); - EventEmitter.call(this); + AsyncObject.call(this); if (!options) options = {}; @@ -51,7 +52,6 @@ function Miner(options) { this.network = this.chain.network; this.running = false; this.timeout = null; - this.loaded = false; this.attempt = null; this.workerPool = null; @@ -66,31 +66,13 @@ function Miner(options) { this._init(); } -utils.inherits(Miner, EventEmitter); +utils.inherits(Miner, AsyncObject); /** - * Open the miner, wait for the chain and mempool to load. - * @param {Function} callback + * Initialize the miner. + * @private */ -Miner.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - return this.once('open', callback); -}; - -/** - * Close the miner. - * @method - * @param {Function} callback - */ - -Miner.prototype.close = -Miner.prototype.destroy = function destroy(callback) { - return utils.nextTick(callback); -}; - Miner.prototype._init = function _init() { var self = this; @@ -134,18 +116,29 @@ Miner.prototype._init = function _init() { stat.height, stat.best); }); +}; - function done(err) { - if (err) - return self.emit('error', err); - self.loaded = true; - self.emit('open'); - } +/** + * Open the miner, wait for the chain and mempool to load. + * @alias Miner#open + * @param {Function} callback + */ +Miner.prototype._open = function open(callback) { if (this.mempool) - this.mempool.open(done); + this.mempool.open(callback); else - this.chain.open(done); + this.chain.open(callback); +}; + +/** + * Close the miner. + * @alias Miner#close + * @param {Function} callback + */ + +Miner.prototype._close = function close(callback) { + return utils.nextTick(callback); }; /** diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index 98fce7b1..22619ef8 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -9,6 +9,7 @@ var bcoin = require('./env'); var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('./async'); var utils = require('./utils'); /** @@ -24,7 +25,7 @@ function Node(options) { if (!(this instanceof Node)) return new Node(options); - EventEmitter.call(this); + AsyncObject.call(this); if (!options) options = {}; @@ -37,9 +38,10 @@ function Node(options) { this.chain = null; this.miner = null; this.walletdb = null; + this.wallet = null; } -utils.inherits(Node, EventEmitter); +utils.inherits(Node, AsyncObject); /* * Expose diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 8a336cb3..3c9d9344 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -8,6 +8,7 @@ 'use strict'; var bcoin = require('./env'); +var AsyncObject = require('./async'); var EventEmitter = require('events').EventEmitter; var utils = require('./utils'); var IP = require('./ip'); @@ -85,7 +86,7 @@ function Pool(options) { if (!(this instanceof Pool)) return new Pool(options); - EventEmitter.call(this); + AsyncObject.call(this); if (!options) options = {}; @@ -129,8 +130,6 @@ function Pool(options) { }); this.server = null; - this.destroyed = false; - this.loaded = false; this.size = options.size || 8; this.maxLeeches = options.maxLeeches || 8; this.connected = false; @@ -215,67 +214,12 @@ function Pool(options) { }; this._init(); -} - -utils.inherits(Pool, EventEmitter); - -/** - * Open the pool, wait for the chain to load. - * @param {Function} callback - */ - -Pool.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); }; -/** - * Connect to the network. - */ - -Pool.prototype.connect = function connect() { - var self = this; - var i; - - assert(this.loaded, 'Pool is not loaded.'); - - if (this.connected) - return; - - if (!this.options.selfish && !this.options.spv) { - if (this.mempool) { - this.mempool.on('tx', function(tx) { - self.announce(tx); - }); - } - - // Normally we would also broadcast - // competing chains, but we want to - // avoid getting banned if an evil - // miner sends us an invalid competing - // chain that we can't connect and - // verify yet. - this.chain.on('block', function(block) { - if (!self.synced) - return; - self.announce(block); - }); - } - - assert(this.seeds.length !== 0, 'No seeds available.'); - - this._addLoader(); - - for (i = 0; i < this.size - 1; i++) - this._addPeer(); - - this.connected = true; -}; +utils.inherits(Pool, AsyncObject); /** - * Initialize the pool. Bind to events. + * Initialize the pool. * @private */ @@ -328,15 +272,16 @@ Pool.prototype._init = function _init() { bcoin.debug('Chain is fully synced (height=%d).', self.chain.height); }); +}; - function done(err) { - if (err) - return self.emit('error', err); - - self.loaded = true; - self.emit('open'); - } +/** + * Open the pool, wait for the chain to load. + * @alias Pool#open + * @param {Function} callback + */ +Pool.prototype._open = function open(callback) { + var self = this; this.getIP(function(err, ip) { if (err) bcoin.error(err); @@ -347,12 +292,99 @@ Pool.prototype._init = function _init() { } if (self.mempool) - self.mempool.open(done); + self.mempool.open(callback); else - self.chain.open(done); + self.chain.open(callback); }); }; +/** + * Close and destroy the pool. + * @alias Pool#close + * @param {Function} callback + */ + +Pool.prototype._close = function close(callback) { + var i, items, peers, hashes, hash; + + this.stopSync(); + + items = this.inv.items.slice(); + + for (i = 0; i < items.length; i++) + items[i].finish(); + + hashes = Object.keys(this.request.map); + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + this.request.map[hash].finish(new Error('Pool closed.')); + } + + if (this.peers.load) + this.peers.load.destroy(); + + peers = this.peers.regular.slice(); + + for (i = 0; i < peers.length; i++) + peers[i].destroy(); + + peers = this.peers.pending.slice(); + + for (i = 0; i < peers.length; i++) + peers[i].destroy(); + + peers = this.peers.leeches.slice(); + + for (i = 0; i < peers.length; i++) + peers[i].destroy(); + + this.unlisten(callback); +}; + +/** + * Connect to the network. + */ + +Pool.prototype.connect = function connect() { + var self = this; + var i; + + assert(this.loaded, 'Pool is not loaded.'); + + if (this.connected) + return; + + if (!this.options.selfish && !this.options.spv) { + if (this.mempool) { + this.mempool.on('tx', function(tx) { + self.announce(tx); + }); + } + + // Normally we would also broadcast + // competing chains, but we want to + // avoid getting banned if an evil + // miner sends us an invalid competing + // chain that we can't connect and + // verify yet. + this.chain.on('block', function(block) { + if (!self.synced) + return; + self.announce(block); + }); + } + + assert(this.seeds.length !== 0, 'No seeds available.'); + + this._addLoader(); + + for (i = 0; i < this.size - 1; i++) + this._addPeer(); + + this.connected = true; +}; + /** * Start listening on a server socket. * @param {Function} callback @@ -524,7 +556,7 @@ Pool.prototype._addLoader = function _addLoader() { var self = this; var peer; - if (this.destroyed) + if (!this.loaded) return; if (this.peers.load) @@ -861,7 +893,7 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { block.rhash, utils.date(block.ts), self.chain.height, - (Math.floor(self.chain.getProgress() * 10000) / 100) + '%', + (self.chain.getProgress() * 100).toFixed(2) + '%', self.chain.total, self.chain.orphan.count, self.request.activeBlocks, @@ -934,7 +966,7 @@ Pool.prototype._createPeer = function _createPeer(options) { peer.once('close', function() { self._removePeer(peer); - if (self.destroyed) + if (!self.loaded) return; if (!peer.loader) @@ -1223,7 +1255,7 @@ Pool.prototype._addLeech = function _addLeech(socket) { var self = this; var peer; - if (this.destroyed) + if (!this.loaded) return socket.destroy(); peer = this._createPeer({ @@ -1254,7 +1286,7 @@ Pool.prototype._addPeer = function _addPeer() { var self = this; var peer, host; - if (this.destroyed) + if (!this.loaded) return; if (this.peers.regular.length + this.peers.pending.length >= this.size - 1) @@ -1395,7 +1427,7 @@ Pool.prototype.getData = function getData(peer, type, hash, options, callback) { callback = utils.ensure(callback); - if (this.destroyed) + if (!this.loaded) return callback(); if (options == null) @@ -1664,58 +1696,6 @@ Pool.prototype.setFeeRate = function setFeeRate(rate) { this.peers.regular[i].setFeeRate(rate); }; -/** - * Close and destroy the pool. - * @method - * @param {Function} callback - */ - -Pool.prototype.close = -Pool.prototype.destroy = function destroy(callback) { - var i, items, peers, hashes, hash; - - callback = utils.ensure(callback); - - if (this.destroyed) - return utils.nextTick(callback); - - this.destroyed = true; - - this.stopSync(); - - items = this.inv.items.slice(); - - for (i = 0; i < items.length; i++) - items[i].finish(); - - hashes = Object.keys(this.request.map); - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - this.request.map[hash].finish(new Error('Pool closed.')); - } - - if (this.peers.load) - this.peers.load.destroy(); - - peers = this.peers.regular.slice(); - - for (i = 0; i < peers.length; i++) - peers[i].destroy(); - - peers = this.peers.pending.slice(); - - for (i = 0; i < peers.length; i++) - peers[i].destroy(); - - peers = this.peers.leeches.slice(); - - for (i = 0; i < peers.length; i++) - peers[i].destroy(); - - this.unlisten(callback); -}; - /** * Get peer by host. * @param {String} addr @@ -1859,7 +1839,7 @@ Pool.prototype.getRandom = function getRandom(hosts, unique) { Pool.prototype.addHost = function addHost(host) { if (typeof host === 'string') - host = NetworkAddress.fromHostname(host); + host = NetworkAddress.fromHostname(host, this.network); if (this.hosts.length > 500) return; diff --git a/lib/bcoin/spvnode.js b/lib/bcoin/spvnode.js index bb543bac..f74eb2ae 100644 --- a/lib/bcoin/spvnode.js +++ b/lib/bcoin/spvnode.js @@ -10,6 +10,7 @@ var bcoin = require('./env'); var utils = require('./utils'); var assert = utils.assert; +var Node = bcoin.node; /** * Create an spv node which only maintains @@ -37,20 +38,7 @@ function SPVNode(options) { if (!(this instanceof SPVNode)) return new SPVNode(options); - bcoin.node.call(this, options); - - this.loaded = false; - - this._init(); -} - -utils.inherits(SPVNode, bcoin.node); - -SPVNode.prototype._init = function _init() { - var self = this; - var options; - - this.wallet = null; + Node.call(this, options); this.chain = new bcoin.chain({ network: this.network, @@ -84,6 +72,19 @@ SPVNode.prototype._init = function _init() { }); } + this._init(); +} + +utils.inherits(SPVNode, Node); + +/** + * Initialize the node. + * @private + */ + +SPVNode.prototype._init = function _init() { + var self = this; + // Bind to errors this.pool.on('error', function(err) { self.emit('error', err); @@ -136,14 +137,26 @@ SPVNode.prototype._init = function _init() { this.walletdb.on('save address', function(hash) { self.pool.watch(hash, 'hex'); }); +}; - function load(err) { +/** + * Open the node and all its child objects, + * wait for the database to load. + * @alias SPVNode#open + * @param {Function} callback + */ + +SPVNode.prototype._open = function open(callback) { + var self = this; + var options; + + function done(err) { if (err) - return self.emit('error', err); + return callback(err); - self.loaded = true; - self.emit('open'); bcoin.debug('Node is loaded.'); + + callback(); } options = utils.merge({ @@ -223,7 +236,28 @@ SPVNode.prototype._init = function _init() { return next(); self.http.open(next); } - ], load); + ], done); +}; + +/** + * Close the node, wait for the database to close. + * @alias SPVNode#close + * @param {Function} callback + */ + +SPVNode.prototype._close = function close(callback) { + var self = this; + + utils.parallel([ + function(next) { + if (!self.http) + return next(); + self.http.close(next); + }, + this.walletdb.close.bind(this.walletdb), + this.pool.close.bind(this.pool), + this.chain.close.bind(this.chain) + ], callback); }; /** @@ -284,43 +318,6 @@ SPVNode.prototype.stopSync = function stopSync() { return this.pool.stopSync(); }; -/** - * Open the node and all its child objects, - * wait for the database to load. - * @param {Function} callback - */ - -SPVNode.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); -}; - -/** - * Close the node, wait for the database to close. - * @method - * @param {Function} callback - */ - -SPVNode.prototype.close = -SPVNode.prototype.destroy = function destroy(callback) { - var self = this; - - this.wallet.destroy(); - - utils.parallel([ - function(next) { - if (!self.http) - return next(); - self.http.close(next); - }, - this.walletdb.close.bind(this.walletdb), - this.pool.close.bind(this.pool), - this.chain.close.bind(this.chain) - ], callback); -}; - /** * Create a {@link Wallet} in the wallet database. * @param {Object} options - See {@link Wallet}. diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 0f0e9449..83308e65 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -76,10 +76,22 @@ function TXDB(db, options) { utils.inherits(TXDB, EventEmitter); +/** + * Invoke the mutex lock. + * @private + * @returns {Function} unlock + */ + TXDB.prototype._lock = function _lock(func, args, force) { return this.locker.lock(func, args, force); }; +/** + * Load the bloom filter into memory. + * @private + * @param {Function} callback + */ + TXDB.prototype._loadFilter = function loadFilter(callback) { var self = this; @@ -96,6 +108,13 @@ TXDB.prototype._loadFilter = function loadFilter(callback) { }, callback); }; +/** + * Test the bloom filter against an array of address hashes. + * @private + * @param {Hash[]} addresses + * @returns {Boolean} + */ + TXDB.prototype._testFilter = function _testFilter(addresses) { var i; @@ -1668,6 +1687,10 @@ TXDB.prototype.zap = function zap(id, age, callback, force) { * WalletMap * @constructor * @private + * @property {WalletMember[]} inputs + * @property {WalletMember[]} outputs + * @property {WalletMember[]} accounts + * @property {Object} table */ function WalletMap() { @@ -1680,6 +1703,13 @@ function WalletMap() { this.table = {}; } +/** + * Inject properties from table and tx. + * @private + * @param {Object} table + * @param {TX} tx + */ + WalletMap.prototype.fromTX = function fromTX(table, tx) { var i, members, input, output, key; @@ -1801,10 +1831,24 @@ WalletMap.prototype.fromTX = function fromTX(table, tx) { return this; }; +/** + * Instantiate wallet map from tx. + * @param {Object} table + * @param {TX} tx + * @returns {WalletMap} + */ + WalletMap.fromTX = function fromTX(table, tx) { return new WalletMap().fromTX(table, tx); }; +/** + * Test whether the map has paths + * for a given address hash. + * @param {Hash} address + * @returns {Boolean} + */ + WalletMap.prototype.hasPaths = function hasPaths(address) { var paths; @@ -1816,10 +1860,21 @@ WalletMap.prototype.hasPaths = function hasPaths(address) { return paths && paths.length !== 0; }; +/** + * Get paths for a given address hash. + * @param {Hash} address + * @returns {Path[]|null} + */ + WalletMap.prototype.getPaths = function getPaths(address) { return this.table[address]; }; +/** + * Convert the map to a json-friendly object. + * @returns {Object} + */ + WalletMap.prototype.toJSON = function toJSON() { return { inputs: this.inputs.map(function(input) { @@ -1831,6 +1886,12 @@ WalletMap.prototype.toJSON = function toJSON() { }; }; +/** + * Inject properties from json object. + * @private + * @param {Object} + */ + WalletMap.prototype.fromJSON = function fromJSON(json) { var i, j, table, account, input, output, path; var hash, paths, hashes, accounts, values, key; @@ -1904,6 +1965,12 @@ WalletMap.prototype.fromJSON = function fromJSON(json) { return this; }; +/** + * Instantiate map from json object. + * @param {Object} + * @returns {WalletMap} + */ + WalletMap.fromJSON = function fromJSON(json) { return new WalletMap().fromJSON(json); }; @@ -1912,6 +1979,11 @@ WalletMap.fromJSON = function fromJSON(json) { * MapMember * @constructor * @private + * @property {WalletID} id + * @property {String} name - Account name. + * @property {Number} account - Account index. + * @property {Path[]} paths + * @property {Amount} value */ function MapMember() { @@ -1925,10 +1997,20 @@ function MapMember() { this.value = 0; } +/** + * Convert member to a key in the form of (id|account). + * @returns {String} + */ + MapMember.prototype.toKey = function toKey() { return this.id + '/' + this.account; }; +/** + * Convert the member to a json-friendly object. + * @returns {Object} + */ + MapMember.prototype.toJSON = function toJSON() { return { id: this.id, @@ -1941,6 +2023,12 @@ MapMember.prototype.toJSON = function toJSON() { }; }; +/** + * Inject properties from json object. + * @private + * @param {Object} json + */ + MapMember.prototype.fromJSON = function fromJSON(json) { var i, path; @@ -1958,10 +2046,22 @@ MapMember.prototype.fromJSON = function fromJSON(json) { return this; }; +/** + * Instantiate member from json object. + * @param {Object} json + * @returns {MapMember} + */ + MapMember.fromJSON = function fromJSON(json) { return new MapMember().fromJSON(json); }; +/** + * Inject properties from path. + * @private + * @param {Path} path + */ + MapMember.prototype.fromPath = function fromPath(path) { this.id = path.id; this.name = path.name; @@ -1969,6 +2069,12 @@ MapMember.prototype.fromPath = function fromPath(path) { return this; }; +/** + * Instantiate member from path. + * @param {Path} path + * @returns {MapMember} + */ + MapMember.fromPath = function fromPath(path) { return new MapMember().fromPath(path); }; diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 9fea7cd8..c6dc3581 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -18,6 +18,7 @@ var bcoin = require('./env'); var EventEmitter = require('events').EventEmitter; +var AsyncObject = require('./async'); var utils = require('./utils'); var assert = utils.assert; var constants = bcoin.protocol.constants; @@ -44,17 +45,220 @@ function WalletDB(options) { if (!options) options = {}; - EventEmitter.call(this); + AsyncObject.call(this); - this.watchers = {}; this.options = options; - this.loaded = false; this.network = bcoin.network.get(options.network); + this.watchers = {}; + + this.db = bcoin.ldb({ + network: this.network, + name: this.options.name || 'wallet', + location: this.options.location, + db: this.options.db, + cacheSize: 8 << 20, + writeBufferSize: 4 << 20 + }); + + this.tx = new bcoin.txdb(this, { + network: this.network, + verify: this.options.verify, + useFilter: true + }); this._init(); } -utils.inherits(WalletDB, EventEmitter); +utils.inherits(WalletDB, AsyncObject); + +/** + * Initialize wallet db. + * @private + */ + +WalletDB.prototype._init = function _init() { + var self = this; + + this.tx.on('error', function(err) { + self.emit('error', err); + }); + + this.tx.on('tx', function(tx, map) { + var i, path; + + self.emit('tx', tx, map); + + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; + self.fire(path.id, 'tx', tx, path.name); + } + }); + + this.tx.on('conflict', function(tx, map) { + var i, path; + + self.emit('conflict', tx, map); + + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; + self.fire(path.id, 'conflict', tx, path.name); + } + }); + + this.tx.on('confirmed', function(tx, map) { + var i, path; + + self.emit('confirmed', tx, map); + + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; + self.fire(path.id, 'confirmed', tx, path.name); + } + }); + + this.tx.on('unconfirmed', function(tx, map) { + var i, path; + + self.emit('unconfirmed', tx, map); + + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; + self.fire(path.id, 'unconfirmed', tx, path.name); + } + }); + + this.tx.on('updated', function(tx, map) { + var i, path, keys, id; + + self.emit('updated', tx, map); + + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; + self.fire(path.id, 'updated', tx, path.name); + } + + self.updateBalances(tx, map, function(err, balances) { + if (err) { + self.emit('error', err); + return; + } + + keys = Object.keys(balances); + + for (i = 0; i < keys.length; i++) { + id = keys[i]; + self.fire(id, 'balance', balances[id]); + } + + self.emit('balances', balances, map); + }); + }); +}; + +/** + * Open the walletdb, wait for the database to load. + * @alias WalletDB#open + * @param {Function} callback + */ + +WalletDB.prototype._open = function open(callback) { + var self = this; + + this.db.open(function(err) { + if (err) + return callback(err); + + self.tx._loadFilter(callback); + }); +}; + +/** + * Close the walletdb, wait for the database to close. + * @alias WalletDB#close + * @param {Function} callback + */ + +WalletDB.prototype._close = function close(callback) { + var self = this; + var keys = Object.keys(this.watchers); + var watcher; + + utils.forEachSerial(keys, function(key, next) { + watcher = self.watchers[key]; + watcher.refs = 1; + watcher.object.destroy(next); + }, function(err) { + if (err) + return callback(err); + + self.db.close(callback); + }); +}; + +/** + * Emit balance events after a tx is saved. + * @private + * @param {TX} tx + * @param {WalletMap} map + * @param {Function} callback + */ + +WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { + var self = this; + var balances = {}; + var id; + + utils.forEachSerial(map.outputs, function(output, next) { + id = output.id; + + if (self.listeners('balance').length === 0 + && !self.hasListener(id, 'balance')) { + return next(); + } + + if (balances[id] != null) + return next(); + + self.getBalance(id, function(err, balance) { + if (err) + return next(err); + + balances[id] = balance; + + next(); + }); + }, function(err) { + if (err) + return callback(err); + + return callback(null, balances); + }); +}; + +/** + * Derive new addresses after a tx is saved. + * @private + * @param {TX} tx + * @param {WalletMap} map + * @param {Function} callback + */ + +WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) { + var self = this; + var id; + + utils.forEachSerial(map.outputs, function(output, next) { + id = output.id; + + self.syncOutputDepth(id, tx, function(err, receive, change) { + if (err) + return next(err); + self.fire(id, 'address', receive, change); + self.emit('address', receive, change, map); + next(); + }); + }, callback); +}; /** * Dump database (for debugging). @@ -99,159 +303,6 @@ WalletDB.prototype.dump = function dump(callback) { })(); }; -WalletDB.prototype._init = function _init() { - var self = this; - - if (this.loaded) - return; - - this.db = bcoin.ldb({ - network: this.network, - name: this.options.name || 'wallet', - location: this.options.location, - db: this.options.db, - cacheSize: 8 << 20, - writeBufferSize: 4 << 20 - }); - - this.tx = new bcoin.txdb(this, { - network: this.network, - verify: this.options.verify, - useFilter: true - }); - - this.tx.on('error', function(err) { - self.emit('error', err); - }); - - this.tx.on('tx', function(tx, map) { - self.emit('tx', tx, map); - map.accounts.forEach(function(path) { - self.fire(path.id, 'tx', tx, path.name); - }); - }); - - this.tx.on('conflict', function(tx, map) { - self.emit('conflict', tx, map); - map.accounts.forEach(function(path) { - self.fire(path.id, 'conflict', tx, path.name); - }); - }); - - this.tx.on('confirmed', function(tx, map) { - self.emit('confirmed', tx, map); - map.accounts.forEach(function(path) { - self.fire(path.id, 'confirmed', tx, path.name); - }); - }); - - this.tx.on('unconfirmed', function(tx, map) { - self.emit('unconfirmed', tx, map); - map.accounts.forEach(function(path) { - self.fire(path.id, 'unconfirmed', tx, path.name); - }); - }); - - this.tx.on('updated', function(tx, map) { - self.emit('updated', tx, map); - map.accounts.forEach(function(path) { - self.fire(path.id, 'updated', tx, path.name); - }); - self.updateBalances(tx, map, function(err, balances) { - if (err) - return self.emit('error', err); - - Object.keys(balances).forEach(function(id) { - self.fire(id, 'balance', balances[id]); - }); - - self.emit('balances', balances, map); - }); - }); - - this.db.open(function(err) { - if (err) - return self.emit('error', err); - - self.tx._loadFilter(function(err) { - if (err) - return self.emit('error', err); - - self.emit('open'); - self.loaded = true; - }); - }); -}; - -WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { - var self = this; - var balances = {}; - - utils.forEachSerial(map.outputs, function(output, next) { - var id = output.id; - - if (self.listeners('balance').length === 0 - && !self.hasListener(id, 'balance')) { - return next(); - } - - if (balances[id] != null) - return next(); - - self.getBalance(id, function(err, balance) { - if (err) - return next(err); - - balances[id] = balance; - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - return callback(null, balances); - }); -}; - -WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) { - var self = this; - utils.forEachSerial(map.outputs, function(output, next) { - var id = output.id; - self.syncOutputDepth(id, tx, function(err, receive, change) { - if (err) - return next(err); - self.fire(id, 'address', receive, change); - self.emit('address', receive, change, map); - next(); - }); - }, callback); -}; - -/** - * Open the walletdb, wait for the database to load. - * @param {Function} callback - */ - -WalletDB.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); -}; - -/** - * Close the walletdb, wait for the database to close. - * @method - * @param {Function} callback - */ - -WalletDB.prototype.close = -WalletDB.prototype.destroy = function destroy(callback) { - callback = utils.ensure(callback); - this.db.close(callback); -}; - /** * Register an object with the walletdb. * @param {Object} object @@ -480,7 +531,8 @@ WalletDB.prototype.has = function has(id, callback) { WalletDB.prototype.ensure = function ensure(options, callback) { var self = this; - return this.get(options.id, function(err, wallet) { + + this.get(options.id, function(err, wallet) { if (err) return callback(err); @@ -502,7 +554,7 @@ WalletDB.prototype.getAccount = function getAccount(id, name, callback) { var self = this; var account; - return this.getAccountIndex(id, name, function(err, index) { + this.getAccountIndex(id, name, function(err, index) { if (err) return callback(err); @@ -540,6 +592,7 @@ WalletDB.prototype.getAccount = function getAccount(id, name, callback) { WalletDB.prototype.getAccounts = function getAccounts(id, callback) { var accounts = []; + this.db.iterate({ gte: 'i/' + id + '/', lte: 'i/' + id + '/~', @@ -571,7 +624,7 @@ WalletDB.prototype.getAccountIndex = function getAccountIndex(id, name, callback if (typeof name === 'number') return callback(null, name); - return this.db.get('i/' + id + '/' + name, function(err, index) { + this.db.get('i/' + id + '/' + name, function(err, index) { if (err && err.type !== 'NotFoundError') return callback(); @@ -1149,6 +1202,12 @@ WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) { * Path * @constructor * @private + * @property {WalletID} id + * @property {String} name - Account name. + * @property {Number} account - Account index. + * @property {Number} change - Change index. + * @property {Number} index - Address index. + * @property {Address|null} address */ function Path() { @@ -1163,6 +1222,12 @@ function Path() { this.address = null; } +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + Path.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); this.id = p.readVarString('utf8'); @@ -1173,10 +1238,21 @@ Path.prototype.fromRaw = function fromRaw(data) { return this; }; +/** + * Instantiate path from serialized data. + * @param {Buffer} data + * @returns {Path} + */ + Path.fromRaw = function fromRaw(data) { return new Path().fromRaw(data); }; +/** + * Serialize path. + * @returns {Buffer} + */ + Path.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); @@ -1192,6 +1268,13 @@ Path.prototype.toRaw = function toRaw(writer) { return p; }; +/** + * Inject properties from keyring. + * @private + * @param {WalletID} id + * @param {KeyRing} address + */ + Path.prototype.fromKeyRing = function fromKeyRing(id, address) { this.id = id; this.name = address.name; @@ -1201,22 +1284,32 @@ Path.prototype.fromKeyRing = function fromKeyRing(id, address) { return this; }; +/** + * Instantiate path from keyring. + * @param {WalletID} id + * @param {KeyRing} address + * @returns {Path} + */ + Path.fromKeyRing = function fromKeyRing(id, address) { return new Path().fromKeyRing(id, address); }; +/** + * Convert path object to string derivation path. + * @returns {String} + */ + Path.prototype.toPath = function() { return 'm/' + this.account + '\'/' + this.change + '/' + this.index; }; -Path.prototype.inspect = function() { - return ''; -}; +/** + * Convert path to a json-friendly object. + * @returns {Object} + */ Path.prototype.toJSON = function toJSON() { return { @@ -1226,6 +1319,12 @@ Path.prototype.toJSON = function toJSON() { }; }; +/** + * Inject properties from json object. + * @private + * @param {Object} json + */ + Path.prototype.fromJSON = function fromJSON(json) { var indexes = bcoin.hd.parsePath(json.path, constants.hd.MAX_INDEX); @@ -1242,14 +1341,30 @@ Path.prototype.fromJSON = function fromJSON(json) { return this; }; +/** + * Instantiate path from json object. + * @param {Object} json + * @returns {Path} + */ + Path.fromJSON = function fromJSON(json) { return new Path().fromJSON(json); }; +/** + * Convert path to a key in the form of (id|account). + * @returns {String} + */ + Path.prototype.toKey = function toKey() { return this.id + '/' + this.account; }; +/** + * Convert path to a compact json object. + * @returns {Object} + */ + Path.prototype.toCompact = function toCompact() { return { path: 'm/' + this.change + '/' + this.index, @@ -1257,6 +1372,12 @@ Path.prototype.toCompact = function toCompact() { }; }; +/** + * Inject properties from compact json object. + * @private + * @param {Object} json + */ + Path.prototype.fromCompact = function fromCompact(json) { var indexes = bcoin.hd.parsePath(json.path, constants.hd.MAX_INDEX); @@ -1271,10 +1392,28 @@ Path.prototype.fromCompact = function fromCompact(json) { return this; }; +/** + * Instantiate path from compact json object. + * @param {Object} json + * @returns {Path} + */ + Path.fromCompact = function fromCompact(json) { return new Path().fromCompact(json); }; +/** + * Inspect the path. + * @returns {String} + */ + +Path.prototype.inspect = function() { + return ''; +}; + /* * Helpers */ diff --git a/test/chain-test.js b/test/chain-test.js index cf026c85..e070e980 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -58,7 +58,6 @@ describe('Chain', function() { function deleteCoins(tx) { if (tx.txs) { - delete tx.view; deleteCoins(tx.txs); return; } @@ -67,12 +66,16 @@ describe('Chain', function() { return; } tx.inputs.forEach(function(input) { - delete input.coin; + input.coin = null; }); } it('should open chain and miner', function(cb) { - miner.open(function(err) { + miner.open(cb); + }); + + it('should open walletdb', function(cb) { + walletdb.open(function(err) { assert.ifError(err); walletdb.create({}, function(err, w) { assert.ifError(err); diff --git a/test/mempool-test.js b/test/mempool-test.js index 68395dbf..16b3d355 100644 --- a/test/mempool-test.js +++ b/test/mempool-test.js @@ -21,7 +21,7 @@ describe('Mempool', function() { db: 'memory' }); - var wdb = new bcoin.walletdb({ + var walletdb = new bcoin.walletdb({ name: 'mempool-wallet-test', db: 'memory', verify: true @@ -35,8 +35,12 @@ describe('Mempool', function() { mempool.open(cb); }); + it('should open walletdb', function(cb) { + walletdb.open(cb); + }); + it('should open wallet', function(cb) { - wdb.create({}, function(err, wallet) { + walletdb.create({}, function(err, wallet) { assert.ifError(err); w = wallet; cb();