From abd267b7d2243a61d7a4ac1d79b3ce467b6f19de Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 11 Aug 2016 01:32:49 -0700 Subject: [PATCH] wallet: drop wallet map in favor of path info and tx details. --- lib/bcoin/http/rpc.js | 62 ++- lib/bcoin/http/server.js | 64 ++- lib/bcoin/txdb.js | 831 ++++++++++++--------------------------- lib/bcoin/wallet.js | 27 +- lib/bcoin/walletdb.js | 132 ++----- 5 files changed, 356 insertions(+), 760 deletions(-) diff --git a/lib/bcoin/http/rpc.js b/lib/bcoin/http/rpc.js index 1daeda9c..fbc5c353 100644 --- a/lib/bcoin/http/rpc.js +++ b/lib/bcoin/http/rpc.js @@ -2909,37 +2909,36 @@ RPC.prototype._toWalletTX = function _toWalletTX(tx, callback) { var self = this; var receive, member, json; - this.walletdb.tx.getMap(tx, function(err, map) { + this.walletdb.tx.toDetails(this.wallet.id, tx, function(err, details) { if (err) return callback(err); - if (!map) + if (!details) return callback(new RPCError('TX not found.')); - receive = map.inputs.length === 0; - member = receive ? map.outputs[0] : map.inputs[0]; - assert(member); + receive = details.isReceive(); + member = details.getMember(); json = { - amount: +utils.btc(tx.getOutputValue()), - confirmations: tx.getConfirmations(self.chain.height), - blockhash: tx.block ? utils.revHex(tx.block) : null, - blockindex: tx.index, - blocktime: tx.ts, - txid: tx.rhash, + amount: +utils.btc(details.getValue()), + confirmations: details.confirmations, + blockhash: details.block ? utils.revHex(details.block) : null, + blockindex: details.index, + blocktime: details.ts, + txid: utils.revHex(details.hash), walletconflicts: [], - time: tx.ps, - timereceived: tx.ps, + time: details.ps, + timereceived: details.ps, 'bip125-replaceable': 'no', details: [{ - account: member.name, - address: member.paths[0].address.toBase58(self.network), + account: member.path.name, + address: member.address.toBase58(self.network), category: receive ? 'receive' : 'send', amount: +utils.btc(member.value), - label: member.name, + label: member.path.name, vout: 0 }], - hex: tx.toRaw().toString('hex') + hex: details.tx.toRaw().toString('hex') }; callback(null, json); @@ -3199,32 +3198,31 @@ RPC.prototype._toListTX = function _toListTX(tx, callback) { var self = this; var receive, member, json; - this.walletdb.tx.getMap(tx, function(err, map) { + this.walletdb.tx.toDetails(this.wallet.id, tx, function(err, details) { if (err) return callback(err); - if (!map) + if (!details) return callback(new RPCError('TX not found.')); - receive = map.inputs.length === 0; - member = receive ? map.outputs[0] : map.inputs[0]; - assert(member); + receive = details.isReceive(); + member = details.getMember(); json = { - account: member.name, - address: member.paths[0].address.toBase58(self.network), + account: member.path.name, + address: member.address.toBase58(self.network), category: receive ? 'receive' : 'send', amount: +utils.btc(member.value), - label: member.name, + label: member.path.name, vout: 0, - confirmations: tx.getConfirmations(self.chain.height), - blockhash: tx.block ? utils.revHex(tx.block) : null, - blockindex: tx.index, - blocktime: tx.ts, - txid: tx.rhash, + confirmations: details.confirmations, + blockhash: details.block ? utils.revHex(details.block) : null, + blockindex: details.index, + blocktime: details.ts, + txid: utils.revHex(details.hash), walletconflicts: [], - time: tx.ps, - timereceived: tx.ps, + time: details.ps, + timereceived: details.ps, 'bip125-replaceable': 'no' }; diff --git a/lib/bcoin/http/server.js b/lib/bcoin/http/server.js index a785e6c4..88a12c3c 100644 --- a/lib/bcoin/http/server.js +++ b/lib/bcoin/http/server.js @@ -968,49 +968,36 @@ HTTPServer.prototype._initIO = function _initIO() { }); }); - this.walletdb.on('tx', function(tx, map) { - var summary = map.toJSON(); - tx = tx.toJSON(); - map.getWallets().forEach(function(id) { - self.server.io.to(id).emit('wallet tx', tx, summary); - }); - self.server.io.to('!all').emit('wallet tx', tx, summary); + this.walletdb.on('tx', function(id, tx, details) { + details = details.toJSON(); + self.server.io.to(id).emit('wallet tx', details); + self.server.io.to('!all').emit('wallet tx', id, details); }); - this.walletdb.on('confirmed', function(tx, map) { - var summary = map.toJSON(); - tx = tx.toJSON(); - map.getWallets().forEach(function(id) { - self.server.io.to(id).emit('wallet confirmed', tx, summary); - }); - self.server.io.to('!all').emit('wallet confirmed', tx, summary); + this.walletdb.on('confirmed', function(id, tx, details) { + details = details.toJSON(); + self.server.io.to(id).emit('wallet confirmed', details); + self.server.io.to('!all').emit('wallet confirmed', id, details); }); - this.walletdb.on('updated', function(tx, map) { - var summary = map.toJSON(); - tx = tx.toJSON(); - map.getWallets().forEach(function(id) { - self.server.io.to(id).emit('wallet updated', tx, summary); - }); - self.server.io.to('!all').emit('wallet updated', tx, summary); + this.walletdb.on('updated', function(id, tx, details) { + details = details.toJSON(); + self.server.io.to(id).emit('wallet updated', details); + self.server.io.to('!all').emit('wallet updated', id, details); }); - this.walletdb.on('balances', function(balances) { - var json = {}; - Object.keys(balances).forEach(function(id) { - json[id] = { - confirmed: utils.btc(balances[id].confirmed), - unconfirmed: utils.btc(balances[id].unconfirmed), - total: utils.btc(balances[id].total) - }; - self.server.io.to(id).emit('wallet balance', json[id], id); - self.server.io.to('!all').emit('wallet balance', json[id], id); - }); - self.server.io.to('!all').emit('wallet balances', json); + this.walletdb.on('balance', function(id, balance, details) { + balance = { + confirmed: utils.btc(balance.confirmed), + unconfirmed: utils.btc(balance.unconfirmed), + total: utils.btc(balance.total) + }; + self.server.io.to(id).emit('wallet balance', balance); + self.server.io.to('!all').emit('wallet balance', id, balance); }); - this.walletdb.on('address', function(receive, change, map) { - var summary = map.toJSON(); + this.walletdb.on('address', function(id, receive, change, details) { + details = details.toJSON(); receive = receive.map(function(address) { return address.toJSON(); @@ -1020,11 +1007,8 @@ HTTPServer.prototype._initIO = function _initIO() { return address.toJSON(); }); - map.getWallets().forEach(function(id) { - self.server.io.to(id).emit('wallet address', receive, change, summary); - }); - - self.server.io.to('!all').emit('wallet address', receive, change, summary); + self.server.io.to(id).emit('wallet address', receive, change, details); + self.server.io.to('!all').emit('wallet address', id, receive, change, details); }); }; diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index d1120f6b..a0b199c3 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -139,9 +139,9 @@ TXDB.prototype.testFilter = function testFilter(addresses) { * @param {Function} callback - Returns [Error, {@link WalletMap}]. */ -TXDB.prototype.getMap = function getMap(tx, callback) { +TXDB.prototype.getInfo = function getInfo(tx, callback) { var addresses = tx.getHashes('hex'); - var map; + var info; if (!this.testFilter(addresses)) return callback(); @@ -153,9 +153,9 @@ TXDB.prototype.getMap = function getMap(tx, callback) { if (!table) return callback(); - map = WalletMap.fromTX(table, tx); + info = PathInfo.fromTX(tx, table); - return callback(null, map); + return callback(null, info); }); }; @@ -411,29 +411,29 @@ TXDB.prototype.removeBlock = function removeBlock(block, callback, force) { TXDB.prototype.add = function add(tx, callback, force) { var self = this; - return this.getMap(tx, function(err, map) { + return this.getInfo(tx, function(err, info) { if (err) return callback(err); - if (!map) + if (!info) return callback(null, false); self.logger.info( - 'Incoming transaction for %d accounts.', - map.outputs.length); + 'Incoming transaction for %d addresses.', + info.paths.length); - self.logger.debug(map.outputs); + self.logger.debug(info.paths); - return self._add(tx, map, callback, force); + return self._add(tx, info, callback, force); }); }; -TXDB.prototype._add = function add(tx, map, callback, force) { +TXDB.prototype._add = function add(tx, info, callback, force) { var self = this; var updated = false; var batch, hash, i, j, unlock, path, paths, id; - unlock = this._lock(add, [tx, map, callback], force); + unlock = this._lock(add, [tx, info, callback], force); if (!unlock) return; @@ -444,13 +444,13 @@ TXDB.prototype._add = function add(tx, map, callback, force) { tx = tx.toTX(); // Attempt to confirm tx before adding it. - this._confirm(tx, map, function(err, existing) { + this._confirm(tx, info, function(err, existing) { if (err) return callback(err); // Ignore if we already have this tx. if (existing) - return callback(null, true, map); + return callback(null, true, info); hash = tx.hash('hex'); @@ -465,9 +465,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) { batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); - for (i = 0; i < map.accounts.length; i++) { - path = map.accounts[i]; - id = path.id + '/' + path.account; + for (i = 0; i < info.keys.length; i++) { + id = info.keys[i]; batch.put('T/' + id + '/' + hash, DUMMY); if (tx.ts === 0) batch.put('P/' + id + '/' + hash, DUMMY); @@ -485,9 +484,10 @@ TXDB.prototype._add = function add(tx, map, callback, force) { return next(); address = input.getHash('hex'); + paths = info.getPaths(address); - // Only add orphans if this input is ours. - if (!map.hasPaths(address)) + // Only bother if this input is ours. + if (!paths) return next(); self.getCoin(prevout.hash, prevout.index, function(err, coin) { @@ -510,8 +510,6 @@ TXDB.prototype._add = function add(tx, map, callback, force) { updated = true; - paths = map.getPaths(address); - for (j = 0; j < paths.length; j++) { path = paths[j]; id = path.id + '/' + path.account; @@ -546,23 +544,23 @@ TXDB.prototype._add = function add(tx, map, callback, force) { // Skip invalid transactions if (self.options.verify) { if (!tx.verifyInput(i)) - return callback(null, false, map); + return callback(null, false, info); } - return self._removeConflict(spent, tx, function(err, rtx, rmap) { + return self._removeConflict(spent, tx, function(err, rtx, rinfo) { if (err) return next(err); // Spender was not removed, the current // transaction is not elligible to be added. if (!rtx) - return callback(null, false, map); + return callback(null, false, info); - self.emit('conflict', rtx, rmap); + self.emit('conflict', rtx, rinfo); batch.clear(); - self._add(tx, map, callback, true); + self._add(tx, info, callback, true); }); }); } @@ -588,11 +586,13 @@ TXDB.prototype._add = function add(tx, map, callback, force) { var key = hash + '/' + i; var coin; - // Do not add unspents for outputs that aren't ours. - if (!map.hasPaths(address)) + if (output.script.isUnspendable()) return next(); - if (output.script.isUnspendable()) + paths = info.getPaths(address); + + // Do not add unspents for outputs that aren't ours. + if (!paths) return next(); coin = bcoin.coin.fromTX(tx, i); @@ -648,8 +648,6 @@ TXDB.prototype._add = function add(tx, map, callback, force) { return next(err); if (!orphans) { - paths = map.getPaths(address); - for (j = 0; j < paths.length; j++) { path = paths[j]; id = path.id + '/' + path.account; @@ -676,20 +674,20 @@ TXDB.prototype._add = function add(tx, map, callback, force) { if (err) return callback(err); - self.walletdb.handleTX(tx, map, function(err) { + self.walletdb.handleTX(tx, info, function(err) { if (err) return callback(err); - self.emit('tx', tx, map); + self.emit('tx', tx, info); if (updated) { if (tx.ts !== 0) - self.emit('confirmed', tx, map); + self.emit('confirmed', tx, info); - self.emit('updated', tx, map); + self.emit('updated', tx, info); } - return callback(null, true, map); + return callback(null, true, info); }); }); }); @@ -741,10 +739,10 @@ TXDB.prototype._removeConflict = function _removeConflict(hash, ref, callback) { return callback(); } - self._removeRecursive(tx, function(err, result, map) { + self._removeRecursive(tx, function(err, result, info) { if (err) return callback(err); - return callback(null, tx, map); + return callback(null, tx, info); }); }); }; @@ -831,17 +829,17 @@ TXDB.prototype.isSpent = function isSpent(hash, index, callback) { * Attempt to confirm a transaction. * @private * @param {TX} tx - * @param {AddressMap} map + * @param {AddressMap} info * @param {Function} callback - Returns [Error, Boolean]. `false` if * the transaction should be added to the database, `true` if the * transaction was confirmed, or should be ignored. */ -TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { +TXDB.prototype._confirm = function _confirm(tx, info, callback, force) { var self = this; var hash, batch, unlock, i, path, id; - unlock = this._lock(_confirm, [tx, map, callback], force); + unlock = this._lock(_confirm, [tx, info, callback], force); if (!unlock) return; @@ -856,16 +854,16 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { // Haven't seen this tx before, add it. if (!existing) - return callback(null, false, map); + return callback(null, false, info); // Existing tx is already confirmed. Ignore. if (existing.ts !== 0) - return callback(null, true, map); + return callback(null, true, info); // The incoming tx won't confirm the // existing one anyway. Ignore. if (tx.ts === 0) - return callback(null, true, map); + return callback(null, true, info); batch = self.db.batch(); @@ -878,9 +876,8 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { batch.del('p/' + hash); batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); - for (i = 0; i < map.accounts.length; i++) { - path = map.accounts[i]; - id = path.id + '/' + path.account; + 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); } @@ -890,8 +887,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { var key = hash + '/' + i; // Only update coins if this output is ours. - if (!map.hasPaths(address)) - return next(); + paths = info.getPaths(address); self.getCoin(hash, i, function(err, coin) { if (err) @@ -917,14 +913,14 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { if (err) return callback(err); - self.walletdb.syncOutputs(tx, map, function(err) { + self.walletdb.syncOutputs(tx, info, function(err) { if (err) return callback(err); - self.emit('confirmed', tx, map); - self.emit('tx', tx, map); + self.emit('confirmed', tx, info); + self.emit('tx', tx, info); - return callback(null, true, map); + return callback(null, true, info); }); }); }); @@ -952,14 +948,14 @@ TXDB.prototype.remove = function remove(hash, callback, force) { assert(tx.hash('hex') === hash); - return self.getMap(tx, function(err, map) { + return self.getInfo(tx, function(err, info) { if (err) return callback(err); - if (!map) + if (!info) return callback(null, false); - return self._remove(tx, map, callback, force); + return self._remove(tx, info, callback, force); }); }); }; @@ -975,14 +971,14 @@ TXDB.prototype.remove = function remove(hash, callback, force) { TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) { var self = this; - return this.getMap(tx, function(err, map) { + return this.getInfo(tx, function(err, info) { if (err) return callback(err); - if (!map) + if (!info) return callback(null, false); - return self._remove(tx, map, callback, force); + return self._remove(tx, info, callback, force); }); }; @@ -990,16 +986,16 @@ TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) { * Remove a transaction from the database. Disconnect inputs. * @private * @param {TX} tx - * @param {AddressMap} map + * @param {AddressMap} info * @param {Function} callback - Returns [Error]. */ -TXDB.prototype._remove = function remove(tx, map, callback, force) { +TXDB.prototype._remove = function remove(tx, info, callback, force) { var self = this; var unlock, hash, batch, i, j, path, id; var key, paths, address, input, output, coin; - unlock = this._lock(remove, [tx, map, callback], force); + unlock = this._lock(remove, [tx, info, callback], force); if (!unlock) return; @@ -1019,9 +1015,8 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { batch.del('m/' + pad32(tx.ps) + '/' + hash); - for (i = 0; i < map.accounts.length; i++) { - path = map.accounts[i]; - id = path.id + '/' + path.account; + for (i = 0; i < info.keys.length; i++) { + id = info.keys[i]; batch.del('T/' + id + '/' + hash); if (tx.ts === 0) batch.del('P/' + id + '/' + hash); @@ -1045,10 +1040,10 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { if (!input.coin) continue; - if (!map.hasPaths(address)) - continue; + paths = info.getPaths(address); - paths = map.getPaths(address); + if (!paths) + continue; for (j = 0; j < paths.length; j++) { path = paths[j]; @@ -1070,13 +1065,13 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { key = hash + '/' + i; address = output.getHash('hex'); - if (!map.hasPaths(address)) - continue; - if (output.script.isUnspendable()) continue; - paths = map.getPaths(address); + paths = info.getPaths(address); + + if (!paths) + continue; for (j = 0; j < paths.length; j++) { path = paths[j]; @@ -1093,9 +1088,9 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { if (err) return callback(err); - self.emit('remove tx', tx, map); + self.emit('remove tx', tx, info); - return callback(null, true, map); + return callback(null, true, info); }); }); }; @@ -1123,14 +1118,14 @@ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { assert(tx.hash('hex') === hash); - return self.getMap(tx, function(err, map) { + return self.getInfo(tx, function(err, info) { if (err) return callback(err); - if (!map) + if (!info) return callback(null, false); - return self._unconfirm(tx, map, callback, force); + return self._unconfirm(tx, info, callback, force); }); }); }; @@ -1138,15 +1133,15 @@ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { /** * Unconfirm a transaction. This is usually necessary after a reorg. * @param {Hash} hash - * @param {AddressMap} map + * @param {AddressMap} info * @param {Function} callback */ -TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { +TXDB.prototype._unconfirm = function unconfirm(tx, info, callback, force) { var self = this; var batch, unlock, hash, height, i, path, id; - unlock = this._lock(unconfirm, [tx, map, callback], force); + unlock = this._lock(unconfirm, [tx, info, callback], force); if (!unlock) return; @@ -1159,7 +1154,7 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { batch = this.db.batch(); if (height !== -1) - return callback(null, false, map); + return callback(null, false, info); tx.height = -1; tx.ts = 0; @@ -1171,9 +1166,8 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { batch.put('p/' + hash, DUMMY); batch.del('h/' + pad32(height) + '/' + hash); - for (i = 0; i < map.accounts.length; i++) { - path = map.accounts[i]; - id = path.id + '/' + path.account; + 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); } @@ -1204,9 +1198,9 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { if (err) return callback(err); - self.emit('unconfirmed', tx, map); + self.emit('unconfirmed', tx, info); - return callback(null, true, map); + return callback(null, true, info); }); }); }; @@ -1666,6 +1660,73 @@ TXDB.prototype.getTX = function getTX(hash, callback) { }, 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) { + var self = this; + this.getTX(hash, function(err, tx) { + if (err) + return callback(err); + + if (!tx) + return callback(); + + self.toDetails(id, 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) { + var self = this; + var out; + + if (Array.isArray(tx)) { + out = []; + utils.forEachSerial(tx, function(tx, next) { + self.toDetails(tx, function(err, details) { + if (err) + return next(err); + + if (!details) + return next(); + + out.push(details); + next(); + }); + }, function(err) { + if (err) + return callback(err); + return callback(null, out); + }); + } + + this.fillHistory(tx, function(err) { + if (err) + return callback(err); + + self.getInfo(tx, function(err, info) { + if (err) + return callback(err); + + if (!info) + return callback(); + + return callback(null, info.toDetails(id)); + }); + }); +}; + /** * Test whether the database has a transaction. * @param {Hash} hash @@ -1846,494 +1907,6 @@ TXDB.prototype.zap = function zap(id, age, callback, force) { }); }; -/* - * Address->Wallet Mapping - */ - -// Each address can potentially map to multiple -// accounts and wallets due to the fact that -// multisig accounts can have shared addresses. -// An address could map to 2 accounts on different -// wallets, or 2 accounts on the same wallet! -// In summary, bitcoin is hard. Use Bobchain instead. -// -// Table: -// [address-hash] -> [array of Path objects] -// '1edc6b6858fd12c64b26d8bd1e0e50d44b5bafb9': -// [Path { -// id: 'WLTZ3f5mMBsgWr1TcLzAdtLD8pkLcmWuBfPt', -// name: 'default', -// account: 0, -// change: 0, -// index: 0 -// }] -// - -/** - * WalletMap - * @constructor - * @private - * @property {WalletMember[]} inputs - * @property {WalletMember[]} outputs - * @property {WalletMember[]} accounts - * @property {Object} table - */ - -function WalletMap() { - if (!(this instanceof WalletMap)) - return new WalletMap(); - - this.inputs = []; - this.outputs = []; - this.accounts = []; - this.table = {}; -} - -/** - * Inject properties from table and tx. - * @private - * @param {Object} table - * @param {TX} tx - */ - -WalletMap.prototype.fromTX = function fromTX(table, tx) { - var i, members, input, output, key; - - // This is a scary function, but what it is - // designed to do is uniqify inputs and - // outputs by account. This is easier said - // than done due to two facts: transactions - // can have multiple outputs with the same - // address, and wallets can have multiple - // accounts with the same address. On top - // of that, it will calculate the total - // value sent to or received from each - // account. - - function insert(vector, target) { - var i, io, hash, members, member; - var j, paths, path, key, address, hashes; - - // Keeps track of unique addresses. - hashes = {}; - - // Maps address hashes to members. - members = {}; - - for (i = 0; i < vector.length; i++) { - io = vector[i]; - address = io.getAddress(); - - if (!address) - continue; - - hash = address.getHash('hex'); - - // Get all paths for this address. - paths = table[hash]; - - for (j = 0; j < paths.length; j++) { - path = paths[j]; - key = path.toKey(); - member = members[key]; - - // We no doubt already created a member - // for this account, and not only that, - // we're guaranteed to be on a different - // input/output due to the fact that we - // add the address hash after this loop - // completes. Now we can update the value. - if (hashes[hash]) { - assert(member); - if (io.coin) - member.value += io.coin.value; - else if (io.value) - member.value += io.value; - continue; - } - - // Already have a member for this account. - // i.e. Different address, but same account. - if (member) { - // Increment value. - if (io.coin) - member.value += io.coin.value; - else if (io.value) - member.value += io.value; - - // Set address and add path. - path.address = address; - member.paths.push(path); - - continue; - } - - // Create a member for this account. - assert(!member); - member = MapMember.fromPath(path); - - // Set the _initial_ value. - if (io.coin) - member.value = io.coin.value; - else if (io.value) - member.value = io.value; - - // Add the address to the path object - // and push onto the member's paths. - // We only do this during instantiation, - // since paths are just as unique as - // addresses. - path.address = address; - member.paths.push(path); - - // Remember it by wallet id / account - // name so we can update the value later. - members[key] = member; - - // Push onto _our_ input/output array. - target.push(member); - } - - // Update this guy last so the above if - // clause does not return true while - // we're still iterating over paths. - if (paths.length > 0) - hashes[hash] = true; - } - } - - // Finally, we convert both inputs - // and outputs to map members. - insert(tx.inputs, this.inputs); - insert(tx.outputs, this.outputs); - - // Combine both input and output map - // members and uniqify them by account. - members = {}; - - for (i = 0; i < this.inputs.length; i++) { - input = this.inputs[i]; - key = input.toKey(); - if (!members[key]) { - members[key] = true; - this.accounts.push(input); - } - } - - for (i = 0; i < this.outputs.length; i++) { - output = this.outputs[i]; - key = output.toKey(); - if (!members[key]) { - members[key] = true; - this.accounts.push(output); - } - } - - this.table = table; - - return this; -}; - -/** - * Instantiate wallet map from tx. - * @param {Object} table - * @param {TX} tx - * @returns {WalletMap} - */ - -WalletMap.fromTX = function fromTX(table, tx) { - return new WalletMap().fromTX(table, tx); -}; - -/** - * Test whether the map has paths - * for a given address hash. - * @param {Hash} address - * @returns {Boolean} - */ - -WalletMap.prototype.hasPaths = function hasPaths(address) { - var paths; - - if (!address) - return false; - - paths = this.table[address]; - - return paths && paths.length !== 0; -}; - -/** - * Return a unique list of wallet IDs for the map. - * @returns {WalletID[]} - */ - -WalletMap.prototype.getWallets = function getWallets() { - var ids = {}; - var i, member; - - for (i = 0; i < this.accounts.length; i++) { - member = this.accounts[i]; - ids[member.id] = true; - } - - return Object.keys(ids); -}; - -/** - * Return a unique list of wallet IDs for the map. - * @returns {WalletID[]} - */ - -WalletMap.prototype.getInputWallets = function getInputWallets() { - var ids = {}; - var i, member; - - for (i = 0; i < this.inputs.length; i++) { - member = this.inputs[i]; - ids[member.id] = true; - } - - return Object.keys(ids); -}; - - -/** - * Return a unique list of wallet IDs for the map. - * @returns {WalletID[]} - */ - -WalletMap.prototype.getOutputWallets = function getOutputWallets() { - var ids = {}; - var i, member; - - for (i = 0; i < this.outputs.length; i++) { - member = this.outputs[i]; - ids[member.id] = true; - } - - return Object.keys(ids); -}; - -/** - * Get paths for a given address hash. - * @param {Hash} address - * @returns {Path[]|null} - */ - -WalletMap.prototype.getPaths = function getPaths(address) { - return this.table[address]; -}; - -/** - * Convert the map to a json-friendly object. - * @returns {Object} - */ - -WalletMap.prototype.toJSON = function toJSON() { - return { - inputs: this.inputs.map(function(input) { - return input.toJSON(); - }), - outputs: this.outputs.map(function(output) { - return output.toJSON(); - }) - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} - */ - -WalletMap.prototype.fromJSON = function fromJSON(json) { - var i, j, table, input, output, path; - var hash, paths, hashes, accounts, values, key; - - table = {}; - accounts = {}; - - for (i = 0; i < json.inputs.length; i++) { - input = json.inputs[i]; - input = MapMember.fromJSON(input); - this.inputs.push(input); - key = input.toKey(); - if (!accounts[key]) { - accounts[key] = true; - this.accounts.push(input); - } - for (j = 0; j < input.paths.length; j++) { - path = input.paths[j]; - path.id = input.id; - path.name = input.name; - path.account = input.account; - hash = path.address.getHash('hex'); - if (!table[hash]) - table[hash] = []; - table[hash].push(path); - } - } - - for (i = 0; i < json.outputs.length; i++) { - output = json.outputs[i]; - output = MapMember.fromJSON(output); - this.outputs.push(output); - key = output.toKey(); - if (!accounts[key]) { - accounts[key] = true; - this.accounts.push(output); - } - for (j = 0; j < output.paths.length; j++) { - path = output.paths[j]; - path.id = output.id; - path.name = output.name; - path.account = output.account; - hash = path.address.getHash('hex'); - if (!table[hash]) - table[hash] = []; - table[hash].push(path); - } - } - - // We need to rebuild to address->paths table. - hashes = Object.keys(table); - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - paths = table[hash]; - values = []; - accounts = {}; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - key = path.toKey(); - if (!accounts[key]) { - accounts[key] = true; - values.push(path); - } - } - table[hash] = values; - } - - this.table = table; - - return this; -}; - -/** - * Instantiate map from json object. - * @param {Object} - * @returns {WalletMap} - */ - -WalletMap.fromJSON = function fromJSON(json) { - return new WalletMap().fromJSON(json); -}; - -/** - * MapMember - * @constructor - * @private - * @property {WalletID} id - * @property {String} name - Account name. - * @property {Number} account - Account index. - * @property {Path[]} paths - * @property {Amount} value - */ - -function MapMember() { - if (!(this instanceof MapMember)) - return new MapMember(); - - this.id = null; - this.name = null; - this.account = 0; - this.paths = []; - this.value = 0; -} - -/** - * Convert member to a key in the form of (id|account). - * @returns {String} - */ - -MapMember.prototype.toKey = function toKey() { - return this.id + '/' + this.account; -}; - -/** - * Convert the member to a json-friendly object. - * @returns {Object} - */ - -MapMember.prototype.toJSON = function toJSON() { - return { - id: this.id, - name: this.name, - account: this.account, - paths: this.paths.map(function(path) { - return path.toCompact(); - }), - value: utils.btc(this.value) - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -MapMember.prototype.fromJSON = function fromJSON(json) { - var i, path; - - this.id = json.id; - this.name = json.name; - this.account = json.account; - - for (i = 0; i < json.paths.length; i++) { - path = json.paths[i]; - this.paths.push(bcoin.path.fromCompact(path)); - } - - this.value = utils.satoshi(json.value); - - return this; -}; - -/** - * Instantiate member from json object. - * @param {Object} json - * @returns {MapMember} - */ - -MapMember.fromJSON = function fromJSON(json) { - return new MapMember().fromJSON(json); -}; - -/** - * Inject properties from path. - * @private - * @param {Path} path - */ - -MapMember.prototype.fromPath = function fromPath(path) { - this.id = path.id; - this.name = path.name; - this.account = path.account; - return this; -}; - -/** - * Instantiate member from path. - * @param {Path} path - * @returns {MapMember} - */ - -MapMember.fromPath = function fromPath(path) { - return new MapMember().fromPath(path); -}; - function PathInfo(tx, table) { // All relevant Wallet-ID/Accounts for // inputs and outputs (for database indexing). @@ -2342,11 +1915,17 @@ function PathInfo(tx, table) { // All output paths (for deriving during sync). this.paths = []; - // All output wallet IDs (for balance & syncing). + // All wallet IDs (for balance & syncing). this.wallets = []; // Map of address hashes->paths (for everything). - this.table = {}; + this.table = null; + + // Current transaction. + this.tx = null; + + // Wallet-specific details cache. + this._cache = {}; if (tx) this.fromTX(tx, table); @@ -2355,9 +1934,11 @@ function PathInfo(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++) { @@ -2367,12 +1948,13 @@ PathInfo.prototype.fromTX = function fromTX(tx, table) { path = paths[j]; key = path.id + '/' + path.account; keys[key] = true; + wallets[path.id] = true; } } this.keys = Object.keys(keys); + this.wallets = Object.keys(wallets); - wallets = {}; hashes = tx.getOutputHashes('hex'); for (i = 0; i < hashes.length; i++) { @@ -2381,11 +1963,10 @@ PathInfo.prototype.fromTX = function fromTX(tx, table) { for (j = 0; j < paths.length; j++) { path = paths[j]; this.paths.push(path); - wallets[path.id] = true; } } - this.wallets = Object.keys(wallets); + return this; }; PathInfo.fromTX = function fromTX(tx, table) { @@ -2430,7 +2011,23 @@ PathInfo.prototype.getPaths = function getPaths(address) { return paths; }; -function Details(tx, id, table) { +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'); this.height = tx.height; this.block = tx.block; @@ -2442,12 +2039,15 @@ function Details(tx, id, table) { this.tx = tx; this.inputs = []; this.outputs = []; - - if (id) - this.init(id, table); + this.init(table); } -Details.prototype._insert = function _insert(vector, target, id, table) { +Details.prototype.init = function init(table) { + this._insert(this.tx.inputs, this.inputs, table); + this._insert(this.tx.outputs, this.outputs, table); +}; + +Details.prototype._insert = function _insert(vector, target, table) { var i, j, io, address, hash, paths, path, member; for (i = 0; i < vector.length; i++) { @@ -2469,7 +2069,7 @@ Details.prototype._insert = function _insert(vector, target, id, table) { for (j = 0; j < paths.length; j++) { path = paths[j]; - if (path.id === id) { + if (path.id === this.id) { member.path = path; break; } @@ -2480,13 +2080,9 @@ Details.prototype._insert = function _insert(vector, target, id, table) { } }; -Details.prototype.init = function init(id, table) { - this._insert(this.tx.inputs, this.inputs, id, table); - this._insert(this.tx.outputs, this.outputs, id, table); -}; - Details.prototype.toJSON = function toJSON() { return { + id: this.id, hash: utils.revHex(this.hash), height: this.height, block: this.block ? utils.revHex(this.block) : null, @@ -2505,6 +2101,61 @@ Details.prototype.toJSON = function toJSON() { }; }; +Details.prototype.isReceive = function isReceive() { + var i, input; + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + if (!input.path) + continue; + if (input.path.id === this.id) + return false; + } + + return true; +}; + +Details.prototype.getValue = function getValue() { + var value = 0; + var i, vector, member; + + if (this.isReceive()) + vector = this.outputs; + else + vector = this.inputs; + + for (i = 0; i < this.outputs.length; i++) { + member = this.outputs[i]; + if (!member.path) + continue; + if (member.path.id !== this.id) + continue; + value += member.value; + } + + return value; +}; + +Details.prototype.getMember = function getMember() { + var i, vector, member; + + if (this.isReceive()) + vector = this.outputs; + else + vector = this.inputs; + + for (i = 0; i < this.outputs.length; i++) { + member = this.outputs[i]; + if (!member.path) + continue; + if (member.path.id !== this.id) + continue; + return member; + } + + assert(false); +}; + function DetailsMember() { this.value = 0; this.address = null; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 52c22117..143a9e02 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -1030,18 +1030,19 @@ 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} map + * @param {WalletMap} info * @param {Function} callback - Returns [Errr, Boolean] * (true if new addresses were allocated). */ -Wallet.prototype.syncOutputDepth = function syncOutputDepth(map, callback) { +Wallet.prototype.syncOutputDepth = function syncOutputDepth(info, callback) { var self = this; var change = []; var receive = []; + var accounts = {}; var i, path, unlock; - unlock = this.writeLock.lock(syncOutputDepth, [map, callback]); + unlock = this.writeLock.lock(syncOutputDepth, [info, callback]); if (!unlock) return; @@ -1050,8 +1051,22 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(map, callback) { this.start(); - utils.forEachSerial(map.outputs, function(output, next) { - var paths = output.paths; + 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] = []; + + accounts[path.account].push(path); + } + + accounts = utils.values(accounts); + + utils.forEachSerial(accounts, function(paths, next) { + var account = paths[0].account; var receiveDepth = -1; var changeDepth = -1; @@ -1070,7 +1085,7 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(map, callback) { receiveDepth += 2; changeDepth += 2; - self.getAccount(output.account, function(err, account) { + self.getAccount(account, function(err, account) { if (err) return next(err); diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 7786dbc3..4a545ed1 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -101,35 +101,35 @@ WalletDB.prototype._init = function _init() { }); } - function handleEvent(event, tx, map) { - var i, path; + function handleEvent(event, tx, info) { + var i, id, details; - self.emit(event, tx, map); - - for (i = 0; i < map.accounts.length; i++) { - path = map.accounts[i]; - self.fire(path.id, event, tx, path.name); + 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, map) { - handleEvent('tx', tx, map); + this.tx.on('tx', function(tx, info) { + handleEvent('tx', tx, info); }); - this.tx.on('conflict', function(tx, map) { - handleEvent('conflict', tx, map); + this.tx.on('conflict', function(tx, info) { + handleEvent('conflict', tx, info); }); - this.tx.on('confirmed', function(tx, map) { - handleEvent('confirmed', tx, map); + this.tx.on('confirmed', function(tx, info) { + handleEvent('confirmed', tx, info); }); - this.tx.on('unconfirmed', function(tx, map) { - handleEvent('unconfirmed', tx, map); + this.tx.on('unconfirmed', function(tx, info) { + handleEvent('unconfirmed', tx, info); }); - this.tx.on('updated', function(tx, map) { - handleEvent('updated', tx, map); + this.tx.on('updated', function(tx, info) { + handleEvent('updated', tx, info); }); }; @@ -248,16 +248,15 @@ WalletDB.prototype.commit = function commit(id, callback) { * Emit balance events after a tx is saved. * @private * @param {TX} tx - * @param {WalletMap} map + * @param {WalletMap} info * @param {Function} callback */ -WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { +WalletDB.prototype.updateBalances = function updateBalances(tx, info, callback) { var self = this; - var balances = {}; - var i, id, keys; + var details; - utils.forEachSerial(map.getOutputWallets(), function(id, next) { + utils.forEachSerial(info.wallets, function(id, next) { if (self.listeners('balances').length === 0 && !self.hasListener(id, 'balance')) { return next(); @@ -267,44 +266,34 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { if (err) return next(err); - balances[id] = balance; + details = info.toDetails(id); + self.emit('balance', id, balance, details); + self.fire(id, 'balance', balance, details); next(); }); - }, function(err) { - if (err) - return callback(err); - - keys = Object.keys(balances); - - for (i = 0; i < keys.length; i++) { - id = keys[i]; - self.fire(id, 'balance', balances[id]); - } - - self.emit('balances', balances, map); - - return callback(null, balances); - }); + }, callback); }; /** * Derive new addresses after a tx is saved. * @private * @param {TX} tx - * @param {WalletMap} map + * @param {WalletMap} info * @param {Function} callback */ -WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) { +WalletDB.prototype.syncOutputs = function syncOutputs(tx, info, callback) { var self = this; + var details; - utils.forEachSerial(map.getOutputWallets(), function(id, next) { - self.syncOutputDepth(id, map, function(err, receive, change) { + utils.forEachSerial(info.wallets, function(id, next) { + self.syncOutputDepth(id, info, function(err, receive, change) { if (err) return next(err); - self.fire(id, 'address', receive, change); - self.emit('address', receive, change, map); + details = info.toDetails(id); + self.emit('address', id, receive, change, details); + self.fire(id, 'address', receive, change, details); next(); }); }, callback); @@ -314,17 +303,17 @@ WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) { * Derive new addresses and emit balance. * @private * @param {TX} tx - * @param {WalletMap} map + * @param {WalletMap} info * @param {Function} callback */ -WalletDB.prototype.handleTX = function handleTX(tx, map, callback) { +WalletDB.prototype.handleTX = function handleTX(tx, info, callback) { var self = this; - this.syncOutputs(tx, map, function(err) { + this.syncOutputs(tx, info, function(err) { if (err) return callback(err); - self.updateBalances(tx, map, callback); + self.updateBalances(tx, info, callback); }); }; @@ -1366,9 +1355,9 @@ WalletDB.prototype.fetchWallet = function fetchWallet(id, callback, handler) { }); }; -WalletDB.prototype.syncOutputDepth = function syncOutputDepth(id, map, callback) { +WalletDB.prototype.syncOutputDepth = function syncOutputDepth(id, info, callback) { this.fetchWallet(id, callback, function(wallet, callback) { - wallet.syncOutputDepth(map, callback); + wallet.syncOutputDepth(info, callback); }); }; @@ -1591,6 +1580,7 @@ Path.prototype.toJSON = function toJSON() { return { id: this.id, name: this.name, + change: this.change === 1, path: this.toPath() }; }; @@ -1636,48 +1626,6 @@ Path.prototype.toKey = function toKey() { return this.id + '/' + this.account; }; -/** - * Convert path to a compact json object. - * @returns {Object} - */ - -Path.prototype.toCompact = function toCompact() { - return { - path: 'm/' + this.change + '/' + this.index, - address: this.address ? this.address.toBase58() : null - }; -}; - -/** - * Inject properties from compact json object. - * @private - * @param {Object} json - */ - -Path.prototype.fromCompact = function fromCompact(json) { - var indexes = bcoin.hd.parsePath(json.path, constants.hd.MAX_INDEX); - - assert(indexes.length === 2); - - this.change = indexes[0]; - this.index = indexes[1]; - this.address = json.address - ? bcoin.address.fromBase58(json.address) - : null; - - return this; -}; - -/** - * Instantiate path from compact json object. - * @param {Object} json - * @returns {Path} - */ - -Path.fromCompact = function fromCompact(json) { - return new Path().fromCompact(json); -}; - /** * Inspect the path. * @returns {String}