diff --git a/bench/walletdb.js b/bench/walletdb.js index fd8d8d1f..0440b8ff 100644 --- a/bench/walletdb.js +++ b/bench/walletdb.js @@ -79,7 +79,7 @@ function runBench(callback) { function(next) { var nonce = new bn(0); var end; - utils.forRange(0, 100000, function(i, next) { + utils.forRange(0, 10000, function(i, next) { var t1 = bcoin.mtx() .addOutput(addrs[(i + 0) % addrs.length], 50460) .addOutput(addrs[(i + 1) % addrs.length], 50460) @@ -97,7 +97,7 @@ function runBench(callback) { }); }, function(err) { assert.ifError(err); - end(100000); + end(10000); next(); }); end = bench('tx'); diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index 66a3ee49..b81e15ab 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.tx.writeTip(self.chain.tip.hash, next); + self.walletdb.writeTip(self.chain.tip.hash, next); return next(); } // Always rescan to make sure we didn't miss anything: @@ -307,7 +307,7 @@ Fullnode.prototype._open = function open(callback) { }, function(next) { var i; - self.walletdb.getUnconfirmed(function(err, txs) { + self.wallet.getUnconfirmed(function(err, txs) { if (err) return next(err); diff --git a/lib/bcoin/http/rpc.js b/lib/bcoin/http/rpc.js index ccfd0f37..1fd31ee4 100644 --- a/lib/bcoin/http/rpc.js +++ b/lib/bcoin/http/rpc.js @@ -2516,7 +2516,7 @@ RPC.prototype.resendwallettransactions = function resendwallettransactions(args, if (args.help || args.length !== 0) return callback(new RPCError('resendwallettransactions')); - this.walletdb.getUnconfirmed(function(err, txs) { + this.wallet.getUnconfirmed(function(err, txs) { if (err) return callback(err); @@ -2603,7 +2603,7 @@ RPC.prototype.dumpwallet = function dumpwallet(args, callback) { '' ]; - this.walletdb.getAddresses(this.wallet.id, function(err, hashes) { + this.wallet.getAddresses(function(err, hashes) { if (err) return callback(err); @@ -2728,7 +2728,7 @@ RPC.prototype.getaddressesbyaccount = function getaddressesbyaccount(args, callb addrs = []; - this.walletdb.getAddresses(this.wallet.id, function(err, hashes) { + this.wallet.getAddresses(function(err, hashes) { if (err) return callback(err); @@ -2909,7 +2909,7 @@ RPC.prototype._toWalletTX = function _toWalletTX(tx, callback) { var self = this; var i, det, receive, member, sent, received, json; - this.walletdb.tx.toDetails(this.wallet.id, tx, function(err, details) { + this.wallet.tx.toDetails(tx, function(err, details) { if (err) return callback(err); @@ -3000,7 +3000,7 @@ RPC.prototype.gettransaction = function gettransaction(args, callback) { hash = utils.revHex(hash); - this.walletdb.getTX(hash, function(err, tx) { + this.wallet.getTX(hash, function(err, tx) { if (err) return callback(err); @@ -3024,7 +3024,7 @@ RPC.prototype.abandontransaction = function abandontransaction(args, callback) { hash = utils.revHex(hash); - this.walletdb.tx.remove(hash, function(err, result) { + this.wallet.abandon(hash, function(err, result) { if (err) return callback(err); @@ -3057,7 +3057,7 @@ RPC.prototype.getwalletinfo = function getwalletinfo(args, callback) { if (err) return callback(err); - self.walletdb.tx.getHistoryHashes(self.wallet.id, function(err, hashes) { + self.wallet.tx.getHistoryHashes(self.wallet.id, function(err, hashes) { if (err) return callback(err); @@ -3124,7 +3124,7 @@ RPC.prototype.listaccounts = function listaccounts(args, callback) { map = {}; - this.walletdb.getAccounts(this.wallet.id, function(err, accounts) { + this.wallet.getAccounts(function(err, accounts) { if (err) return callback(err); @@ -3240,7 +3240,7 @@ RPC.prototype._toListTX = function _toListTX(tx, callback) { var i, receive, member, det, sent, received, index; var sendMember, recMember, sendIndex, recIndex, json; - this.walletdb.tx.toDetails(this.wallet.id, tx, function(err, details) { + this.wallet.tx.toDetails(tx, function(err, details) { if (err) return callback(err); diff --git a/lib/bcoin/lowlevelup.js b/lib/bcoin/lowlevelup.js index 0eed986b..d075cfd1 100644 --- a/lib/bcoin/lowlevelup.js +++ b/lib/bcoin/lowlevelup.js @@ -251,6 +251,44 @@ LowlevelUp.prototype.fetch = function fetch(key, parse, callback) { }); }; +LowlevelUp.prototype.each = function each(options, handler, callback) { + var opt, iter; + + opt = { + gte: options.gte, + lte: options.lte, + keys: true, + values: options.values || false, + fillCache: options.fillCache || false, + keyAsBuffer: options.keyAsBuffer || false, + valueAsBuffer: true, + reverse: options.reverse || false + }; + + if (options.limit != null) + opt.limit = options.limit; + + iter = this.iterator(opt); + + (function next(stop) { + if (stop === true) + return iter.end(callback); + + iter.next(function(err, key, value) { + if (err) { + return iter.end(function() { + callback(err); + }); + } + + if (key === undefined) + return iter.end(callback); + + handler(key, value, next); + }); + })(); +}; + /** * Collect all keys from iterator options. * @param {Object} options - Iterator options. diff --git a/lib/bcoin/spvnode.js b/lib/bcoin/spvnode.js index 5e8493a2..6d45c10f 100644 --- a/lib/bcoin/spvnode.js +++ b/lib/bcoin/spvnode.js @@ -213,7 +213,7 @@ SPVNode.prototype._open = function open(callback) { }, function(next) { var i; - self.walletdb.getUnconfirmed(function(err, txs) { + self.wallet.getUnconfirmed(function(err, txs) { if (err) return next(err); diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 914eab9b..d812873d 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -26,7 +26,6 @@ var bcoin = require('./env'); var utils = require('./utils'); var assert = bcoin.utils.assert; -var EventEmitter = require('events').EventEmitter; var DUMMY = new Buffer([0]); var pad32 = utils.pad32; var BufferReader = require('./reader'); @@ -36,50 +35,48 @@ var BufferWriter = require('./writer'); * TXDB * @exports TXDB * @constructor - * @param {LowlevelUp} db - * @param {Object?} options - * @param {Boolean?} options.mapAddress - Map addresses to IDs. - * @param {Boolean?} options.indexAddress - Index addresses/IDs. - * @param {Boolean?} options.indexExtra - Index timestamps, heights, etc. - * @param {Boolean?} options.verify - Verify transactions as they - * come in (note that this will not happen on the worker - * pool -- only used for SPV). + * @param {WalletDB} db + * @param {WalletID} id */ -function TXDB(db, options) { +function TXDB(db, id) { if (!(this instanceof TXDB)) - return new TXDB(db, options); - - EventEmitter.call(this); - - if (!options) - options = {}; + return new TXDB(db, id); + this.id = id || null; this.walletdb = db; this.db = db.db; this.logger = db.logger; this.network = db.network; - this.options = options; - this.network = bcoin.network.get(options.network); + this.options = db.options; this.busy = false; this.jobs = []; this.locker = new bcoin.locker(this); this.current = null; - this.coinCache = new bcoin.lru(10000, 1); - - // Try to optimize for up to 1m addresses. - // We use a regular bloom filter here - // because we never want members to - // lose membership, even if quality - // degrades. - // Memory used: 1.7mb - this.filter = this.options.useFilter - ? bcoin.bloom.fromRate(1000000, 0.001, -1) - : null; } -utils.inherits(TXDB, EventEmitter); +/** + * Compile wallet prefix. + * @param {String} key + */ + +TXDB.prototype.prefix = function prefix(key) { + assert(this.id); + return 't/' + this.id + '/' + key; +}; + +/** + * Emit transaction event. + * @private + * @param {String} event + * @param {TX} tx + * @param {PathInfo} info + */ + +TXDB.prototype.emit = function emit(event, tx, info) { + return this.walletdb.emitTX(event, tx, info); +}; /** * Invoke the mutex lock. @@ -110,7 +107,7 @@ TXDB.prototype.start = function start() { TXDB.prototype.put = function put(key, value) { assert(this.current); - this.current.put(key, value); + this.current.put(this.prefix(key), value); }; /** @@ -120,7 +117,7 @@ TXDB.prototype.put = function put(key, value) { TXDB.prototype.del = function del(key) { assert(this.current); - this.current.del(key); + this.current.del(this.prefix(key)); }; /** @@ -144,6 +141,47 @@ TXDB.prototype.drop = function drop() { this.current = null; }; +/** + * Fetch. + * @param {String} key + */ + +TXDB.prototype.fetch = function fetch(key, parse, callback) { + this.db.fetch(this.prefix(key), parse, callback); +}; + +/** + * Get. + * @param {String} key + */ + +TXDB.prototype.get = function get(key, callback) { + this.db.get(this.prefix(key), callback); +}; + +/** + * Has. + * @param {String} key + */ + +TXDB.prototype.has = function has(key, callback) { + this.db.has(this.prefix(key), callback); +}; + +/** + * Iterate. + * @param {Object} options + * @param {Function} callback + */ + +TXDB.prototype.iterate = function iterate(options, callback) { + if (options.gte) + options.gte = this.prefix(options.gte); + if (options.lte) + options.lte = this.prefix(options.lte); + this.db.iterate(options, callback); +}; + /** * Commit current batch. * @param {Function} callback @@ -162,119 +200,14 @@ TXDB.prototype.commit = function commit(callback) { }); }; -/** - * Load the bloom filter into memory. - * @private - * @param {Function} callback - */ - -TXDB.prototype.loadFilter = function loadFilter(callback) { - var self = this; - - if (!this.filter) - return callback(); - - this.db.iterate({ - gte: 'W', - lte: 'W~', - transform: function(key) { - key = key.split('/')[1]; - self.filter.add(key, 'hex'); - } - }, 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; - - if (!this.filter) - return true; - - for (i = 0; i < addresses.length; i++) { - if (this.filter.test(addresses[i], 'hex')) - return true; - } - - return false; -}; - /** * Map a transactions' addresses to wallet IDs. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link WalletMap}]. + * @param {Function} callback - Returns [Error, {@link PathInfo}]. */ TXDB.prototype.getInfo = function getInfo(tx, callback) { - var addresses = tx.getHashes('hex'); - var info; - - if (!this.testFilter(addresses)) - return callback(); - - this.getTable(addresses, function(err, table) { - if (err) - return callback(err); - - if (!table) - return callback(); - - info = PathInfo.fromTX(tx, table); - - return callback(null, info); - }); -}; - -/** - * Map address hashes to paths. - * @param {Hash[]} address - Address hashes. - * @param {Function} callback - Returns [Error, {@link AddressTable}]. - */ - -TXDB.prototype.getTable = function getTable(address, callback) { - var self = this; - var table = {}; - var count = 0; - var i, keys, values; - - utils.forEachSerial(address, function(address, next) { - self.walletdb.getAddress(address, function(err, paths) { - if (err) - return next(err); - - if (!paths) { - assert(!table[address]); - table[address] = []; - return next(); - } - - keys = Object.keys(paths); - values = []; - - for (i = 0; i < keys.length; i++) - values.push(paths[keys[i]]); - - assert(!table[address]); - table[address] = values; - count += values.length; - - return next(); - }); - }, function(err) { - if (err) - return callback(err); - - if (count === 0) - return callback(); - - return callback(null, table); - }); + this.walletdb.getPathInfo(this.id, tx, callback); }; /** @@ -288,10 +221,11 @@ TXDB.prototype.getTable = function getTable(address, callback) { */ TXDB.prototype._addOrphan = function _addOrphan(key, outpoint, callback) { - var batch = this.batch(); + var self = this; var p = new BufferWriter(); + var k = 'o/' + key; - this.db.get('o/' + key, function(err, data) { + this.get(k, function(err, data) { if (err) return callback(err); @@ -300,7 +234,7 @@ TXDB.prototype._addOrphan = function _addOrphan(key, outpoint, callback) { p.writeBytes(outpoint); - batch.put('o/' + key, p.render()); + self.put(k, p.render()); return callback(); }); @@ -317,7 +251,7 @@ TXDB.prototype._getOrphans = function _getOrphans(key, callback) { var self = this; var items = []; - this.db.fetch('o/' + key, function(data) { + this.fetch('o/' + key, function(data) { var p = new BufferReader(data); var orphans = []; @@ -350,160 +284,6 @@ TXDB.prototype._getOrphans = function _getOrphans(key, callback) { }); }; -/** - * Write the genesis block as the best hash. - * @param {Function} callback - */ - -TXDB.prototype.writeGenesis = function writeGenesis(callback) { - var self = this; - var unlock, hash; - - unlock = this._lock(writeGenesis, [callback]); - - if (!unlock) - return; - - callback = utils.wrap(callback, unlock); - - this.db.has('R', function(err, result) { - if (err) - return callback(err); - - if (result) - return callback(); - - hash = new Buffer(self.network.genesis.hash, 'hex'); - - self.db.put('R', hash, callback); - }); -}; - -/** - * Get the best block hash. - * @param {Function} callback - */ - -TXDB.prototype.getTip = function getTip(callback) { - this.db.fetch('R', function(data) { - return data.toString('hex'); - }, callback); -}; - -/** - * Write the best block hash. - * @param {Hash} hash - * @param {Function} callback - */ - -TXDB.prototype.writeTip = function writeTip(hash, callback) { - if (typeof hash === 'string') - hash = new Buffer(hash, 'hex'); - this.db.put('R', hash, callback); -}; - -/** - * Add a block's transactions and write the new best hash. - * @param {Block} block - * @param {Function} callback - */ - -TXDB.prototype.addBlock = function addBlock(block, txs, callback, force) { - var self = this; - var unlock; - - unlock = this._lock(addBlock, [block, txs, callback], force); - - if (!unlock) - return; - - callback = utils.wrap(callback, unlock); - - if (this.options.useCheckpoints) { - if (block.height < this.network.checkpoints.lastHeight) - return this.writeTip(block.hash, callback); - } - - if (!Array.isArray(txs)) - txs = [txs]; - - utils.forEachSerial(txs, function(tx, next) { - self.add(tx, next, true); - }, function(err) { - if (err) - return callback(err); - - self.writeTip(block.hash, callback); - }); -}; - -/** - * Unconfirm a block's transactions - * and write the new best hash (SPV version). - * @param {Block} block - * @param {Function} callback - */ - -TXDB.prototype.removeBlock = function removeBlock(block, callback, force) { - var self = this; - var unlock; - - unlock = this._lock(removeBlock, [block, callback], force); - - if (!unlock) - return; - - callback = utils.wrap(callback, unlock); - - this.getHeightHashes(block.height, function(err, hashes) { - if (err) - return callback(err); - - utils.forEachSerial(hashes, function(hash, next) { - self.unconfirm(hash, next, true); - }, function(err) { - if (err) - return callback(err); - - self.writeTip(block.prevBlock, callback); - }); - }); -}; - -/** - * Add a transaction to the database, map addresses - * to wallet IDs, potentially store orphans, resolve - * orphans, or confirm a transaction. - * @param {TX} tx - * @param {Function} callback - Returns [Error]. - */ - -TXDB.prototype.add = function add(tx, callback, force) { - var self = this; - var unlock = this._lock(add, [tx, callback], force); - - if (!unlock) - return; - - callback = utils.wrap(callback, unlock); - - this.getInfo(tx, function(err, info) { - if (err) - return callback(err); - - if (!info) - return callback(null, false); - - self.logger.info( - 'Incoming transaction for %d addresses.', - info.paths.length); - - self.logger.debug(info.paths); - - self._add(tx, info, callback); - }); -}; - /** * Retrieve coins for own inputs, remove * double spenders, and verify inputs. @@ -518,16 +298,15 @@ TXDB.prototype._verify = function _verify(tx, info, callback) { utils.forEachSerial(tx.inputs, function(input, next, i) { var prevout = input.prevout; - var address, paths; + var address; if (tx.isCoinbase()) return next(); address = input.getHash('hex'); - paths = info.getPaths(address); // Only bother if this input is ours. - if (!paths) + if (!info.hasPath(address)) return next(); self.getCoin(prevout.hash, prevout.index, function(err, coin) { @@ -565,6 +344,8 @@ TXDB.prototype._verify = function _verify(tx, info, callback) { if (!prev) return callback(new Error('Could not find double-spent coin.')); + // NOTE: Could use d/spent.hash/spent.index + // here instead of getting a tx. input.coin = bcoin.coin.fromTX(prev, prevout.index); // Skip invalid transactions @@ -606,7 +387,6 @@ TXDB.prototype._verify = function _verify(tx, info, callback) { TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) { var self = this; - var batch = this.batch(); var hash = tx.hash('hex'); var key = hash + '/' + index; var coin; @@ -618,7 +398,7 @@ TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) { if (!orphans) return callback(null, false); - batch.del('o/' + key); + self.del('o/' + key); coin = bcoin.coin.fromTX(tx, index); @@ -639,7 +419,7 @@ TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) { // Verify that input script is correct, if not - add // output to unspent and remove orphan from storage if (!self.options.verify || orphan.verifyInput(input.index)) { - batch.put('d/' + input.hash + '/' + pad32(input.index), coin.toRaw()); + self.put('d/' + input.hash + '/' + pad32(input.index), coin.toRaw()); return callback(null, true); } @@ -662,9 +442,15 @@ TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) { * @param {Function} callback */ -TXDB.prototype._add = function add(tx, info, callback) { +TXDB.prototype.add = function add(tx, info, callback) { var self = this; - var batch, hash, i, j, path, paths, id; + var unlock = this._lock(add, [tx, info, callback]); + var hash, i, path, id; + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); if (tx.mutable) tx = tx.toTX(); @@ -687,24 +473,24 @@ TXDB.prototype._add = function add(tx, info, callback) { hash = tx.hash('hex'); - batch = self.start(); - batch.put('t/' + hash, tx.toExtended()); + self.start(); + self.put('t/' + hash, tx.toExtended()); if (tx.ts === 0) - batch.put('p/' + hash, DUMMY); + self.put('p/' + hash, DUMMY); else - batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); + self.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); + self.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); - for (i = 0; i < info.keys.length; i++) { - id = info.keys[i]; - batch.put('T/' + id + '/' + hash, DUMMY); + for (i = 0; i < info.accounts.length; i++) { + id = info.accounts[i]; + self.put('T/' + id + '/' + hash, DUMMY); if (tx.ts === 0) - batch.put('P/' + id + '/' + hash, DUMMY); + self.put('P/' + id + '/' + hash, DUMMY); else - batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); + self.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); + self.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); } // Consume unspent money or add orphans @@ -716,31 +502,27 @@ TXDB.prototype._add = function add(tx, info, callback) { return next(); address = input.getHash('hex'); - paths = info.getPaths(address); + path = info.getPath(address); // Only bother if this input is ours. - if (!paths) + if (!path) return next(); key = prevout.hash + '/' + prevout.index; // s/[outpoint-key] -> [spender-hash]|[spender-input-index] outpoint = bcoin.outpoint.fromTX(tx, i).toRaw(); - batch.put('s/' + key, outpoint); + self.put('s/' + key, outpoint); if (!input.coin) { // Add orphan, if no parent transaction is yet known return self._addOrphan(key, outpoint, next); } - for (j = 0; j < paths.length; j++) { - path = paths[j]; - id = path.id + '/' + path.account; - batch.del('C/' + id + '/' + key); - } + self.del('C/' + path.account + '/' + key); - batch.del('c/' + key); - batch.put('d/' + hash + '/' + pad32(i), input.coin.toRaw()); + self.del('c/' + key); + self.put('d/' + hash + '/' + pad32(i), input.coin.toRaw()); self.coinCache.remove(key); @@ -760,10 +542,10 @@ TXDB.prototype._add = function add(tx, info, callback) { if (output.script.isUnspendable()) return next(); - paths = info.getPaths(address); + path = info.getPath(address); // Do not add unspents for outputs that aren't ours. - if (!paths) + if (!path) return next(); self._resolveOrphans(tx, i, function(err, orphans) { @@ -775,15 +557,11 @@ TXDB.prototype._add = function add(tx, info, callback) { coin = bcoin.coin.fromTX(tx, i); - for (j = 0; j < paths.length; j++) { - path = paths[j]; - id = path.id + '/' + path.account; - batch.put('C/' + id + '/' + key, DUMMY); - } + self.put('C/' + path.account + '/' + key, DUMMY); coin = coin.toRaw(); - batch.put('c/' + key, coin); + self.put('c/' + key, coin); self.coinCache.set(key, coin); @@ -799,17 +577,12 @@ TXDB.prototype._add = function add(tx, info, callback) { if (err) return callback(err); - self.walletdb.handleTX(tx, info, function(err) { - if (err) - return callback(err); + self.emit('tx', tx, info); - self.emit('tx', tx, info); + if (tx.ts !== 0) + self.emit('confirmed', tx, info); - if (tx.ts !== 0) - self.emit('confirmed', tx, info); - - return callback(null, true, info); - }); + return callback(null, true, info); }); }); }); @@ -955,7 +728,7 @@ TXDB.prototype.isDoubleSpend = function isDoubleSpend(tx, callback) { TXDB.prototype.isSpent = function isSpent(hash, index, callback) { var key = 's/' + hash + '/' + index; - this.db.fetch(key, function(data) { + this.fetch(key, function(data) { return bcoin.outpoint.fromRaw(data); }, callback); }; @@ -973,7 +746,7 @@ TXDB.prototype.isSpent = function isSpent(hash, index, callback) { TXDB.prototype._confirm = function _confirm(tx, info, callback) { var self = this; var hash = tx.hash('hex'); - var batch, i, id; + var i, id; this.getTX(hash, function(err, existing) { if (err) @@ -999,17 +772,17 @@ TXDB.prototype._confirm = function _confirm(tx, info, callback) { // Save the original received time. tx.ps = existing.ps; - batch = self.start(); + self.start(); - batch.put('t/' + hash, tx.toExtended()); + self.put('t/' + hash, tx.toExtended()); - batch.del('p/' + hash); - batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); + self.del('p/' + hash); + self.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); - for (i = 0; i < info.keys.length; i++) { - id = info.keys[i]; - batch.del('P/' + id + '/' + hash); - batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); + for (i = 0; i < info.accounts.length; i++) { + id = info.accounts[i]; + self.del('P/' + id + '/' + hash); + self.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); } utils.forEachSerial(tx.outputs, function(output, next, i) { @@ -1017,7 +790,7 @@ TXDB.prototype._confirm = function _confirm(tx, info, callback) { var key = hash + '/' + i; // Only update coins if this output is ours. - if (!info.hasPaths(address)) + if (!info.hasPath(address)) return next(); self.getCoin(hash, i, function(err, coin) { @@ -1032,7 +805,7 @@ TXDB.prototype._confirm = function _confirm(tx, info, callback) { coin.height = tx.height; coin = coin.toRaw(); - batch.put('c/' + key, coin); + self.put('c/' + key, coin); self.coinCache.set(key, coin); @@ -1044,12 +817,13 @@ TXDB.prototype._confirm = function _confirm(tx, info, callback) { return callback(err); } - self.emit('tx', tx, info); - self.emit('confirmed', tx, info); - self.commit(function(err) { if (err) return callback(err); + + self.emit('tx', tx, info); + self.emit('confirmed', tx, info); + return callback(null, true, info); }); }); @@ -1110,26 +884,25 @@ TXDB.prototype._lazyRemove = function lazyRemove(tx, callback) { TXDB.prototype._remove = function remove(tx, info, callback) { var self = this; var hash = tx.hash('hex'); - var batch = this.batch(); - var i, j, path, id, key, paths, address, input, output, coin; + var i, path, id, key, address, input, output, coin; - batch.del('t/' + hash); + this.del('t/' + hash); if (tx.ts === 0) - batch.del('p/' + hash); + this.del('p/' + hash); else - batch.del('h/' + pad32(tx.height) + '/' + hash); + this.del('h/' + pad32(tx.height) + '/' + hash); - batch.del('m/' + pad32(tx.ps) + '/' + hash); + this.del('m/' + pad32(tx.ps) + '/' + hash); - for (i = 0; i < info.keys.length; i++) { - id = info.keys[i]; - batch.del('T/' + id + '/' + hash); + for (i = 0; i < info.accounts.length; i++) { + id = info.accounts[i]; + this.del('T/' + id + '/' + hash); if (tx.ts === 0) - batch.del('P/' + id + '/' + hash); + this.del('P/' + id + '/' + hash); else - batch.del('H/' + id + '/' + pad32(tx.height) + '/' + hash); - batch.del('M/' + id + '/' + pad32(tx.ps) + '/' + hash); + this.del('H/' + id + '/' + pad32(tx.height) + '/' + hash); + this.del('M/' + id + '/' + pad32(tx.ps) + '/' + hash); } this.fillHistory(tx, function(err) { @@ -1147,23 +920,19 @@ TXDB.prototype._remove = function remove(tx, info, callback) { if (!input.coin) continue; - paths = info.getPaths(address); + path = info.getPath(address); - if (!paths) + if (!path) continue; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - id = path.id + '/' + path.account; - batch.put('C/' + id + '/' + key, DUMMY); - } + self.put('C/' + path.account + '/' + key, DUMMY); coin = input.coin.toRaw(); - batch.put('c/' + key, coin); - batch.del('d/' + hash + '/' + pad32(i)); - batch.del('s/' + key); - batch.del('o/' + key); + self.put('c/' + key, coin); + self.del('d/' + hash + '/' + pad32(i)); + self.del('s/' + key); + self.del('o/' + key); self.coinCache.set(key, coin); } @@ -1176,18 +945,14 @@ TXDB.prototype._remove = function remove(tx, info, callback) { if (output.script.isUnspendable()) continue; - paths = info.getPaths(address); + path = info.getPath(address); - if (!paths) + if (!path) continue; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - id = path.id + '/' + path.account; - batch.del('C/' + id + '/' + key); - } + self.del('C/' + path.account + '/' + key); - batch.del('c/' + key); + self.del('c/' + key); self.coinCache.remove(key); } @@ -1254,7 +1019,6 @@ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { TXDB.prototype._unconfirm = function unconfirm(tx, info, callback, force) { var self = this; - var batch = this.batch(); var hash = tx.hash('hex'); var height = tx.height; var i, id; @@ -1267,15 +1031,15 @@ TXDB.prototype._unconfirm = function unconfirm(tx, info, callback, force) { tx.index = -1; tx.block = null; - batch.put('t/' + hash, tx.toExtended()); + this.put('t/' + hash, tx.toExtended()); - batch.put('p/' + hash, DUMMY); - batch.del('h/' + pad32(height) + '/' + hash); + this.put('p/' + hash, DUMMY); + this.del('h/' + pad32(height) + '/' + hash); - for (i = 0; i < info.keys.length; i++) { - id = info.keys[i]; - batch.put('P/' + id + '/' + hash, DUMMY); - batch.del('H/' + id + '/' + pad32(height) + '/' + hash); + for (i = 0; i < info.accounts.length; i++) { + id = info.accounts[i]; + this.put('P/' + id + '/' + hash, DUMMY); + this.del('H/' + id + '/' + pad32(height) + '/' + hash); } utils.forEachSerial(tx.outputs, function(output, next, i) { @@ -1292,7 +1056,7 @@ TXDB.prototype._unconfirm = function unconfirm(tx, info, callback, force) { coin.height = tx.height; coin = coin.toRaw(); - batch.put('c/' + key, coin); + self.put('c/' + key, coin); self.coinCache.set(key, coin); @@ -1310,79 +1074,79 @@ TXDB.prototype._unconfirm = function unconfirm(tx, info, callback, force) { /** * Get hashes of all transactions in the database. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getHistoryHashes = function getHistoryHashes(id, callback) { - if (typeof id === 'function') { - callback = id; - id = null; +TXDB.prototype.getHistoryHashes = function getHistoryHashes(account, callback) { + if (typeof account === 'function') { + callback = account; + account = null; } - this.db.iterate({ - gte: id ? 'T/' + id + '/' : 't', - lte: id ? 'T/' + id + '/~' : 't~', + this.iterate({ + gte: account ? 'T/' + account + '/' : 't', + lte: account ? 'T/' + account + '/~' : 't~', transform: function(key) { key = key.split('/'); - if (id) - return key[3]; - return key[1]; + if (account) + return key[4]; + return key[3]; } }, callback); }; /** * Get hashes of all unconfirmed transactions in the database. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(id, callback) { - if (typeof id === 'function') { - callback = id; - id = null; +TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account, callback) { + if (typeof account === 'function') { + callback = account; + account = null; } - this.db.iterate({ - gte: id ? 'P/' + id + '/' : 'p', - lte: id ? 'P/' + id + '/~' : 'p~', + this.iterate({ + gte: account ? 'P/' + account + '/' : 'p', + lte: account ? 'P/' + account + '/~' : 'p~', transform: function(key) { key = key.split('/'); - if (id) - return key[3]; - return key[1]; + if (account) + return key[4]; + return key[3]; } }, callback); }; /** * Get all coin hashes in the database. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getCoinHashes = function getCoinHashes(id, callback) { - if (typeof id === 'function') { - callback = id; - id = null; +TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) { + if (typeof account === 'function') { + callback = account; + account = null; } - this.db.iterate({ - gte: id ? 'C/' + id + '/' : 'c', - lte: id ? 'C/' + id + '/~' : 'c~', + this.iterate({ + gte: account ? 'C/' + account + '/' : 'c', + lte: account ? 'C/' + account + '/~' : 'c~', transform: function(key) { key = key.split('/'); - if (id) - return [key[3], +key[4]]; - return [key[1], +key[2]]; + if (account) + return [key[4], +key[5]]; + return [key[3], +key[4]]; } }, callback); }; /** * Get TX hashes by height range. - * @param {WalletID?} id + * @param {Number?} account * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1391,27 +1155,27 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(id, callback) { * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(id, options, callback) { - if (typeof id !== 'string') { +TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, options, callback) { + if (typeof account !== 'string') { callback = options; - options = id; - id = null; + options = account; + account = null; } - this.db.iterate({ - gte: id - ? 'H/' + id + '/' + pad32(options.start) + '/' + this.iterate({ + gte: account + ? 'H/' + account + '/' + pad32(options.start) + '/' : 'h/' + pad32(options.start) + '/', - lte: id - ? 'H/' + id + '/' + pad32(options.end) + '/~' + lte: account + ? 'H/' + account + '/' + pad32(options.end) + '/~' : 'h/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); - if (id) + if (account) return key[4]; - return key[2]; + return key[3]; } }, callback); }; @@ -1428,7 +1192,7 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) { /** * Get TX hashes by timestamp range. - * @param {WalletID?} id + * @param {Number?} account * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1437,33 +1201,33 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) { * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getRangeHashes = function getRangeHashes(id, options, callback) { - if (typeof id === 'function') { - callback = id; - id = null; +TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callback) { + if (typeof account === 'function') { + callback = account; + account = null; } - this.db.iterate({ - gte: id - ? 'M/' + id + '/' + pad32(options.start) + '/' + this.iterate({ + gte: account + ? 'M/' + account + '/' + pad32(options.start) + '/' : 'm/' + pad32(options.start) + '/', - lte: id - ? 'M/' + id + '/' + pad32(options.end) + '/~' + lte: account + ? 'M/' + account + '/' + pad32(options.end) + '/~' : 'm/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); - if (id) + if (account) return key[4]; - return key[2]; + return key[3]; } }, callback); }; /** * Get transactions by timestamp range. - * @param {WalletID?} id + * @param {Number?} account * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1472,16 +1236,16 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(id, options, callback) { * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getRange = function getLast(id, options, callback) { +TXDB.prototype.getRange = function getLast(account, options, callback) { var self = this; var txs = []; - if (typeof id === 'function') { - callback = id; - id = null; + if (typeof account === 'function') { + callback = account; + account = null; } - this.getRangeHashes(id, options, function(err, hashes) { + this.getRangeHashes(account, options, function(err, hashes) { if (err) return callback(err); @@ -1508,19 +1272,19 @@ TXDB.prototype.getRange = function getLast(id, options, callback) { /** * Get last N transactions. - * @param {WalletID?} id + * @param {Number?} account * @param {Number} limit - Max number of transactions. * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getLast = function getLast(id, limit, callback) { +TXDB.prototype.getLast = function getLast(account, limit, callback) { if (typeof limit === 'function') { callback = limit; - limit = id; - id = null; + limit = account; + account = null; } - this.getRange(id, { + this.getRange(account, { start: 0, end: 0xffffffff, reverse: true, @@ -1530,20 +1294,20 @@ TXDB.prototype.getLast = function getLast(id, limit, callback) { /** * Get all transactions. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getHistory = function getHistory(id, callback) { +TXDB.prototype.getHistory = function getHistory(account, callback) { var self = this; var txs = []; - if (typeof id === 'function') { - callback = id; - id = null; + if (typeof account === 'function') { + callback = account; + account = null; } - this.getHistoryHashes(id, function(err, hashes) { + this.getHistoryHashes(account, function(err, hashes) { if (err) return callback(err); @@ -1563,26 +1327,26 @@ TXDB.prototype.getHistory = function getHistory(id, callback) { if (err) return callback(err); - return callback(null, utils.sortTX(txs)); + return callback(null, sortTX(txs)); }); }); }; /** * Get last active timestamp and height. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, Number(ts), Number(height)]. */ -TXDB.prototype.getLastTime = function getLastTime(id, callback) { +TXDB.prototype.getLastTime = function getLastTime(account, callback) { var i, tx, lastTs, lastHeight; - if (typeof id === 'function') { - callback = id; - id = null; + if (typeof account === 'function') { + callback = account; + account = null; } - this.getHistory(id, function(err, txs) { + this.getHistory(account, function(err, txs) { if (err) return callback(err); @@ -1605,20 +1369,20 @@ TXDB.prototype.getLastTime = function getLastTime(id, callback) { /** * Get unconfirmed transactions. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getUnconfirmed = function getUnconfirmed(id, callback) { +TXDB.prototype.getUnconfirmed = function getUnconfirmed(account, callback) { var self = this; var txs = []; - if (typeof id === 'function') { - callback = id; - id = null; + if (typeof account === 'function') { + callback = account; + account = null; } - this.getUnconfirmedHashes(id, function(err, hashes) { + this.getUnconfirmedHashes(account, function(err, hashes) { if (err) return callback(err); @@ -1638,27 +1402,60 @@ TXDB.prototype.getUnconfirmed = function getUnconfirmed(id, callback) { if (err) return callback(err); - return callback(null, txs); + return callback(null, sortTX(txs)); }); }); }; /** * Get coins. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, {@link Coin}[]]. */ -TXDB.prototype.getCoins = function getCoins(id, callback) { +TXDB.prototype.getCoins = function getCoins(account, callback) { + var self = this; + + if (typeof account === 'function') { + callback = account; + account = null; + } + + // Slow case + if (account) + return this.getAccountCoins(account, callback); + + // Fast case + this.iterate({ + gte: 'c', + lte: 'c~', + keys: true, + values: true, + parse: function(data, key) { + var parts = key.split('/'); + var hash = parts[3]; + var index = +parts[4]; + var coin = bcoin.coin.fromRaw(data); + coin.hash = hash; + coin.index = index; + key = hash + '/' + index; + self.coinCache.set(key, data); + return coin; + } + }, callback); +}; + +/** + * Get coins by account. + * @param {Number} account + * @param {Function} callback - Returns [Error, {@link Coin}[]]. + */ + +TXDB.prototype.getAccountCoins = function getCoins(account, callback) { var self = this; var coins = []; - if (typeof id === 'function') { - callback = id; - id = null; - } - - this.getCoinHashes(id, function(err, hashes) { + this.getCoinHashes(account, function(err, hashes) { if (err) return callback(err); @@ -1699,13 +1496,13 @@ TXDB.prototype.fillHistory = function fillHistory(tx, callback) { hash = tx.hash('hex'); - this.db.iterate({ + this.iterate({ gte: 'd/' + hash + '/' + pad32(0), lte: 'd/' + hash + '/' + pad32(0xffffffff), keys: true, values: true, parse: function(value, key) { - index = +key.split('/')[2]; + index = +key.split('/')[4]; coin = bcoin.coin.fromRaw(value); input = tx.inputs[index]; coin.hash = input.prevout.hash; @@ -1762,19 +1559,18 @@ TXDB.prototype.fillCoins = function fillCoins(tx, callback) { */ TXDB.prototype.getTX = function getTX(hash, callback) { - this.db.fetch('t/' + hash, function(tx) { + this.fetch('t/' + hash, function(tx) { return bcoin.tx.fromExtended(tx); }, callback); }; /** * Get transaction details. - * @param {WalletID} id * @param {Hash} hash * @param {Function} callback - Returns [Error, {@link TXDetails}]. */ -TXDB.prototype.getDetails = function getDetails(id, hash, callback) { +TXDB.prototype.getDetails = function getDetails(hash, callback) { var self = this; this.getTX(hash, function(err, tx) { if (err) @@ -1783,18 +1579,17 @@ TXDB.prototype.getDetails = function getDetails(id, hash, callback) { if (!tx) return callback(); - self.toDetails(id, tx, callback); + self.toDetails(tx, callback); }); }; /** * Convert transaction to transaction details. - * @param {WalletID} id * @param {TX|TX[]} tx * @param {Function} callback */ -TXDB.prototype.toDetails = function toDetails(id, tx, callback) { +TXDB.prototype.toDetails = function toDetails(tx, callback) { var self = this; var out; @@ -1829,7 +1624,7 @@ TXDB.prototype.toDetails = function toDetails(id, tx, callback) { if (!info) return callback(); - return callback(null, info.toDetails(id)); + return callback(null, info.toDetails()); }); }); }; @@ -1841,7 +1636,7 @@ TXDB.prototype.toDetails = function toDetails(id, tx, callback) { */ TXDB.prototype.hasTX = function hasTX(hash, callback) { - this.db.has('t/' + hash, callback); + this.has('t/' + hash, callback); }; /** @@ -1867,7 +1662,7 @@ TXDB.prototype.getCoin = function getCoin(hash, index, callback) { return callback(null, coin); } - this.db.fetch('c/' + key, function(data) { + this.fetch('c/' + key, function(data) { coin = bcoin.coin.fromRaw(data); coin.hash = hash; coin.index = index; @@ -1888,26 +1683,77 @@ TXDB.prototype.hasCoin = function hasCoin(hash, index, callback) { if (this.coinCache.has(key)) return callback(null, true); - this.db.has('c/' + key, callback); + this.has('c/' + key, callback); }; /** * Calculate balance. - * @param {WalletID?} id + * @param {Number?} account * @param {Function} callback - Returns [Error, {@link Balance}]. */ -TXDB.prototype.getBalance = function getBalance(id, callback) { +TXDB.prototype.getBalance = function getBalance(account, callback) { + var self = this; + var confirmed = 0; + var unconfirmed = 0; + + if (typeof account === 'function') { + callback = account; + account = null; + } + + // Slow case + if (account) + return this.getAccountBalance(account, callback); + + // Fast case + this.iterate({ + gte: 'c', + lte: 'c~', + keys: true, + values: true, + parse: function(data, key) { + var parts = key.split('/'); + var hash = parts[3]; + var index = +parts[4]; + var height = data.readUInt32LE(4, true); + var value = utils.read64N(data, 8); + + assert(data.length >= 16); + + if (height === 0x7fffffff) + unconfirmed += value; + else + confirmed += value; + + key = hash + '/' + index; + + self.coinCache.set(key, data); + } + }, function(err) { + if (err) + return callback(err); + + return callback(null, { + confirmed: confirmed, + unconfirmed: unconfirmed, + total: confirmed + unconfirmed + }); + }); +}; + +/** + * Calculate balance by account. + * @param {Number} account + * @param {Function} callback - Returns [Error, {@link Balance}]. + */ + +TXDB.prototype.getAccountBalance = function getBalance(account, callback) { var self = this; var confirmed = 0; var unconfirmed = 0; var key, coin; - if (typeof id === 'function') { - callback = id; - id = null; - } - function parse(data) { var height = data.readUInt32LE(4, true); var value = utils.read64N(data, 8); @@ -1920,7 +1766,7 @@ TXDB.prototype.getBalance = function getBalance(id, callback) { confirmed += value; } - this.getCoinHashes(id, function(err, hashes) { + this.getCoinHashes(account, function(err, hashes) { if (err) return callback(err); @@ -1937,7 +1783,7 @@ TXDB.prototype.getBalance = function getBalance(id, callback) { return next(); } - self.db.get('c/' + key, function(err, data) { + self.get('c/' + key, function(err, data) { if (err) return next(err); @@ -1968,23 +1814,23 @@ TXDB.prototype.getBalance = function getBalance(id, callback) { }; /** - * @param {WalletID?} id + * @param {Number?} account * @param {Number} age - Age delta (delete transactions older than `now - age`). * @param {Function} callback */ -TXDB.prototype.zap = function zap(id, age, callback, force) { +TXDB.prototype.zap = function zap(account, age, callback, force) { var self = this; var unlock; if (typeof age === 'function') { force = callback; callback = age; - age = id; - id = null; + age = account; + account = null; } - unlock = this._lock(zap, [id, age, callback], force); + unlock = this._lock(zap, [account, age, callback], force); if (!unlock) return; @@ -1994,7 +1840,7 @@ TXDB.prototype.zap = function zap(id, age, callback, force) { if (!utils.isNumber(age)) return callback(new Error('Age must be a number.')); - this.getRange(id, { + this.getRange(account, { start: 0, end: bcoin.now() - age }, function(err, txs) { @@ -2017,7 +1863,7 @@ TXDB.prototype.zap = function zap(id, age, callback, force) { TXDB.prototype.abandon = function abandon(hash, callback, force) { var self = this; - this.db.has('p/' + hash, function(err, result) { + this.has('p/' + hash, function(err, result) { if (err) return callback(err); @@ -2028,125 +1874,10 @@ TXDB.prototype.abandon = function abandon(hash, callback, force) { }); }; -function PathInfo(tx, table) { - // All relevant Wallet-ID/Accounts for - // inputs and outputs (for database indexing). - this.keys = []; - - // All output paths (for deriving during sync). - this.paths = []; - - // All wallet IDs (for balance & syncing). - this.wallets = []; - - // Map of address hashes->paths (for everything). - this.table = null; - - // Current transaction. - this.tx = null; - - // Wallet-specific details cache. - this._cache = {}; - - if (tx) - this.fromTX(tx, table); -} - -PathInfo.prototype.fromTX = function fromTX(tx, table) { - var i, j, keys, wallets, hashes, hash, paths, path, key; - - this.tx = tx; - this.table = table; - - keys = {}; - wallets = {}; - hashes = Object.keys(table); - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - paths = table[hash]; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - key = path.toKey(); - keys[key] = true; - wallets[path.id] = true; - } - } - - this.keys = Object.keys(keys); - this.wallets = Object.keys(wallets); - - hashes = tx.getOutputHashes('hex'); - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - paths = table[hash]; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - this.paths.push(path); - } - } - - return this; -}; - -PathInfo.fromTX = function fromTX(tx, table) { - return new PathInfo().fromTX(tx, table); -}; - /** - * Test whether the map has paths - * for a given address hash. - * @param {Hash} address - * @returns {Boolean} + * Details */ -PathInfo.prototype.hasPaths = function hasPaths(address) { - var paths; - - if (!address) - return false; - - paths = this.table[address]; - - return paths && paths.length !== 0; -}; - -/** - * Get paths for a given address hash. - * @param {Hash} address - * @returns {Path[]|null} - */ - -PathInfo.prototype.getPaths = function getPaths(address) { - var paths; - - if (!address) - return; - - paths = this.table[address]; - - if (!paths || paths.length === 0) - return; - - return paths; -}; - -PathInfo.prototype.toDetails = function toDetails(id) { - var details; - - assert(utils.isAlpha(id)); - - details = this._cache[id]; - - if (!details) { - details = new Details(id, this.tx, this.table); - this._cache[id] = details; - } - - return details; -}; - function Details(id, tx, table) { this.id = id; this.hash = tx.hash('hex'); @@ -2222,6 +1953,10 @@ Details.prototype.toJSON = function toJSON() { }; }; +/** + * DetailsMember + */ + function DetailsMember() { this.value = 0; this.address = null; @@ -2240,8 +1975,27 @@ DetailsMember.prototype.toJSON = function toJSON() { }; }; +/* + * Helpers + */ + +function sortTX(txs) { + return txs.sort(function(a, b) { + return a.ps - b.ps; + }); +} + +function sortCoins(coins) { + return coins.sort(function(a, b) { + a = a.height === -1 ? 0x7fffffff : a.height; + b = b.height === -1 ? 0x7fffffff : b.height; + return a - b; + }); +} + /* * Expose */ +TXDB.Details = Details; module.exports = TXDB; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index d270be1d..49a77f9f 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -884,20 +884,6 @@ utils.sortKeys = function sortKeys(keys) { }); }; -/** - * Sort transactions by timestamp. - * @param {TX[]} txs - * @returns {TX[]} Sorted transactions. - */ - -utils.sortTX = function sortTX(txs) { - return txs.slice().sort(function(a, b) { - a = a.ts || a.ps; - b = b.ts || b.ps; - return a - b; - }); -}; - /** * Unique-ify an array of strings. * @param {String[]} obj diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 143a9e02..8348af63 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -14,6 +14,7 @@ var utils = require('./utils'); var assert = utils.assert; var BufferReader = require('./reader'); var BufferWriter = require('./writer'); +var TXDB = require('./txdb'); /** * BIP44 Wallet @@ -60,6 +61,7 @@ function Wallet(db, options) { this.accountDepth = 0; this.token = constants.ZERO_HASH; this.tokenDepth = 0; + this.tx = new TXDB(this.db); this.account = null; @@ -126,6 +128,7 @@ Wallet.prototype.fromOptions = function fromOptions(options) { this.id = id; this.token = token; + this.tx.id = this.id; return this; }; @@ -517,6 +520,15 @@ Wallet.prototype.getAccounts = function getAccounts(callback) { this.db.getAccounts(this.id, callback); }; +/** + * Get all wallet address hashes. + * @param {Function} callback - Returns [Error, Array]. + */ + +Wallet.prototype.getAddresses = function getAddresses(callback) { + this.db.getAddresses(this.id, callback); +}; + /** * Retrieve an account from the database. * @param {Number|String} account @@ -1030,7 +1042,7 @@ Wallet.prototype.getOutputPaths = function getOutputPaths(tx, callback) { * Sync address depths based on a transaction's outputs. * This is used for deriving new addresses when * a confirmed transaction is seen. - * @param {WalletMap} info + * @param {PathInfo} info * @param {Function} callback - Returns [Errr, Boolean] * (true if new addresses were allocated). */ @@ -1054,9 +1066,6 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(info, callback) { for (i = 0; i < info.paths.length; i++) { path = info.paths[i]; - if (path.id !== this.id) - continue; - if (!accounts[path.account]) accounts[path.account] = []; @@ -1278,7 +1287,7 @@ Wallet.prototype._sign = function _sign(addresses, master, tx, index, type, call */ Wallet.prototype.fillCoins = function fillCoins(tx, callback) { - return this.db.fillCoins(tx, callback); + return this.tx.fillCoins(tx, callback); }; /** @@ -1289,7 +1298,7 @@ Wallet.prototype.fillCoins = function fillCoins(tx, callback) { */ Wallet.prototype.getCoin = function getCoin(hash, index, callback) { - return this.db.getCoin(hash, index, callback); + return this.tx.getCoin(hash, index, callback); }; /** @@ -1299,7 +1308,7 @@ Wallet.prototype.getCoin = function getCoin(hash, index, callback) { */ Wallet.prototype.getTX = function getTX(hash, callback) { - return this.db.getTX(hash, callback); + return this.tx.getTX(hash, callback); }; /** @@ -1309,7 +1318,7 @@ Wallet.prototype.getTX = function getTX(hash, callback) { */ Wallet.prototype.addTX = function addTX(tx, callback) { - return this.db.addTX(tx, callback); + this.db.addTX(tx, callback); }; /** @@ -1319,7 +1328,9 @@ Wallet.prototype.addTX = function addTX(tx, callback) { */ Wallet.prototype.getHistory = function getHistory(account, callback) { - return this.db.getHistory(this.id, account, callback); + this._getKey(account, callback, function(account, callback) { + this.tx.getHistory(account, callback); + }); }; /** @@ -1329,7 +1340,9 @@ Wallet.prototype.getHistory = function getHistory(account, callback) { */ Wallet.prototype.getCoins = function getCoins(account, callback) { - return this.db.getCoins(this.id, account, callback); + this._getKey(account, callback, function(account, callback) { + this.tx.getCoins(account, callback); + }); }; /** @@ -1339,7 +1352,9 @@ Wallet.prototype.getCoins = function getCoins(account, callback) { */ Wallet.prototype.getUnconfirmed = function getUnconfirmed(account, callback) { - return this.db.getUnconfirmed(this.id, account, callback); + this._getKey(account, callback, function(account, callback) { + this.tx.getUnconfirmed(account, callback); + }); }; /** @@ -1349,7 +1364,9 @@ Wallet.prototype.getUnconfirmed = function getUnconfirmed(account, callback) { */ Wallet.prototype.getBalance = function getBalance(account, callback) { - return this.db.getBalance(this.id, account, callback); + this._getKey(account, callback, function(account, callback) { + this.tx.getBalance(account, callback); + }); }; /** @@ -1361,7 +1378,9 @@ Wallet.prototype.getBalance = function getBalance(account, callback) { */ Wallet.prototype.getLastTime = function getLastTime(account, callback) { - return this.db.getLastTime(this.id, account, callback); + this._getKey(account, callback, function(account, callback) { + this.tx.getLastTime(account, callback); + }); }; /** @@ -1372,7 +1391,14 @@ Wallet.prototype.getLastTime = function getLastTime(account, callback) { */ Wallet.prototype.getLast = function getLast(account, limit, callback) { - return this.db.getLast(this.id, account, limit, callback); + if (typeof limit === 'function') { + callback = limit; + limit = account; + account = null; + } + this._getKey(account, callback, function(account, callback) { + this.tx.getLast(account, limit, callback); + }); }; /** @@ -1385,7 +1411,14 @@ Wallet.prototype.getLast = function getLast(account, limit, callback) { */ Wallet.prototype.getTimeRange = function getTimeRange(account, options, callback) { - return this.db.getTimeRange(this.id, account, options, callback); + if (typeof options === 'function') { + callback = options; + options = account; + account = null; + } + this._getKey(account, callback, function(account, callback) { + this.tx.getTimeRange(account, options, callback); + }); }; /** @@ -1396,7 +1429,61 @@ Wallet.prototype.getTimeRange = function getTimeRange(account, options, callback */ Wallet.prototype.zap = function zap(account, age, callback) { - return this.db.zap(this.id, account, age, callback); + if (typeof age === 'function') { + callback = age; + age = account; + account = null; + } + this._getKey(account, callback, function(account, callback) { + this.tx.zap(account, age, callback); + }); +}; + +/** + * Abandon transaction (accesses db). + * @param {Hash} hash + * @param {Function} callback - Returns [Error]. + */ + +Wallet.prototype.abandon = function abandon(account, hash, callback) { + if (typeof hash === 'function') { + callback = hash; + hash = account; + account = null; + } + this._getKey(account, callback, function(account, callback) { + this.tx.abandon(account, hash, callback); + }); +}; + +/** + * Resolve account index. + * @private + * @param {(Number|String)?} account + * @param {Function} errback - Returns [Error]. + * @param {Function} callback + */ + +Wallet.prototype._getKey = function _getKey(account, errback, callback) { + var self = this; + + if (typeof account === 'function') { + errback = account; + account = null; + } + + if (account == null) + return callback.call(this, null, errback); + + this.db.getAccountIndex(this.id, account, function(err, index) { + if (err) + return errback(err); + + if (index === -1) + return errback(new Error('Account not found.')); + + return callback.call(self, index, errback); + }); }; /** @@ -1695,6 +1782,7 @@ Wallet.prototype.fromJSON = function fromJSON(json) { this.accountDepth = json.accountDepth; this.token = new Buffer(json.token, 'hex'); this.master = MasterKey.fromJSON(json.master); + this.tx.id = this.id; return this; }; @@ -1736,6 +1824,7 @@ Wallet.prototype.fromRaw = function fromRaw(data) { this.token = p.readBytes(32); this.tokenDepth = p.readU32(); this.master = MasterKey.fromRaw(p.readVarBytes()); + this.tx.id = this.id; return this; }; diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index b1a1fca5..51262d27 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -23,6 +23,7 @@ var assert = utils.assert; var constants = bcoin.protocol.constants; var BufferReader = require('./reader'); var BufferWriter = require('./writer'); +var TXDB = require('./txdb'); /** * WalletDB @@ -57,11 +58,23 @@ function WalletDB(options) { // We need one read lock for `get` and `create`. // It will hold locks specific to wallet ids. this.readLock = new ReadLock(this); + this.locker = new bcoin.locker(this); this.walletCache = new bcoin.lru(10000, 1); this.accountCache = new bcoin.lru(10000, 1); this.pathCache = new bcoin.lru(100000, 1); + // TODO: Move to walletdb. + // Try to optimize for up to 1m addresses. + // We use a regular bloom filter here + // because we never want members to + // lose membership, even if quality + // degrades. + // Memory used: 1.7mb + this.filter = this.options.useFilter !== false + ? bcoin.bloom.fromRate(1000000, 0.001, -1) + : null; + this.db = bcoin.ldb({ location: this.options.location, db: this.options.db, @@ -69,12 +82,6 @@ function WalletDB(options) { writeBufferSize: 4 << 20 }); - this.tx = new bcoin.txdb(this, { - verify: this.options.verify, - useCheckpoints: this.options.useCheckpoints, - useFilter: true - }); - if (bcoin.useWorkers) this.workerPool = new bcoin.workers(); @@ -91,42 +98,11 @@ utils.inherits(WalletDB, AsyncObject); WalletDB.prototype._init = function _init() { var self = this; - this.tx.on('error', function(err) { - self.emit('error', err); - }); - if (bcoin.useWorkers) { this.workerPool.on('error', function(err) { self.emit('error', err); }); } - - function handleEvent(event, tx, info) { - var i, id, details; - - for (i = 0; i < info.wallets.length; i++) { - id = info.wallets[i]; - details = info.toDetails(id); - self.emit(event, id, tx, details); - self.fire(id, event, tx, details); - } - } - - this.tx.on('tx', function(tx, info) { - handleEvent('tx', tx, info); - }); - - this.tx.on('conflict', function(tx, info) { - handleEvent('conflict', tx, info); - }); - - this.tx.on('confirmed', function(tx, info) { - handleEvent('confirmed', tx, info); - }); - - this.tx.on('unconfirmed', function(tx, info) { - handleEvent('unconfirmed', tx, info); - }); }; /** @@ -142,15 +118,15 @@ WalletDB.prototype._open = function open(callback) { if (err) return callback(err); - self.db.checkVersion('V', 0, function(err) { + self.db.checkVersion('V', 1, function(err) { if (err) return callback(err); - self.tx.writeGenesis(function(err) { + self.writeGenesis(function(err) { if (err) return callback(err); - self.tx.loadFilter(callback); + self.loadFilter(callback); }); }); }); @@ -241,75 +217,132 @@ WalletDB.prototype.commit = function commit(id, callback) { }; /** - * Emit balance events after a tx is saved. + * Load the bloom filter into memory. * @private - * @param {TX} tx - * @param {WalletMap} info * @param {Function} callback */ -WalletDB.prototype.updateBalances = function updateBalances(tx, info, callback) { +WalletDB.prototype.loadFilter = function loadFilter(callback) { + var self = this; + + if (!this.filter) + return callback(); + + this.db.iterate({ + gte: 'W', + lte: 'W~', + transform: function(key) { + key = key.split('/')[1]; + self.filter.add(key, 'hex'); + } + }, callback); +}; + +/** + * Test the bloom filter against an array of address hashes. + * @private + * @param {Hash[]} addresses + * @returns {Boolean} + */ + +WalletDB.prototype.testFilter = function testFilter(addresses) { + var i; + + if (!this.filter) + return true; + + for (i = 0; i < addresses.length; i++) { + if (this.filter.test(addresses[i], 'hex')) + return true; + } + + return false; +}; + +/** + * Emit balance events after a tx is saved. + * @private + * @param {TX} tx + * @param {PathInfo} info + * @param {Function} callback + */ + +WalletDB.prototype.updateBalances = function updateBalances(wallet, info, callback) { var self = this; var details; - utils.forEachSerial(info.wallets, function(id, next) { - if (self.listeners('balances').length === 0 - && !self.hasListener(id, 'balance')) { - return next(); - } + if (this.listeners('balances').length === 0 + && !this.hasListener(wallet.id, 'balance')) { + return callback(); + } - self.getBalance(id, function(err, balance) { - if (err) - return next(err); + wallet.getBalance(function(err, balance) { + if (err) + return callback(err); - details = info.toDetails(id); - self.emit('balance', id, balance, details); - self.fire(id, 'balance', balance, details); + details = info.toDetails(); - next(); - }); - }, callback); + self.emit('balance', wallet.id, balance, details); + self.fire(wallet.id, 'balance', balance, details); + + return callback(); + }); }; /** * Derive new addresses after a tx is saved. * @private * @param {TX} tx - * @param {WalletMap} info + * @param {PathInfo} info * @param {Function} callback */ -WalletDB.prototype.syncOutputs = function syncOutputs(tx, info, callback) { +WalletDB.prototype.syncOutputs = function syncOutputs(wallet, info, callback) { var self = this; var details; - utils.forEachSerial(info.wallets, function(id, next) { - self.syncOutputDepth(id, info, function(err, receive, change) { - if (err) - return next(err); - details = info.toDetails(id); - self.emit('address', id, receive, change, details); - self.fire(id, 'address', receive, change, details); - next(); - }); - }, callback); + wallet.syncOutputDepth(info, function(err, receive, change) { + if (err) + return callback(err); + + details = info.toDetails(); + + self.emit('address', wallet.id, receive, change, details); + self.fire(wallet.id, 'address', receive, change, details); + + return callback(); + }); +}; + +/** + * Emit transaction event. + * @private + * @param {String} event + * @param {TX} tx + * @param {PathInfo} info + */ + +WalletDB.prototype.emitTX = function emitTX(event, tx, info) { + var details = info.toDetails(); + this.emit(event, info.id, tx, details); + this.fire(info.id, event, tx, details); }; /** * Derive new addresses and emit balance. * @private * @param {TX} tx - * @param {WalletMap} info + * @param {PathInfo} info * @param {Function} callback */ -WalletDB.prototype.handleTX = function handleTX(tx, info, callback) { +WalletDB.prototype.handleTX = function handleTX(wallet, info, callback) { var self = this; - this.syncOutputs(tx, info, function(err) { + this.syncOutputs(wallet, info, function(err) { if (err) return callback(err); - self.updateBalances(tx, info, callback); + self.updateBalances(wallet, info, callback); }); }; @@ -320,8 +353,7 @@ WalletDB.prototype.handleTX = function handleTX(tx, info, callback) { WalletDB.prototype.dump = function dump(callback) { var records = {}; - - var iter = this.db.iterator({ + this.db.each({ gte: 'w', lte: 'w~', keys: true, @@ -329,31 +361,14 @@ WalletDB.prototype.dump = function dump(callback) { fillCache: false, keyAsBuffer: false, valueAsBuffer: true + }, function(key, value, next) { + records[key] = value; + next(); + }, function(err) { + if (err) + return callback(err); + return callback(null, records); }); - - callback = utils.ensure(callback); - - (function next() { - iter.next(function(err, key, value) { - if (err) { - return iter.end(function() { - callback(err); - }); - } - - if (key === undefined) { - return iter.end(function(err) { - if (err) - return callback(err); - return callback(null, records); - }); - } - - records[key] = value; - - next(); - }); - })(); }; /** @@ -386,6 +401,9 @@ WalletDB.prototype.unregister = function unregister(object) { var id = object.id; var watcher = this.watchers[id]; + // NOP for now! + return false; + if (!watcher) return false; @@ -938,8 +956,8 @@ WalletDB.prototype.saveAddress = function saveAddress(id, addresses, callback) { var path = item[1]; var hash = address.getHash('hex'); - if (self.tx.filter) - self.tx.filter.add(hash, 'hex'); + if (self.filter) + self.filter.add(hash, 'hex'); self.emit('save address', address, path); @@ -1027,7 +1045,7 @@ WalletDB.prototype.getAddress = function getAddress(address, callback) { /** * Get all address hashes. - * @param {WalletId} id + * @param {WalletID} id * @param {Function} callback */ @@ -1052,6 +1070,21 @@ WalletDB.prototype.getAddresses = function getAddresses(id, callback) { }, callback); }; +/** + * Get all wallet ids. + * @param {Function} callback + */ + +WalletDB.prototype.getWallets = function getWallets(callback) { + this.db.iterate({ + gte: 'w', + lte: 'w~', + transform: function(key) { + return key.split('/')[1]; + } + }, callback); +}; + /** * Rescan the blockchain. * @param {ChainDB} chaindb @@ -1060,7 +1093,7 @@ WalletDB.prototype.getAddresses = function getAddresses(id, callback) { WalletDB.prototype.rescan = function rescan(chaindb, callback) { var self = this; - this.tx.getTip(function(err, hash) { + this.getTip(function(err, hash) { if (err) return callback(err); @@ -1074,255 +1107,16 @@ WalletDB.prototype.rescan = function rescan(chaindb, callback) { self.logger.info('Scanning for %d addresses.', hashes.length); chaindb.scan(hash, hashes, function(tx, block, next) { - self.tx.add(tx, function(err) { + self.addTX(tx, function(err) { if (err) return next(err); - self.tx.writeTip(block.hash, next); + self.writeTip(block.hash, next); }); }, callback); }); }); }; -/** - * Get the corresponding path for an address hash. - * @param {WalletID} id - * @param {Hash} address - * @param {Function} callback - */ - -WalletDB.prototype.getPath = function getPath(id, address, callback) { - this.getAddress(address, function(err, paths) { - if (err) - return callback(err); - - if (!paths || !paths[id]) - return callback(); - - return callback(null, paths[id]); - }); -}; - -/** - * @see {@link TXDB#add}. - */ - -WalletDB.prototype.addTX = function addTX(tx, callback) { - return this.tx.add(tx, callback); -}; - -/** - * @see {@link TXDB#getTX}. - */ - -WalletDB.prototype.getTX = function getTX(hash, callback) { - return this.tx.getTX(hash, callback); -}; - -/** - * @see {@link TXDB#getCoin}. - */ - -WalletDB.prototype.getCoin = function getCoin(hash, index, callback) { - return this.tx.getCoin(hash, index, callback); -}; - -/** - * @see {@link TXDB#getHistory}. - */ - -WalletDB.prototype.getHistory = function getHistory(id, account, callback) { - var self = this; - this._getKey(id, account, callback, function(id, callback) { - self.tx.getHistory(id, callback); - }); -}; - -/** - * @see {@link TXDB#getCoins}. - */ - -WalletDB.prototype.getCoins = function getCoins(id, account, callback) { - var self = this; - this._getKey(id, account, callback, function(id, callback) { - self.tx.getCoins(id, callback); - }); -}; - -/** - * @see {@link TXDB#getUnconfirmed}. - */ - -WalletDB.prototype.getUnconfirmed = function getUnconfirmed(id, account, callback) { - var self = this; - this._getKey(id, account, callback, function(id, callback) { - self.tx.getUnconfirmed(id, callback); - }); -}; - -/** - * @see {@link TXDB#getBalance}. - */ - -WalletDB.prototype.getBalance = function getBalance(id, account, callback) { - var self = this; - this._getKey(id, account, callback, function(id, callback) { - self.tx.getBalance(id, callback); - }); -}; - -/** - * @see {@link TXDB#getLastTime}. - */ - -WalletDB.prototype.getLastTime = function getLastTime(id, account, callback) { - var self = this; - - if (typeof account === 'function') { - callback = account; - account = null; - } - - this._getKey(id, account, callback, function(id, callback) { - self.tx.getLastTime(id, callback); - }); -}; - -/** - * @see {@link TXDB#getLast}. - */ - -WalletDB.prototype.getLast = function getLast(id, account, limit, callback) { - var self = this; - - if (typeof limit === 'function') { - callback = limit; - limit = account; - account = null; - } - - this._getKey(id, account, callback, function(id, callback) { - self.tx.getLast(id, limit, callback); - }); -}; - -WalletDB.prototype.getTimeRange = function getTimeRange(id, account, options, callback) { - var self = this; - - if (typeof options === 'function') { - callback = options; - options = account; - account = null; - } - - this._getKey(id, account, callback, function(id, callback) { - self.tx.getTimeRange(id, options, callback); - }); -}; - -/** - * @see {@link TXDB#getRange}. - */ - -WalletDB.prototype.getRange = function getRange(id, account, options, callback) { - var self = this; - - if (typeof options === 'function') { - callback = options; - options = account; - account = null; - } - - this._getKey(id, account, callback, function(id, callback) { - self.tx.getRange(id, options, callback); - }); -}; - -/** - * @see {@link TXDB#fillHistory}. - */ - -WalletDB.prototype.fillHistory = function fillHistory(tx, callback) { - this.tx.fillHistory(tx, callback); -}; - -/** - * @see {@link TXDB#fillCoins}. - */ - -WalletDB.prototype.fillCoins = function fillCoins(tx, callback) { - this.tx.fillCoins(tx, callback); -}; - -/** - * Zap all walletdb transactions. - * @see {@link TXDB#zap}. - */ - -WalletDB.prototype.zap = function zap(id, account, age, callback) { - var self = this; - - if (typeof age === 'function') { - callback = age; - age = account; - account = null; - } - - this._getKey(id, account, callback, function(id, callback) { - self.tx.zap(id, age, callback); - }); -}; - -/** - * Parse arguments and return an id - * consisting of `walletid/accountname`. - * @private - * @param {WalletID} id - * @param {String|Number} account - * @param {Function} errback - * @param {Function} callback - Returns [String, Function]. - */ - -WalletDB.prototype._getKey = function _getKey(id, account, errback, callback) { - if (typeof account === 'function') { - errback = account; - account = null; - } - - if (account == null) - return callback(id, errback); - - this.getAccountIndex(id, account, function(err, index) { - if (err) - return errback(err); - - if (index === -1) - return errback(new Error('Account not found.')); - - return callback(id + '/' + index, errback); - }); -}; - -/** - * Add a block's transactions and write the new best hash. - * @param {Block} block - * @param {Function} callback - */ - -WalletDB.prototype.addBlock = function addBlock(block, txs, callback) { - this.tx.addBlock(block, txs, callback); -}; - -/** - * Unconfirm a block's transactions and write the new best hash. - * @param {Block} block - * @param {Function} callback - */ - -WalletDB.prototype.removeBlock = function removeBlock(block, callback) { - this.tx.removeBlock(block, callback); -}; - /** * Helper function to get a wallet. * @private @@ -1351,9 +1145,462 @@ WalletDB.prototype.fetchWallet = function fetchWallet(id, callback, handler) { }); }; -WalletDB.prototype.syncOutputDepth = function syncOutputDepth(id, info, callback) { +/** + * Map a transactions' addresses to wallet IDs. + * @param {TX} tx + * @param {Function} callback - Returns [Error, {@link PathInfo[]}]. + */ + +WalletDB.prototype.mapWallets = function mapWallets(tx, callback) { + var addresses = tx.getHashes('hex'); + var info; + + if (!this.testFilter(addresses)) + return callback(); + + this.getTable(addresses, function(err, table) { + if (err) + return callback(err); + + if (!table) + return callback(); + + info = PathInfo.map(tx, table); + + return callback(null, info); + }); +}; + +/** + * Map a transactions' addresses to wallet IDs. + * @param {TX} tx + * @param {Function} callback - Returns [Error, {@link PathInfo}]. + */ + +WalletDB.prototype.getPathInfo = function getPathInfo(id, tx, callback) { + var addresses = tx.getHashes('hex'); + var info; + + this.getTable(addresses, function(err, table) { + if (err) + return callback(err); + + if (!table) + return callback(); + + info = new PathInfo(id, tx, table); + + return callback(null, info); + }); +}; + +/** + * Map address hashes to paths. + * @param {Hash[]} address - Address hashes. + * @param {Function} callback - Returns [Error, {@link AddressTable}]. + */ + +WalletDB.prototype.getTable = function getTable(address, callback) { + var self = this; + var table = {}; + var count = 0; + var i, keys, values; + + utils.forEachSerial(address, function(address, next) { + self.getAddress(address, function(err, paths) { + if (err) + return next(err); + + if (!paths) { + assert(!table[address]); + table[address] = []; + return next(); + } + + keys = Object.keys(paths); + values = []; + + for (i = 0; i < keys.length; i++) + values.push(paths[keys[i]]); + + assert(!table[address]); + table[address] = values; + count += values.length; + + return next(); + }); + }, function(err) { + if (err) + return callback(err); + + if (count === 0) + return callback(); + + return callback(null, table); + }); +}; + +/** + * Write the genesis block as the best hash. + * @param {Function} callback + */ + +WalletDB.prototype.writeGenesis = function writeGenesis(callback) { + var self = this; + var hash; + + this.db.has('R', function(err, result) { + if (err) + return callback(err); + + if (result) + return callback(); + + hash = new Buffer(self.network.genesis.hash, 'hex'); + + self.db.put('R', hash, callback); + }); +}; + +/** + * Get the best block hash. + * @param {Function} callback + */ + +WalletDB.prototype.getTip = function getTip(callback) { + this.db.fetch('R', function(data) { + return data.toString('hex'); + }, callback); +}; + +/** + * Write the best block hash. + * @param {Hash} hash + * @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); +}; + +/** + * Add a block's transactions and write the new best hash. + * @param {Block} block + * @param {Function} callback + */ + +WalletDB.prototype.addBlock = function addBlock(block, txs, callback, force) { + var self = this; + var unlock; + + unlock = this.locker.lock(addBlock, [block, txs, callback], force); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + + if (this.options.useCheckpoints) { + if (block.height < this.network.checkpoints.lastHeight) + return this.writeTip(block.hash, callback); + } + + if (!Array.isArray(txs)) + txs = [txs]; + + utils.forEachSerial(txs, function(tx, next) { + self.addTX(tx, next, true); + }, function(err) { + if (err) + return callback(err); + + self.writeTip(block.hash, callback); + }); +}; + +/** + * Unconfirm a block's transactions + * and write the new best hash (SPV version). + * @param {Block} block + * @param {Function} callback + */ + +WalletDB.prototype.removeBlock = function removeBlock(block, callback, force) { + var self = this; + var unlock; + + unlock = this.locker.lock(removeBlock, [block, callback], force); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + + this.getWallets(function(err, wallets) { + if (err) + return callback(err); + + utils.forEachSerial(wallets, function(id, next) { + self.get(id, function(err, wallet) { + if (err) + return next(err); + + if (!wallet) + return next(); + + wallet.tx.getHeightHashes(block.height, function(err, hashes) { + if (err) + return callback(err); + + utils.forEachSerial(hashes, function(hash, next) { + wallet.tx.unconfirm(hash, next); + }, next); + }); + }); + }, function(err) { + if (err) + return callback(err); + self.writeTip(block.prevBlock, callback); + }); + }); +}; + +/** + * Add a transaction to the database, map addresses + * to wallet IDs, potentially store orphans, resolve + * orphans, or confirm a transaction. + * @param {TX} tx + * @param {Function} callback - Returns [Error]. + */ + +WalletDB.prototype.addTX = function addTX(tx, callback, force) { + var self = this; + this.mapWallets(tx, function(err, wallets) { + if (err) + return callback(err); + + if (!wallets) + return callback(null, false); + + self.logger.info( + 'Incoming transaction for %d wallets.', + wallets.length); + + self.logger.debug(wallets); + + utils.forEachSerial(wallets, function(info, next) { + self.get(info.id, function(err, wallet) { + if (err) + return next(err); + + if (!wallet) + return next(); + + wallet.tx.add(tx, info, function(err) { + if (err) + return next(err); + + self.handleTX(wallet, info, next); + }); + }); + }, callback); + }); +}; + +/** + * Get the corresponding path for an address hash. + * @param {WalletID} id + * @param {Hash} address + * @param {Function} callback + */ + +WalletDB.prototype.getPath = function getPath(id, address, callback) { + this.getAddress(address, function(err, paths) { + if (err) + return callback(err); + + if (!paths || !paths[id]) + return callback(); + + return callback(null, paths[id]); + }); +}; + +/** + * @see {@link TXDB#getTX}. + */ + +WalletDB.prototype.getTX = function getTX(id, hash, callback) { this.fetchWallet(id, callback, function(wallet, callback) { - wallet.syncOutputDepth(info, 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); }); }; @@ -1634,6 +1881,147 @@ Path.prototype.inspect = function() { + '>'; }; +/** + * Path Info + */ + +function PathInfo(id, tx, table) { + // All relevant Accounts for + // inputs and outputs (for database indexing). + this.accounts = []; + + // All output paths (for deriving during sync). + this.paths = []; + + // Wallet ID + this.id = id; + + // Map of address hashes->paths (for everything). + this.table = null; + + // Map of address hashes->paths (specific to wallet). + this.pathMap = {}; + + // Current transaction. + this.tx = null; + + // Wallet-specific details cache. + this._details = null; + + if (tx) + this.fromTX(tx, table); +} + +PathInfo.map = function map(tx, table) { + var hashes = Object.keys(table); + var wallets = {}; + var info = []; + var i, j, hash, paths, path, id; + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + paths = table[hash]; + for (j = 0; j < paths.length; j++) { + path = paths[j]; + wallets[path.id] = true; + } + } + + wallets = Object.keys(wallets); + + if (wallets.length === 0) + return; + + for (i = 0; i < wallets.length; i++) { + id = wallets[i]; + info.push(new PathInfo(id, tx, table)); + } + + return info; +}; + +PathInfo.prototype.fromTX = function fromTX(tx, table) { + var uniq = {}; + var i, j, hashes, hash, paths, path; + + this.tx = tx; + this.table = table; + + hashes = Object.keys(table); + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + paths = table[hash]; + for (j = 0; j < paths.length; j++) { + path = paths[j]; + if (path.id !== this.id) + continue; + this.pathMap[hash] = path; + if (!uniq[path.account]) { + uniq[path.account] = true; + this.accounts.push(path.account); + } + } + } + + hashes = tx.getOutputHashes('hex'); + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + paths = table[hash]; + for (j = 0; j < paths.length; j++) { + path = paths[j]; + if (path.id !== this.id) + continue; + this.paths.push(path); + } + } + + return this; +}; + +PathInfo.fromTX = function fromTX(id, tx, table) { + return new PathInfo(id).fromTX(tx, table); +}; + +/** + * Test whether the map has paths + * for a given address hash. + * @param {Hash} address + * @returns {Boolean} + */ + +PathInfo.prototype.hasPath = function hasPath(address) { + if (!address) + return false; + + return this.pathMap[address] != null; +}; + +/** + * Get paths for a given address hash. + * @param {Hash} address + * @returns {Path[]|null} + */ + +PathInfo.prototype.getPath = function getPath(address) { + if (!address) + return; + + return this.pathMap[address]; +}; + +PathInfo.prototype.toDetails = function toDetails() { + var details = this._details; + + if (!details) { + details = new TXDB.Details(this.id, this.tx, this.table); + this._details = details; + } + + return details; +}; + /* * Helpers */