diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 4cadaaa4..41c6c31d 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -14,11 +14,11 @@ * p/[hash] -> dummy (pending flag) * m/[time]/[hash] -> dummy (tx by time) * h/[height]/[hash] -> dummy (tx by height) - * T/[id]/[hash] -> dummy (tx by wallet id) - * P/[id]/[hash] -> dummy (pending tx by wallet id) - * M/[id]/[time]/[hash] -> dummy (tx by time + id) - * H/[id]/[height]/[hash] -> dummy (tx by height + id) - * C/[id]/[hash]/[index] -> dummy (coin by address) + * T/[id]/[name]/[hash] -> dummy (tx by wallet id) + * P/[id]/[name]/[hash] -> dummy (pending tx by wallet/account id) + * M/[id]/[name]/[time]/[hash] -> dummy (tx by time + id/account) + * H/[id]/[name]/[height]/[hash] -> dummy (tx by height + id/account) + * C/[id]/[name]/[hash]/[index] -> dummy (coin by id/account) */ var bcoin = require('./env'); @@ -40,7 +40,8 @@ var BufferWriter = require('./writer'); * @param {Boolean?} options.indexAddress - Index addresses/IDs. * @param {Boolean?} options.indexExtra - Index timestamps, heights, etc. * @param {Boolean?} options.verify - Verify transactions as they - * come in (note that this will not happen on the worker pool). + * come in (note that this will not happen on the worker + * pool -- only used for SPV). */ function TXDB(db, options) { @@ -52,7 +53,7 @@ function TXDB(db, options) { if (!options) options = {}; - this.wdb = db; + this.walletdb = db; this.db = db.db; this.options = options; this.network = bcoin.network.get(options.network); @@ -126,27 +127,8 @@ TXDB.prototype._testFilter = function _testFilter(addresses) { * @param {Function} callback - Returns [Error, {@link AddressMap}]. */ -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; -} - TXDB.prototype.getMap = function getMap(tx, callback) { - var input, output, addresses, table, map; + var i, input, output, address, addresses, table, map; input = tx.getInputHashes(); output = tx.getOutputHashes(); @@ -155,7 +137,7 @@ TXDB.prototype.getMap = function getMap(tx, callback) { if (!this._testFilter(addresses)) return callback(); - function cb(err, table) { + this.mapAddresses(addresses, function(err, table) { if (err) return callback(err); @@ -169,29 +151,29 @@ TXDB.prototype.getMap = function getMap(tx, callback) { all: [] }; - input.forEach(function(address) { + for (i = 0; i < input.length; i++) { + address = input[i]; assert(map.table[address]); map.input = map.input.concat(map.table[address]); - }); + } - output.forEach(function(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)); return callback(null, map); - } - - return this.mapAddresses(addresses, cb); + }); }; /** - * Map an address to a wallet ID. - * @param {Base58Address|Base58Address[]} address + * Map address hashes to a wallet ID. + * @param {Hash[]} address - Address hashes. * @param {Function} callback - Returns [Error, {@link AddressTable}]. */ @@ -201,7 +183,7 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { var i, keys, values; return utils.forEachSerial(address, function(address, next) { - self.wdb.getAddress(address, function(err, paths) { + self.walletdb.getAddress(address, function(err, paths) { if (err) return next(err); @@ -389,6 +371,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { // Consume unspent money or add orphans utils.forEachSerial(tx.inputs, function(input, next, i) { var key, address; + var prevout = input.prevout; if (tx.isCoinbase()) return next(); @@ -399,11 +382,11 @@ TXDB.prototype._add = function add(tx, map, callback, force) { if (!address || !map.table[address].length) return next(); - self.getCoin(input.prevout.hash, input.prevout.index, function(err, coin) { + self.getCoin(prevout.hash, prevout.index, function(err, coin) { if (err) return next(err); - key = input.prevout.hash + '/' + input.prevout.index; + key = prevout.hash + '/' + prevout.index; if (coin) { // Add TX to inputs and spend money @@ -432,21 +415,21 @@ TXDB.prototype._add = function add(tx, map, callback, force) { input.coin = null; - self.isSpent(input.prevout.hash, input.prevout.index, function(err, spentBy) { + self.isSpent(prevout.hash, prevout.index, function(err, spentBy) { if (err) return next(err); // Are we double-spending? // Replace older txs with newer ones. if (spentBy) { - return self.getTX(input.prevout.hash, function(err, prev) { + return self.getTX(prevout.hash, function(err, prev) { if (err) return next(err); if (!prev) return callback(new Error('Could not find double-spent coin.')); - input.coin = bcoin.coin.fromTX(prev, input.prevout.index); + input.coin = bcoin.coin.fromTX(prev, prevout.index); // Skip invalid transactions if (self.options.verify) { @@ -577,7 +560,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { if (err) return callback(err); - self.wdb.sync(tx, map, function(err) { + self.walletdb.sync(tx, map, function(err) { if (err) return callback(err); @@ -768,7 +751,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { if (err) return callback(err); - self.wdb.sync(tx, map, function(err) { + self.walletdb.sync(tx, map, function(err) { if (err) return callback(err); @@ -1070,47 +1053,22 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { /** * Get hashes of all transactions in the database. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getHistoryHashes = function getHistoryHashes(address, callback) { - var self = this; - var txs = []; - - if (typeof address === 'function') { - callback = address; - address = null; - } - - callback = utils.ensure(callback); - - if (Array.isArray(address)) { - return utils.forEachSerial(address, function(address, next) { - self.getHistoryHashes(address, function(err, tx) { - if (err) - return next(err); - - txs = txs.concat(tx); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - txs = utils.uniq(txs); - - return callback(null, txs); - }); +TXDB.prototype.getHistoryHashes = function getHistoryHashes(id, callback) { + if (typeof id === 'function') { + callback = id; + id = null; } this.db.iterate({ - gte: address ? 'T/' + address + '/' : 't', - lte: address ? 'T/' + address + '/~' : 't~', + gte: id ? 'T/' + id + '/' : 't', + lte: id ? 'T/' + id + '/~' : 't~', transform: function(key) { key = key.split('/'); - if (address) + if (id) return key[3]; return key[1]; } @@ -1119,48 +1077,22 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(address, callback) { /** * Get hashes of all unconfirmed transactions in the database. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(address, callback) { - var self = this; - var txs = []; - - if (typeof address === 'function') { - callback = address; - address = null; - } - - callback = utils.ensure(callback); - - if (Array.isArray(address)) { - return utils.forEachSerial(address, function(address, next) { - assert(address); - self.getUnconfirmedHashes(address, function(err, tx) { - if (err) - return next(err); - - txs = txs.concat(tx); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - txs = utils.uniq(txs); - - return callback(null, txs); - }); +TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(id, callback) { + if (typeof id === 'function') { + callback = id; + id = null; } this.db.iterate({ - gte: address ? 'P/' + address + '/' : 'p', - lte: address ? 'P/' + address + '/~' : 'p~', + gte: id ? 'P/' + id + '/' : 'p', + lte: id ? 'P/' + id + '/~' : 'p~', transform: function(key) { key = key.split('/'); - if (address) + if (id) return key[3]; return key[1]; } @@ -1169,45 +1101,22 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(address, cal /** * Get all coin hashes in the database. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getCoinHashes = function getCoinHashes(address, callback) { - var self = this; - var coins = []; - - if (typeof address === 'function') { - callback = address; - address = null; - } - - callback = utils.ensure(callback); - - if (Array.isArray(address)) { - return utils.forEachSerial(address, function(address, next) { - self.getCoinHashes(address, function(err, coin) { - if (err) - return next(err); - - coins = coins.concat(coin); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - return callback(null, coins); - }); +TXDB.prototype.getCoinHashes = function getCoinHashes(id, callback) { + if (typeof id === 'function') { + callback = id; + id = null; } this.db.iterate({ - gte: address ? 'C/' + address + '/' : 'c', - lte: address ? 'C/' + address + '/~' : 'c~', + gte: id ? 'C/' + id + '/' : 'c', + lte: id ? 'C/' + id + '/~' : 'c~', transform: function(key) { key = key.split('/'); - if (address) + if (id) return [key[3], +key[4]]; return [key[1], +key[2]]; } @@ -1216,7 +1125,7 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(address, callback) { /** * Get TX hashes by height range. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1225,27 +1134,25 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(address, callback) { * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(address, options, callback) { - if (typeof address !== 'string') { +TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(id, options, callback) { + if (typeof id !== 'string') { callback = options; - options = address; - address = null; + options = id; + id = null; } - callback = utils.ensure(callback); - this.db.iterate({ - gte: address - ? 'H/' + address + '/' + pad32(options.start) + '/' + gte: id + ? 'H/' + id + '/' + pad32(options.start) + '/' : 'h/' + pad32(options.start) + '/', - lte: address - ? 'H/' + address + '/' + pad32(options.end) + '/~' + lte: id + ? 'H/' + id + '/' + pad32(options.end) + '/~' : 'h/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); - if (address) + if (id) return key[4]; return key[2]; } @@ -1264,7 +1171,7 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) { /** * Get TX hashes by timestamp range. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1273,26 +1180,24 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) { * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ -TXDB.prototype.getRangeHashes = function getRangeHashes(address, options, callback) { - if (typeof address === 'function') { - callback = address; - address = null; +TXDB.prototype.getRangeHashes = function getRangeHashes(id, options, callback) { + if (typeof id === 'function') { + callback = id; + id = null; } - callback = utils.ensure(callback); - this.db.iterate({ - gte: address - ? 'M/' + address + '/' + pad32(options.start) + '/' + gte: id + ? 'M/' + id + '/' + pad32(options.start) + '/' : 'm/' + pad32(options.start) + '/', - lte: address - ? 'M/' + address + '/' + pad32(options.end) + '/~' + lte: id + ? 'M/' + id + '/' + pad32(options.end) + '/~' : 'm/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); - if (address) + if (id) return key[4]; return key[2]; } @@ -1301,7 +1206,7 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(address, options, callba /** * Get transactions by timestamp range. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1310,16 +1215,16 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(address, options, callba * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getRange = function getLast(address, options, callback) { +TXDB.prototype.getRange = function getLast(id, options, callback) { var self = this; var txs = []; - if (typeof address === 'function') { - callback = address; - address = null; + if (typeof id === 'function') { + callback = id; + id = null; } - return this.getRangeHashes(address, options, function(err, hashes) { + return this.getRangeHashes(id, options, function(err, hashes) { if (err) return callback(err); @@ -1346,19 +1251,19 @@ TXDB.prototype.getRange = function getLast(address, options, callback) { /** * Get last N transactions. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Number} limit - Max number of transactions. * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getLast = function getLast(address, limit, callback) { +TXDB.prototype.getLast = function getLast(id, limit, callback) { if (typeof limit === 'function') { callback = limit; - limit = address; - address = null; + limit = id; + id = null; } - return this.getRange(address, { + return this.getRange(id, { start: 0, end: 0xffffffff, reverse: true, @@ -1368,20 +1273,20 @@ TXDB.prototype.getLast = function getLast(address, limit, callback) { /** * Get all transactions. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getHistory = function getHistory(address, callback) { +TXDB.prototype.getHistory = function getHistory(id, callback) { var self = this; var txs = []; - if (typeof address === 'function') { - callback = address; - address = null; + if (typeof id === 'function') { + callback = id; + id = null; } - return this.getHistoryHashes(address, function(err, hashes) { + return this.getHistoryHashes(id, function(err, hashes) { if (err) return callback(err); @@ -1408,32 +1313,34 @@ TXDB.prototype.getHistory = function getHistory(address, callback) { /** * Get last active timestamp and height. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, Number(ts), Number(height)]. */ -TXDB.prototype.getLastTime = function getLastTime(address, callback) { - if (typeof address === 'function') { - callback = address; - address = null; +TXDB.prototype.getLastTime = function getLastTime(id, callback) { + var i, tx, lastTs, lastHeight; + + if (typeof id === 'function') { + callback = id; + id = null; } - return this.getHistory(address, function(err, txs) { - var lastTs, lastHeight; - + return this.getHistory(id, function(err, txs) { if (err) return callback(err); lastTs = 0; lastHeight = -1; - txs.forEach(function(tx) { + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + if (tx.ts > lastTs) lastTs = tx.ts; if (tx.height > lastHeight) lastHeight = tx.height; - }); + } return callback(null, lastTs, lastHeight); }); @@ -1441,20 +1348,20 @@ TXDB.prototype.getLastTime = function getLastTime(address, callback) { /** * Get unconfirmed transactions. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -TXDB.prototype.getUnconfirmed = function getUnconfirmed(address, callback) { +TXDB.prototype.getUnconfirmed = function getUnconfirmed(id, callback) { var self = this; var txs = []; - if (typeof address === 'function') { - callback = address; - address = null; + if (typeof id === 'function') { + callback = id; + id = null; } - return this.getUnconfirmedHashes(address, function(err, hashes) { + return this.getUnconfirmedHashes(id, function(err, hashes) { if (err) return callback(err); @@ -1481,20 +1388,20 @@ TXDB.prototype.getUnconfirmed = function getUnconfirmed(address, callback) { /** * Get coins. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Coin}[]]. */ -TXDB.prototype.getCoins = function getCoins(address, callback) { +TXDB.prototype.getCoins = function getCoins(id, callback) { var self = this; var coins = []; - if (typeof address === 'function') { - callback = address; - address = null; + if (typeof id === 'function') { + callback = id; + id = null; } - return this.getCoinHashes(address, function(err, hashes) { + return this.getCoinHashes(id, function(err, hashes) { if (err) return callback(err); @@ -1629,8 +1536,8 @@ TXDB.prototype.hasTX = function hasTX(hash, callback) { */ TXDB.prototype.getCoin = function getCoin(hash, index, callback) { - this.db.fetch('c/' + hash + '/' + index, function(coin) { - coin = bcoin.coin.fromRaw(coin); + this.db.fetch('c/' + hash + '/' + index, function(data) { + var coin = bcoin.coin.fromRaw(data); coin.hash = hash; coin.index = index; return coin; @@ -1649,39 +1556,21 @@ TXDB.prototype.hasCoin = function hasCoin(hash, index, callback) { /** * Calculate balance. - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Balance}]. */ -TXDB.prototype.getBalance = function getBalance(address, callback) { +TXDB.prototype.getBalance = function getBalance(id, callback) { var self = this; var confirmed = 0; var unconfirmed = 0; - if (typeof address === 'function') { - callback = address; - address = null; + if (typeof id === 'function') { + callback = id; + id = null; } - // return this.getCoins(address, function(err, coins) { - // if (err) - // return callback(err); - // - // for (i = 0; i < coins.length; i++) { - // if (coins[i].height === -1) - // unconfirmed += coins[i].value; - // else - // confirmed += coins[i].value; - // } - // - // return callback(null, { - // confirmed: confirmed, - // unconfirmed: unconfirmed, - // total: confirmed + unconfirmed - // }); - // }); - - return this.getCoinHashes(address, function(err, hashes) { + return this.getCoinHashes(id, function(err, hashes) { if (err) return callback(err); @@ -1711,30 +1600,31 @@ TXDB.prototype.getBalance = function getBalance(address, callback) { }; /** - * @param {WalletID|WalletID[]} address - By address (can be null). + * @param {WalletID?} id * @param {Number} age - Age delta (delete transactions older than `now - age`). * @param {Function} callback */ -TXDB.prototype.zap = function zap(address, age, callback, force) { +TXDB.prototype.zap = function zap(id, age, callback, force) { var self = this; - if (typeof address !== 'string') { + if (typeof age === 'function') { force = callback; callback = age; - age = address; - address = null; + age = id; + id = null; } - var unlock = this._lock(zap, [address, age, callback], force); + var unlock = this._lock(zap, [id, age, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); - assert(utils.isNumber(age)); + if (!utils.isNumber(age)) + return callback(new Error('Age must be a number.')); - return this.getRange(address, { + return this.getRange(id, { start: 0, end: bcoin.now() - age }, function(err, txs) { @@ -1748,12 +1638,35 @@ TXDB.prototype.zap = function zap(address, age, callback, force) { utils.forEachSerial(txs, function(tx, next) { if (tx.ts !== 0) return next(); - self.lazyRemove(tx, next); + self.lazyRemove(tx, next, true); }, callback); }); }); }; +/* + * 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; +} + /* * Expose */ diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 98afee98..a19c2d97 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -2277,14 +2277,14 @@ utils.parallel = function parallel(stack, callback) { for (i = 0; i < stack.length; i++) { try { - if (0 && stack[i].length >= 2) { - stack[i](error, next); - error = null; - } else { - if (error) - continue; - stack[i](next); - } + // if (stack[i].length >= 2) { + // stack[i](error, next); + // error = null; + // continue; + // } + if (error) + continue; + stack[i](next); } catch (e) { pending--; error = e; @@ -2306,14 +2306,13 @@ utils.serial = function serial(stack, callback) { if (!cb) return callback(err); - if (0) - if (cb.length >= 2) { - try { - return cb(err, next); - } catch (e) { - return next(e); - } - } + // if (cb.length >= 2) { + // try { + // return cb(err, next); + // } catch (e) { + // return next(e); + // } + // } if (err) return utils.nextTick(next.bind(null, err)); diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 59fa4c81..dd67f312 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -76,6 +76,7 @@ function Wallet(options) { if (!this.id) this.id = this.getID(); + // An in-memory database for testing. if (!this.db) { this.db = new bcoin.walletdb({ network: this.network, @@ -83,9 +84,6 @@ function Wallet(options) { db: 'memory' }); } - - // Non-alphanumeric IDs will break leveldb sorting. - assert(/^[a-zA-Z0-9]+$/.test(this.id), 'Wallet IDs must be alphanumeric.'); } utils.inherits(Wallet, EventEmitter); @@ -214,6 +212,7 @@ Wallet.prototype.addKey = function addKey(account, key, callback) { key = account; account = 0; } + this.getAccount(account, function(err, account) { if (err) return callback(err); @@ -238,6 +237,7 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) { key = account; account = 0; } + this.getAccount(account, function(err, account) { if (err) return callback(err); @@ -294,7 +294,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback) { options = { network: this.network, - wid: this.id, + id: this.id, name: this.accountDepth === 0 ? 'default' : options.name, witness: options.witness, accountKey: key.hdPublicKey, @@ -854,31 +854,31 @@ Wallet.prototype.zap = function zap(account, age, callback) { }; /** - * Scan for addresses. - * @param {Function} getByAddress - Must be a callback which accepts - * a callback and returns transactions by address. - * @param {Function} callback - Returns [Error, Boolean, TX[]]. + * Scan for active accounts and addresses. Used for importing a wallet. + * @param {Function} getByAddress - Must be a function which accepts + * a {@link Base58Address} as well as a callback and returns + * transactions by address. + * @param {Function} callback - Return [Error, Number] (total number + * of addresses allocated). */ Wallet.prototype.scan = function scan(getByAddress, callback) { var self = this; - var result = false; - var txs = []; + var total = 0; (function next() { self.createAccount(self.options, function(err, account) { if (err) return callback(err); - account.scan(getByAddress, function(err, res, tx) { + account.scan(getByAddress, function(err, result) { if (err) return callback(err); - if (!res) - return callback(null, result, txs); + if (result === 0) + return callback(null, total); - result = true; - txs = txs.concat(tx); + total += result; next(); }); @@ -978,6 +978,7 @@ Wallet.prototype.addTX = function addTX(tx, callback) { /** * Get all transactions in transaction history (accesses db). + * @param {(String|Number)?} account * @param {Function} callback - Returns [Error, {@link TX}[]]. */ @@ -1444,8 +1445,8 @@ Wallet.fromJSON = function fromJSON(json) { Wallet.isWallet = function isWallet(obj) { return obj - && typeof obj.receiveDepth === 'number' - && obj.deriveAddress === 'function'; + && typeof obj.accountDepth === 'number' + && obj.scriptInputs === 'function'; }; /** @@ -1454,11 +1455,9 @@ Wallet.isWallet = function isWallet(obj) { * @constructor * @param {Object} options * @param {WalletDB} options.db - * present, no coins will be available. - * @param {(HDPrivateKey|HDPublicKey)?} options.master - Master HD key. If not - * present, it will be generated. + * @param {HDPublicKey} options.accountKey * @param {Boolean?} options.witness - Whether to use witness programs. - * @param {Number?} options.accountIndex - The BIP44 account index (default=0). + * @param {Number} options.accountIndex - The BIP44 account index. * @param {Number?} options.receiveDepth - The index of the _next_ receiving * address. * @param {Number?} options.changeDepth - The index of the _next_ change @@ -1467,8 +1466,8 @@ Wallet.isWallet = function isWallet(obj) { * (default=pubkeyhash). * @param {Number?} options.m - `m` value for multisig. * @param {Number?} options.n - `n` value for multisig. - * @param {String?} options.wid - Account ID (used for storage) - * (default=account key "address"). + * @param {String?} options.id - Wallet ID + * @param {String?} options.name - Account name */ function Account(options) { @@ -1481,19 +1480,20 @@ function Account(options) { assert(options, 'Options are required.'); assert(options.db, 'Database is required.'); - assert(options.wid, 'Wallet ID is required.'); + assert(options.id, 'Wallet ID is required.'); assert(options.accountKey, 'Account key is required.'); + assert(utils.isNumber(options.accountIndex), 'Account index is required.'); this.options = options; this.network = bcoin.network.get(options.network); this.db = options.db; this.lookahead = options.lookahead != null ? options.lookahead : 5; - this.wid = options.wid; + this.id = options.id; this.name = options.name; this.witness = options.witness || false; this.accountKey = options.accountKey; - this.accountIndex = options.accountIndex || 0; + this.accountIndex = options.accountIndex; this.receiveDepth = options.receiveDepth || 1; this.changeDepth = options.changeDepth || 1; this.type = options.type || 'pubkeyhash'; @@ -1949,7 +1949,7 @@ Account.prototype.save = function save(callback) { */ Account.prototype.saveAddress = function saveAddress(address, callback) { - this.db.saveAddress(this.wid, address, callback); + this.db.saveAddress(this.id, address, callback); }; /** @@ -2032,92 +2032,62 @@ Account.prototype.setChangeDepth = function setChangeDepth(depth, callback) { * Scan for addresses. * @param {Function} getByAddress - Must be a callback which accepts * a callback and returns transactions by address. - * @param {Function} callback - Returns [Boolean, TX[]]. + * @param {Function} callback - Return [Error, Number] (total number + * of addresses allocated). */ Account.prototype.scan = function scan(getByAddress, callback) { var self = this; - var result = false; + var total = 0; - return this._scan(getByAddress, function(err, depth, txs) { - if (err) - return callback(err); + if (!this.initialized) + return callback(new Error('Account is uninitialized.')); - self.setChangeDepth(depth.changeDepth + 1, function(err, res) { + function addTX(txs, calback) { + if (!Array.isArray(txs) || txs.length === 0) + return callback(null, false); + + utils.forEachSerial(txs, function(tx, next) { + self.db.addTX(tx, next); + }, function(err) { if (err) return callback(err); - if (res) - result = true; - - self.setReceiveDepth(depth.receiveDepth + 1, function(err, res) { - if (err) - return callback(err); - - if (res) - result = true; - - return callback(null, result, txs); - }); + return callback(null, true); }); - }); -}; - -Account.prototype._scan = function _scan(getByAddress, callback) { - var self = this; - var depth = { changeDepth: 0, receiveDepth: 0 }; - var all = []; - - assert(this.initialized); + } (function chainCheck(change) { - var addressIndex = 0; - var total = 0; var gap = 0; (function next() { - var address = self.deriveAddress(change, addressIndex++); - - getByAddress(address.getAddress(), function(err, txs) { - var result; - + self.createAddress(change, function(err, address) { if (err) return callback(err); - if (txs) { - if (typeof txs === 'boolean') - result = txs; - else if (typeof txs === 'number') - result = txs > 0; - else if (Array.isArray(txs)) - result = txs.length > 0; - else - result = false; + getByAddress(address.getAddress(), function(err, txs) { + if (err) + return callback(err); - if (Array.isArray(txs) && (txs[0] instanceof bcoin.tx)) - all = all.concat(txs); - } + addTX(txs, function(err, result) { + if (err) + return callback(err); - if (result) { - total++; - gap = 0; - return next(); - } + if (result) { + total++; + gap = 0; + return next(); + } - if (++gap < 20) - return next(); + if (++gap < 20) + return next(); - assert(depth.receiveDepth === 0 || change === true); + if (!change) + return chainCheck(true); - if (change === false) - depth.receiveDepth = addressIndex - gap; - else - depth.changeDepth = addressIndex - gap; - - if (change === false) - return chainCheck(true); - - return callback(null, depth, all); + return callback(null, total); + }); + }); }); })(); })(false); @@ -2130,9 +2100,9 @@ Account.prototype._scan = function _scan(getByAddress, callback) { Account.prototype.inspect = function inspect() { return { - wid: this.wid, + id: this.id, name: this.name, - network: this.network.type, + network: this.network, initialized: this.initialized, type: this.type, m: this.m, @@ -2167,7 +2137,7 @@ Account.prototype.inspect = function inspect() { Account.prototype.toJSON = function toJSON() { return { network: this.network.type, - wid: this.wid, + id: this.id, name: this.name, initialized: this.initialized, type: this.type, @@ -2198,7 +2168,7 @@ Account.prototype.toJSON = function toJSON() { Account.parseJSON = function parseJSON(json) { return { network: json.network, - wid: json.wid, + id: json.id, name: json.name, initialized: json.initialized, type: json.type, @@ -2225,7 +2195,7 @@ Account.prototype.toRaw = function toRaw(writer) { var i; p.writeU32(this.network.magic); - p.writeVarString(this.wid, 'utf8'); + p.writeVarString(this.id, 'utf8'); p.writeVarString(this.name, 'utf8'); p.writeU8(this.initialized ? 1 : 0); p.writeU8(this.type === 'pubkeyhash' ? 0 : 1); @@ -2258,7 +2228,7 @@ Account.prototype.toRaw = function toRaw(writer) { Account.parseRaw = function parseRaw(data) { var p = new BufferReader(data); var network = bcoin.network.fromMagic(p.readU32()); - var wid = p.readVarString('utf8'); + var id = p.readVarString('utf8'); var name = p.readVarString('utf8'); var initialized = p.readU8() === 1; var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; @@ -2277,7 +2247,7 @@ Account.parseRaw = function parseRaw(data) { return { network: network.type, - wid: wid, + id: id, name: name, initialized: initialized, type: type, diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 3ccf58a2..d313cf7c 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -10,6 +10,8 @@ * (inherits all from txdb) * W/[address] -> id & path data * w/[id] -> wallet + * a/[id]/[index] -> account + * i/[id]/[name] -> account index */ var bcoin = require('./env'); @@ -346,140 +348,12 @@ WalletDB.prototype.get = function get(id, callback) { */ WalletDB.prototype.save = function save(wallet, callback) { + if (!isAlpha(wallet.id)) + return callback(new Error('Wallet IDs must be alphanumeric.')); + this.db.put('w/' + wallet.id, wallet.toRaw(), callback); }; -/** - * Get an account from the database. - * @param {WalletID} id - * @param {Function} callback - Returns [Error, {@link Wallet}]. - */ - -WalletDB.prototype.getAccountIndex = function getAccountIndex(wid, name, callback) { - return this.db.get('i/' + wid + '/' + name, function(err, index) { - if (err && err.type !== 'NotFoundError') - return callback(); - - if (!index) - return callback(null, -1); - - return callback(null, index.readUInt32LE(0, true)); - }); -}; - -WalletDB.prototype.getAccount = function getAccount(wid, id, callback) { - var self = this; - var aid = wid + '/' + id; - var account; - - if (id == null) - return callback(); - - if (typeof id === 'string') { - return this.getAccountIndex(wid, id, function(err, index) { - if (err) - return callback(err); - - if (index === -1) - return callback(); - - return self.getAccount(wid, index, callback); - }); - } - - this.db.get('a/' + aid, function(err, data) { - if (err && err.type !== 'NotFoundError') - return callback(err); - - if (!data) - return callback(); - - try { - data = bcoin.account.parseRaw(data); - data.db = self; - account = new bcoin.account(data); - } catch (e) { - return callback(e); - } - - account.open(function(err) { - if (err) - return callback(err); - - return callback(null, account); - }); - }); -}; - -/** - * Remove wallet from the database. Destroy wallet if passed in. - * @param {WalletID} id - * @param {Function} callback - */ - -WalletDB.prototype.remove = function remove(id, callback) { - this.db.del('w/' + id, function(err) { - if (err && err.type !== 'NotFoundError') - return callback(err); - - return callback(); - }); -}; - -/** - * Save a wallet to the database. - * @param {Wallet} wallet - * @param {Function} callback - */ - -WalletDB.prototype.saveAccount = function saveAccount(account, callback) { - var index = new Buffer(4); - var batch = this.db.batch(); - index.writeUInt32LE(account.accountIndex, 0, true); - batch.put('a/' + account.wid + '/' + account.accountIndex, account.toRaw()); - batch.put('i/' + account.wid + '/' + account.name, index); - batch.write(callback); -}; - -WalletDB.prototype.createAccount = function createAccount(options, callback) { - var self = this; - var account; - - this.hasAccount(options.wid, options.accountIndex, function(err, exists) { - if (err) - return callback(err); - - if (err) - return callback(err); - - if (exists) - return callback(new Error('account already exists.')); - - options = utils.merge({}, options); - - if (self.network.witness) - options.witness = options.witness !== false; - - options.network = self.network; - options.db = self; - account = new bcoin.account(options); - - account.open(function(err) { - if (err) - return callback(err); - - return callback(null, account); - }); - }); -}; - -WalletDB.prototype.hasAccount = function hasAccount(wid, account, callback) { - if (!wid || account == null) - return callback(null, false); - - this.db.has('a/' + wid + '/' + account, callback); -}; - /** * Create a new wallet, save to database, setup watcher. * @param {Object} options - See {@link Wallet}. @@ -502,9 +376,6 @@ WalletDB.prototype.create = function create(options, callback) { options = utils.merge({}, options); - if (self.network.witness) - options.witness = options.witness !== false; - options.network = self.network; options.db = self; wallet = new bcoin.wallet(options); @@ -551,6 +422,160 @@ WalletDB.prototype.ensure = function ensure(options, callback) { }); }; +/** + * Get an account from the database. + * @param {WalletID} id + * @param {String|Number} name - Account name/index. + * @param {Function} callback - Returns [Error, {@link Wallet}]. + */ + +WalletDB.prototype.getAccount = function getAccount(id, name, callback) { + var self = this; + var account; + + return this.getAccountIndex(id, name, function(err, index) { + if (err) + return callback(err); + + if (index === -1) + return callback(); + + self.db.get('a/' + id + '/' + index, function(err, data) { + if (err && err.type !== 'NotFoundError') + return callback(err); + + if (!data) + return callback(); + + try { + data = bcoin.account.parseRaw(data); + data.db = self; + account = new bcoin.account(data); + } catch (e) { + return callback(e); + } + + account.open(function(err) { + if (err) + return callback(err); + + return callback(null, account); + }); + }); + }); +}; + +/** + * Lookup the corresponding account name's index. + * @param {WalletID} id + * @param {String|Number} name - Account name/index. + * @param {Function} callback - Returns [Error, Number]. + */ + +WalletDB.prototype.getAccountIndex = function getAccountIndex(id, name, callback) { + if (name == null) + return callback(null, -1); + + if (typeof name === 'number') + return callback(null, name); + + return this.db.get('i/' + id + '/' + name, function(err, index) { + if (err && err.type !== 'NotFoundError') + return callback(); + + if (!index) + return callback(null, -1); + + return callback(null, index.readUInt32LE(0, true)); + }); +}; + + +/** + * Save an account to the database. + * @param {Account} account + * @param {Function} callback + */ + +WalletDB.prototype.saveAccount = function saveAccount(account, callback) { + var index, batch; + + if (!isAlpha(account.name)) + return callback(new Error('Account names must be alphanumeric.')); + + batch = this.db.batch(); + + index = new Buffer(4); + index.writeUInt32LE(account.accountIndex, 0, true); + + batch.put('a/' + account.id + '/' + account.accountIndex, account.toRaw()); + batch.put('i/' + account.id + '/' + account.name, index); + + batch.write(callback); +}; + +/** + * Create an account. + * @param {Object} options - See {@link Account} options. + * @param {Function} callback - Returns [Error, {@link Account}]. + */ + +WalletDB.prototype.createAccount = function createAccount(options, callback) { + var self = this; + var account; + + this.hasAccount(options.id, options.accountIndex, function(err, exists) { + if (err) + return callback(err); + + if (err) + return callback(err); + + if (exists) + return callback(new Error('Account already exists.')); + + options = utils.merge({}, options); + + if (self.network.witness) + options.witness = options.witness !== false; + + options.network = self.network; + options.db = self; + account = new bcoin.account(options); + + account.open(function(err) { + if (err) + return callback(err); + + return callback(null, account); + }); + }); +}; + +/** + * Test for the existence of an account. + * @param {WalletID} id + * @param {String|Number} account + * @param {Function} callback - Returns [Error, Boolean]. + */ + +WalletDB.prototype.hasAccount = function hasAccount(id, account, callback) { + var self = this; + + if (!id) + return callback(null, false); + + this.getAccountIndex(id, account, function(err, index) { + if (err) + return callback(err); + + if (index === -1) + return callback(null, false); + + self.db.has('a/' + id + '/' + index, callback); + }); +}; + /** * Save an address to the path map. * The path map exists in the form of: @@ -958,6 +983,10 @@ function serializePaths(out) { return p.render(); } +function isAlpha(key) { + return /^[a-zA-Z0-9]+$/.test(key); +} + /* * Expose */