diff --git a/lib/bcoin/network.js b/lib/bcoin/network.js index 2bfabdf1..03d81313 100644 --- a/lib/bcoin/network.js +++ b/lib/bcoin/network.js @@ -79,6 +79,12 @@ function Network(options) { Network.primary = null; +Network.main = null; +Network.testnet = null; +Network.regtest = null; +Network.segnet3 = null; +Network.segnet4 = null; + /** * Update the height of the network. * @param {Number} height diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 63cebd4b..cdb9742b 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -110,6 +110,131 @@ TXDB.prototype._testFilter = function _testFilter(addresses) { return false; }; +// 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 +// }] +// + +// 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: [ - SAME AS OUTPUTS ABOVE!!! +// // Sum of value: +// { value: 0, id: wallet-id, name: account, index: account } +// ] + +function WalletMap(table) { + var i, j, keys, key, paths, path; + + this.inputs = []; + this.outputs = []; + this.paths = []; + this.accounts = null; + this.wallets = []; + this.table = null; + + keys = Object.keys(table); + + // 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.wallets = utils.uniq(this.wallets); + this.accounts = uniq(this.paths); + this.table = table; +} + +WalletMap.fromTX = function fromTX(table, tx) { + var map = new WalletMap(table); + var i, input, output; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + map.inputs.push(MapMember.fromMember(table, input)); + } + + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + map.outputs.push(MapMember.fromMember(table, output)); + } + + return map; +}; + +WalletMap.prototype.hasPaths = function hasPaths(address) { + var paths; + + if (!address) + return false; + + paths = this.table[address]; + + return paths && paths.length !== 0; +}; + +WalletMap.prototype.getPaths = function getPaths(address) { + return this.table[address]; +}; + +function MapMember() { + this.value = 0; + this.hash = null; + this.wallets = []; + this.paths = []; +} + +MapMember.fromMember = function fromMember(table, io) { + var hash = io.getHash('hex'); + var member = new MapMember(); + var i, paths; + + member.value = io.coin + ? io.coin.value + : io.value || 0; + + if (!hash) + return member; + + paths = table[hash]; + + assert(paths); + + member.hash = hash; + member.paths = paths; + + for (i = 0; i < paths.length; i++) + member.wallets.push(paths[i].id); + + member.wallets = utils.uniq(member.wallets); + + return member; +}; + /** * Map a transactions' addresses to wallet IDs. * @param {TX} tx @@ -119,9 +244,7 @@ TXDB.prototype._testFilter = function _testFilter(addresses) { TXDB.prototype.getMap = function getMap(tx, callback) { var i, input, output, address, addresses, map; - input = tx.getInputHashes('hex'); - output = tx.getOutputHashes('hex'); - addresses = utils.uniq(input.concat(output)); + addresses = tx.getHashes('hex'); if (!this._testFilter(addresses)) return callback(); @@ -130,31 +253,12 @@ TXDB.prototype.getMap = function getMap(tx, callback) { if (err) return callback(err); - if (table.count === 0) + if (!table) return callback(); - map = { - table: table, - input: [], - output: [], - all: [] - }; + map = WalletMap.fromTX(table, tx); - for (i = 0; i < input.length; i++) { - address = input[i]; - assert(map.table[address]); - map.input = map.input.concat(map.table[address]); - } - - for (i = 0; i < output.length; i++) { - address = output[i]; - assert(map.table[address]); - map.output = map.output.concat(map.table[address]); - } - - map.input = uniq(map.input); - map.output = uniq(map.output); - map.all = uniq(map.input.concat(map.output)); + utils.print(map); return callback(null, map); }); @@ -168,7 +272,8 @@ TXDB.prototype.getMap = function getMap(tx, callback) { TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { var self = this; - var table = { count: 0 }; + var table = {}; + var count = 0; var i, keys, values; return utils.forEachSerial(address, function(address, next) { @@ -190,7 +295,7 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { assert(!table[address]); table[address] = values; - table.count += values.length; + count += values.length; return next(); }); @@ -198,6 +303,9 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { if (err) return callback(err); + if (count === 0) + return callback(); + return callback(null, table); }); }; @@ -338,8 +446,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) { batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); } - for (i = 0; i < map.all.length; i++) { - path = map.all[i]; + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; id = path.id + '/' + path.account; batch.put('T/' + id + '/' + hash, DUMMY); if (tx.ts === 0) { @@ -362,7 +470,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { address = input.getHash('hex'); // Only add orphans if this input is ours. - if (!address || !map.table[address].length) + if (!map.hasPaths(address)) return next(); self.getCoin(prevout.hash, prevout.index, function(err, coin) { @@ -385,7 +493,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { updated = true; - paths = map.table[address]; + paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; @@ -462,7 +570,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { var coin; // Do not add unspents for outputs that aren't ours. - if (!address || !map.table[address].length) + if (!map.hasPaths(address)) return next(); if (output.script.isUnspendable()) @@ -521,7 +629,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { return next(err); if (!orphans) { - paths = map.table[address]; + paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; @@ -749,8 +857,8 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { batch.del('m/' + pad32(existing.ps) + '/' + hash); batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); - for (i = 0; i < map.all.length; i++) { - path = map.all[i]; + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; id = path.id + '/' + path.account; batch.del('P/' + id + '/' + hash); batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); @@ -762,7 +870,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { var address = output.getHash('hex'); // Only update coins if this output is ours. - if (!address || !map.table[address].length) + if (!map.hasPaths(address)) return next(); self.getCoin(hash, i, function(err, coin) { @@ -888,8 +996,8 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { batch.del('m/' + pad32(tx.ts) + '/' + hash); } - for (i = 0; i < map.all.length; i++) { - path = map.all[i]; + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; id = path.id + '/' + path.account; batch.del('T/' + id + '/' + hash); if (tx.ts === 0) { @@ -916,10 +1024,10 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { if (!input.coin) continue; - if (!address || !map.table[address].length) + if (!map.hasPaths(address)) continue; - paths = map.table[address]; + paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; @@ -937,13 +1045,13 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { key = hash + '/' + i; address = output.getHash('hex'); - if (!address || !map.table[address].length) + if (!map.hasPaths(address)) continue; if (output.script.isUnspendable()) continue; - paths = map.table[address]; + paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; @@ -1040,8 +1148,8 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { batch.del('m/' + pad32(ts) + '/' + hash); batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); - for (i = 0; i < map.all.length; i++) { - path = map.all[i]; + for (i = 0; i < map.accounts.length; i++) { + path = map.accounts[i]; id = path.id + '/' + path.account; batch.put('P/' + id + '/' + hash, DUMMY); batch.del('H/' + id + '/' + pad32(height) + '/' + hash); diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index d27315aa..c8f71119 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -125,35 +125,35 @@ WalletDB.prototype._init = function _init() { this.tx.on('tx', function(tx, map) { self.emit('tx', tx, map); - map.all.forEach(function(path) { + map.accounts.forEach(function(path) { self.fire(path.id, 'tx', tx, path.name); }); }); this.tx.on('conflict', function(tx, map) { self.emit('conflict', tx, map); - map.all.forEach(function(path) { + map.accounts.forEach(function(path) { self.fire(path.id, 'conflict', tx, path.name); }); }); this.tx.on('confirmed', function(tx, map) { self.emit('confirmed', tx, map); - map.all.forEach(function(path) { + map.accounts.forEach(function(path) { self.fire(path.id, 'confirmed', tx, path.name); }); }); this.tx.on('unconfirmed', function(tx, map) { self.emit('unconfirmed', tx, map); - map.all.forEach(function(path) { + map.accounts.forEach(function(path) { self.fire(path.id, 'unconfirmed', tx, path.name); }); }); this.tx.on('updated', function(tx, map) { self.emit('updated', tx, map); - map.all.forEach(function(path) { + map.accounts.forEach(function(path) { self.fire(path.id, 'updated', tx, path.name); }); self.updateBalances(tx, map, function(err, balances) { @@ -186,21 +186,28 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { var self = this; var balances = {}; - utils.forEachSerial(map.output, function(path, next) { - if (self.listeners('balance').length === 0 - && !self.hasListener(path.id, 'balance')) { - return next(); - } + 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(); + } - if (balances[path.id] != null) - return next(); + if (balances[id] != null) + return next(); - self.getBalance(path.id, function(err, balance) { + self.getBalance(id, function(err, balance) { + if (err) + return next(err); + + balances[id] = balance; + + next(); + }); + }, function(err) { if (err) return next(err); - balances[path.id] = balance; - next(); }); }, function(err) { @@ -213,12 +220,19 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) { WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) { var self = this; - utils.forEachSerial(map.output, function(path, next) { - self.syncOutputDepth(path.id, tx, function(err, receive, change) { + 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) { if (err) return next(err); - self.fire(path.id, 'address', receive, change, path.name); - self.emit('address', receive, change, map); + next(); }); }, callback); @@ -702,7 +716,7 @@ WalletDB.prototype.saveAddress = function saveAddress(id, addresses, callback) { if (paths[id]) return next(); - paths[id] = hash[1]; + paths[id] = Path.fromKeyRing(id, hash[1]); batch.put('W/' + hash[0], serializePaths(paths)); @@ -1143,6 +1157,75 @@ WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) { }); }; +/** + * Path + * @constructor + */ + +function Path() { + this.id = null; + this.name = null; + this.account = 0; + this.change = 0; + this.index = 0; + this.hash = null; +} + +Path.prototype.fromRaw = function fromRaw(data) { + var p = new BufferReader(data); + this.id = p.readVarString('utf8'); + this.name = p.readVarString('utf8'); + this.account = p.readU32(); + this.change = p.readU32(); + this.index = p.readU32(); + return this; +}; + +Path.fromRaw = function fromRaw(data) { + return new Path().fromRaw(data); +}; + +Path.prototype.fromKeyRing = function fromKeyRing(id, address) { + this.id = id; + this.name = address.name; + this.account = address.account; + this.change = address.change; + this.index = address.index; + return this; +}; + +Path.fromKeyRing = function fromKeyRing(id, address) { + return new Path().fromKeyRing(id, address); +}; + +Path.prototype.toPath = function() { + return 'm/' + this.account + + '\'/' + this.change + + '/' + this.index; +}; + +Path.prototype.inspect = function() { + return ''; +}; + +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; +}; + /* * Helpers */ @@ -1150,17 +1233,11 @@ WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) { function parsePaths(data) { var p = new BufferReader(data); var out = {}; - var id; + var path; while (p.left()) { - id = p.readVarString('utf8'); - out[id] = { - id: id, - name: p.readVarString('utf8'), - account: p.readU32(), - change: p.readU32(), - index: p.readU32() - }; + path = Path.fromRaw(p); + out[path.id] = path; } return out; @@ -1174,11 +1251,7 @@ function serializePaths(out) { for (i = 0; i < keys.length; i++) { id = keys[i]; path = out[id]; - p.writeVarString(id, 'utf8'); - p.writeVarString(path.name, 'utf8'); - p.writeU32(path.account); - p.writeU32(path.change); - p.writeU32(path.index); + path.toRaw(p); } return p.render(); diff --git a/test/wallet-test.js b/test/wallet-test.js index 51dfe1e2..9d5b4391 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -433,8 +433,10 @@ describe('Wallet', function() { var t3 = bcoin.mtx().addOutput(w2, 15000); w1.fill(t3, { rate: 10000 }, function(err) { assert(err); - assert(balance.total === 5460); - cb(); + setTimeout(function() { + assert(balance.total === 5460); + cb(); + }, 100); }); }); });