diff --git a/lib/bcoin/http.js b/lib/bcoin/http.js index 710b14df..34d036ba 100644 --- a/lib/bcoin/http.js +++ b/lib/bcoin/http.js @@ -67,7 +67,7 @@ HTTPServer.prototype._init = function _init() { }); // UTXO by address - this.get('/utxo/address/:address', function(req, res, next, send) { + this.get('/coin/address/:address', function(req, res, next, send) { var addresses = req.params.address.split(','); self.node.getCoinByAddress(addresses, function(err, coins) { if (err) @@ -81,7 +81,7 @@ HTTPServer.prototype._init = function _init() { }); // UTXO by id - this.get('/utxo/:hash/:index', function(req, res, next, send) { + this.get('/coin/:hash/:index', function(req, res, next, send) { req.params.hash = utils.revHex(req.params.hash); self.node.getCoin(req.params.hash, +req.params.index, function(err, coin) { if (err) @@ -95,7 +95,7 @@ HTTPServer.prototype._init = function _init() { }); // Bulk read UTXOs - this.post('/utxo/address', function(req, res, next, send) { + this.post('/coin/address', function(req, res, next, send) { self.node.getCoinByAddress(req.body.addresses, function(err, coins) { if (err) return next(err); @@ -228,8 +228,8 @@ HTTPServer.prototype._init = function _init() { }); // Wallet UTXOs - this.get('/wallet/:id/utxo', function(req, res, next, send) { - self.node.walletdb.getUnspent(req.params.id, function(err, coins) { + this.get('/wallet/:id/coin', function(req, res, next, send) { + self.node.walletdb.getCoins(req.params.id, function(err, coins) { if (err) return next(err); @@ -241,7 +241,7 @@ HTTPServer.prototype._init = function _init() { }); // Wallet TXs - this.get('/wallet/:id/tx', function(req, res, next, send) { + this.get('/wallet/:id/all', function(req, res, next, send) { self.node.walletdb.getAll(req.params.id, function(err, txs) { if (err) return next(err); diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index c05159e8..177d1375 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -109,7 +109,7 @@ Fullnode.prototype._init = function _init() { }); this.on('tx', function(tx) { - self.walletdb.tx.addTX(tx, function(err) { + self.walletdb.addTX(tx, function(err) { if (err) self.emit('error', err); }); @@ -134,19 +134,7 @@ Fullnode.prototype._init = function _init() { this.chain.on('remove block', function(block) { self.mempool.removeBlock(block); - }); - - // Handle forks by unconfirming txs - // in our wallets' tx pools. - this.chain.on('remove entry', function(entry) { - self.walletdb.tx.getHeightHashes(entry.height, function(err, txs) { - if (err) - return self.emit('error', err); - - txs.forEach(function(tx) { - self.walletdb.tx.unconfirm(tx); - }); - }); + self.walletdb.removeBlock(block); }); function load() { diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 6311bdb0..eceaad3b 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -9,6 +9,7 @@ var bcoin = require('../bcoin'); var utils = bcoin.utils; var assert = bcoin.utils.assert; var EventEmitter = require('events').EventEmitter; +var DUMMY = new Buffer([]); /** * TXPool @@ -30,10 +31,6 @@ function TXPool(prefix, db, options) { this.options = options; this.busy = false; this.jobs = []; - this.ids = true; - - if (options.addressFilter) - this._hasAddress = options.addressFilter; this.setMaxListeners(Number.MAX_SAFE_INTEGER); } @@ -77,54 +74,67 @@ TXPool.prototype._lock = function _lock(func, args, force) { }; }; -TXPool.prototype.add = function add(tx, callback) { +TXPool.prototype.getMap = function getMap(tx, callback) { var self = this; + var addresses = tx.getAddresses(); + var map; - if (Array.isArray(tx)) { - return utils.forEachSerial(tx, function(tx, next) { - self.add(tx, next); - }, callback); - } - - //if (!this.ids) - return this._add(tx, this.__getIDs(tx), callback); - - return self._getIDs(tx.getAddresses(), function(err, ids) { + function cb(err, map) { if (err) return callback(err); - return self._add(tx, ids, callback); - }); + map.input = []; + map.output = []; + map.all = []; + map.table = {}; + + tx.getInputAddresses().forEach(function(address) { + assert(map[address]); + map.input = map.input.concat(map[address]); + }); + + tx.getOutputAddresses().forEach(function(address) { + assert(map[address]); + map.output = map.output.concat(map[address]); + }); + + map.input = utils.uniqs(map.input); + map.output = utils.uniqs(map.output); + map.all = utils.uniqs(map.input.concat(map.output)); + map.table = map.all.reduce(function(out, id) { + out[id] = true; + return out; + }, {}); + + return callback(null, map); + } + + if (!this.options.ids) { + map = addresses.reduce(function(out, address) { + out[address] = [address]; + return out; + }, {}); + return cb(null, map); + } + + return this.mapAddresses(addresses, cb); }; -// Map of address->id. -TXPool.prototype.__getIDs = function _getIDs(tx, callback) { - var map = tx.getAddresses(); - return map.reduce(function(out, addr) { - out[addr] = [addr]; - return out; - }, {}); -}; - -TXPool.prototype.__hasAddress = function __hasAddress(map, address, callback) { - if (!address || !map[address] || !map[address].length) - return callback(null, false); - return callback(null, true); -}; - -TXPool.prototype._getIDs = function getIDs(address, callback) { - var p = this.prefix + '/'; +TXPool.prototype.mapAddresses = function mapAddresses(address, callback) { + var prefix = this.prefix + '/'; var self = this; var map = {}; + var iter; if (Array.isArray(address)) { address = utils.uniqs(address); return utils.forEachSerial(address, function(address, next) { - self._getIDs(address, function(err, m) { + self.mapAddresses(address, function(err, res) { if (err) return next(err); - map[address] = m[address]; + assert(res[address]); + map[address] = res[address]; next(); }); @@ -136,9 +146,9 @@ TXPool.prototype._getIDs = function getIDs(address, callback) { }); } - var iter = this.db.db.iterator({ - gte: p + 'a/' + address, - lte: p + 'a/' + address + '~', + iter = this.db.db.iterator({ + gte: prefix + 'a/' + address, + lte: prefix + 'a/' + address + '~', keys: true, values: false, fillCache: false, @@ -166,31 +176,114 @@ TXPool.prototype._getIDs = function getIDs(address, callback) { }); } - map[address].push(key.split('/')[3]); + key = key.split('/')[3]; + map[address].push(key); next(); }); })(); }; +TXPool.prototype._addOrphan = function add(key, hash, index, callback) { + var prefix = this.prefix + '/'; + var self = this; + var orphans; + + this.db.get(prefix + 'o/' + key, function(err, buf) { + if (err && err.type !== 'NotFoundError') + return callback(err); + + if (!buf) + buf = new Buffer([]); + + orphans = new Buffer(buf.length + 36); + utils.copy(buf, orphans, 0); + utils.copy(new Buffer(hash, 'hex'), orphans, buf.length); + utils.writeU32(orphans, index, buf.length + 32); + + return callback(null, orphans); + }); +}; + +TXPool.prototype._getOrphans = function _getOrphans(key, callback) { + var prefix = this.prefix + '/'; + var self = this; + var orphans = []; + var i; + + this.db.get(prefix + 'o/' + key, function(err, buf) { + if (err && err.type !== 'NotFoundError') + return callback(err); + + if (!buf) + return callback(); + + for (i = 0; i < buf.length; i += 36) { + if (i + 36 > buf.length) + return callback(new Error('Incomplete orphan list.')); + + orphans.push({ + hash: utils.toHex(buf.slice(i, i + 32)), + index: utils.readU32(buf, i + 32) + }); + } + + utils.forEach(orphans, function(orphan, next) { + self.getTX(orphan.hash, function(err, tx) { + if (err) + return done(err); + + orphan.tx = tx; + + next(); + }); + }, function(err) { + if (err) + return callback(err); + + return callback(null, orphans); + }); + }); +}; + +TXPool.prototype.add = function add(tx, callback) { + var self = this; + + if (Array.isArray(tx)) { + return utils.forEachSerial(tx, function(tx, next) { + self.add(tx, next); + }, callback); + } + + return this.getMap(tx, function(err, map) { + if (err) + return callback(err); + + if (map.all.length === 0) + return callback(null, false); + + return self._add(tx, map, callback); + }); +}; + // This big scary function is what a persistent tx pool // looks like. It's a semi mempool in that it can handle // receiving txs out of order. TXPool.prototype._add = function add(tx, map, callback, force) { var self = this; - var p = this.prefix + '/'; + var prefix = this.prefix + '/'; var hash = tx.hash('hex'); var updated = false; var own = false; var uniq = {}; var batch; - // var unlock = this._lock(add, [tx, map, callback], force); - // if (!unlock) - // return; + var unlock = this._lock(add, [tx, map, callback], force); + if (!unlock) + return; function done(err, result) { - //unlock(); + unlock(); if (callback) callback(err, result); }; @@ -206,43 +299,31 @@ TXPool.prototype._add = function add(tx, map, callback, force) { // Tricky - update the tx and coin in storage, // and remove pending flag to mark as confirmed. if (existing.ts === 0 && tx.ts !== 0) { - batch.put(p + 't/t/' + hash, tx.toExtended()); - batch.put(p + 't/h/' + tx.height + '/' + hash, new Buffer([])); - batch.del(p + 'p/t/' + hash); + batch.put(prefix + 't/t/' + hash, tx.toExtended()); + batch.put(prefix + 't/h/' + tx.height + '/' + hash, DUMMY); + batch.del(prefix + 'p/t/' + hash); tx.inputs.forEach(function(input) { - var type = input.getType(); var address = input.getAddress(); - var uaddr; if (input.isCoinbase()) return; if (address && !uniq[address]) { uniq[address] = true; - uaddr = address; - } - - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.del(p + 'p/a/' + id + '/' + hash); + map[address].forEach(function(id) { + batch.del(prefix + 'p/a/' + id + '/' + hash); }); } }); utils.forEachSerial(tx.outputs, function(output, next, i) { - var type = output.getType(); var address = output.getAddress(); - var uaddr; if (address && !uniq[address]) { uniq[address] = true; - uaddr = address; - } - - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.del(p + 'p/a/' + id + '/' + hash); + map[address].forEach(function(id) { + batch.del(prefix + 'p/a/' + id + '/' + hash); }); } @@ -255,7 +336,7 @@ TXPool.prototype._add = function add(tx, map, callback, force) { coin.height = tx.height; - batch.put(p + 'u/t/' + hash + '/' + i, coin.toRaw()); + batch.put(prefix + 'u/t/' + hash + '/' + i, coin.toRaw()); next(); }); @@ -267,8 +348,8 @@ TXPool.prototype._add = function add(tx, map, callback, force) { if (err) return done(err); - self.emit('confirmed', tx); - self.emit('tx', tx); + self.emit('confirmed', tx, map); + self.emit('tx', tx, map); return done(null, true); }); @@ -281,15 +362,30 @@ TXPool.prototype._add = function add(tx, map, callback, force) { // Consume unspent money or add orphans utils.forEachSerial(tx.inputs, function(input, next, i) { - var key = input.prevout.hash + '/' + input.prevout.index; + var key; + if (input.isCoinbase()) return next(); + + key = input.prevout.hash + '/' + input.prevout.index; + self.getCoin(input.prevout.hash, input.prevout.index, function(err, coin) { - var type, address, uaddr; + var address; if (err) return next(err); + address = input.getAddress(); + + if (address && !uniq[address]) { + uniq[address] = true; + map[address].forEach(function(id) { + batch.put(prefix + 't/a/' + id + '/' + hash, DUMMY); + if (tx.ts === 0) + batch.put(prefix + 'p/a/' + id + '/' + hash, DUMMY); + }); + } + if (coin) { // Add TX to inputs and spend money input.output = coin; @@ -304,33 +400,17 @@ TXPool.prototype._add = function add(tx, map, callback, force) { updated = true; own = true; - type = input.getType(); - address = input.getAddress(); - - if (address && !uniq[address]) { - uniq[address] = true; - uaddr = address; - } - if (address) { map[address].forEach(function(id) { batch.del( - p + 'u/a/' + id + prefix + 'u/a/' + id + '/' + input.prevout.hash + '/' + input.prevout.index); }); } - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.put(p + 't/a/' + id + '/' + hash, new Buffer([])); - if (tx.ts === 0) - batch.put(p + 'p/a/' + id + '/' + hash, new Buffer([])); - }); - } - batch.del( - p + 'u/t/' + prefix + 'u/t/' + input.prevout.hash + '/' + input.prevout.index); @@ -338,49 +418,27 @@ TXPool.prototype._add = function add(tx, map, callback, force) { } // Only add orphans if this input is ours. - self.__hasAddress(map, input.getAddress(), function(err, result) { + if (!address || !map[address].length) + return next(); + + own = true; + + self.getTX(input.prevout.hash, function(err, result) { if (err) return done(err); - if (!result) - return next(); + // Are we double-spending? + if (result) + return done(new Error('Transaction is double-spending.')); - own = true; - - self.getTX(input.prevout.hash, function(err, result) { + // Add orphan, if no parent transaction is yet known + self._addOrphan(key, hash, i, function(err, orphans) { if (err) return done(err); - // Are we double-spending? - if (result) - return done(new Error('Transaction is double-spending.')); + batch.put(prefix + 'o/' + key, orphans); - // Add orphan, if no parent transaction is yet known - self.db.get(p + 'o/' + key, function(err, orphans) { - if (err && err.type !== 'NotFoundError') - return done(err); - - if (orphans) { - try { - orphans = JSON.parse(orphans.toString('ascii')); - } catch (e) { - return done(e); - } - } else { - orphans = []; - } - - orphans.push({ - tx: hash, - index: i - }); - - orphans = new Buffer(JSON.stringify(orphans), 'ascii'); - - batch.put(p + 'o/' + key, orphans); - - return next(); - }); + return next(); }); }); }); @@ -390,121 +448,91 @@ TXPool.prototype._add = function add(tx, map, callback, force) { // Add unspent outputs or resolve orphans utils.forEachSerial(tx.outputs, function(output, next, i) { + var address = output.getAddress(); + var key, coin; + // Do not add unspents for outputs that aren't ours. - self.__hasAddress(map, output.getAddress(), function(err, result) { - var key, coin; + if (!address || !map[address].length) + return next(); + + key = hash + '/' + i; + coin = bcoin.coin(tx, i); + own = true; + + self._getOrphans(key, function(err, orphans) { + var some = false; if (err) return done(err); - if (!result) - return next(); + if (!orphans) + return finish(); - key = hash + '/' + i; - coin = bcoin.coin(tx, i); - own = true; + // Add input to orphan + utils.forEachSerial(orphans, function(orphan, next, j) { + if (some) + return next(); - self.db.get(p + 'o/' + key, function(err, orphans) { - var some; + // Probably removed by some other means. + if (!orphan.tx) + return next(); - if (err && err.type !== 'NotFoundError') - return done(err); + orphan.tx.inputs[orphan.index].output = coin; - // Add input to orphan - if (orphans) { - some = false; + assert(orphan.tx.inputs[orphan.index].prevout.hash === hash); + assert(orphan.tx.inputs[orphan.index].prevout.index === i); - try { - orphans = JSON.parse(orphans.toString('ascii')); - } catch (e) { - return done(e); - } - - utils.forEachSerial(orphans, function(orphan, next, j) { - if (some) - return next(); - - self.getTX(orphan.tx, function(err, otx) { - if (err) - return done(err); - - // Probably removed by some other means. - if (!otx) - return next(); - - orphan.tx = otx; - orphan.tx.inputs[orphan.index].output = coin; - - assert(orphan.tx.inputs[orphan.index].prevout.hash === hash); - assert(orphan.tx.inputs[orphan.index].prevout.index === i); - - // Verify that input script is correct, if not - add - // output to unspent and remove orphan from storage - if (orphan.tx.verify(orphan.index)) { - some = true; - return next(); - } - - self.remove(orphan.tx, function(err) { - if (err) - return next(err); - return next(); - }); - }); - }, function(err) { - if (err) - return next(err); - - if (!some) - orphans = null; - - self.db.del(p + 'o/' + key, finish); - }); - } else { - finish(); + // Verify that input script is correct, if not - add + // output to unspent and remove orphan from storage + if (orphan.tx.verify(orphan.index)) { + some = true; + return next(); } - function finish(err) { - var type, adddress, uaddr; - + self.lazyRemove(orphan.tx, function(err) { if (err) return next(err); + return next(); + }); + }, function(err) { + if (err) + return next(err); - if (!orphans) { - type = output.getType(); - address = output.getAddress(); + if (!some) + orphans = null; - if (address && !uniq[address]) { - uniq[address] = true; - uaddr = address; - } - - if (address) { - map[address].forEach(function(id) { - batch.put( - p + 'u/a/' + id - + '/' + hash + '/' + i, - new Buffer([])); - }); - } - - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.put(p + 't/a/' + id + '/' + hash, new Buffer([])); - if (tx.ts === 0) - batch.put(p + 'p/a/' + id + '/' + hash, new Buffer([])); - }); - } - - batch.put(p + 'u/t/' + hash + '/' + i, coin.toRaw()); - updated = true; - } - - next(); - } + self.db.del(prefix + 'o/' + key, finish); }); - return true; + function finish(err) { + if (err) + return next(err); + + if (!orphans) { + if (address && !uniq[address]) { + uniq[address] = true; + map[address].forEach(function(id) { + batch.put(prefix + 't/a/' + id + '/' + hash, DUMMY); + if (tx.ts === 0) + batch.put(prefix + 'p/a/' + id + '/' + hash, DUMMY); + }); + } + + if (address) { + map[address].forEach(function(id) { + batch.put( + prefix + 'u/a/' + id + + '/' + hash + '/' + i, + DUMMY); + }); + } + + batch.put(prefix + 'u/t/' + hash + '/' + i, coin.toRaw()); + updated = true; + } + + next(); + } }); }, function(err) { if (err) @@ -513,11 +541,11 @@ TXPool.prototype._add = function add(tx, map, callback, force) { if (!own) return done(null, false); - batch.put(p + 't/t/' + hash, tx.toExtended()); + batch.put(prefix + 't/t/' + hash, tx.toExtended()); if (tx.ts === 0) - batch.put(p + 'p/t/' + hash, new Buffer([])); + batch.put(prefix + 'p/t/' + hash, DUMMY); else - batch.put(p + 't/h/' + tx.height + '/' + hash, new Buffer([])); + batch.put(prefix + 't/h/' + tx.height + '/' + hash, DUMMY); batch.write(function(err) { if (err) @@ -554,121 +582,124 @@ TXPool.prototype.remove = function remove(hash, callback) { this.getTX(hash, function(err, tx) { if (err) return callback(err); - return self._getIDs(tx.getAddresses(), function(err, map) { + + if (!tx) + return callback(null, true); + + assert(tx.hash('hex') === hash); + + return self.getMap(tx, function(err, map) { if (err) return callback(err); + if (map.all.length === 0) + return callback(null, false); + return self._remove(tx, map, callback); }); }); }; -TXPool.prototype._remove = function remove(hash, map, callback) { +TXPool.prototype.lazyRemove = function lazyRemove(tx, callback) { var self = this; - var p = this.prefix + '/'; - var uniq = {}; - if (hash.hash) - hash = hash.hash('hex'); - - // XXX Remove this - this.getTX(hash, function(err, tx) { - var batch; + if (Array.isArray(tx)) { + return utils.forEachSerial(tx, function(tx, next) { + self.lazyRemove(tx, next); + }, callback); + } + return this.getMap(tx, function(err, map) { if (err) return callback(err); - if (!tx) - return callback(null, true); + if (map.all.length === 0) + return callback(null, false); - batch = self.db.batch(); + return self._remove(tx, map, callback); + }); +}; - assert(tx.hash('hex') === hash); +TXPool.prototype._remove = function remove(tx, map, callback) { + var self = this; + var prefix = this.prefix + '/'; + var hash = tx.hash('hex'); + var batch = this.db.batch(); + var uniq = {}; - batch.del(p + 't/t/' + hash); - if (tx.ts === 0) - batch.del(p + 'p/t/' + hash); - else - batch.del(p + 't/h/' + tx.height + '/' + hash); + batch.del(prefix + 't/t/' + hash); + if (tx.ts === 0) + batch.del(prefix + 'p/t/' + hash); + else + batch.del(prefix + 't/h/' + tx.height + '/' + hash); - self.fillTX(tx, function(err) { - if (err) - return next(err); + this.fillTX(tx, function(err) { + if (err) + return next(err); - tx.inputs.forEach(function(input) { - var type = input.getType(); - var address = input.getAddress(); - var uaddr; + tx.inputs.forEach(function(input) { + var address = input.getAddress(); - if (input.isCoinbase()) - return; + if (input.isCoinbase()) + return; - if (address && !uniq[address]) { - uniq[address] = true; - uaddr = address; - } + if (address && !uniq[address]) { + uniq[address] = true; + map[address].forEach(function(id) { + batch.del(prefix + 't/a/' + id + '/' + hash); + if (tx.ts === 0) + batch.del(prefix + 'p/a/' + id + '/' + hash); + }); + } - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.del(p + 't/a/' + id + '/' + hash); - if (tx.ts === 0) - batch.del(p + 'p/a/' + id + '/' + hash); - }); - } + if (!input.output) + return; - if (address) { - map[address].forEach(function(id) { - batch.put(p + 'u/a/' + id - + '/' + input.prevout.hash - + '/' + input.prevout.index, - new Buffer([])); - }); - } - - if (input.output) { - batch.put(p + 'u/t/' - + input.prevout.hash + if (address) { + map[address].forEach(function(id) { + batch.put(prefix + 'u/a/' + id + + '/' + input.prevout.hash + '/' + input.prevout.index, - input.output.toRaw()); - } + DUMMY); + }); + } - batch.del(p + 'o/' + input.prevout.hash + '/' + input.prevout.index); - }); + batch.put(prefix + 'u/t/' + + input.prevout.hash + + '/' + input.prevout.index, + input.output.toRaw()); - tx.outputs.forEach(function(output, i) { - var type = output.getType(); - var address = output.getAddress(); - var uaddr; + batch.del(prefix + 'o/' + input.prevout.hash + '/' + input.prevout.index); + }); - if (address && !uniq[address]) { - uniq[address] = true; - uaddr = address; - } + tx.outputs.forEach(function(output, i) { + var address = output.getAddress(); - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.del(p + 't/a/' + id + '/' + hash); - if (tx.ts === 0) - batch.del(p + 'p/a/' + id + '/' + hash); - }); - } + if (address && !uniq[address]) { + uniq[address] = true; + map[address].forEach(function(id) { + batch.del(prefix + 't/a/' + id + '/' + hash); + if (tx.ts === 0) + batch.del(prefix + 'p/a/' + id + '/' + hash); + }); + } - if (address) { - map[address].forEach(function(id) { - batch.del(p + 'u/a/' + id + '/' + hash + '/' + i); - }); - } + if (address) { + map[address].forEach(function(id) { + batch.del(prefix + 'u/a/' + id + '/' + hash + '/' + i); + }); + } - batch.del(p + 'u/t/' + hash + '/' + i); - }); + batch.del(prefix + 'u/t/' + hash + '/' + i); + }); - batch.write(function(err) { - if (err) - return callback(err); + batch.write(function(err) { + if (err) + return callback(err); - self.emit('remove tx', tx); - return callback(null, true); - }); + self.emit('remove tx', tx, map); + + return callback(null, true); }); }); }; @@ -688,109 +719,98 @@ TXPool.prototype.unconfirm = function unconfirm(hash, callback) { this.getTX(hash, function(err, tx) { if (err) return callback(err); - return self._getIDs(tx.getAddresses(), function(err, map) { + + if (!tx) + return callback(null, true); + + assert(tx.hash('hex') === hash); + + return self.getMap(tx, function(err, map) { if (err) return callback(err); + if (map.all.length === 0) + return callback(null, false); + return self._unconfirm(tx, map, callback); }); }); }; -TXPool.prototype._unconfirm = function unconfirm(hash, map, callback) { +TXPool.prototype._unconfirm = function unconfirm(tx, map, callback) { var self = this; - var p = this.prefix + '/'; + var prefix = this.prefix + '/'; var uniq = {}; + var hash = tx.hash('hex'); + var batch = this.db.batch(); + var height = tx.height; - if (hash.hash) - hash = hash.hash('hex'); + tx.height = -1; + tx.ps = utils.now(); + tx.ts = 0; + tx.index = -1; + tx.block = null; - // XXX Remove this. - this.getTX(hash, function(err, tx) { - var batch, height; + batch.put(prefix + 't/t/' + hash, tx.toExtended()); + batch.put(prefix + 'p/t/' + hash, DUMMY); + batch.del(prefix + 't/h/' + height + '/' + hash); + tx.inputs.forEach(function(input) { + var address; + + if (input.isCoinbase()) + return; + + address = input.getAddress(); + + if (address && !uniq[address]) { + uniq[address] = true; + map[address].forEach(function(id) { + batch.put(prefix + 'p/a/' + id + '/' + hash, DUMMY); + }); + } + }); + + utils.forEachSerial(tx.outputs, function(output, next, i) { + var address = output.getAddress(); + + if (address && !uniq[address]) { + uniq[address] = true; + map[address].forEach(function(id) { + batch.put(prefix + 'p/a/' + id + '/' + hash, DUMMY); + }); + } + + self.getCoin(hash, i, function(err, coin) { + if (err) + return next(err); + + if (!coin) + return next(); + + coin.height = tx.height; + + batch.put(prefix + 'u/t/' + hash + '/' + i, coin.toRaw()); + + next(); + }); + }, function(err) { if (err) return callback(err); - if (!tx) - return callback(null, true); - - height = tx.height; - tx.height = -1; - tx.ps = utils.now(); - tx.ts = 0; - tx.index = -1; - tx.block = null; - - batch.put(p + 't/t/' + hash, tx.toExtended()); - batch.put(p + 'p/t/' + hash, new Buffer([])); - batch.del(p + 't/h/' + height + '/' + hash); - - tx.inputs.forEach(function(input) { - var type = input.getType(); - var address = input.getAddress(); - var uaddr; - - if (input.isCoinbase()) - return; - - if (address && !uniq[address]) { - uniq[address] = true; - uaddr = address; - } - - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.put(p + 'p/a/' + id + '/' + hash, new Buffer([])); - }); - } - }); - - utils.forEachSerial(tx.outputs, function(output, next, i) { - var type = output.getType(); - var address = output.getAddress(); - var uaddr; - - if (address && !uniq[address]) { - uniq[address] = true; - uaddr = address; - } - - if (uaddr) { - map[uaddr].forEach(function(id) { - batch.put(p + 'p/a/' + id + '/' + hash, new Buffer([])); - }); - } - - self.getCoin(hash, i, function(err, coin) { - if (err) - return next(err); - - if (!coin) - return next(); - - coin.height = tx.height; - - batch.put(p + 'u/t/' + hash + '/' + i, coin.toRaw()); - - next(); - }); - }, function(err) { + batch.write(function(err) { if (err) return callback(err); - batch.write(function(err) { - if (err) - return callback(err); - self.emit('unconfirmed', tx); - return callback(null, true); - }); + self.emit('unconfirmed', tx, map); + + return callback(null, true); }); }); }; TXPool.prototype.getTXHashes = function getTXHashes(address, callback) { - var p = this.prefix + '/'; + var prefix = this.prefix + '/'; var self = this; var txs = []; var iter; @@ -818,8 +838,8 @@ TXPool.prototype.getTXHashes = function getTXHashes(address, callback) { } iter = this.db.db.iterator({ - gte: address ? p + 't/a/' + address : p + 't/t', - lte: address ? p + 't/a/' + address + '~' : p + 't/t~', + gte: address ? prefix + 't/a/' + address : prefix + 't/t', + lte: address ? prefix + 't/a/' + address + '~' : prefix + 't/t~', keys: true, values: false, fillCache: false, @@ -853,7 +873,7 @@ TXPool.prototype.getTXHashes = function getTXHashes(address, callback) { }; TXPool.prototype.getPendingHashes = function getPendingHashes(address, callback) { - var p = this.prefix + '/'; + var prefix = this.prefix + '/'; var self = this; var txs = []; var iter; @@ -882,8 +902,8 @@ TXPool.prototype.getPendingHashes = function getPendingHashes(address, callback) } iter = this.db.db.iterator({ - gte: address ? p + 'p/a/' + address : p + 'p/t', - lte: address ? p + 'p/a/' + address + '~' : p + 'p/t~', + gte: address ? prefix + 'p/a/' + address : prefix + 'p/t', + lte: address ? prefix + 'p/a/' + address + '~' : prefix + 'p/t~', keys: true, values: false, fillCache: false, @@ -917,7 +937,7 @@ TXPool.prototype.getPendingHashes = function getPendingHashes(address, callback) }; TXPool.prototype.getCoinIDs = function getCoinIDs(address, callback) { - var p = this.prefix + '/'; + var prefix = this.prefix + '/'; var self = this; var coins = []; var iter; @@ -945,8 +965,8 @@ TXPool.prototype.getCoinIDs = function getCoinIDs(address, callback) { } iter = this.db.db.iterator({ - gte: address ? p + 'u/a/' + address : p + 'u/t', - lte: address ? p + 'u/a/' + address + '~' : p + 'u/t~', + gte: address ? prefix + 'u/a/' + address : prefix + 'u/t', + lte: address ? prefix + 'u/a/' + address + '~' : prefix + 'u/t~', keys: true, values: false, fillCache: false, @@ -980,7 +1000,7 @@ TXPool.prototype.getCoinIDs = function getCoinIDs(address, callback) { }; TXPool.prototype.getHeightHashes = function getHeightHashes(height, callback) { - var p = this.prefix + '/'; + var prefix = this.prefix + '/'; var self = this; var txs = []; var iter; @@ -1008,8 +1028,8 @@ TXPool.prototype.getHeightHashes = function getHeightHashes(height, callback) { } iter = this.db.db.iterator({ - gte: p + 't/h/' + height, - lte: p + 't/h/' + height + '~', + gte: prefix + 't/h/' + height, + lte: prefix + 't/h/' + height + '~', keys: true, values: false, fillCache: false, @@ -1039,7 +1059,7 @@ TXPool.prototype.getHeightHashes = function getHeightHashes(height, callback) { })(); }; -TXPool.prototype.getTXByAddress = function getTXByAddress(address, callback) { +TXPool.prototype.getAllByAddress = function getAllByAddress(address, callback) { var self = this; var txs = []; @@ -1069,7 +1089,7 @@ TXPool.prototype.getTXByAddress = function getTXByAddress(address, callback) { }; TXPool.prototype.getLast = function getLast(address, callback) { - return this.getTXByAddress(address, function(err, txs) { + return this.getAllByAddress(address, function(err, txs) { var lastTs, lastHeight; if (err) @@ -1118,15 +1138,15 @@ TXPool.prototype.getPendingByAddress = function getPendingByAddress(address, cal }); }; -TXPool.prototype.getCoinByAddress = function getCoinByAddress(address, callback) { +TXPool.prototype.getCoinsByAddress = function getCoinsByAddress(address, callback) { var self = this; var coins = []; - return this.getCoinIDs(address, function(err, ids) { + return this.getCoinIDs(address, function(err, map) { if (err) return callback(err); - utils.forEachSerial(ids, function(id, next) { + utils.forEachSerial(map, function(id, next) { var parts = id.split('/'); self.getCoin(parts[0], +parts[1], function(err, coin) { if (err) @@ -1227,8 +1247,8 @@ TXPool.prototype.fillCoin = function fillCoin(tx, callback) { }; TXPool.prototype.getTX = function getTX(hash, callback) { - var p = this.prefix + '/'; - var id = p + 't/t/' + hash; + var prefix = this.prefix + '/'; + var id = prefix + 't/t/' + hash; this.db.get(id, function(err, tx) { if (err) { @@ -1241,8 +1261,8 @@ TXPool.prototype.getTX = function getTX(hash, callback) { }; TXPool.prototype.getCoin = function getCoin(hash, index, callback) { - var p = this.prefix + '/'; - var id = p + 'u/t/' + hash + '/' + index; + var prefix = this.prefix + '/'; + var id = prefix + 'u/t/' + hash + '/' + index; this.db.get(id, function(err, coin) { if (err) { @@ -1255,36 +1275,8 @@ TXPool.prototype.getCoin = function getCoin(hash, index, callback) { }); }; -TXPool.prototype.getAll = function getAll(callback) { - return this.getTXByAddress(null, callback); -}; - -TXPool.prototype.getUnspent = function getUnspent(callback) { - return this.getCoinByAddress(null, callback); -}; - -TXPool.prototype.getPending = function getPending(callback) { - return this.getPendingByAddress(null, callback); -}; - -TXPool.prototype.getBalance = function getBalance(callback) { - return this.getBalanceByAddress(null, callback); -}; - -TXPool.prototype.getAllByAddress = function getAllByAddress(address, callback) { - return this.getTXByAddress(address, callback); -}; - -TXPool.prototype.getUnspentByAddress = function getUnspentByAddress(address, callback) { - return this.getCoinByAddress(address, callback); -}; - -TXPool.prototype.getPendingByAddress = function getPendingByAddress(address, callback) { - return this.getPendingByAddress(address, callback); -}; - TXPool.prototype.getBalanceByAddress = function getBalanceByAddress(address, callback) { - return this.getCoinByAddress(address, function(err, coins) { + return this.getCoinsByAddress(address, function(err, coins) { if (err) return callback(err); @@ -1296,6 +1288,22 @@ TXPool.prototype.getBalanceByAddress = function getBalanceByAddress(address, cal }); }; +TXPool.prototype.getAll = function getAll(callback) { + return this.getAllByAddress(null, callback); +}; + +TXPool.prototype.getCoins = function getCoins(callback) { + return this.getCoinsByAddress(null, callback); +}; + +TXPool.prototype.getPending = function getPending(callback) { + return this.getPendingByAddress(null, callback); +}; + +TXPool.prototype.getBalance = function getBalance(callback) { + return this.getBalanceByAddress(null, callback); +}; + /** * Expose */ diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 1d8399a1..d6642a6a 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -513,12 +513,12 @@ Wallet.prototype.fill = function fill(tx, options, callback) { assert(this._initialized); - this.getUnspent(function(err, unspent) { + this.getCoins(function(err, coins) { if (err) return callback(err); try { - tx.fill(unspent, { + tx.fill(coins, { selection: options.selection || 'age', fee: options.fee, subtractFee: options.subtractFee, @@ -814,11 +814,11 @@ Wallet.prototype.getAll = function getAll(callback) { return this.provider.getAll(this, callback); }; -Wallet.prototype.getUnspent = function getUnspent(callback) { +Wallet.prototype.getCoins = function getCoins(callback) { if (!this.provider) return callback(new Error('No wallet provider available.')); - return this.provider.getUnspent(this, callback); + return this.provider.getCoins(this, callback); }; Wallet.prototype.getPending = function getPending(callback) { diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 784a7e66..767c99a1 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -14,6 +14,7 @@ var network = bcoin.protocol.network; var utils = bcoin.utils; var assert = utils.assert; var fs = bcoin.fs; +var DUMMY = new Buffer([]); /** * WalletDB @@ -117,44 +118,39 @@ WalletDB.prototype._init = function _init() { this.db = WalletDB._db[this.file]; this.tx = new bcoin.txdb('w', this.db, { - addressFilter: this.hasAddress.bind(this) + ids: true }); this.tx.on('error', function(err) { self.emit('error', err); }); - this.tx.on('updated', function(tx) { - self.getIDs(tx.getOutputAddresses(), function(err, ids) { - if (err) - return self.emit('error', err); + this.tx.on('updated', function(tx, map) { + self.emit('wallet tx', tx, map); - self.emit('wallet tx', tx, ids); + // Only sync for confirmed txs. + if (tx.ts === 0) + return; - // Only sync for confirmed txs. - if (tx.ts === 0) - return; - - utils.forEachSerial(ids, function(id, next) { - self.getJSON(id, function(err, json) { - if (err) { - self.emit('error', err); - return next(); - } - - // Allocate new addresses if necessary. - json = bcoin.wallet.sync(json, { txs: [tx] }); - - self.saveJSON(id, json, function(err) { - if (err) - return next(err); - next(); - }); - }); - }, function(err) { - if (err) + utils.forEachSerial(map.output, function(id, next) { + self.getJSON(id, function(err, json) { + if (err) { self.emit('error', err); + return next(); + } + + // Allocate new addresses if necessary. + json = bcoin.wallet.sync(json, { txs: [tx] }); + + self.saveJSON(id, json, function(err) { + if (err) + return next(err); + next(); + }); }); + }, function(err) { + if (err) + self.emit('error', err); }); }); }; @@ -210,7 +206,7 @@ WalletDB.prototype.saveJSON = function saveJSON(id, json, callback) { if (json) { batch = self.db.batch(); Object.keys(json.addressMap).forEach(function(address) { - batch.put('w/a/' + address + '/' + json.id, new Buffer([])); + batch.put('w/a/' + address + '/' + json.id, DUMMY); }); return batch.write(function(err) { if (err) @@ -439,18 +435,18 @@ WalletDB.prototype.update = function update(wallet, address) { batch.put( 'w/a/' + address.getKeyAddress() + '/' + wallet.id, - new Buffer([])); + DUMMY); if (address.type === 'multisig') { batch.put( 'w/a/' + address.getScriptAddress() + '/' + wallet.id, - new Buffer([])); + DUMMY); } if (address.witness) { batch.put( 'w/a/' + address.getProgramAddress() + '/' + wallet.id, - new Buffer([])); + DUMMY); } batch.write(function(err) { @@ -468,184 +464,75 @@ WalletDB.prototype.addTX = function addTX(tx, callback) { return this.tx.add(tx, callback); }; -WalletDB.prototype.getAll = function getAll(id, callback) { - var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); - - return self.tx.getAllByAddress(addresses, callback); - }); +WalletDB.prototype.getTX = function getTX(hash, callback) { + return this.tx.getTX(hash, callback); }; -WalletDB.prototype.getUnspent = function getUnspent(id, callback) { - var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); +WalletDB.prototype.getCoin = function getCoin(hash, index, callback) { + return this.tx.getCoin(hash, index, callback); +}; - return self.tx.getUnspentByAddress(addresses, callback); - }); +WalletDB.prototype.getAll = function getAll(id, callback) { + var self = this; + id = id.id || id; + return this.tx.getAllByAddress(id, callback); +}; + +WalletDB.prototype.getCoins = function getCoins(id, callback) { + var self = this; + id = id.id || id; + return this.tx.getCoinsByAddress(id, callback); }; WalletDB.prototype.getPending = function getPending(id, callback) { var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); - - return self.tx.getPendingByAddress(addresses, callback); - }); + id = id.id || id; + return this.tx.getPendingByAddress(id, callback); }; WalletDB.prototype.getBalance = function getBalance(id, callback) { var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); - - return self.tx.getBalanceByAddress(addresses, callback); - }); + id = id.id || id; + return this.tx.getBalanceByAddress(id, callback); }; WalletDB.prototype.getLast = function getLast(id, callback) { var self = this; - return this.getAddresses(id, function(err, addresses) { + id = id.id || id; + return this.tx.getLast(id, callback); +}; + +WalletDB.prototype.fillTX = function fillTX(tx, callback) { + return this.tx.fillTX(tx, callback); +}; + +WalletDB.prototype.fillCoin = function fillCoin(tx, callback) { + return this.tx.fillCoin(tx, callback); +}; + +WalletDB.prototype.removeBlockSPV = function removeBlockSPV(block, callback) { + var self = this; + callback = utils.ensure(callback); + this.tx.getHeightHashes(block.height, function(err, txs) { if (err) return callback(err); - return self.tx.getLast(addresses, callback); + txs.forEach(function(tx) { + self.tx.unconfirm(tx); + }); + + callback(); }); }; -WalletDB.prototype.getAddresses = function getAddresses(id, callback) { - // Try to avoid a database lookup if we can... - if (typeof id === 'string' && bcoin.address.validate(id)) - return callback(null, [id]); - - if (Array.isArray(id)) - return callback(null, id); - - if (id.addressMap) - return callback(null, Object.keys(id.addressMap)); - - if (typeof id === 'object') - return callback(null, Object.keys(id)); - - return this.db.get('w/w/' + id, function(err, buf) { - var json; - - if (err) - return callback(err); - - try { - json = JSON.parse(buf.toString('utf8')); - } catch (e) { - return callback(e); - } - - return callback(null, Object.keys(json.addressMap)); - }); -}; - -WalletDB.prototype.hasAddress = function hasAddress(address, callback) { +WalletDB.prototype.removeBlock = function removeBlock(block, callback) { var self = this; callback = utils.ensure(callback); - if (!address) - return callback(null, false); - - var iter = this.db.db.iterator({ - gte: 'w/a/' + address, - lte: 'w/a/' + address + '~', - keys: true, - values: false, - fillCache: false, - keyAsBuffer: false - }); - - (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, false); - }); - } - - return iter.end(function(err) { - if (err) - return callback(err); - callback(null, true); - }); - }); - })(); -}; - -WalletDB.prototype.getIDs = function getIDs(address, callback) { - var self = this; - var ids = []; - - if (Array.isArray(address)) { - return utils.forEachSerial(address, function(address, next) { - self.getIDs(address, function(err, id) { - if (err) - return next(err); - - ids = ids.concat(id); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - ids = utils.uniqs(ids); - - return callback(null, ids); - }); - } - - var iter = this.db.db.iterator({ - gte: 'w/a/' + address, - lte: 'w/a/' + address + '~', - keys: true, - values: false, - fillCache: false, - keyAsBuffer: false - }); - - 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, ids); - }); - } - - ids.push(key.split('/')[3]); - - next(); - }); - })(); + utils.forEach(block.txs, function(tx, next) { + self.tx.unconfirm(tx.hash('hex')); + }, callback); }; /** diff --git a/test/wallet-test.js b/test/wallet-test.js index b1b9652a..1bb54081 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -179,30 +179,30 @@ describe('Wallet', function() { fake.hint = 'fake'; // Fake TX should temporarly change output - // w.addTX(fake); + // wdb.addTX(fake); - w.addTX(fake, function(err) { + wdb.addTX(fake, function(err) { assert(!err); - w.addTX(t4, function(err) { + wdb.addTX(t4, function(err) { assert(!err); w.getBalance(function(err, balance) { assert(!err); assert.equal(balance.toString(10), '22500'); - w.addTX(t1, function(err) { + wdb.addTX(t1, function(err) { w.getBalance(function(err, balance) { assert(!err); assert.equal(balance.toString(10), '73000'); - w.addTX(t2, function(err) { + wdb.addTX(t2, function(err) { assert(!err); w.getBalance(function(err, balance) { assert(!err); assert.equal(balance.toString(10), '47000'); - w.addTX(t3, function(err) { + wdb.addTX(t3, function(err) { assert(!err); w.getBalance(function(err, balance) { assert(!err); assert.equal(balance.toString(10), '22000'); - w.addTX(f1, function(err) { + wdb.addTX(f1, function(err) { assert(!err); w.getBalance(function(err, balance) { assert(!err); @@ -250,7 +250,7 @@ describe('Wallet', function() { t1.addInput(dummyInput); // Fake TX should temporarly change output - w1.addTX(t1, function(err) { + wdb.addTX(t1, function(err) { assert(!err); // Create new transaction @@ -307,9 +307,9 @@ describe('Wallet', function() { t2.addInput(dummyInput); // Fake TX should temporarly change output - w1.addTX(t1, function(err) { + wdb.addTX(t1, function(err) { assert(!err); - w2.addTX(t2, function(err) { + wdb.addTX(t2, function(err) { assert(!err); // Create our tx with an output @@ -319,18 +319,18 @@ describe('Wallet', function() { var cost = tx.getOutputValue(); var total = cost.add(new bn(constants.tx.minFee)); - w1.getUnspent(function(err, unspent1) { + w1.getCoins(function(err, coins1) { assert(!err); - w2.getUnspent(function(err, unspent2) { + w2.getCoins(function(err, coins2) { assert(!err); // Add dummy output (for `left`) to calculate maximum TX size tx.addOutput(w1, new bn(0)); // Add our unspent inputs to sign - tx.addInput(unspent1[0]); - tx.addInput(unspent1[1]); - tx.addInput(unspent2[0]); + tx.addInput(coins1[0]); + tx.addInput(coins1[1]); + tx.addInput(coins2[0]); var left = tx.getInputValue().sub(total); if (left.cmpn(constants.tx.dustThreshold) < 0) { @@ -351,9 +351,9 @@ describe('Wallet', function() { // Sign transaction using `inputs` and `off` params. tx.inputs.length = 0; - tx.addInput(unspent1[1]); - tx.addInput(unspent1[2]); - tx.addInput(unspent2[1]); + tx.addInput(coins1[1]); + tx.addInput(coins1[2]); + tx.addInput(coins2[1]); assert.equal(w1.sign(tx, 'all'), 2); assert.equal(w2.sign(tx, 'all'), 1); @@ -438,11 +438,11 @@ describe('Wallet', function() { assert.equal(w1.receiveDepth, 1); - w1.addTX(utx, function(err) { + wdb.addTX(utx, function(err) { assert(!err); - w2.addTX(utx, function(err) { + wdb.addTX(utx, function(err) { assert(!err); - w3.addTX(utx, function(err) { + wdb.addTX(utx, function(err) { assert(!err); assert.equal(w1.receiveDepth, 2); @@ -479,11 +479,11 @@ describe('Wallet', function() { send.ts = 1; send.height = 1; - w1.addTX(send, function(err) { + wdb.addTX(send, function(err) { assert(!err); - w2.addTX(send, function(err) { + wdb.addTX(send, function(err) { assert(!err); - w3.addTX(send, function(err) { + wdb.addTX(send, function(err) { assert(!err); assert.equal(w1.receiveDepth, 2);