diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index 975f051b..d6f07e9f 100644 --- a/lib/bcoin/fullnode.js +++ b/lib/bcoin/fullnode.js @@ -298,7 +298,7 @@ Fullnode.prototype._open = function open(callback) { }, function(next) { if (self.options.noScan) { - self.walletdb.writeTip(self.chain.tip.hash, next); + self.walletdb.setTip(self.chain.tip.hash, 0, next); return next(); } // Always rescan to make sure we didn't miss anything: diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 1127c5f5..323e7e8b 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -48,12 +48,39 @@ function TXDB(wallet) { this.logger = wallet.db.logger; this.network = wallet.db.network; this.options = wallet.db.options; + this.locker = new bcoin.locker(this); - this.current = null; this.coinCache = new bcoin.lru(10000, 1); - this.balance = new Balance(); + + this.current = null; + this.balance = null; } +/** + * Open TXDB. + * @param {Function} callback + */ + +TXDB.prototype.open = function open(callback) { + var self = this; + + this.getBalance(function(err, balance) { + if (err) + return callback(err); + + self.logger.info('TXDB loaded for %s.', self.wallet.id); + self.logger.info( + 'Balance: unconfirmed=%s confirmed=%s total=%s.', + utils.btc(balance.unconfirmed), + utils.btc(balance.confirmed), + utils.btc(balance.total)); + + self.balance = balance; + + return callback(); + }); +}; + /** * Compile wallet prefix. * @param {String} key @@ -1086,11 +1113,11 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(account, callback) { } this.iterate({ - gte: account ? 'T/' + account + '/' : 't', - lte: account ? 'T/' + account + '/~' : 't~', + gte: account != null ? 'T/' + account + '/' : 't', + lte: account != null ? 'T/' + account + '/~' : 't~', transform: function(key) { key = key.split('/'); - if (account) + if (account != null) return key[4]; return key[3]; } @@ -1110,11 +1137,11 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account, cal } this.iterate({ - gte: account ? 'P/' + account + '/' : 'p', - lte: account ? 'P/' + account + '/~' : 'p~', + gte: account != null ? 'P/' + account + '/' : 'p', + lte: account != null ? 'P/' + account + '/~' : 'p~', transform: function(key) { key = key.split('/'); - if (account) + if (account != null) return key[4]; return key[3]; } @@ -1134,11 +1161,11 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) { } this.iterate({ - gte: account ? 'C/' + account + '/' : 'c', - lte: account ? 'C/' + account + '/~' : 'c~', + gte: account != null ? 'C/' + account + '/' : 'c', + lte: account != null ? 'C/' + account + '/~' : 'c~', transform: function(key) { key = key.split('/'); - if (account) + if (account != null) return [key[4], +key[5]]; return [key[3], +key[4]]; } @@ -1157,24 +1184,24 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) { */ TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, options, callback) { - if (typeof account !== 'string') { + if (typeof account !== 'number') { callback = options; options = account; account = null; } this.iterate({ - gte: account + gte: account != null ? 'H/' + account + '/' + pad32(options.start) + '/' : 'h/' + pad32(options.start) + '/', - lte: account + lte: account != null ? 'H/' + account + '/' + pad32(options.end) + '/~' : 'h/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); - if (account) + if (account != null) return key[4]; return key[3]; } @@ -1209,17 +1236,17 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callba } this.iterate({ - gte: account + gte: account != null ? 'M/' + account + '/' + pad32(options.start) + '/' : 'm/' + pad32(options.start) + '/', - lte: account + lte: account != null ? 'M/' + account + '/' + pad32(options.end) + '/~' : 'm/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); - if (account) + if (account != null) return key[4]; return key[3]; } @@ -1237,7 +1264,7 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callba * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getRange = function getLast(account, options, callback) { +TXDB.prototype.getRange = function getRange(account, options, callback) { var self = this; var txs = []; @@ -1308,6 +1335,36 @@ TXDB.prototype.getHistory = function getHistory(account, callback) { account = null; } + // Slow case + if (account != null) + return this.getAccountHistory(account, callback); + + // Fast case + this.iterate({ + gte: 't', + lte: 't~', + values: true, + parse: function(data) { + return bcoin.tx.fromExtended(data); + } + }, callback); +}; + +/** + * Get all account transactions. + * @param {Number?} account + * @param {Function} callback - Returns [Error, {@link TX}[]]. + */ + +TXDB.prototype.getAccountHistory = function getAccountHistory(account, callback) { + var self = this; + var txs = []; + + if (typeof account === 'function') { + callback = account; + account = null; + } + this.getHistoryHashes(account, function(err, hashes) { if (err) return callback(err); @@ -1423,7 +1480,7 @@ TXDB.prototype.getCoins = function getCoins(account, callback) { } // Slow case - if (account) + if (account != null) return this.getAccountCoins(account, callback); // Fast case @@ -1623,7 +1680,7 @@ TXDB.prototype.toDetails = function toDetails(tx, callback) { return callback(err); if (!info) - return callback(); + return callback(new Error('Info not found.')); return callback(null, info.toDetails()); }); @@ -1695,7 +1752,7 @@ TXDB.prototype.hasCoin = function hasCoin(hash, index, callback) { TXDB.prototype.getBalance = function getBalance(account, callback) { var self = this; - var balance = new Balance(this.wallet.id); + var balance; if (typeof account === 'function') { callback = account; @@ -1703,10 +1760,16 @@ TXDB.prototype.getBalance = function getBalance(account, callback) { } // Slow case - if (account) + if (account != null) return this.getAccountBalance(account, callback); + // Really fast case + if (this.balance) + return callback(null, this.balance); + // Fast case + balance = new Balance(this.wallet.id); + this.iterate({ gte: 'c', lte: 'c~', @@ -1832,7 +1895,7 @@ TXDB.prototype.zap = function zap(account, age, callback, force) { callback = utils.wrap(callback, unlock); - if (!utils.isNumber(age)) + if (!utils.isUInt32(age)) return callback(new Error('Age must be a number.')); this.getRange(account, { @@ -1863,7 +1926,7 @@ TXDB.prototype.abandon = function abandon(hash, callback, force) { return callback(err); if (!result) - return callback(new Error('TX not found.')); + return callback(new Error('TX not eligible.')); self.remove(hash, callback, force); }); @@ -2016,6 +2079,14 @@ Balance.prototype.toJSON = function toJSON() { }; }; +Balance.prototype.toString = function toString() { + return ''; +}; + /* * Helpers */ diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 7a4fb998..7dec14a7 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -170,7 +170,7 @@ Wallet.prototype.init = function init(options, callback) { self.account = account; - return callback(); + self.tx.open(callback); }); }); }; @@ -194,7 +194,7 @@ Wallet.prototype.open = function open(callback) { self.account = account; - return callback(); + self.tx.open(callback); }); }; @@ -207,8 +207,8 @@ Wallet.prototype.destroy = function destroy(callback) { callback = utils.ensure(callback); try { - if (this.db.unregister(this)) - this.master.destroy(); + this.db.unregister(this); + this.master.destroy(); } catch (e) { this.emit('error', e); return callback(e); @@ -792,7 +792,7 @@ Wallet.prototype.fund = function fund(tx, options, callback, force) { fee: options.fee, subtractFee: options.subtractFee, changeAddress: account.changeAddress.getAddress(), - height: self.network.height, + height: self.db.height, rate: rate, wallet: self, m: self.m, @@ -850,12 +850,12 @@ Wallet.prototype.createTX = function createTX(options, callback, force) { // if (options.locktime != null) // tx.setLocktime(options.locktime); // else - // tx.avoidFeeSniping(options.height); + // tx.avoidFeeSniping(self.db.height); if (!tx.isSane()) return callback(new Error('CheckTransaction failed.')); - if (!tx.checkInputs(options.height)) + if (!tx.checkInputs(self.db.height)) return callback(new Error('CheckInputs failed.')); self.scriptInputs(tx, function(err, total) { diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 486b5d53..2706285b 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -52,9 +52,12 @@ function WalletDB(options) { this.fees = options.fees; this.logger = options.logger || bcoin.defaultLogger; this.batches = {}; - this.watchers = {}; + this.wallets = {}; this.workerPool = null; + this.tip = this.network.genesis.hash; + this.height = 0; + // We need one read lock for `get` and `create`. // It will hold locks specific to wallet ids. this.readLock = new ReadLock(this); @@ -140,13 +143,12 @@ WalletDB.prototype._open = function open(callback) { WalletDB.prototype._close = function close(callback) { var self = this; - var keys = Object.keys(this.watchers); - var watcher; + var keys = Object.keys(this.wallets); + var wallet; utils.forEachSerial(keys, function(key, next) { - watcher = self.watchers[key]; - watcher.refs = 1; - watcher.object.destroy(next); + wallet = self.wallets[key]; + wallet.destroy(next); }, function(err) { if (err) return callback(err); @@ -286,19 +288,9 @@ WalletDB.prototype.dump = function dump(callback) { * @param {Object} object */ -WalletDB.prototype.register = function register(object) { - var id = object.id; - - if (!this.watchers[id]) - this.watchers[id] = { object: object, refs: 0 }; - - // Should never happen, and if it does, I will cry. - assert(this.watchers[id].object === object, 'I\'m crying.'); - - // We do some reference counting here - // because we're thug like that (police - // have a fit when your papers legit). - this.watchers[id].refs++; +WalletDB.prototype.register = function register(wallet) { + assert(!this.wallets[wallet.id]); + this.wallets[wallet.id] = wallet; }; /** @@ -307,25 +299,9 @@ WalletDB.prototype.register = function register(object) { * @returns {Boolean} */ -WalletDB.prototype.unregister = function unregister(object) { - var id = object.id; - var watcher = this.watchers[id]; - - // NOP for now! - return false; - - if (!watcher) - return false; - - assert(watcher.object === object); - assert(watcher.refs !== 0, '`destroy()` called twice!'); - - if (--watcher.refs === 0) { - delete this.watchers[id]; - return true; - } - - return false; +WalletDB.prototype.unregister = function unregister(wallet) { + assert(this.wallets[wallet.id]); + delete this.wallets[wallet.id]; }; /** @@ -336,7 +312,7 @@ WalletDB.prototype.unregister = function unregister(object) { WalletDB.prototype.get = function get(id, callback) { var self = this; - var unlock, watcher; + var unlock, wallet; unlock = this._lock(id, get, [id, callback]); @@ -348,12 +324,10 @@ WalletDB.prototype.get = function get(id, callback) { if (!id) return callback(); - watcher = this.watchers[id]; + wallet = this.wallets[id]; - if (watcher) { - watcher.refs++; - return callback(null, watcher.object); - } + if (wallet) + return callback(null, wallet); this._get(id, function(err, wallet) { if (err) @@ -521,7 +495,7 @@ WalletDB.prototype.has = function has(id, callback) { if (!id) return callback(null, false); - if (this.watchers[id]) + if (this.wallets[id]) return callback(null, true); if (this.walletCache.has(id)) @@ -948,7 +922,7 @@ WalletDB.prototype.getWallets = function getWallets(callback) { WalletDB.prototype.rescan = function rescan(chaindb, callback) { var self = this; - this.getTip(function(err, hash) { + this.getTip(function(err, hash, height) { if (err) return callback(err); @@ -965,7 +939,7 @@ WalletDB.prototype.rescan = function rescan(chaindb, callback) { self.addTX(tx, function(err) { if (err) return next(err); - self.writeTip(block.hash, next); + self.setTip(block.hash, block.height, next); }); }, callback); }); @@ -989,9 +963,6 @@ WalletDB.prototype.fetchWallet = function fetchWallet(id, callback, handler) { return callback(new Error('No wallet.')); handler(wallet, function(err, res1, res2) { - // Kill the reference. - // wallet.destroy(); - if (err) return callback(err); @@ -1102,18 +1073,18 @@ WalletDB.prototype.getTable = function getTable(address, callback) { WalletDB.prototype.writeGenesis = function writeGenesis(callback) { var self = this; - var hash; - this.db.has('R', function(err, result) { + this.getTip(function(err, hash, height) { if (err) return callback(err); - if (result) + if (hash) { + self.tip = hash; + self.height = height; return callback(); + } - hash = new Buffer(self.network.genesis.hash, 'hex'); - - self.db.put('R', hash, callback); + self.setTip(self.tip, self.height, callback); }); }; @@ -1124,8 +1095,17 @@ WalletDB.prototype.writeGenesis = function writeGenesis(callback) { WalletDB.prototype.getTip = function getTip(callback) { this.db.fetch('R', function(data) { - return data.toString('hex'); - }, callback); + var p = new BufferReader(data); + return [p.readHash('hex'), p.readU32()]; + }, function(err, items) { + if (err) + return callback(err); + + if (!items) + return callback(null, null, -1); + + return callback(null, items[0], items[1]); + }); }; /** @@ -1134,10 +1114,22 @@ WalletDB.prototype.getTip = function getTip(callback) { * @param {Function} callback */ -WalletDB.prototype.writeTip = function writeTip(hash, callback) { - if (typeof hash === 'string') - hash = new Buffer(hash, 'hex'); - this.db.put('R', hash, callback); +WalletDB.prototype.setTip = function setTip(hash, height, callback) { + var self = this; + var p = new BufferWriter(); + + p.writeHash(hash); + p.writeU32(height); + + this.db.put('R', p.render(), function(err) { + if (err) + return callback(err); + + self.tip = hash; + self.height = height; + + return callback(); + }); }; /** @@ -1159,7 +1151,7 @@ WalletDB.prototype.addBlock = function addBlock(block, txs, callback, force) { if (this.options.useCheckpoints) { if (block.height < this.network.checkpoints.lastHeight) - return this.writeTip(block.hash, callback); + return this.setTip(block.hash, block.height, callback); } if (!Array.isArray(txs)) @@ -1171,7 +1163,7 @@ WalletDB.prototype.addBlock = function addBlock(block, txs, callback, force) { if (err) return callback(err); - self.writeTip(block.hash, callback); + self.setTip(block.hash, block.height, callback); }); }; @@ -1217,7 +1209,7 @@ WalletDB.prototype.removeBlock = function removeBlock(block, callback, force) { }, function(err) { if (err) return callback(err); - self.writeTip(block.prevBlock, callback); + self.setTip(block.prevBlock, block.height - 1, callback); }); }); }; @@ -1283,300 +1275,6 @@ WalletDB.prototype.getPath = function getPath(id, address, callback) { }); }; -/** - * @see {@link TXDB#toDetails}. - */ - -WalletDB.prototype.toDetails = function toDetails(id, tx, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.toDetails(tx, callback); - }); -}; - -/** - * @see {@link TXDB#getTX}. - */ - -WalletDB.prototype.getTX = function getTX(id, hash, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getTX(hash, callback); - }); -}; - -/** - * @see {@link TXDB#getCoin}. - */ - -WalletDB.prototype.getCoin = function getCoin(id, hash, index, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getCoin(hash, index, callback); - }); -}; - -/** - * @see {@link TXDB#getHistory}. - */ - -WalletDB.prototype.getHistory = function getHistory(id, account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getHistory(account, callback); - }); -}; - -/** - * @see {@link TXDB#getCoins}. - */ - -WalletDB.prototype.getCoins = function getCoins(id, account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getCoins(account, callback); - }); -}; - -/** - * @see {@link TXDB#getUnconfirmed}. - */ - -WalletDB.prototype.getUnconfirmed = function getUnconfirmed(id, account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getUnconfirmed(account, callback); - }); -}; - -/** - * @see {@link TXDB#getBalance}. - */ - -WalletDB.prototype.getBalance = function getBalance(id, account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getBalance(account, callback); - }); -}; - -/** - * @see {@link TXDB#getLastTime}. - */ - -WalletDB.prototype.getLastTime = function getLastTime(id, account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getLastTime(account, callback); - }); -}; - -/** - * @see {@link TXDB#getLast}. - */ - -WalletDB.prototype.getLast = function getLast(id, account, limit, callback) { - if (typeof limit === 'function') { - callback = limit; - limit = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getLast(account, limit, callback); - }); -}; - -WalletDB.prototype.getTimeRange = function getTimeRange(id, account, options, callback) { - if (typeof options === 'function') { - callback = options; - options = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getTimeRange(account, options, callback); - }); -}; - -/** - * @see {@link TXDB#getRange}. - */ - -WalletDB.prototype.getRange = function getRange(id, account, options, callback) { - if (typeof options === 'function') { - callback = options; - options = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.getRange(account, options, callback); - }); -}; - -/** - * @see {@link TXDB#fillHistory}. - */ - -WalletDB.prototype.fillHistory = function fillHistory(id, tx, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.fillHistory(tx, callback); - }); -}; - -/** - * @see {@link TXDB#fillCoins}. - */ - -WalletDB.prototype.fillCoins = function fillCoins(id, tx, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.fillCoins(tx, callback); - }); -}; - -/** - * Zap all walletdb transactions. - * @see {@link TXDB#zap}. - */ - -WalletDB.prototype.zap = function zap(id, account, age, callback) { - if (typeof age === 'function') { - callback = age; - age = account; - account = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.tx.zap(account, age, callback); - }); -}; - -WalletDB.prototype.createAddress = function createAddress(id, name, change, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.createAddress(name, change, callback); - }); -}; - -WalletDB.prototype.fund = function fund(id, tx, options, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.fund(tx, options, callback); - }); -}; - -WalletDB.prototype.scriptInputs = function scriptInputs(id, tx, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.scriptInputs(tx, callback); - }); -}; - -WalletDB.prototype.sign = function sign(id, tx, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.sign(tx, options, callback); - }); -}; - -WalletDB.prototype.createTX = function createTX(id, options, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.createTX(options, callback); - }); -}; - -WalletDB.prototype.send = function send(id, options, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.send(options, callback); - }); -}; - -WalletDB.prototype.addKey = function addKey(id, name, key, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.addKey(name, key, callback); - }); -}; - -WalletDB.prototype.removeKey = function removeKey(id, name, key, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.removeKey(name, key, callback); - }); -}; - -WalletDB.prototype.setPassphrase = function setPassphrase(id, old, new_, callback) { - if (typeof new_ === 'function') { - callback = new_; - new_ = old; - old = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.setPassphrase(old, new_, callback); - }); -}; - -WalletDB.prototype.retoken = function retoken(id, passphrase, callback) { - if (typeof passphrase === 'function') { - callback = passphrase; - passphrase = null; - } - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.retoken(passphrase, callback); - }); -}; - -WalletDB.prototype.getInfo = function getInfo(id, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - callback(null, wallet); - }); -}; - -WalletDB.prototype.ensureAccount = function ensureAccount(id, options, callback) { - var self = this; - var account = options.account; - - if (typeof options.name === 'string') - account = options.name; - - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.hasAccount(account, function(err, exists) { - if (err) - return callback(err); - - if (exists) - return wallet.getAccount(account, callback); - - wallet.createAccount(options, callback); - }); - }); -}; - -WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) { - this.fetchWallet(id, callback, function(wallet, callback) { - wallet.getRedeem(hash, callback); - }); -}; - /** * Path * @constructor