diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index f850cfee..c1aea19e 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -113,7 +113,7 @@ TXDB.prototype._testFilter = function _testFilter(addresses) { /** * Map a transactions' addresses to wallet IDs. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link AddressMap}]. + * @param {Function} callback - Returns [Error, {@link WalletMap}]. */ TXDB.prototype.getMap = function getMap(tx, callback) { @@ -133,8 +133,6 @@ TXDB.prototype.getMap = function getMap(tx, callback) { map = WalletMap.fromTX(table, tx); - utils.print(map.toJSON()); - return callback(null, map); }); }; @@ -1667,88 +1665,140 @@ TXDB.prototype.zap = function zap(id, age, callback, force) { // }] // -// What we need: -// Table: Above. -// All: uniqified paths by id/name -// Outputs: Technically uniqified by ID, but id/name works too. - -// What they need (api): -// outputs: [ -// // Sum of value: -// { value: 0, id: wallet-id, name: account, index: account } -// ] - /** * WalletMap * @constructor * @private */ -function WalletMap(table) { +function WalletMap() { + if (!(this instanceof WalletMap)) + return new WalletMap(); + this.inputs = []; this.outputs = []; - this.paths = []; - this.accounts = null; - this.wallets = []; + this.accounts = []; this.table = null; } WalletMap.prototype.fromTX = function fromTX(table, tx) { - var keys = Object.keys(table); - var i, input, output, hash, members, member; - var j, key, paths, path; + var i, members, input, output, key; - // Flatten paths and push all of - // them onto the `paths` array. - for (i = 0; i < keys.length; i++) { - key = keys[i]; - paths = table[key]; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - this.wallets.push(path.id); - this.paths.push(path); + // 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; + } + + // 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. + 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.wallets = utils.uniq(this.wallets); - this.accounts = uniq(this.paths); this.table = table; - members = {}; - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - hash = input.getHash('hex'); - if (!hash) - continue; - member = members[hash]; - if (!member) { - member = MapMember.fromMember(table, input); - members[hash] = member; - this.inputs.push(member); - continue; - } - if (input.coin) - member.value += input.coin.value; - } - - members = {}; - - for (i = 0; i < tx.outputs.length; i++) { - output = tx.outputs[i]; - hash = output.getHash('hex'); - member = members[hash]; - if (!hash) - continue; - if (!member) { - member = MapMember.fromMember(table, output); - members[hash] = member; - this.outputs.push(member); - continue; - } - member.value += output.value; - } - return this; }; @@ -1779,58 +1829,82 @@ WalletMap.prototype.toJSON = function toJSON() { outputs: this.outputs.map(function(output) { return output.toJSON(); }), - paths: this.paths.map(function(path) { - return path.toJSON(); - }), accounts: this.accounts.map(function(path) { - return path.toAccount(); - }), - wallets: this.wallets + return path.toKey(); + }) }; }; WalletMap.prototype.fromJSON = function fromJSON(json) { var table = {}; - var i, account, path, input, output, hash; + var i, j, account, input, output, path; + var hash, paths, hashes, accounts, values, key; for (i = 0; i < json.inputs.length; i++) { input = json.inputs[i]; input = MapMember.fromJSON(input); - hash = input.getHash('hex'); - table[hash] = input.paths; this.inputs.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); - hash = output.getHash('hex'); - if (!table[hash]) - table[hash] = output.paths; this.outputs.push(output); - } - - for (i = 0; i < json.paths.length; i++) { - path = json.paths[i]; - this.paths.push(bcoin.path.fromJSON(path)); + 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); + } } for (i = 0; i < json.accounts.length; i++) { account = json.accounts[i]; - this.accounts.push(bcoin.path.fromAccount(account)); + this.accounts.push(bcoin.path.fromKey(account)); + } + + // 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.wallets = json.wallets; this.table = table; return this; }; WalletMap.fromJSON = function fromJSON(json) { - return new MapMember({}).fromJSON(json); + return new WalletMap().fromJSON(json); }; - /** * MapMember * @constructor @@ -1838,49 +1912,45 @@ WalletMap.fromJSON = function fromJSON(json) { */ function MapMember() { - this.value = 0; - this.address = null; + if (!(this instanceof MapMember)) + return new MapMember(); + + this.id = null; + this.name = null; + this.account = 0; this.paths = []; - this.accounts = []; - this.wallets = []; + this.value = 0; } +MapMember.prototype.toKey = function toKey() { + return this.id + '/' + this.name + ':' + this.account; +}; + MapMember.prototype.toJSON = function toJSON() { return { - value: utils.btc(this.value), - address: this.address - ? this.address.toBase58() - : null, - hash: this.address - ? this.address.getHash('hex') - : null, + id: this.id, + name: this.name, + account: this.account, paths: this.paths.map(function(path) { - return path.toJSON(); + return path.toCompact(); }), - accounts: this.accounts.map(function(path) { - return path.toAccount(); - }), - wallets: this.wallets + value: utils.btc(this.value) }; }; MapMember.prototype.fromJSON = function fromJSON(json) { var i, account, path; - this.value = utils.satoshi(json.value); - this.address = bcoin.address.fromBase58(json.address); + 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.fromJSON(path)); + this.paths.push(bcoin.path.fromCompact(path)); } - for (i = 0; i < json.accounts.length; i++) { - account = json.accounts[i]; - this.accounts.push(bcoin.path.fromAccount(account)); - } - - this.wallets = json.wallets; + this.value = utils.satoshi(json.value); return this; }; @@ -1889,57 +1959,16 @@ MapMember.fromJSON = function fromJSON(json) { return new MapMember().fromJSON(json); }; -MapMember.fromMember = function fromMember(table, io) { - var address = io.getAddress(); - var member = new MapMember(); - var i, paths; - - if (io instanceof bcoin.input) - member.value = io.coin ? io.coin.value : 0; - else - member.value = io.value; - - if (!address) - return member; - - paths = table[address.getHash('hex')]; - - assert(paths); - - member.address = address; - member.paths = paths; - - for (i = 0; i < paths.length; i++) - member.wallets.push(paths[i].id); - - member.accounts = uniq(member.paths); - member.wallets = utils.uniq(member.wallets); - - return member; +MapMember.prototype.fromPath = function fromPath(path) { + this.id = path.id; + this.name = path.name; + this.account = path.account; + return this; }; -/* - * Helpers - */ - -function uniq(obj) { - var uniq = {}; - var out = []; - var i, key, value; - - for (i = 0; i < obj.length; i++) { - value = obj[i]; - key = value.id + '/' + value.account; - - if (uniq[key]) - continue; - - uniq[key] = true; - out.push(value); - } - - return out; -} +MapMember.fromPath = function fromPath(path) { + return new MapMember().fromPath(path); +}; /* * Expose diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 400948b0..1c5b9fa4 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -187,27 +187,22 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { var balances = {}; utils.forEachSerial(map.outputs, function(output, next) { - utils.forEachSerial(output.wallets, function(id, next) { - if (self.listeners('balance').length === 0 - && !self.hasListener(id, 'balance')) { - return next(); - } + var id = output.id; - if (balances[id] != null) - return next(); + if (self.listeners('balance').length === 0 + && !self.hasListener(id, 'balance')) { + return next(); + } - self.getBalance(id, function(err, balance) { - if (err) - return next(err); + if (balances[id] != null) + return next(); - balances[id] = balance; - - next(); - }); - }, function(err) { + self.getBalance(id, function(err, balance) { if (err) return next(err); + balances[id] = balance; + next(); }); }, function(err) { @@ -221,18 +216,12 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) { var self = this; utils.forEachSerial(map.outputs, function(output, next) { - utils.forEachSerial(output.wallets, function(id, next) { - self.syncOutputDepth(id, tx, function(err, receive, change) { - if (err) - return next(err); - self.fire(id, 'address', receive, change); - self.emit('address', receive, change, map); - next(); - }); - }, function(err) { + var id = output.id; + self.syncOutputDepth(id, tx, function(err, receive, change) { if (err) return next(err); - + self.fire(id, 'address', receive, change); + self.emit('address', receive, change, map); next(); }); }, callback); @@ -1164,12 +1153,15 @@ WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) { */ function Path() { + if (!(this instanceof Path)) + return new Path(); + this.id = null; this.name = null; this.account = 0; this.change = 0; this.index = 0; - this.hash = null; + this.address = null; } Path.prototype.fromRaw = function fromRaw(data) { @@ -1186,6 +1178,21 @@ Path.fromRaw = function fromRaw(data) { return new Path().fromRaw(data); }; +Path.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + + p.writeVarString(this.id, 'utf8'); + p.writeVarString(this.name, 'utf8'); + p.writeU32(this.account); + p.writeU32(this.change); + p.writeU32(this.index); + + if (!writer) + p = p.render(); + + return p; +}; + Path.prototype.fromKeyRing = function fromKeyRing(id, address) { this.id = id; this.name = address.name; @@ -1212,14 +1219,6 @@ Path.prototype.inspect = function() { + '>'; }; -Path.prototype.toAccount = function toAccount() { - return { - id: this.id, - name: this.name, - account: this.account - }; -}; - Path.prototype.toJSON = function toJSON() { return { id: this.id, @@ -1228,19 +1227,6 @@ Path.prototype.toJSON = function toJSON() { }; }; -Path.prototype.fromAccount = function fromAccount(json) { - this.id = json.id; - this.name = json.name; - this.account = json.account; - this.change = 0; - this.index = 0; - return this; -}; - -Path.fromAccount = function fromAccount(json) { - return new Path().fromAccount(json); -}; - Path.prototype.fromJSON = function fromJSON(json) { var indexes = bcoin.hd.parsePath(json.path, constants.hd.MAX_INDEX); @@ -1261,19 +1247,46 @@ Path.fromJSON = function fromJSON(json) { return new Path().fromJSON(json); }; -Path.prototype.toRaw = function toRaw(writer) { - var p = new BufferWriter(writer); +Path.prototype.toKey = function toKey() { + return this.id + '/' + this.name + ':' + this.account; +}; - p.writeVarString(this.id, 'utf8'); - p.writeVarString(this.name, 'utf8'); - p.writeU32(this.account); - p.writeU32(this.change); - p.writeU32(this.index); +Path.prototype.fromKey = function fromKey(key) { + var parts = key.split('/'); + this.id = parts[0]; + parts = parts[1].split(':'); + this.name = parts[0]; + this.account = +parts[1]; + return this; +}; - if (!writer) - p = p.render(); +Path.fromKey = function fromKey(json) { + return new Path().fromKey(key); +}; - return p; +Path.prototype.toCompact = function toCompact() { + return { + path: 'm/' + this.change + '/' + this.index, + address: this.address ? this.address.toBase58() : null + }; +}; + +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; +}; + +Path.fromCompact = function fromCompact(json) { + return new Path().fromCompact(json); }; /*